PHP Closures
A closure in PHP is an anonymous function that can “remember” variables from the scope in which it was created, even after that scope has finished executing. Closures are the backbone of modern PHP callbacks, array-processing functions like array_map and usort, and patterns like factories and event handlers. Understanding exactly how and when variables are captured is one of the trickiest but most useful things to master in PHP.
Overview / How Closures Work
Under the hood, an anonymous function you write with the function keyword (or the shorter fn arrow-function syntax) is not just a block of code — PHP compiles it into an instance of the built-in Closure class. This object carries around its own bytecode plus a snapshot of any outer variables it was told to capture. That is why you can create a closure inside one function, return it, and call it later somewhere else entirely: the captured variables travel with the object.
By default, a closure has no access to variables from the enclosing scope, even though it is textually written inside that scope. This is different from JavaScript, where inner functions automatically see outer variables. In PHP you must explicitly list which outer variables to import using the use clause. You can import a variable by value (a copy is taken at the moment the closure is created) or by reference (the closure shares the exact same variable storage as the outer scope, so later changes in either place are visible in both).
PHP 7.4 added arrow functions (fn(...) => expression), which automatically capture any variable used in the expression by value — you never write a use clause for them. They are limited to a single expression whose result is implicitly returned, which makes them perfect for short callbacks passed to array functions.
Closures are also aware of object context. When you define a closure inside a method (not as a static function), it automatically captures $this from the enclosing object, letting it call methods and read properties on that object. You can also manually bind or rebind a closure’s $this and visibility scope with Closure::bind() or the instance method bindTo(), which is how libraries implement things like macro systems.
Syntax
$closure = function(parameters) use ($byValue, &$byReference): returnType {
// function body, can read $byValue and $byReference
return expression;
};
$arrow = fn(parameters): returnType => expression; // auto-captures by value
| Part | Meaning |
|---|---|
function(...) { ... } |
Defines an anonymous function; produces a Closure object |
use ($var) |
Imports $var from the enclosing scope by value (a snapshot taken at creation time) |
use (&$var) |
Imports $var by reference; the closure and outer scope share the same storage |
static function(...) {} |
An anonymous function that never binds $this, even inside a class method |
fn(...) => expr |
Arrow function; implicitly returns expr and auto-imports outer variables by value |
Closure::bind($c, $obj, $scope) |
Returns a new closure with $this bound to $obj and visibility scoped to $scope |
Examples
Example 1: Capturing a variable by value
<?php
$name = "Ada";
$greet = function() use ($name) {
echo "Hello, $name!" . PHP_EOL;
};
$greet();
$name = "Grace";
$greet();
Output:
Hello, Ada!
Hello, Ada!
The closure takes a copy of $name at the moment it is defined. Reassigning $name afterward has no effect on the closure, because the closure is holding its own private copy, not a link back to the original variable.
Example 2: Capturing by reference to build a stateful counter
<?php
function makeCounter(): Closure
{
$count = 0;
return function() use (&$count) {
$count++;
return $count;
};
}
$counter = makeCounter();
echo $counter() . PHP_EOL;
echo $counter() . PHP_EOL;
echo $counter() . PHP_EOL;
$counter2 = makeCounter();
echo $counter2() . PHP_EOL;
Output:
1
2
3
1
Because $count is imported by reference, the returned closure keeps a persistent link to the same storage across calls, letting it accumulate state — this is a common way to build private, encapsulated state without a class. Each call to makeCounter() creates a brand new $count, so $counter2 starts fresh at 1.
Example 3: Closures as callbacks with array functions and arrow functions
<?php
$numbers = [1, 2, 3, 4, 5];
$threshold = 3;
$aboveThreshold = array_filter($numbers, fn($n) => $n > $threshold);
$squared = array_map(fn($n) => $n * $n, $numbers);
print_r(array_values($aboveThreshold));
print_r($squared);
Output:
Array
(
[0] => 4
[1] => 5
)
Array
(
[0] => 1
[1] => 4
[2] => 9
[3] => 16
[4] => 25
)
The arrow function fn($n) => $n > $threshold automatically captures $threshold from the surrounding scope without a use clause. array_filter preserves original keys, which is why array_values() is used to reindex the result from 0.
Under the Hood: Binding $this
A closure created outside of any object has no owning object. You can attach one after the fact with Closure::bind(), which also lets you choose which class’s private/protected members it is allowed to see:
<?php
class Counter
{
private int $value = 0;
}
$increment = function(int $by = 1): int {
$this->value += $by;
return $this->value;
};
$bound = Closure::bind($increment, new Counter(), Counter::class);
echo $bound() . PHP_EOL;
echo $bound(5) . PHP_EOL;
Output:
1
6
The third argument to Closure::bind() sets the scope, which is what grants access to the private property $value even though the closure was defined completely outside the Counter class. Without specifying the scope, the closure could only reach public members.
Common Mistakes
Mistake 1: Capturing a loop variable by reference
Wrong code — every closure ends up sharing the same reference, so they all see the loop’s final value:
<?php
$callbacks = [];
for ($i = 1; $i <= 3; $i++) {
$callbacks[] = function() use (&$i) {
echo $i . PHP_EOL;
};
}
foreach ($callbacks as $callback) {
$callback();
}
Output:
4
4
4
Because &$i imports $i by reference, all three closures point at the exact same storage location, which holds 4 once the loop finishes. Fix it by capturing $i by value instead, so each closure gets its own independent snapshot from that iteration:
<?php
$callbacks = [];
for ($i = 1; $i <= 3; $i++) {
$callbacks[] = function() use ($i) {
echo $i . PHP_EOL;
};
}
foreach ($callbacks as $callback) {
$callback();
}
Output:
1
2
3
Mistake 2: Expecting a value-captured variable to update later
Wrong code — the developer assumes changing $multiplier before invoking the closure will change its behavior:
<?php
$multiplier = 2;
$double = fn($n) => $n * $multiplier;
$multiplier = 10;
echo $double(5) . PHP_EOL;
Output:
10
The arrow function captured $multiplier's value (2) at the moment it was defined, so the later reassignment to 10 is invisible to it — the result is 5 * 2 = 10, not the 50 a reader might expect. Arrow functions cannot capture by reference, so if you genuinely need the closure to track a variable's current value, use a full closure with an explicit reference import:
<?php
$multiplier = 2;
$double = function($n) use (&$multiplier) {
return $n * $multiplier;
};
$multiplier = 10;
echo $double(5) . PHP_EOL;
Output:
50
Best Practices
- Default to capturing by value; only use
&when the closure genuinely needs to observe or mutate the outer variable. - Prefer arrow functions (
fn() => ...) for short, single-expression callbacks passed to functions likearray_map,array_filter, andusort— they are shorter and capture automatically. - Use a full
function(...) use (...) { ... }closure when the body needs multiple statements, or when you specifically need reference capture or astaticclosure. - Declare parameter and return types on closures just as you would on named functions; it catches bugs early and documents intent.
- Use
static function(...) {}for closures that don't need$this, to avoid accidentally holding an unnecessary reference to the enclosing object. - When a closure is meant to hold private state (like a counter or accumulator), capture the state variable by reference and keep it out of any public API.
- Type-hint a returned closure as
Closureorcallablein function signatures so callers know what they're getting.
Practice Exercises
- Write a function
makeMultiplier(int $factor): Closurethat returns a closure which multiplies any number passed to it by$factor. Create two multipliers with different factors and call both to confirm they behave independently. - Given an array of names, use
array_mapwith an arrow function to produce a new array where each name is uppercased and prefixed with "Hello, ". Expected output for["amy", "bo"]is["Hello, AMY", "Hello, BO"]. - Write a closure-based
makeLogger(string $prefix): Closurethat returns a closure accepting a message and echoing"[$prefix] $message". Then explain, in a comment, whether$prefixneeds to be captured by value or by reference, and why.
Summary
- A closure is an anonymous function compiled into an instance of the built-in
Closureclass. - Closures do not see outer variables automatically — you must import them with a
useclause. use ($var)captures a snapshot by value;use (&$var)shares the same storage by reference.- Arrow functions (
fn() => expr) auto-capture outer variables by value and always return their single expression. - Closures defined inside instance methods automatically capture
$this;static functionclosures never do. Closure::bind()andbindTo()let you attach or change a closure's$thisand visibility scope after creation.- Reference capture inside loops is a classic source of bugs — all closures end up sharing the loop variable's final value.
