0

Late static binding (LSB) — static:: vs self:: deep dive

Advanced5 min read·php-08-009
interviewlaravel-src

Concept

Late Static Binding (LSB) solves the problem that self:: always refers to the class where a method was defined at compile time, regardless of which class called it at runtime. static:: resolves to the actual calling class — the class that was used to invoke the method.

This distinction only matters in inherited contexts — when a parent class defines a static method that's called on a child class, or when static:: is used in an instance method to refer to the actual runtime class.

Practical scenarios:

  • Static factory methods in a class hierarchy: BaseModel::create() should return an instance of the calling class, not always BaseModel. Using new static() instead of new self() enables this.
  • Singleton per class: Each class in a hierarchy should have its own singleton instance. Using static::class as the key ensures Child::getInstance() is separate from Base::getInstance().
  • Fluent builder patterns: When a parent class's fluent methods return $this, they work fine. But when they return new self(), a child class's fluent chain becomes a Base instance mid-chain. Use new static().

get_called_class(): Returns the called class name from a static context — the string equivalent of static::class. Deprecated in PHP 8.0 in favor of static::class.

Code Example

php
<?php
declare(strict_types=1);

// The problem with self::
class Base
{
    public static function selfCreate(): static
    {
        return new self(); // ALWAYS creates Base, even when called on Child
    }

    public static function staticCreate(): static
    {
        return new static(); // creates the CALLING class
    }
}

class Child extends Base
{
    public string $type = 'child';
}

$a = Child::selfCreate();   // returns Base instance — probably wrong!
$b = Child::staticCreate(); // returns Child instance — correct!

var_dump($a instanceof Child); // false
var_dump($b instanceof Child); // true

// ActiveRecord-style model registry
class Model
{
    protected static array $instances = [];
    protected static string $table = 'models';

    public static function getTable(): string
    {
        return static::$table; // each subclass can override this
    }

    public static function find(int $id): static
    {
        // In a real implementation: SELECT * FROM static::getTable() WHERE id = ?
        return new static();
    }
}

class UserModel extends Model
{
    protected static string $table = 'users'; // overrides parent
}
class PostModel extends Model
{
    protected static string $table = 'posts';
}

echo UserModel::getTable(); // "users" — late static binding gets UserModel::$table
echo PostModel::getTable(); // "posts"

$user = UserModel::find(1); // returns UserModel instance
var_dump($user instanceof UserModel); // true

// LSB in instance context
class FluentBuilder
{
    protected array $data = [];

    public function set(string $key, mixed $value): static
    {
        $clone = clone $this;
        $clone->data[$key] = $value;
        return $clone; // returns the actual type of $this
    }
}
class SpecialBuilder extends FluentBuilder
{
    public function special(): static { return $this->set('special', true); }
}
$builder = (new SpecialBuilder())->set('a', 1)->special();
var_dump($builder instanceof SpecialBuilder); // true — chain preserved correct type