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 like array_map, array_filter, and usort — 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 a static closure.
  • 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 Closure or callable in function signatures so callers know what they're getting.

Practice Exercises

  • Write a function makeMultiplier(int $factor): Closure that 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_map with 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): Closure that returns a closure accepting a message and echoing "[$prefix] $message". Then explain, in a comment, whether $prefix needs 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 Closure class.
  • Closures do not see outer variables automatically — you must import them with a use clause.
  • 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 function closures never do.
  • Closure::bind() and bindTo() let you attach or change a closure's $this and 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.