Late Static Binding in PHP: static:: vs self::

1 min readProgramming

Late static binding (LSB) in PHP resolves the called class at runtime rather than the class where the method is defined. static:: refers to the class that was called; self:: always refers to the class where the code is written. Without LSB, factory methods and singleton patterns in parent classes return instances of the wrong class when called on subclasses.

phpoop

The problem: self:: breaks inheritance

self:: always resolves to the class in which the code is written, regardless of which class calls it:

class Base {
    public static function create(): static {
        return new self();  // always creates Base, never a subclass
    }

    public static function className(): string {
        return self::class;  // always returns "Base"
    }
}

class Child extends Base {}

$obj = Child::create();
var_dump($obj instanceof Child); // false — got Base, not Child
echo Child::className();         // "Base" — not "Child"

This breaks the factory pattern in inheritance hierarchies.

static:: — late static binding

static:: resolves to the class that received the method call at runtime:

class Base {
    public static function create(): static {
        return new static();  // creates the called class
    }

    public static function className(): string {
        return static::class;  // resolves at call time
    }
}

class Child extends Base {}

$obj = Child::create();
var_dump($obj instanceof Child); // true — created Child
echo Child::className();         // "Child"
echo Base::className();          // "Base"

static:: is resolved at runtime based on the calling context. self:: is resolved at compile time based on where the method is defined.

Singleton with late static binding

The classic singleton breaks in inheritance without LSB:

// BROKEN: all subclasses share Base::$instance
class Base {
    private static ?self $instance = null;

    public static function getInstance(): static {
        if (self::$instance === null) {
            self::$instance = new self();  // always creates Base
        }
        return self::$instance;
    }
}

class Logger extends Base {}
class Cache extends Base {}

// Both return the same Base instance — wrong
$logger = Logger::getInstance();
$cache = Cache::getInstance();
var_dump($logger === $cache); // true (both are the same Base instance)

With LSB, each subclass gets its own instance:

class Base {
    private static array $instances = [];

    public static function getInstance(): static {
        $class = static::class;  // resolves to calling class
        if (!isset(self::$instances[$class])) {
            self::$instances[$class] = new static();
        }
        return self::$instances[$class];
    }
}

class Logger extends Base {}
class Cache extends Base {}

$logger = Logger::getInstance();
$cache = Cache::getInstance();
var_dump($logger === $cache); // false — separate instances
var_dump($logger instanceof Logger); // true
var_dump($cache instanceof Cache);   // true

static:: resolves at runtime; self:: resolves at definition time — mixing them gives confusing results

ConceptPHP

PHP resolves static:: by looking at the 'called class' — the class used in the original method call, tracked through inheritance. self:: bypasses this and always uses the class where the method source code lives. In a deep inheritance chain, static:: follows the chain all the way to the concrete class being called; self:: stops at the first class up the chain that defines the method.

Prerequisites

  • PHP static methods
  • PHP inheritance
  • PHP class instantiation

Key Points

  • self:: — the class where the method is defined (compile-time resolution).
  • static:: — the class that received the call (runtime resolution, follows inheritance).
  • parent:: — the parent of the class where the method is defined.
  • new self() in a parent method always creates the parent class. new static() creates the subclass when called on a subclass.

Inheritance comparison

class Animal {
    public static function speak(): string {
        return self::class . " says " . static::sound();
    }

    protected static function sound(): string {
        return "...";
    }
}

class Dog extends Animal {
    protected static function sound(): string {
        return "woof";
    }
}

class Cat extends Animal {
    protected static function sound(): string {
        return "meow";
    }
}

echo Dog::speak(); // "Animal says woof" — self::class is "Animal", static::sound() calls Dog::sound()
echo Cat::speak(); // "Animal says meow"

self::class returns "Animal" (definition class). static::sound() calls the subclass's sound() method (calling class). Both self:: and static:: can be useful in the same method — self:: for things that should never change (class name of the base, shared constants), static:: for things that should follow the subclass.

A PHP Base class has a static factory method using new self(). A Child class extends Base and calls Child::factory(). What does factory() return?

easy

The factory method is defined in Base and uses new self() internally.

  • AAn instance of Child — PHP automatically adjusts self:: to the calling class
    Incorrect.PHP does not adjust self:: for late static binding. self:: always resolves to the class where the method is written, regardless of which class made the call.
  • BAn instance of Base — self:: resolves to Base because that's where factory() is defined
    Correct!self:: is resolved at definition time. The factory() method is defined in Base, so new self() creates a Base instance even when called as Child::factory(). To make factory() return the calling class, replace new self() with new static(). static:: is the late static binding mechanism — it resolves to the class that received the call at runtime.
  • CA fatal error — new self() is invalid in a static method
    Incorrect.new self() is valid in both static and instance methods. It creates an instance of the class where the code is written.
  • DIt depends on whether Child overrides factory()
    Incorrect.If Child overrides factory(), then Child::factory() calls Child's version. If not overriding, it calls Base::factory(), which uses self:: = Base. The result (Base instance) doesn't depend on whether Child overrides the method — it depends on which class's factory() code runs and whether it uses self:: or static::.

Hint:self:: always resolves to the class where the method is written. Which class contains the factory() method?