PHP Functions: Parameters, Closures, Arrow Functions, and Generators
Master PHP function fundamentals — parameters, defaults, variadics, named arguments, and references — plus the more advanced tools: recursion, closures, arrow functions, callables, first-class callable syntax, and generators built with yield.
Functions are the basic unit of reusable logic in PHP, and the language has steadily added expressive power to them over the years: default and named parameters make call sites self-documenting, closures and arrow functions make passing behavior around as a value natural, and generators let a function produce a sequence of values lazily, one at a time, without building the whole sequence in memory first. This lesson covers the full range, from a plain function declaration to the tools that make modern PHP feel genuinely functional when it needs to.
Parameters, Defaults, Variadics, and Named Arguments
A basic function declares typed parameters and a return type: function greet(string $name, string $greeting = 'Hello'): string { return "$greeting, $name!"; } — the $greeting = 'Hello' gives that parameter a default value, making it optional at the call site; PHP requires all parameters with defaults to come after any required parameters (with one exception — named arguments, covered next, relax this ordering restriction). Type declarations on parameters and return values are enforced at runtime (a TypeError is thrown on mismatch, unless the file/function opts into weak typing coercion rules) and are one of the most valuable additions to modern PHP for catching bugs early and documenting intent.
Variadic parameters, written ...$args, let a function accept any number of trailing arguments, which are collected into an array inside the function: function sum(int ...$numbers): int { return array_sum($numbers); } lets you call sum(1, 2, 3, 4) with any argument count. A variadic parameter must be the last one declared, and you can combine it with the spread operator at the call site — sum(...[1, 2, 3]) — to pass an existing array's elements as individual variadic arguments.
Named arguments (PHP 8.0+) let you pass arguments by parameter name instead of position: greet(greeting: 'Hi', name: 'Ada') — order no longer matters, and it becomes self-documenting at complex call sites, especially for functions with several optional boolean-ish parameters where positional calls become unreadable (str_pad($str, 10, '0', STR_PAD_LEFT) versus the clearer str_pad($str, length: 10, pad_string: '0', pad_type: STR_PAD_LEFT)). Named and positional arguments can be mixed, as long as all positional arguments come first.
• function greet(string $name, string $greeting = 'Hello'): string { ... } — default parameter must follow required ones positionally. • function sum(int ...$nums): int { return array_sum($nums); } — variadic, collects extra args into an array. • sum(...[1, 2, 3, 4]); — spreading an existing array into variadic arguments. • array_slice($arr, offset: 2, length: 3); — named arguments improve readability at complex call sites. • A TypeError is thrown at runtime if a strictly-typed argument doesn't match its declared type.
References, Recursion, and Callable Values
By default, PHP passes arguments by value — the function receives a copy, and changes inside it don't affect the caller's variable. Prefixing a parameter with & makes it pass by reference instead: function double(int &$n): void { $n *= 2; } mutates the caller's actual variable. This is used sparingly in modern PHP (returning a new value is usually clearer than mutating a parameter), but it's essential for functions like array_walk()'s callback or sort(), which are built around in-place mutation by design.
Recursion is a function calling itself to solve a problem by breaking it into smaller instances of the same problem, always with a base case that stops the recursion: function factorial(int $n): int { return $n <= 1 ? 1 : $n * factorial($n - 1); }. Recursion is elegant for naturally tree-shaped or self-similar problems (traversing nested arrays/directories, quicksort, computing Fibonacci) but every PHP recursive call adds a stack frame, so very deep recursion (tens of thousands of levels) can exhaust PHP's call stack — an iterative loop is usually preferred for simple linear repetition.
A 'callable' in PHP is any value that can be invoked as a function: a string naming a function ('strlen'), a string 'ClassName::method' or array [ClassName::class, 'method'] for a static method, an array [$object, 'method'] for an instance method, a Closure (an anonymous function or arrow function), or any object implementing __invoke(). Functions like array_map(), usort(), and call_user_func() all accept any of these forms, which is what makes PHP's higher-order function library so flexible.
• function increment(int &$n): void { $n++; } $x = 5; increment($x); // $x is now 6 • function fib(int $n): int { return $n <= 1 ? $n : fib($n - 1) + fib($n - 2); } // classic (inefficient) recursion example • array_map('strtoupper', $words); — a plain string naming a built-in function is a valid callable. • array_map([$formatter, 'format'], $items); — [object, 'method'] is a valid instance-method callable. • is_callable($value) checks whether a given value can currently be invoked.
Closures and Arrow Functions
A closure, created with the function () { } anonymous function syntax, can capture variables from its surrounding scope with the use () clause: $multiplier = 3; $triple = function (int $n) use ($multiplier): int { return $n * $multiplier; };. By default, use ($multiplier) captures by value — a snapshot taken at the moment the closure is created, so later changes to $multiplier outside don't affect the closure. Prefixing with & in the use clause (use (&$multiplier)) captures by reference instead, letting the closure see (and even modify) the outer variable's live value — useful for accumulator-style closures that build up state across multiple calls.
Arrow functions (fn, PHP 7.4+) are a compact single-expression alternative: $triple = fn(int $n): int => $n * $multiplier; — critically, arrow functions automatically capture any outer variable they reference by value, with no use clause needed at all, which is why they've become the default choice for short callbacks passed to array_map(), array_filter(), and usort(). The tradeoff is that an arrow function's body must be a single expression (whatever it evaluates to is implicitly returned) — for anything needing multiple statements, a loop, or intermediate variables, you still need a full closure.
Both closures and arrow functions are instances of the built-in Closure class under the hood, which is why you can call methods on them like $fn->bindTo($newThis) to rebind what $this refers to inside a closure created in a class context — a technique occasionally used for things like giving a closure access to a specific object's private state.
• $adder = function (int $x) use ($base) { return $x + $base; }; // captures $base by VALUE at creation time • $counter = function () use (&$count) { return ++$count; }; // captures $count by REFERENCE — sees live updates • $double = fn($n) => $n * 2; // arrow function: implicit return, automatic by-value capture, no use() needed • array_map(fn($p) => $p['price'] * $taxRate, $products); // $taxRate auto-captured from surrounding scope
First-Class Callables and Generators
PHP 8.1 added first-class callable syntax: instead of passing a function's name as a string (array_map('strlen', $words)), you write array_map(strlen(...), $words) — the ... after the function name (with no arguments inside) creates a proper Closure value referencing that function, method, or static method. This is preferred over the string/array-callable forms because it's checked by IDEs and static analysis tools at the point of use (a typo in a string callable name is only caught at runtime), and it works uniformly for free functions (strlen(...)), static methods (Math::square(...)), and instance methods ($formatter->format(...)).
A generator is a function that uses the yield keyword instead of (or alongside) return to produce a sequence of values one at a time, lazily, pausing its execution between each yield and resuming exactly where it left off when the next value is requested. Just writing yield anywhere inside a function automatically makes it a generator — calling it doesn't run the body immediately; it returns a Generator object (itself an Iterator), and the body only actually executes as you iterate (with foreach, for example) or call ->current()/->next() on it.
The key advantage of generators is memory efficiency: function readLargeFile(string $path) { $handle = fopen($path, 'r'); while (($line = fgets($handle)) !== false) { yield $line; } fclose($handle); } can process a multi-gigabyte file one line at a time without ever holding the whole file in memory, unlike a function that reads all lines into an array and returns it. You can also yield $key => $value; for associative generator output, and yield from $otherIterable; to delegate to and flatten another generator or array in place.
• array_map(strtoupper(...), $words); — first-class callable syntax, PHP 8.1+, IDE/static-analysis friendly. • $sq = Calculator::square(...); // first-class callable for a static method • function countUpTo(int $n) { for ($i = 1; $i <= $n; $i++) { yield $i; } } — a generator, lazy and memory-light. • foreach (countUpTo(1000000) as $num) { ... } — iterates one value at a time, never builds a million-element array. • yield from range(1, 3); — delegates to (flattens) another iterable inside a generator.
Functions, closures, arrow functions, and a generator together
Arrow functions (fn) auto-capture outer variables by value with no use() clause — closures need use() and default to by-value unless you write use (&$var). First-class callable syntax name(...) is preferred over string/array callables for IDE support and static analysis. A function becomes a generator the moment it contains a yield anywhere in its body, and only executes as it's iterated.
<?php
// named + default arguments
function formatPrice(float $amount, string $currency = 'USD', int $decimals = 2): string {
return sprintf('%.' . $decimals . 'f %s', $amount, $currency);
}
echo formatPrice(19.999, decimals: 2); // named argument skips over $currency's default cleanly
// closure with explicit capture, vs arrow function's automatic capture
$taxRate = 0.08;
$withTaxClosure = function (float $price) use ($taxRate): float { return $price * (1 + $taxRate); };
$withTaxArrow = fn(float $price): float => $price * (1 + $taxRate);
// first-class callable
$upper = strtoupper(...);
echo $upper('hello'); // HELLO
// generator: lazy sequence
function evenNumbersUpTo(int $max) {
for ($n = 2; $n <= $max; $n += 2) {
yield $n;
}
}
foreach (evenNumbersUpTo(10) as $even) {
echo $even . ' ';
}Code Examples
The arrow function is ideal for a pure, stateless per-element transform inside array_map — it reads $taxRate from the enclosing scope with no ceremony. The closure needs use (&$runningTotal) specifically because it must mutate a variable that persists and accumulates across separate calls, which an arrow function's implicit-return, single-expression design can't do. Expected result: Array ( [0] => 11 [1] => 28.05 [2] => 7.98) 10 35.5 42.75 use (&$runningTotal) — the & makes the closure share and mutate the outer variable, not just read a snapshot of it.
fibonacci() has an infinite while (true) loop, which would hang forever (and eventually exhaust memory) as a normal function trying to return a complete array — but as a generator it's perfectly safe, because each yield pauses execution and hands control back to the foreach loop, which only pulls as many values as it actually consumes before break stops iteration entirely. Expected result: 0 1 1 2 3 5 8 13 21 34 yield $a; — pauses here and hands $a back to the foreach, resuming from this exact point on the next iteration.
<?php
$prices = [10.0, 25.5, 7.25];
$taxRate = 0.10;
// arrow function: automatic by-value capture, concise
$withTax = array_map(fn($p) => round($p * (1 + $taxRate), 2), $prices);
print_r($withTax);
// closure with by-reference capture: builds a running total across calls
$runningTotal = 0.0;
$accumulate = function (float $amount) use (&$runningTotal) {
$runningTotal += $amount;
return $runningTotal;
};
foreach ($prices as $p) {
echo $accumulate($p) . "\n";
}<?php
function fibonacci(): \Generator {
[$a, $b] = [0, 1];
while (true) {
yield $a;
[$a, $b] = [$b, $a + $b];
}
}
$count = 0;
foreach (fibonacci() as $num) {
echo $num . ' ';
if (++$count >= 10) {
break;
}
}Regular function vs. generator execution
A generator's body doesn't execute when called — it executes incrementally, one yield at a time, as something actually asks it for the next value.
REGULAR FUNCTION GENERATOR (contains yield)
call it -> runs to completion call it -> returns a Generator object immediately, body NOT run yet
-> returns ONE value foreach pulls next value ->
(all results built/held in memory) body resumes, runs to next yield, pauses
(only ONE value in memory at a time)Important Rules
Prefer arrow functions (fn) for short, single-expression callbacks; use full closures when you need multiple statements or use (&$var) reference capture.
Prefer first-class callable syntax (name(...)) over string/array callables in PHP 8.1+ for better IDE and static-analysis support.
Reach for a generator (yield) instead of returning a large array when a sequence could be large or produced lazily/infinitely.
Use named arguments to clarify call sites for functions with several optional or same-typed parameters.
Avoid deep unbounded recursion for simple linear iteration — an explicit loop is safer against stack exhaustion.
Where this shows up
Route handlers and service classes lean on named arguments and defaults for readable call sites; array_map/array_filter callbacks are almost always arrow functions in modern codebases; event listeners and deferred callbacks are closures capturing state by reference; and generators are the standard tool for streaming large CSV exports, paginated API results, or database cursors without loading everything into memory at once.
$count = 0; $increment = function () { return ++$count; }; // use() clause missing entirely — $count is undefined inside
$count = 0; $increment = function () use (&$count) { return ++$count; };
A closure does not automatically see outer variables the way an arrow function does — without an explicit use() clause, $count simply isn't visible inside the closure at all, triggering an undefined-variable warning. And because the closure needs to persist and mutate the value across calls, the capture must additionally be by reference (&$count), not by value.
function getAllLogLines(string $path): array { return file($path); } // loads the entire file into memory as an array function getLogLines(string $path): \Generator { $h = fopen($path, 'r'); while (($line = fgets($h)) !== false) { yield $line; } fclose($h); } file() reads the whole file into an array up front, which is fine for small files but can exhaust memory on a large log file. The generator version yields one line at a time, so memory usage stays roughly constant regardless of file size, and a caller can stop iterating early (e.g. with break) without ever reading the rest of the file.
How does an arrow function (fn) differ from a regular closure regarding variable capture from the outer scope? A. Arrow functions cannot access outer variables at all B. Arrow functions require an explicit use() clause just like closures C. Arrow functions automatically capture outer variables by value with no use() clause needed D. Arrow functions always capture outer variables by reference
C. Arrow functions automatically capture outer variables by value with no use() clause needed — Arrow functions automatically capture any variable referenced from the enclosing scope by value, with no use() clause at all — this is exactly why they're the default choice for short callbacks. A full closure needs an explicit use() clause, and use (&$var) is required only when reference capture is specifically needed.
What happens when you call a function whose body contains a yield statement? A. It executes the entire function body immediately and returns the last yielded value B. It returns a Generator object immediately, and the body only executes incrementally as the generator is iterated C. It throws a syntax error unless yield is the first statement D. It behaves identically to a function using return
B. It returns a Generator object immediately, and the body only executes incrementally as the generator is iterated — A function containing yield is a generator: calling it does not run the body — it immediately returns a Generator object, and the body executes lazily, one segment at a time, pausing at each yield until the next value is requested (e.g., by a foreach loop).
What does the first-class callable syntax strlen(...) produce? A. A string containing 'strlen' B. An array [null, 'strlen'] C. A Closure object referencing the strlen function, usable anywhere a callable is expected D. It immediately calls strlen() with no arguments
C. A Closure object referencing the strlen function, usable anywhere a callable is expected — The (...) syntax (PHP 8.1+) creates a proper Closure value pointing at the named function, method, or static method — preferred over passing the function name as a string because it's checked by IDEs and static analysis at the point of use.
Write a generator function batches(array $items, int $size): Generator that yields successive chunks (sub-arrays) of at most $size items from $items, without building the whole list of chunks in memory up front (do not just wrap array_chunk() and yield from it — slice manually inside the loop).
function batches(array $items, int $size): Generator { $total = count($items); for ($i = 0; $i < $total; $i += $size) { yield array_slice($items, $i, $size); } }
Loop with a step of $size using a for loop over indices, slicing out each chunk with array_slice($items, $i, $size) and yielding it before moving to the next index.