0

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!