Closures & Callables
In PHP you can treat a function like a value: store it in a variable, put it in an array, pass it to another function, or return it from one. Anything PHP can call in this way is a callable. Callables power sorting with usort(), event systems, routers and middleware in every framework.
You met closures, arrow functions and the strlen(...) syntax in Functions. This lesson goes further: every kind of callable PHP accepts, how closures remember values, the difference between capturing by value and by reference, turning methods into closures, and binding $this to a closure. It ends with a small event system that accepts any callable.
What counts as a callable?
PHP accepts several shapes of callable. Some are real Closure objects, but others are just strings or arrays that name a function or method:
<?php
declare(strict_types=1);
final class Gst
{
public function __construct(private int $rate) {}
public function add(int $price): int
{
return $price + intdiv($price * $this->rate, 100);
}
public static function label(int $price): string
{
return '₹' . number_format($price);
}
}
$gst = new Gst(18);
$callables = [
'strtoupper', // a function name as a string
fn(string $s): string => "[$s]", // an arrow function
[$gst, 'add'], // [object, method name]
[Gst::class, 'label'], // [class name, static method]
'Gst::label', // "Class::method" string
];
foreach ($callables as $c) {
echo get_debug_type($c), ' ', is_callable($c) ? 'callable' : 'NOT callable', "\n";
}
echo $callables[0]('kochi'), "\n";
echo $callables[2](1000), "\n";
echo call_user_func($callables[4], 125000), "\n";
var_dump(is_callable('strtouper')); // a typo is just a stringstring callable Closure callable array callable array callable string callable KOCHI 1180 ₹125,000 bool(false)
You can call any callable by adding brackets after the variable, as in $callables[0]('kochi'). The function call_user_func() does the same job and appears often in older code. is_callable() checks whether a value can be called, and a parameter typed callable makes PHP check it for you.
Notice the last line. The string 'strtouper' has a typo, but it is still a perfectly normal string. PHP only finds the mistake when it tries to call it. That is the main weakness of string and array callables, and the reason modern code prefers real closures.
Closures remember their surroundings
A closure is an object of the built-in class Closure. When you create one inside a function, it keeps the variables it captured even after that function has returned. This lets you write factory functions that build customised functions:
<?php
declare(strict_types=1);
function makeDiscount(int $percent): Closure
{
return fn(int $price): int => $price - intdiv($price * $percent, 100);
}
$diwaliSale = makeDiscount(20);
$studentOffer = makeDiscount(10);
echo $diwaliSale(1500), "\n";
echo $studentOffer(1500), "\n";
function makeCounter(): Closure
{
$count = 0;
return function () use (&$count): int {
return ++$count;
};
}
$nextToken = makeCounter();
echo $nextToken(), ' ', $nextToken(), ' ', $nextToken(), "\n";
$other = makeCounter();
echo $other(), "\n";1200 1350 1 2 3 1
Each call to makeDiscount() creates a separate closure with its own $percent. The counter captures $count by reference with &, so every call changes the same variable and the closure keeps state between calls, like a token machine at a bank. A second counter starts from 1 again, because it has its own $count.
By value or by reference
The capture rules decide whether a closure can change an outside variable:
| Form | How it captures | Can it change the outside variable? |
|---|---|---|
function () use ($x) | Copies $x when the closure is created | No |
function () use (&$x) | Shares the real $x | Yes |
fn() => $x | Copies every outside variable it uses, automatically | No |
<?php
declare(strict_types=1);
$runs = 0;
$addFour = fn() => $runs += 4; // works on a copy of $runs
$addFour();
echo "After arrow function: $runs\n";
$addSix = function () use (&$runs): void {
$runs += 6; // works on the real $runs
};
$addSix();
echo "After closure with &: $runs\n";
$runs = 100;
$show = fn(): string => "Score: $runs"; // captured now, by value
$runs = 200;
echo $show(), "\n";After arrow function: 0 After closure with &: 6 Score: 100
Arrow functions are perfect for short callbacks that read outside values. When a callback must change something outside, use a full closure with use (&$x), or better, return a value and let the caller decide.
First-class callables for methods
The name(...) syntax from PHP 8.1 works for methods and static methods as well as functions. It turns them into real Closure objects, so a typo becomes an immediate error instead of a hidden bug:
<?php
declare(strict_types=1);
final class Cart
{
private array $prices = [];
public function add(int $price): void
{
$this->prices[] = $price;
}
public function total(): int
{
return array_sum($this->prices);
}
public static function format(int $amount): string
{
return '₹' . number_format($amount);
}
}
$cart = new Cart();
$addToCart = $cart->add(...); // a Closure bound to $cart
$fmt = Cart::format(...); // a static method
$len = strlen(...); // a built-in function
foreach ([499, 1299, 250] as $price) {
$addToCart($price);
}
echo $fmt($cart->total()), "\n";
echo $len('Rohtak'), "\n";
echo get_debug_type($addToCart), "\n";
// Older equivalent from PHP 7.1
$old = Closure::fromCallable([$cart, 'total']);
echo $old(), "\n";₹2,048 6 Closure 2048
$cart->add(...) remembers the object, so calling $addToCart(499) adds to that same cart. Before PHP 8.1, the way to get a closure from a method was Closure::fromCallable([$cart, 'total']). It still works, and you will see it in older libraries, but the (...) form is shorter and easier for editors to check.
Binding $this to a closure
A closure written inside a class method automatically gets that object as $this. You can also attach a closure to an object later. Closure::bind($closure, $object, $scope) returns a copy of the closure whose $this is $object and which may see the private members of the class $scope. $closure->call($object) binds and runs in one step:
<?php
declare(strict_types=1);
final class Wallet
{
private int $balance = 500;
public function __construct(private string $owner) {}
}
// Not written inside Wallet, yet it uses $this->balance
$peek = function (): string {
return "{$this->owner} has ₹{$this->balance}";
};
$wallet = new Wallet('Rohan');
// bind(closure, new $this, class scope) returns a new closure
$bound = Closure::bind($peek, $wallet, Wallet::class);
echo $bound(), "\n";
// call() binds and runs in one step
echo $peek->call(new Wallet('Isha')), "\n";
try {
$peek(); // no $this at all
} catch (Error $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}Rohan has ₹500 Isha has ₹500 Error: Using $this when not in object context
Binding lets code reach private properties, so treat it as a power tool. Frameworks and testing libraries use it; in your own application code, a public method is almost always the better answer. The method $closure->bindTo($object, $scope) is the object-style version of Closure::bind().
If a closure inside a class does not need $this, mark it static: static fn(int $a, int $b): int => $a <=> $b. A static closure cannot be bound to an object and does not keep the object alive in memory.
Advanced: an event system that accepts any callable
Here is a small event bus: parts of an app register listeners for an event name, and when the event happens, every listener runs. The on() method accepts any callable and normalises it into a Closure with $listener(...), so the rest of the class only deals with one type:
<?php
declare(strict_types=1);
final class EventBus
{
/** @var array<string, list<Closure>> */
private array $listeners = [];
public function on(string $event, callable $listener): void
{
// $listener(...) turns any callable into a Closure
$this->listeners[$event][] = $listener(...);
}
public function emit(string $event, mixed ...$args): int
{
$called = 0;
foreach ($this->listeners[$event] ?? [] as $listener) {
$listener(...$args);
$called++;
}
return $called;
}
}
final class Sms
{
public function orderPlaced(string $id, int $amount): void
{
echo "SMS: order $id of ₹$amount confirmed\n";
}
}
$log = [];
$bus = new EventBus();
$bus->on('order.placed', function (string $id, int $amount) use (&$log): void {
$log[] = "$id:$amount";
});
$bus->on('order.placed', [new Sms(), 'orderPlaced']);
$bus->on('order.placed', static function (string $id): void {
echo "Packing slip printed for $id\n";
});
$count = $bus->emit('order.placed', 'ORD-77', 1299);
echo "$count listeners ran; log = ", implode(', ', $log), "\n";
echo $bus->emit('order.cancelled', 'ORD-77'), " listeners for cancel\n";SMS: order ORD-77 of ₹1299 confirmed Packing slip printed for ORD-77 3 listeners ran; log = ORD-77:1299 0 listeners for cancel
Three different kinds of callable were registered: a closure that captures $log by reference, an array callable pointing at a method, and a static closure. The third listener takes only one parameter; PHP simply ignores the extra argument for user-defined functions. mixed ...$args collects the arguments and $listener(...$args) spreads them out again.
Common mistakes
- Expecting
fnto change an outside variable. Arrow functions capture by value. Usefunction () use (&$x)when you really need to change$x. - Forgetting
use. A normal closure cannot see outside variables at all withoutuse; you get an "Undefined variable" warning. - Typos in string callables.
'strtouper'fails only when called. Preferstrtoupper(...). - Typing a property as
callable. PHP does not allowcallableas a property type. UseClosureand convert with(...). - Using
bind()to dodge visibility in normal code. If you need private data, add a method to the class.
Key points
- A callable can be a closure, an arrow function, a function name string, an
[object, 'method']or[Class::class, 'method']array, or a'Class::method'string. - Closures are
Closureobjects that remember captured variables, which makes factory functions and counters possible. use ($x)andfncapture by value;use (&$x)shares the real variable.$obj->method(...),Class::method(...)and$callable(...)create closures;Closure::fromCallable()is the older way.Closure::bind(),bindTo()andcall()attach a closure to an object;staticclosures cannot be bound.- PHP 8.5 allows static closures and first-class callables in constant expressions such as default values.
Exercise
Write a function pipeline(callable ...$steps): Closure that returns a closure taking a string and passing it through every step in order (use array_reduce()). Also write a factory limit(int $max): Closure that returns a closure cutting a string to $max characters with mb_substr(). Build a slug maker from trim(...), strtolower(...), an arrow function that replaces spaces with -, and limit(12), then run it on ' Paneer Tikka Masala '.
Show solution
<?php
declare(strict_types=1);
function pipeline(callable ...$steps): Closure
{
return fn(string $input): string => array_reduce(
$steps,
fn(string $carry, callable $step): string => $step($carry),
$input
);
}
function limit(int $max): Closure
{
return fn(string $s): string => mb_substr($s, 0, $max);
}
$slug = pipeline(
trim(...),
strtolower(...),
fn(string $s): string => str_replace(' ', '-', $s),
limit(12),
);
echo $slug(' Paneer Tikka Masala '), "\n";paneer-tikka
array_reduce() starts with the input and hands the running result to each step. limit(12) is a closure that remembers $max, so it fits into the pipeline like any other callable.
Quick Quiz
1. Which of these is NOT a valid callable in PHP?
An array callable must list the object or class first and the method name second.
2. After $n = 1; $f = fn() => $n++; $f();, what is $n?
Arrow functions capture outside variables by value, so the closure only changed its own copy.
3. What does $cart->add(...) create?
First-class callable syntax turns the method into a closure bound to the object.
4. Why mark a closure inside a class as static?
A static closure has no $this; use it when the closure does not need the object.
Learn PHP with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
