skip to content

In PHP, what do Closure::bind(), bindTo(), call() and fromCallable() do, and how do they let a closure reach private members?

level: middleimportance: should knowfreq 30%

answer

  1. new copy, original untouched
  2. newThis and newScope, 'static' keeps scope
  3. scope decides private access
  4. call(): bind, invoke, discard
  5. fromCallable(): TypeError if not callable here

basics

~10 s

bindTo() and Closure::bind() return a copy of a closure with a new $this and class scope; the scope grants private access. call() binds temporarily and invokes. fromCallable() wraps a callable using the caller's scope.

solid answer

~50 s

A `Closure` carries a bound object (`$this`) and a **class scope**, and the scope decides which `private` and `protected` members it may touch. `$c->bindTo($newThis, $newScope)` and `Closure::bind($c, $newThis, $newScope)` do the same thing: return a **new** closure with that object and scope and leave the original untouched. `$newScope` defaults to `'static'`, meaning keep the current scope, and takes a class name or an object. `$c->call($obj, ...$args)` binds to `$obj` with `$obj`'s class as scope **for one call** and returns the result, handy for reading private state in a test. `Closure::fromCallable($callable)` wraps a callable in a `Closure` using the **calling scope**, so a class can hand out one of its private methods; it throws `TypeError` if the callable is not callable from there. Invalid bindings, such as an object on a static closure, warn and return `null`.

code

php · 25 lines
php
<?php
declare(strict_types=1);

final class Session
{
    private int $seatsLeft = 40;
}

$session = new Session();
$peek = fn () => $this->seatsLeft;

// bind object AND scope, then call the returned copy
$bound = Closure::bind($peek, $session, Session::class);
echo $bound(), "\n";           // 40

// call(): temporary binding, scope = the object's class
echo $peek->call($session), "\n"; // 40

// object without scope: $this is set, private access is not
$noScope = $peek->bindTo($session);
try {
    $noScope();
} catch (Error $e) {
    echo $e->getMessage(), "\n"; // Cannot access private property Session::$seatsLeft
}

go deeper

for a junior

Recall that closures can be bound to an object with bindTo() or call(), and that this can give them access to private members.

for a middle

Explain $newThis versus $newScope, why the return value must be used, what call() adds, and how fromCallable() checks the calling scope.

for a senior

Judge when binding into objects is justified, such as tests or hydrators, and treat null results from failed binds as bugs rather than fallbacks.

for a principal

Set limits on encapsulation-bypassing techniques in a codebase, weighing hydrator convenience against analysability and PHP 9 turning bad binds into errors.

## What a closure carries Every PHP `Closure` object has two pieces of context besides its code and captured variables: - a **bound object**, available as `$this` (or none); - a **class scope**, the class whose `private` and `protected` members the code may access. A closure created in a method gets both automatically. The binding methods let you choose them explicitly, which is how test helpers, hydrators and some framework features read or write private properties without reflection. ## `bindTo()` and `Closure::bind()` ```php public function bindTo(?object $newThis, object|string|null $newScope = "static"): ?Closure public static function bind(Closure $closure, ?object $newThis, object|string|null $newScope = "static"): ?Closure ``` They are the same operation, one as an instance method and one static: 1. **They return a new closure.** The original keeps its old binding; forgetting to use the return value is a common bug. 2. **`$newThis`** is the object to bind, or `null` for none. 3. **`$newScope`** is a class name or an object whose class is used. The default `'static'` keeps the closure's current scope. Binding an object **does not** change the scope by itself: to read a private property of `$session`, pass `Session::class` or `$session` as the scope too. 4. **On an invalid combination they warn and return `null`.** ## `call()` ```php public function call(object $newThis, mixed ...$args): mixed ``` `call()` binds the closure to `$newThis`, uses **`$newThis`'s class as the scope**, invokes it with the remaining arguments and returns the result, without creating a lasting bound copy. It is the shortest way to run code "inside" an object: ```php $seatsLeft = (fn () => $this->seatsLeft)->call($session); ``` ## `fromCallable()` ```php public static function fromCallable(callable $callback): Closure ``` `fromCallable()` converts any callable, such as a function name, an `[object, 'method']` pair or an invokable object, into a `Closure`. The check happens **in the calling scope**: inside a class, `Closure::fromCallable([$this, 'notifyWaitlist'])` works for a private method and the resulting closure can be handed to outside code, which can call it but still cannot name the private method itself. From outside the class the same call throws `TypeError`. If the argument is already a `Closure`, it is returned as is. Since PHP 8.1 the first-class callable syntax has the same semantics and is the more common spelling in new code. Closures made this way wrap a real function or method, and their scope is fixed: rebinding it to another class warns `Cannot rebind scope of closure created from method` and returns `null`. ## Binding rules that fail | Attempt | Result in PHP 8.5 | |---|---| | bind an object to a `static` closure | `Warning: Cannot bind an instance to a static closure ...`, returns `null` | | unbind `$this` (`null`) from a closure whose body uses `$this` | `Warning: Cannot unbind $this of closure using $this ...`, returns `null` | | use an internal class such as `ArrayObject` as the scope | `Warning: Cannot bind closure to scope of internal class ...`, returns `null` | | change the scope of a closure from `fromCallable()` | `Warning: Cannot rebind scope of closure created from method ...`, returns `null` | Each warning ends with "this will be an error in PHP 9", and PHP 8.5 formally deprecated these cases. Treat a `null` result as a bug, not a fallback. ## What `$newScope` accepts - a **class name** string, such as `Session::class`, which grants that class's private and protected access; - an **object**, whose class is used, handy when you already have the instance: `bindTo($session, $session)`; - **`'static'`**, the default, which keeps whatever scope the closure already had; - `null`, which removes the class scope altogether. Scope and object are independent: the manual's own example binds a `static` closure with `null` as the object and a class name as the scope, and the result reads that class's private **static** property. An unknown class name gives `Warning: Class "..." not found` and `null`. ## When binding is appropriate - **Tests** that must assert on private state of a third-party object with no accessor. - **Hydrators and mappers** that set private properties in bulk without per-property reflection calls. - **Handing out a private method** as a callback without making it public, via `fromCallable()`. Binding bypasses encapsulation on purpose, so outside those cases a public method is usually the better design. Code that binds closures into objects is also harder for static analysers to follow; tools often need an annotation to know what `$this` is inside.

  • In PHP, why does $closure->bindTo($obj) alone not grant access to $obj's private properties?
    Binding sets `$this`, but visibility is decided by the closure's class scope, and the default `$newScope` value `'static'` keeps whatever scope the closure had, which is none for a closure created outside any class. Pass the class name or the object as the second argument, `bindTo($obj, $obj)`, or use `call($obj)`, which scopes to the object's class automatically.
  • How can a PHP class give outside code a callback to one of its private methods without making the method public?
    Create the closure inside the class: `Closure::fromCallable([$this, 'notifyWaitlist'])`, or the equivalent first-class callable syntax. The check runs in the class's own scope, so it succeeds, and the returned `Closure` can be passed anywhere and invoked. Outside code calling `fromCallable()` with the same array would get a `TypeError`.

saying these in an interview costs you the question

  • bindTo() changes the closure it is called on in place.
  • Binding an object is enough to access its private properties.
  • Closure::call() permanently rebinds the closure to the new object.
  • fromCallable() on a private method works from any scope.
  • An invalid bind throws an exception you can catch in PHP 8.5.