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 alwaysBaseModel. Usingnew static()instead ofnew self()enables this. - Singleton per class: Each class in a hierarchy should have its own singleton instance. Using
static::classas the key ensuresChild::getInstance()is separate fromBase::getInstance(). - Fluent builder patterns: When a parent class's fluent methods return
$this, they work fine. But when they returnnew self(), a child class's fluent chain becomes aBaseinstance mid-chain. Usenew 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