Closures & Callables

PHP · Lesson 15 of 50 8 min read Updated

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
<?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 string
Output
string 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
<?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";
Output
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:

FormHow it capturesCan it change the outside variable?
function () use ($x)Copies $x when the closure is createdNo
function () use (&$x)Shares the real $xYes
fn() => $xCopies every outside variable it uses, automaticallyNo
PHP
<?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";
Output
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
<?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";
Output
₹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
<?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";
}
Output
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
<?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";
Output
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 fn to change an outside variable. Arrow functions capture by value. Use function () use (&$x) when you really need to change $x.
  • Forgetting use. A normal closure cannot see outside variables at all without use; you get an "Undefined variable" warning.
  • Typos in string callables. 'strtouper' fails only when called. Prefer strtoupper(...).
  • Typing a property as callable. PHP does not allow callable as a property type. Use Closure and 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 Closure objects that remember captured variables, which makes factory functions and counters possible.
  • use ($x) and fn capture 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() and call() attach a closure to an object; static closures 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
<?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";
Output
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?

2. After $n = 1; $f = fn() => $n++; $f();, what is $n?

3. What does $cart->add(...) create?

4. Why mark a closure inside a class as static?

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