0

Closures — anonymous functions and lexical scope

Intermediate5 min read·php-06-006
interviewcompare

Concept

A closure in PHP is an anonymous function that can capture variables from the enclosing scope. Closures are instances of the built-in Closure class. Unlike named functions, which have a fixed global scope, closures carry their own lexical environment — the subset of outer-scope variables they capture via the use clause.

Lexical scope means the captured variable is resolved at closure definition time, not call time. When you write use ($discount), PHP captures the current value of $discount. If $discount changes after the closure is defined, the closure still holds the original value — unless you capture by reference with use (&$discount). This distinction is the source of the classic "loop closure bug": capturing a loop variable by value captures its current value per iteration; capturing by reference captures a reference to the same variable, which will hold the final loop value by the time any closure executes.

Closures do not automatically access $this. Inside a closure defined outside a class, $this is undefined. Inside a closure defined within a class method, PHP automatically binds $this to the current object — the closure inherits the object context. This automatic binding is how Laravel's route closures in web.php can reference $request, and how Eloquent model closures access properties.

The closure's captured environment is stored in the Closure object itself. Each closure definition creates a fresh object with its own copy of the captured values (unless captured by reference). This has a memory implication: closures that capture large objects or arrays hold references to them, preventing garbage collection for as long as the closure exists.

Capture typeSyntaxBehaviour
By valueuse ($x)Snapshot of $x at definition
By referenceuse (&$x)Alias to outer $x; changes propagate both ways
Auto $thisImplicit in class method bodyBound to the current object

Code Example

php
<?php
declare(strict_types=1);

// Basic closure — capture by value
$multiplier = 3;
$triple = function (int $n) use ($multiplier): int {
    return $n * $multiplier;
};

$multiplier = 99; // change after definition — has no effect
echo $triple(5) . PHP_EOL; // 15 — captured 3, not 99

// Capture by reference — mutable accumulator
function makeCounter(): Closure
{
    $count = 0;
    return function () use (&$count): int {
        return ++$count;
    };
}

$counter = makeCounter();
echo $counter() . PHP_EOL; // 1
echo $counter() . PHP_EOL; // 2
echo $counter() . PHP_EOL; // 3

// Closure within a class — automatic $this binding
class EventDispatcher
{
    private array $listeners = [];

    public function listen(string $event, Closure $callback): void
    {
        $this->listeners[$event][] = $callback;
    }

    public function dispatch(string $event, mixed $payload = null): void
    {
        foreach ($this->listeners[$event] ?? [] as $listener) {
            $listener($payload);
        }
    }

    public function listenWithContext(string $event): void
    {
        // $this is automatically available inside this closure
        $this->listen($event, function (mixed $payload): void {
            echo get_class($this) . ' handled: ' . json_encode($payload) . PHP_EOL;
        });
    }
}

$dispatcher = new EventDispatcher();
$dispatcher->listenWithContext('order.placed');
$dispatcher->dispatch('order.placed', ['id' => 42]);
// EventDispatcher handled: {"id":42}

// Classic loop closure bug — avoid with by-value capture
$functions = [];
for ($i = 0; $i < 3; $i++) {
    $functions[] = function () use ($i): int { return $i; }; // by-value: correct
}
echo $functions[0]() . PHP_EOL; // 0
echo $functions[1]() . PHP_EOL; // 1
echo $functions[2]() . PHP_EOL; // 2

Interview Q&A

Q: What is the difference between capturing a variable by value and by reference in a closure, and when is each appropriate?

Capturing by value (use ($x)) creates a snapshot of the variable at the time the closure is defined. Later changes to $x in the outer scope do not propagate into the closure, and changes the closure makes to its copy do not propagate outward. This is the safe default for read-only dependencies. Capturing by reference (use (&$x)) establishes an alias: both the outer scope and the closure share the same zval. Changes in either direction are visible to the other. Use reference capture when the closure needs to mutate shared state — counters, accumulators, stateful callbacks. Avoid it when the capture is only for reading, because it prevents COW optimisation and makes the data flow less obvious.


Q: How does $this work inside a closure defined in a class method?

When a closure is created inside a non-static class method, PHP automatically binds the current object ($this) and its class scope to the closure. The closure can access $this->property and call $this->method() as if it were written inline in the method. This automatic binding occurs at definition time and captures the object by reference-handle — the same object is shared. Importantly, if the closure is passed to an external function or stored on another object and called later, it still refers to the original object. If you want a closure to carry no object binding (for security or isolation), use a static closure: static function () { /* no $this */ }.


Q: How does Laravel's service container use closures to defer object construction?

Laravel's Container::bind() accepts a resolver closure as its second argument: $this->app->bind(PaymentGateway::class, function (Application $app) { return new StripeGateway($app->make(Config::class)); }). This closure is stored in the container's $bindings array without being called. Only when the container resolves PaymentGateway::class — via make() or automatic injection — does it invoke the closure, passing itself as $app. This lazy-instantiation pattern means expensive objects (database connections, HTTP clients) are never constructed unless the current request actually needs them. The closure's lexical capture of $app via the parameter (not use) ensures the correct container instance is used even if the binding is registered during bootstrap and resolved later under a different context.