Building a Factorial Calculator in PHP
Functions, loops and recursion applied to a problem small enough to hold in your head — plus the integer overflow that every factorial tutorial forgets to mention.
Factorial is the perfect first algorithm. The definition fits on one line, you can check the answers by hand, and it exposes the difference between iteration and recursion better than anything else.
5! means 5 × 4 × 3 × 2 × 1 = 120. And 0! is 1 — not zero, not undefined. That is a definition, not a trick, and it is where most buggy implementations go wrong.
#Version 1 — a loop
<?php
function factorial(int $n): int {
$result = 1;
for ($i = 2; $i <= $n; $i++) {
$result *= $i;
}
return $result;
}
echo factorial(5); // 120
echo factorial(0); // 1Start at 2, not 1 — multiplying by one does nothing. And because the loop never runs when $n is 0 or 1, the initial $result = 1 handles both base cases for free. That is not an accident; it is what makes this the cleanest version.
#The type declarations
function factorial(int $n): int {int $n says the parameter must be an integer; : int says the return value is one. PHP will coerce a numeric string like "5" for you by default, and reject anything genuinely wrong.
Add this at the very top of the file and it stops coercing entirely:
declare(strict_types=1);Now factorial("5") throws a TypeError instead of quietly converting. Use it in every new file. Catching a type mistake at the boundary beats debugging a nonsensical result three functions deeper.
#Version 2 — recursion
function factorialRecursive(int $n): int {
if ($n <= 1) {
return 1; // base case
}
return $n * factorialRecursive($n - 1); // recursive case
}Every recursive function needs exactly two things:
- A base case that returns without recursing.
- A recursive case that moves measurably closer to the base case.
Miss the base case and you get infinite recursion until PHP runs out of stack. Have a recursive case that does not shrink the problem and you get the same thing.
Trace factorialRecursive(4):
factorialRecursive(4)
= 4 * factorialRecursive(3)
= 4 * (3 * factorialRecursive(2))
= 4 * (3 * (2 * factorialRecursive(1)))
= 4 * (3 * (2 * 1))
= 24Nothing multiplies until the innermost call returns. All four frames sit on the call stack waiting.
#Handling bad input
factorial(-3) returns 1 in both versions above. That is wrong — factorial is not defined for negatives — and returning a plausible-looking wrong answer is worse than failing.
declare(strict_types=1);
function factorial(int $n): int {
if ($n < 0) {
throw new InvalidArgumentException("Factorial is undefined for negative numbers.");
}
if ($n > 20) {
throw new RangeException("$n! exceeds the maximum integer size on this platform.");
}
$result = 1;
for ($i = 2; $i <= $n; $i++) {
$result *= $i;
}
return $result;
}Which brings us to the thing most tutorials skip.
#The overflow nobody mentions
Try it:
var_dump(factorial(20)); // int(2432902008176640000) exact
var_dump(factorial(21)); // float(5.1090942171709E+19) approximateNothing warned you. This class of bug — a number that silently stops being exact — is behind real financial and scientific software failures.
For genuinely large factorials, use an arbitrary-precision library. PHP ships with GMP and BCMath:
function bigFactorial(int $n): string {
$result = gmp_init(1);
for ($i = 2; $i <= $n; $i++) {
$result = gmp_mul($result, $i);
}
return gmp_strval($result);
}
echo bigFactorial(30);
// 265252859812191058636308480000000 — exact, every digitEnable GMP in XAMPP by removing the ; from ;extension=gmp in php.ini, then restart Apache. If GMP is unavailable, bcmul() from BCMath does the same job with strings.
Note the return type is string. Once a number exceeds the native integer range, a string is the only way to hold it exactly.
#Put it in a web page
Combining this with the form lesson gives a real, complete mini-app:
<?php
declare(strict_types=1);
function factorial(int $n): string {
if (function_exists('gmp_init')) {
$result = gmp_init(1);
for ($i = 2; $i <= $n; $i++) {
$result = gmp_mul($result, $i);
}
return gmp_strval($result);
}
// Fall back to native integers, capped where they stay exact
$result = 1;
for ($i = 2; $i <= $n; $i++) {
$result *= $i;
}
return (string) $result;
}
function e(string $v): string {
return htmlspecialchars($v, ENT_QUOTES, 'UTF-8');
}
$input = '';
$answer = null;
$error = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$input = trim($_POST['number'] ?? '');
if ($input === '') {
$error = 'Please enter a number.';
} elseif (!ctype_digit($input)) {
$error = 'Whole numbers of zero or more only.';
} elseif ((int) $input > 500) {
$error = 'Let us keep it under 500 so the page still loads.';
} else {
$answer = factorial((int) $input);
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Factorial Calculator</title>
</head>
<body>
<h1>Factorial Calculator</h1>
<form method="post" action="">
<label for="number">n =</label>
<input type="text" id="number" name="number" value="<?= e($input) ?>" autofocus>
<button type="submit">Calculate</button>
</form>
<?php if ($error !== null): ?>
<p class="error"><?= e($error) ?></p>
<?php elseif ($answer !== null): ?>
<p><strong><?= e($input) ?>!</strong> = <?= e($answer) ?></p>
<p><?= strlen($answer) ?> digits.</p>
<?php endif; ?>
</body>
</html>The validation chain is the interesting part. ctype_digit() is a better check than is_numeric() here because it rejects -3, 3.7 and 1e5 — all of which are numeric but none of which are valid input for factorial. It works on the raw string, before any conversion, which is exactly where input checks belong.
The upper bound of 500 is not arbitrary caution. 1000! has 2,568 digits and is fine; but user input that drives an unbounded loop is a denial-of-service waiting to happen. Every input that controls how much work the server does needs a ceiling.
Check yourself
4 questions on what you just read. No score is kept — it is only here to catch the bits that did not stick.
-
1What is
0!?Show the answer
A.
1— by definition.Starting
$resultat1and looping from2handles0!and1!for free. That is what makes the loop version the clean one. -
2
factorial(21)returns5.1090942171709E+19with no warning. What happened?Show the answer
A. The result exceeded
PHP_INT_MAX, so PHP silently converted it to an imprecise float.20!is the last exact one on a 64-bit build. Past that you need GMP or BCMath — and the return type becomes a string. -
3Which check correctly rejects
-3,3.7and1e5for a factorial input?Show the answer
A.
ctype_digit($input)on the raw string.All three of those inputs are numeric; none are valid here. Validate the raw string before converting, and always bound anything that drives a loop.
-
4Every recursive function needs which two things?
Show the answer
A. A base case, and a recursive case that shrinks the problem.
Miss the base case and you exhaust the stack. Fail to shrink the problem and you get the same result more slowly.
#Key takeaways
0!is1. Starting$resultat1and looping from2handles every base case for free.- Recursion needs a base case and a shrinking problem. Prefer loops unless the data itself is nested.
- Use
declare(strict_types=1)and typed signatures — bugs get caught at the boundary. - PHP integers silently become imprecise floats past
20!. Use GMP or BCMath for exact big numbers. - Validate the raw string with
ctype_digit()before converting, and always bound user-driven loops.
Next: input validation as a discipline, on both sides of the wire.
Finished this one?
The challenge above has no posted solution — that is deliberate. Get it working, then come back for the next lesson.