Late static binding — self vs static vs parent — all edge cases
Advanced5 min read·eng-09-008
interview
Concept
Late Static Binding (LSB) — static:: resolves to the class that was called at runtime, not the class where the method is defined. Introduced in PHP 5.3 to solve the limitation of self::.
The three keywords:
self::— refers to the class where the method is DEFINED (compile-time binding). Never changes, even in subclasses.static::— refers to the class that was CALLED at runtime (late static binding). Changes based on which class invoked the method.parent::— refers to the parent class of where the method is DEFINED. Used to call parent's implementation.
The problem self:: creates: A static factory method in a base class that creates new self() always creates an instance of the base class, even when called on a subclass. static:: fixes this.
When to use each:
self::— when you intentionally want the defining class, not the calling class.static::— when subclasses should inherit and override the behavior. Static factory methods, registries.parent::— when extending a method and calling the parent's version.
static::class: Returns the runtime class name as a string.
new static(): Creates an instance of the calling class — essential for static factory methods and method chaining that returns $this in a fluent interface.
get_called_class(): Legacy function — same as static::class (use static::class instead).
Code Example
php
<?php
class Base
{
public static function create(): static
{
return new static(); // static::— creates instance of the CALLED class
}
public static function whoAmISelf(): string
{
return self::class; // always 'Base', even when called on Child
}
public static function whoAmIStatic(): string
{
return static::class; // runtime class: 'Base' or 'Child'
}
}
class Child extends Base
{
// Inherits all static methods from Base
}
$base = Base::create(); // returns Base instance
$child = Child::create(); // returns Child instance (because static:: not self::)
echo Base::whoAmISelf(); // 'Base'
echo Child::whoAmISelf(); // 'Base' — self:: resolves to defining class!
echo Base::whoAmIStatic(); // 'Base'
echo Child::whoAmIStatic(); // 'Child' — static:: resolves to calling class!
// ============================================================
// Real-world: Eloquent's method chaining
// ============================================================
class Model
{
protected array $attributes = [];
public static function query(): static
{
return new static(); // returns Child model, not Model
}
public function where(string $column, mixed $value): static
{
$this->attributes[$column] = $value;
return $this; // returns $this (which is the Child, not Model)
}
}
class User extends Model {}
$user = User::query()->where('active', true); // User instance, not Model
// ============================================================
// parent:: — calling parent implementation
// ============================================================
class HttpRequest
{
public function validate(): bool
{
echo "HttpRequest::validate\n";
return true;
}
}
class ApiRequest extends HttpRequest
{
public function validate(): bool
{
$baseValid = parent::validate(); // calls HttpRequest::validate
echo "ApiRequest::validate — extra checks\n";
return $baseValid && $this->checkToken();
}
private function checkToken(): bool { return true; }
}
// ============================================================
// Edge case: static property inheritance
// ============================================================
class Counter
{
protected static int $count = 0;
public static function increment(): void
{
static::$count++; // increments the CHILD's $count if overridden
}
public static function getCount(): int { return static::$count; }
}
class PageCounter extends Counter
{
protected static int $count = 0; // own static property
}
Counter::increment();
Counter::increment();
PageCounter::increment();
echo Counter::getCount(); // 2
echo PageCounter::getCount(); // 1 — separate static property!