Late Static Binding, Closures & Copy-on-Write

Understand the difference between static:: and self:: (late static binding), how to rebind a closure's $this and scope with bindTo()/Closure::bind(), how PHP references actually work under the hood, and copy-on-write semantics for arrays and objects.

This lesson covers PHP's most subtle 'mechanics' topics — the ones that produce correct-looking code that behaves wrong in an inherited class, or a closure that silently can't see the private data it needs, or a variable mutation that mysteriously affects a variable that was supposedly 'copied.' Each of these has a precise, learnable rule; once the rule clicks, the surprising behavior becomes predictable instead of magical.

static:: vs self:: — late static binding

Inside a class method, self:: always refers to the class the method is literally written in — resolved at compile time, regardless of what subclass actually calls the method. static:: instead refers to whichever class was actually used to make the call — resolved at runtime based on the actual calling context. This distinction is called 'late static binding' (the resolution happens 'late,' at call time, not 'early,' at definition time) and was introduced in PHP 5.3 specifically to fix a longstanding limitation.

The classic demonstration is a static factory method pattern: abstract class Model { public static function create(): static { return new static(); } } — if User extends Model and you call User::create(), new static() inside create() correctly creates a User, because static:: resolved to User (the class actually invoked), even though create() itself is defined in Model. If that method had used new self() instead, User::create() would incorrectly create a plain Model instance, because self:: is hardwired to Model (where the method is written) no matter who calls it.

The rule of thumb: use self:: when you deliberately want to reference the exact defining class regardless of subclassing (rare — usually for private implementation details that should never vary by subclass), and use static:: (or the static return type, itself introduced later as syntax) whenever you want inheritance-aware behavior — which is the overwhelmingly common case for factory methods, fluent builders, and any method that should 'know' which subclass it's actually operating on.

Closure binding: bindTo() and Closure::bind()

A closure created inside a method normally captures $this automatically, bound to the object it was defined in, and can access that object's private/protected members. But a closure can also be created standalone or rebound: Closure::bindTo(object $newThis, $newScope = 'static') returns a new closure with $this (and the visibility scope for private/protected access) changed to the given object, and the static method Closure::bind($closure, $newThis, $newScope) does the same thing without mutating the original (both return a new closure; closures are otherwise immutable in this respect).

The $newScope parameter is the subtle, important part: it controls which class's private/protected members the rebound closure is allowed to see — by default it's inferred from $newThis's class, but you can pass a class name or object explicitly to bind a closure to see private members as though it were code written inside a specific class, even a class it wasn't originally associated with. This is exactly how mocking/testing libraries implement 'call this private method for testing' features: bind a closure that calls $this->privateMethod() to the target object with that object's own class as scope, then invoke the closure.

A related, simpler tool for one-off cases is Closure::fromCallable() combined with bindTo(), or in PHP 8.1+ first-class callable syntax $obj->method(...) for straightforward cases — but bindTo()/bind() remain the tool specifically for changing which object and which class-scope a closure runs with, which first-class callable syntax does not do.

How PHP references actually work

A PHP reference (&) makes two variable names into aliases for the exact same underlying value — $b = &$a; means $a and $b are now two names for one storage location, so $a = 5; immediately makes $b also 5, and vice versa, for as long as both remain bound. This differs fundamentally from normal assignment ($b = $a;), which by default copies the value (with the copy-on-write optimization described below softening the literal-copy cost).

References are most commonly used as function parameters (function increment(int &$n) { $n++; }) to let a function modify a caller's variable directly, and in foreach (foreach ($arr as &$value) { $value *= 2; }) to modify array elements in place during iteration — the latter has a well-known gotcha: after such a loop, $value remains bound as a reference to the array's last element, so a subsequent foreach ($arr as $value) (without &) can silently overwrite that last element by reusing the same $value variable name; the fix is unset($value); immediately after any by-reference foreach loop.

Objects are often mistakenly described as 'passed by reference' in PHP, but this is imprecise: what's actually passed around is an object *handle* (like a lightweight pointer to the object) — assigning or passing that handle by value (the default) still lets you mutate the object through it, which is what creates the reference-like feeling, but reassigning the variable itself ($obj = new Foo(); inside a function receiving $obj normally) does not affect the caller's variable, exactly the way passing a pointer by value works in C. True reference semantics for the variable itself (where reassignment IS visible to the caller) require an explicit &, e.g. function replace(Foo &$obj) { $obj = new Foo(); }.

Copy-on-write for arrays and objects

PHP arrays have value semantics: $b = $a; for arrays is defined to behave as if $b received a full, independent copy. Implementing a literal copy on every assignment would be wasteful, so PHP's Zend engine actually implements this lazily via copy-on-write (COW) — internally, both $a and $b share the same underlying data structure (with a reference count) until one of them is actually modified, at which point PHP transparently makes a real, separate copy for the one being written to, leaving the other untouched. From the language's perspective this is invisible — it's a pure performance optimization, and the array value semantics you observe are always exactly as if a full copy happened at assignment time.

Objects are different: as established above, objects are handled through handles, and assigning an object handle does not create a new object or trigger any copy-on-write behavior — both variables genuinely share the one object, and mutating through either is visible through both, permanently (not just until a write, as with arrays). This is the single most important semantic difference between arrays and objects in PHP, and it's why the explicit clone keyword exists specifically for objects — arrays never need an equivalent because plain assignment already gives you independent value semantics.

Practically, this means passing a large array into a function that doesn't modify it is cheap (no real copy ever happens, thanks to COW), but passing an array into a function that DOES modify it locally (without &) triggers one real copy at the point of modification — usually negligible, but worth knowing when reasoning about memory behavior with very large arrays modified inside hot loops.

Late static binding, closure binding, and reference syntax

static:: is resolved at call time based on the actually-invoked class; self:: is resolved at compile time to the class the code is literally written in. Always unset() a by-reference foreach loop variable immediately after the loop to avoid the classic lingering-reference bug on a subsequent foreach.

// Late static binding
class Base {
    public static function create(): static { return new static(); }
    public static function whoAmI(): string { return static::class; } // late-bound
    public static function alwaysBase(): string { return self::class; } // always Base
}

// Closure binding
$closure = function () { return $this->secret; };
$bound = Closure::bind($closure, $targetObject, TargetClass::class);
$bound(); // runs with $this = $targetObject, private-member visibility of TargetClass

// References
function increment(int &$n): void { $n++; }
$x = 5;
increment($x); // $x is now 6

foreach ($arr as &$v) { $v *= 2; }
unset($v); // required: break the lingering reference after a by-reference foreach

// Copy-on-write is automatic and invisible -- no special syntax needed:
$a = [1, 2, 3];
$b = $a;       // no real copy happens yet (shared internally)
$b[] = 4;      // NOW a real, independent copy of the array is made for $b

Code Examples

new static() inside create() resolves to User because static:: is late-bound to the class actually used in the call (User::create()), even though create() is defined in Model. definedIn() uses self::, which always resolves to Model regardless of the calling class. Expected result: User Model

The closure was defined completely outside Account's class body, so it has no $this and no special access to private members on its own. Closure::bind() attaches it to a specific object ($account) AND grants it Account's own visibility scope, letting it read the otherwise-inaccessible private $balance property -- exactly the mechanism testing tools use to call/read private members for white-box tests. Expected result: 100 demonstrateCow() shows that assigning an array is cheap until a write actually happens, at which point PHP transparently makes a real copy. demonstrateReferenceGotcha() shows the classic lingering-reference bug: a by-reference foreach leaves its loop variable bound to the array's last element, and skipping unset() before reusing that variable name in a later loop would silently corrupt array data. Expected result: 1000000 1000001 Array ( [0] => 10 [1] => 20 [2] => 30) unset($n); // CRITICAL: break that reference before reusing the variable name

abstract class Model
{
    public static function create(): static
    {
        return new static(); // late static binding: resolves to the calling class
    }

    public static function definedIn(): string
    {
        return self::class; // always 'Model', regardless of who calls it
    }
}

class User extends Model {}

$user = User::create();
echo get_class($user) . "\n";   // User -- static:: correctly picked up User
echo User::definedIn() . "\n";  // Model -- self:: is hardwired to where it's written
class Account
{
    private float $balance = 100.0;
}

// A standalone closure with no automatic $this
$getBalance = function () {
    return $this->balance; // would error without binding -- $this is undefined here
};

$account = new Account();

// Bind the closure to $account, with Account::class as the visibility scope
// so it's allowed to read the private $balance property
$bound = Closure::bind($getBalance, $account, Account::class);

echo $bound() . "\n"; // 100
function demonstrateCow(): void
{
    $original = range(1, 1_000_000); // a large array
    $copy = $original;                 // NO real copy yet (COW: shared internally)

    // Reading from $copy doesn't trigger a copy
    $firstElement = $copy[0];

    // Writing to $copy DOES trigger a real, independent copy at this point
    $copy[] = 1_000_001;

    echo count($original) . "\n"; // 1000000 -- untouched
    echo count($copy) . "\n";     // 1000001 -- the real copy that was made
}

function demonstrateReferenceGotcha(): void
{
    $numbers = [1, 2, 3];

    foreach ($numbers as &$n) {
        $n *= 10;
    }
    // $n is now a lingering reference to $numbers[2] (the last element)!
    unset($n); // CRITICAL: break that reference before reusing the variable name

    foreach ($numbers as $n) {
        // safe now -- without the unset() above, this loop would silently
        // overwrite $numbers[2] on its later iterations
    }

    print_r($numbers); // [10, 20, 30] -- correct, thanks to unset()
}

self:: vs static:: resolution

static:: is resolved at call time from the actual invoked class; self:: is fixed at compile time to the class the method text lives in.

class Model {
    static function create() { return new static(); }  // <- late-bound
    static function base()   { return new self(); }     // <- always Model
  }
  class User extends Model {}

  User::create()
        |
        v
  static:: looks at the ACTUAL calling class --> User --> new User()

  User::base()
        |
        v
  self:: is fixed to where the code is written --> Model --> new Model()

Important Rules

static:: resolves at call time to the actually-invoked class (late static binding); self:: always resolves to the class the method is textually defined in, regardless of caller.

Use static:: (or the static return type) for factory methods, fluent builders, and anything that should behave correctly across subclasses; reserve self:: for deliberately non-overridable internals.

Closure::bind()/bindTo() rebind both a closure's $this and its private/protected visibility scope — essential for calling into another object's private state from outside its own methods.

Always unset() a by-reference (&) foreach loop variable right after the loop to avoid the lingering-reference bug corrupting a later loop that reuses the same variable name.

Arrays have copy-on-write value semantics (cheap to pass around, real copy only made on actual mutation); objects are handled by shared handles with no copy-on-write — mutating a shared object is visible everywhere that handle is held, which is exactly why clone exists for objects but not for arrays.

Where this shows up

Late static binding is the backbone of Eloquent's and Doctrine's static query builder entry points (User::where(...)), where a single method defined once on a base Model class must correctly return builders scoped to whatever subclass actually called it. Closure::bind()/bindTo() power white-box testing utilities that need to read or invoke private members from outside a class, and also underlie how some templating engines execute a template string with $this bound to a specific data/view object. The by-reference foreach + missing unset() bug is one of the most commonly cited real PHP gotchas in code review checklists, precisely because it produces correct-looking code that silently corrupts data only under certain follow-up code shapes. Copy-on-write is why PHP developers are taught 'don't worry about passing large arrays around' as a rule of thumb — the language handles the performance concern transparently, unlike objects, where an accidental shared mutation is a real, common bug class.

abstract class Repository { public static function make(): self { return new self(); } // BUG: always makes a Repository } class UserRepository extends Repository {} UserRepository::make(); // returns a Repository, not a UserRepository!

abstract class Repository { public static function make(): static { return new static(); } // correct: late-bound } class UserRepository extends Repository {} UserRepository::make(); // correctly returns a UserRepository

Using self:: (or a self return type) in a base-class factory method hardcodes it to the base class forever, silently breaking for every subclass that calls it. static:: (late static binding) resolves to whichever class was actually used to make the call, which is almost always the intended behavior for factory methods.

[multiple-choice] A base class method uses new static(). When a subclass calls that inherited static method, what class gets instantiated? A. Always the base class, regardless of caller B. The subclass that actually made the call (late static binding) C. It causes a fatal error unless the subclass overrides the method D. A random class depending on autoloading order

B. The subclass that actually made the call (late static binding) — static:: (and new static()) uses late static binding, resolving at call time to whichever class actually invoked the method — this is precisely what distinguishes it from self::, which is fixed to the defining class.

[multiple-choice] What does Closure::bind($closure, $newThis, $newScope) change about a closure? A. Only the closure's return value B. Both what $this refers to inside the closure and which class's private/protected members it's allowed to access C. Only the closure's parameter list D. It converts the closure into a regular named function

B. Both what $this refers to inside the closure and which class's private/protected members it's allowed to access — Closure::bind() rebinds both the closure's $this (to $newThis) and its visibility scope (to $newScope), which together determine what object the closure operates on and which private/protected members it can legally touch.

[predict-output] Given $a = [1,2,3]; $b = $a; $b[] = 4; -- after this, what is count($a)? A. 3 B. 4 C. 0 D. It depends on whether references were used

A. 3 — Arrays in PHP have value (copy-on-write) semantics: $b = $a shares storage internally until a write happens, at which point $b gets its own real copy. $a is completely unaffected by the later mutation to $b, so count($a) remains 3.

Write an abstract class Shape with a static factory method make(): static that returns new static(). Create two subclasses, Circle and Square, with no additional logic. Verify (in comments or output) that Circle::make() returns a Circle and Square::make() returns a Square. Then separately, write a function doubleAll(array &$numbers): void that doubles every element in place using a by-reference foreach, being careful to unset() the loop variable.

abstract class Shape { public static function make(): static { return new static(); } } class Circle extends Shape {} class Square extends Shape {} echo get_class(Circle::make()) . "\n"; // Circle echo get_class(Square::make()) . "\n"; // Square function doubleAll(array &$numbers): void { foreach ($numbers as &$n) { $n *= 2; } unset($n); }

The factory method needs no changes between subclasses precisely because new static() is late-bound — that's the whole point of the exercise.