Closures
What Is a Closure?
In simple terms: A closure is a function that remembers values from the place where it was created. Even after that surrounding code has finished running, the closure still has access to the values it captured. Technically: In PHP, every anonymous function and arrow function is an instance of the built-in final class Closure. A closure can capture variables from its enclosing scope (explicitly with use for anonymous functions, automatically for arrow functions) and, when created inside a class, is automatically bound to $this. The Closure class also provides methods — bind(), bindTo(), call(), and fromCallable() — to control what a closure is bound to.
Why Closures Matter
Closures let you package behavior together with data:
- A function that produces other, pre-configured functions (factories)
- Callbacks that need extra information, such as a filter that knows the maximum price
- Event listeners and middleware that remember configuration
- Small pieces of private state without creating a class The basics of anonymous functions and
useare covered in Anonymous Functions. This lesson focuses on what makes closures powerful: capturing state, binding, and practical patterns.
Closures Capture Their Environment
<?php
function makeGreeter(string $greeting): Closure
{
return function (string $name) use ($greeting): string {
return "$greeting, $name!";
};
}
$hello = makeGreeter('Hello');
$vanakkam = makeGreeter('Vanakkam');
echo $hello('Asha') . "\n";
echo $vanakkam('Asha') . "\n";
Output
Hello, Asha!
Vanakkam, Asha!
How It Works
- Each call to
makeGreeter()creates a new closure. - The closure captures the current value of
$greetingwithuse. - When
makeGreeter()returns, its local variables are gone — but each closure keeps its own captured copy. $helloand$vanakkamare two separate closures with different captured values. This is the essence of a closure: a function plus the variables it captured.
Closures With Private State
Capturing a variable by reference lets a closure keep state that changes between calls:
<?php
function makeCounter(int $start = 0): Closure
{
$count = $start;
return function () use (&$count): int {
return ++$count;
};
}
$visitors = makeCounter();
$downloads = makeCounter(100);
echo $visitors() . "\n";
echo $visitors() . "\n";
echo $downloads() . "\n";
echo $visitors() . "\n";
Output
1
2
101
3
How It Works
$countis a local variable ofmakeCounter(), captured by reference (&$count).- Each call to the returned closure increments that captured variable, so the value persists between calls.
$visitorsand$downloadseach have their own$count, because eachmakeCounter()call creates a new variable.- No code outside the closure can read or change
$countdirectly — the state is private.
Practical Pattern: Memoization Wrapper
A closure can wrap another function and cache its results:
<?php
function memoize(callable $function): Closure
{
$cache = [];
return function (int $input) use (&$cache, $function): mixed {
if (!array_key_exists($input, $cache)) {
echo "(computing $input)\n";
$cache[$input] = $function($input);
}
return $cache[$input];
};
}
$square = memoize(fn(int $n): int => $n * $n);
echo $square(4) . "\n";
echo $square(4) . "\n";
echo $square(5) . "\n";
Output
(computing 4)
16
16
(computing 5)
25
How It Works
memoize()receives a function and returns a new closure that captures both the function ($function, by value) and a cache array ($cache, by reference).- The first call with
4computes and stores the result; the second call returns the cached value without recomputing. - The wrapped function never needs to know about caching. This is useful for expensive operations, such as reading configuration or calling a slow API with the same arguments during one request.
Closures Inside Classes: Automatic $this
When a closure is created inside a class method, it is automatically bound to the object, so it can use $this — including private properties:
<?php
class Cart
{
private array $items = [];
private float $discountPercent = 10;
public function add(string $name, float $price): void
{
$this->items[] = ['name' => $name, 'price' => $price];
}
public function discountedLabels(): array
{
return array_map(function (array $item): string {
$price = $item['price'] * (1 - $this->discountPercent / 100);
return $item['name'] . ': ₹' . number_format($price, 2);
}, $this->items);
}
}
$cart = new Cart();
$cart->add('Desk Lamp', 1299);
$cart->add('Gel Pen', 20);
echo implode("\n", $cart->discountedLabels()) . "\n";
Output
Desk Lamp: ₹1,169.10
Gel Pen: ₹18.00
The callback passed to array_map() reads $this->discountPercent without any use clause, because closures created in a method are bound to the current object. Classes are covered in the PHP OOP section.
Static Closures
A closure declared static is not bound to $this:
$format = static fn(float $price): string => '₹' . number_format($price, 2);
Use static when a closure does not need the object. It documents that independence and prevents the closure from keeping the object alive in memory longer than necessary.
Binding Closures: call(), bind(), and bindTo()
A closure's binding — which object $this refers to and which class scope it runs in — can be changed. This is mainly used by frameworks and testing tools.
Closure::call() (PHP 7.0+)
call() temporarily binds a closure to an object and calls it immediately:
<?php
class Wallet
{
private int $balance = 500;
}
$readBalance = function (): int {
return $this->balance;
};
echo $readBalance->call(new Wallet()) . "\n";
Output
500
Bound to the Wallet object and running in its class scope, the closure can read the private $balance. This shows how binding works, but it also bypasses encapsulation — in application code, access private data through public methods instead.
bindTo() and Closure::bind()
bindTo() returns a new closure bound to a given object (and optionally a class scope), without calling it:
<?php
class Counter
{
private int $value = 0;
}
$increment = function (int $by): int {
return $this->value += $by;
};
$counter = new Counter();
$boundIncrement = $increment->bindTo($counter, Counter::class);
echo $boundIncrement(5) . "\n";
echo $boundIncrement(3) . "\n";
Output
5
8
Closure::bind($closure, $object, $scope) is the static equivalent of $closure->bindTo($object, $scope). These methods power features such as framework "macros" that add methods to existing classes.
Creating Closures From Existing Functions
Any callable can be turned into a Closure object:
| Technique | Example | Since |
|---|---|---|
| First-class callable syntax | strtoupper(...) | PHP 8.1 |
Closure::fromCallable() | Closure::fromCallable('strtoupper') | PHP 7.1 |
<?php
$upper = strtoupper(...);
$lower = Closure::fromCallable('strtolower');
echo $upper('php') . " / " . $lower('PHP') . "\n";
var_dump($upper instanceof Closure);
Output
PHP / php
bool(true)
First-class callable syntax is the modern form; see First-class Callable Syntax.
Practical Example: A Simple Event System
Closures are ideal for listeners: small pieces of behavior registered to run when something happens, each remembering its own configuration.
<?php
function dispatch(array $listeners, string $event, array $payload): void
{
foreach ($listeners[$event] ?? [] as $listener) {
echo $listener($payload) . "\n";
}
}
$adminEmail = 'orders@gocourse.in';
$pointsPerHundred = 3;
$listeners = [
'order.placed' => [
function (array $order) use ($adminEmail): string {
return "Email to $adminEmail: new order #{$order['id']}";
},
fn(array $order): string => sprintf(
'Award %d points to %s',
intdiv($order['total'], 100) * $pointsPerHundred,
$order['customer'],
),
],
'order.cancelled' => [
fn(array $order): string => "Restock items from order #{$order['id']}",
],
];
dispatch($listeners, 'order.placed', ['id' => 501, 'customer' => 'Asha', 'total' => 1580]);
dispatch($listeners, 'order.cancelled', ['id' => 498, 'customer' => 'Bala', 'total' => 300]);
dispatch($listeners, 'order.refunded', ['id' => 470, 'customer' => 'Charu', 'total' => 900]);
Output
Email to orders@gocourse.in: new order #501
Award 45 points to Asha
Restock items from order #498
Step-by-Step Explanation
$listenersmaps event names to lists of closures.- The first listener captures
$adminEmailwithuse; the second (an arrow function) captures$pointsPerHundredautomatically. dispatch()runs every listener registered for an event, passing the event data. Events without listeners (order.refunded) simply do nothing, thanks to?? [].intdiv(1580, 100) * 3awards 15 × 3 = 45 points. New behavior is added by registering another closure —dispatch()never changes. Frameworks such as Laravel and Symfony use the same idea in their event systems.
Common Mistakes
Capturing by Value When State Must Persist
Effect: A counter closure always returns the same number, because it works on a copy. Fix: Capture by reference (use (&$count)) for intentional shared state.
Capturing by Reference Unnecessarily
Effect: Outside code changes the variable later, unexpectedly changing the closure's behavior. Fix: Capture by value unless sharing is required.
Using $this in a Static Closure or Outside a Class
Effect: Error: Using $this when not in object context. Fix: Remove static, bind the closure, or pass the needed object as a parameter.
Using Binding to Bypass Encapsulation
Effect: Code depends on private internals that may change. Fix: Reserve call()/bind() for framework-level code and tests; use public methods in application code.
Best Practices
- Use closures for small, configured behaviors: callbacks, factories, listeners, and wrappers.
- Capture only what the closure needs, by value by default.
- Declare closures
staticwhen they do not need$this. - Prefer arrow functions for single-expression closures and first-class callable syntax for wrapping existing functions.
- Move complex logic from closures into named functions or classes so it can be tested and reused. Official reference: https://www.php.net/manual/en/class.closure.php