static, self and Late Static Binding
self:: is fixed at definition time; static:: resolves to the class that was actually called. That difference powers fluent factories.
Inside a class, self:: always means this exact class, resolved where the code is written. static:: means the class that was actually called at runtime — that's late static binding. The gap only shows up with inheritance, and it matters most in factory methods.
Easy example
<?php
class Base {
public static function create(): static {
return new static(); // late static binding
}
}
class Child extends Base {}
var_dump(Base::create() instanceof Base); // true
var_dump(Child::create() instanceof Child); // true — not just Base!Had create() used new self(), Child::create() would have handed you a Base — almost never what you want in a factory.
Example
When to use it
- Build a fluent static factory on a base Model class that correctly returns subclass instances with static::class.
- Use static:: in a base class method so each subclass's registry or cache is stored separately.
- Implement a singleton pattern in a base class that creates one instance per concrete subclass using static::.
More examples
self:: vs static:: difference
static:: resolves to the class that was actually called; self:: would always return Base regardless of the subclass.
<?php
class Base {
public static function create(): static {
return new static();
}
public static function className(): string {
return static::class; // late static binding
}
}
class Child extends Base {}
$obj = Child::create();
echo get_class($obj); // Child
echo Child::className(); // Child
echo Base::className(); // BaseFluent builder with LSB
Returning static ensures the fluent chain preserves the subclass type, keeping UserQuery a UserQuery throughout.
<?php
class QueryBuilder {
protected array $wheres = [];
public function where(string $clause): static {
$clone = clone $this;
$clone->wheres[] = $clause;
return $clone;
}
public function toSql(): string {
$w = implode(' AND ', $this->wheres);
return 'SELECT * FROM table' . ($w ? " WHERE $w" : '');
}
}
class UserQuery extends QueryBuilder {}
$q = (new UserQuery())->where('active = 1')->where('role = "admin"');
echo get_class($q); // UserQuery — not QueryBuilder
echo $q->toSql();Per-subclass static registry
Keying the static property by static::class gives each subclass its own isolated registry without extra code.
<?php
class Registry {
private static array $items = [];
public static function register(string $key, mixed $value): void {
static::$items[static::class][$key] = $value;
}
public static function get(string $key): mixed {
return static::$items[static::class][$key] ?? null;
}
}
class PluginRegistry extends Registry {}
Registry::register('db', 'mysql');
PluginRegistry::register('db', 'sqlite');
echo Registry::get('db'); // mysql
echo PluginRegistry::get('db'); // sqlite
Discussion