PHP Closures (Advanced)

Beyond the basics: Closure::bind to rebind $this, fromCallable to wrap any callable, static closures, currying, memoization, and idiomatic callback usage.

Advanced 9 min read 10 examples

Closure Recap

A closure is an anonymous function that captures variables from its surrounding scope. PHP closures are objects of class Closure. The Functions tutorial covers basics - this page goes deeper.

The Closure Class

PHP
<?php
$cb = function ($x) { return $x * 2; };

var_dump($cb);                       // object(Closure)
var_dump($cb instanceof Closure);    // true

// Closure has static methods - no instance methods you call directly
// Closure::bind, Closure::fromCallable, Closure::call

Closure::bind & bindTo

Rebind $this and the scope of an existing closure:

PHP
<?php
class User {
    private string $secret = "hidden-value";
    public function __construct(public string $name) {}
}

$user = new User("Ruban");

// Reach into private property of $user via a rebound closure
$getSecret = function () {
    return $this->secret;
};

$bound = Closure::bind($getSecret, $user, User::class);
echo $bound();    // "hidden-value"

// Equivalent instance method
$bound = $getSecret->bindTo($user, User::class);

// One-off: Closure::call binds and calls in one step
echo $getSecret->call($user);    // same result
Reaching into private state is a smell

If you find yourself rebinding closures to access private properties, ask if the class should expose a method instead. Use this only for framework-level concerns: serialisation, lazy proxies, test fixtures.

Closure::fromCallable

Wrap any callable (string, array, first-class method) as a real Closure object:

PHP
<?php
// From a function name
$strlen = Closure::fromCallable("strlen");
echo $strlen("hello");          // 5

// From a static method
$decode = Closure::fromCallable([JsonHelper::class, "decode"]);

// From an instance method
$repo = new UserRepo();
$find = Closure::fromCallable([$repo, "find"]);
$user = $find(42);

// PHP 8.1+ - first-class callable syntax is shorter
$find = $repo->find(...);       // same effect, modern syntax

static Closures

A static closure does NOT capture $this even inside a method - useful when you want a callable that won\'t accidentally extend object lifetime:

PHP
<?php
class Service {
    public function buildCallback(): Closure {
        // Regular closure - captures $this implicitly
        return function ($x) { return $x * 2; };
    }

    public function buildStaticCallback(): Closure {
        // No $this captured - cant call $this->method() inside
        return static function ($x) { return $x * 2; };
    }
}

// Useful when storing closures long-term (caches, registries) so they
// dont pin the originating object in memory.

Currying & Partial Application

PHP
<?php
// Curry: takes a 2-arg function, returns nested 1-arg functions
function curry(callable $fn): Closure {
    return fn($a) => fn($b) => $fn($a, $b);
}

$add = fn($a, $b) => $a + $b;
$curriedAdd = curry($add);

echo $curriedAdd(3)(4);        // 7

// Partial application - pre-fill some args
function partial(callable $fn, mixed ...$preset): Closure {
    return fn(...$args) => $fn(...$preset, ...$args);
}

$greet = fn($greeting, $name) => "$greeting, $name!";
$hello = partial($greet, "Hello");

echo $hello("Ruban");          // "Hello, Ruban!"
echo $hello("Alice");          // "Hello, Alice!"

Memoization

Cache the results of a pure function so repeat calls are free:

PHP
<?php
function memoize(callable $fn): Closure {
    $cache = [];
    return function (...$args) use ($fn, &$cache) {
        $key = serialize($args);
        return $cache[$key] ??= $fn(...$args);
    };
}

// Expensive Fibonacci
$fib = function ($n) use (&$fib) {
    return $n < 2 ? $n : $fib($n - 1) + $fib($n - 2);
};

$memFib = memoize($fib);

echo $memFib(30);              // first call - computes
echo $memFib(30);              // cached - instant

Closures as Callbacks

PHP
<?php
// Sort
usort($users, fn($a, $b) => $a->age <=> $b->age);

// Filter
$adults = array_filter($users, fn($u) => $u->age >= 18);

// Map
$names = array_map(fn($u) => $u->name, $users);

// Reduce
$total = array_reduce($cart, fn($acc, $i) => $acc + $i->price, 0);

// Custom event dispatcher
$bus->on("user.registered", function (User $u) use ($mailer, $logger) {
    $mailer->welcome($u);
    $logger->info("New user: {$u->email}");
});

// HTTP middleware chain
$handler = fn(Request $req) => array_reduce(
    array_reverse($middlewares),
    fn(Closure $next, callable $mw) => fn($r) => $mw($r, $next),
    fn(Request $r) => $finalHandler($r)
)($req);

Next Steps

Frequently Asked Questions

Mostly in framework code - accessing private properties of an object for serialisation, lazy proxies, or testing. App code rarely needs Closure::bind. If you reach for it, ask if a method on the class would be clearer.

Yes - inside class methods. Marking a closure static stops it from capturing $this, freeing the object for garbage collection sooner and making the closure usable in contexts where $this isn't available.

Not built in - PHP closures take all arguments at once. You can simulate currying with a chain of returned closures, but the syntax is heavier than in languages where currying is first-class.

Only for pure functions of immutable arguments. If the inputs are objects whose state changes, the cache returns stale results. Serialise inputs into a cache key carefully - and prefer pure functions for memoization.

Arrow functions for one-line expressions where you only READ outer variables. Classic closures for multi-line bodies or when you need use (&$var) for mutation.