Using static:: Instead of self:: Correctly
Anyone building inheritance hierarchies in PHP with factory methods or ActiveRecord patterns inevitably runs into the difference between self:: and static::. Late Static Binding solves exactly the problem that self:: always stays bound to the class where the method was defined, instead of the class it was actually called through.
Table of Contents
- 1. What Late Static Binding Actually Solves
- 2. Why self:: Reaches Its Limits
- 3. Late Static Binding in Factory Methods
- 4. Building ActiveRecord Patterns With static::
- 5. Combining Late Static Binding With Abstract Classes
- 6. get_called_class() and Reflection in Detail
- 7. Traits and Late Static Binding Together
- 8. Common Mistakes and Antipatterns
- 9. self:: vs. static:: vs. $this-> Compared
- 10. Summary
- 11. FAQ
1. What Late Static Binding Actually Solves
Late Static Binding is a mechanism available in PHP since version 5.3 that decouples the keyword static:: from the lexical binding of self::. The core of the problem: when a method inside a base class calls self::method(), that call always refers to exactly the class where the method was written, regardless of which child class it was actually called through. Late Static Binding replaces that rigid binding with one that is resolved at runtime based on the calling class.
The name is fitting: "late" means the binding happens late, at runtime when the call actually occurs, rather than being fixed early when the code is written. That sounds like an academic detail at first, but it has massive practical consequences wherever inheritance hierarchies are combined with static methods, for instance in factory methods, ActiveRecord implementations, or fluent interfaces with static entry points. Without Late Static Binding, every child class would have to reimplement its own factory method just to instantiate the correct class.
In this article we work systematically through Late Static Binding: from the historical motivation, through concrete use cases in factories and ActiveRecord patterns, to the pitfalls that even experienced PHP developers regularly overlook. By the end you will have a solid mental model of when static:: is the right choice and when self:: actually delivers the desired behavior.
2. Why self:: Reaches Its Limits
To really understand Late Static Binding, you first need to understand what self:: actually does. self:: is a compile-time reference to the class where the code physically lives. If a method is written in a base class with self::create(), and that base class is inherited by a child class, self::create() still calls the base class's method, even if the child class has overridden its own create() method. That is not a bug, it is exactly the semantics that were hard-wired before PHP 5.3.
The problem becomes concrete as soon as you expect polymorphic behavior from static method calls. For instance methods, $this->method() resolves polymorphically quite naturally thanks to virtual method tables: the call always lands on the overridden method of the actual object class. For static calls with self::, this polymorphism does not exist, because there is no object whose runtime type could be queried. Late Static Binding was introduced precisely to close this gap, without changing the general semantics of self:: for all other cases.
<?php
declare(strict_types=1);
class BaseModel
{
// Using self:: — always resolves to BaseModel, regardless of caller
public static function createWithSelf(): self
{
return new self();
}
// Using static:: — resolves to the class that was actually called
public static function createWithStatic(): static
{
return new static();
}
}
class ProductModel extends BaseModel
{
}
$viaSelf = ProductModel::createWithSelf();
$viaStatic = ProductModel::createWithStatic();
echo get_class($viaSelf); // BaseModel — not what most developers expect
echo get_class($viaStatic); // ProductModel — Late Static Binding resolves correctly
3. Late Static Binding in Factory Methods
The classic use case for Late Static Binding is the static factory method. A base class defines a create() method that should return a new object of the calling class, not the base class itself. Without Late Static Binding, every child class would have to duplicate this method just to replace new static() with the correct class name. With Late Static Binding, a single implementation in the base class is enough, and it works correctly for any number of child classes.
This technique is especially valuable in libraries and frameworks that offer extensibility through inheritance. A common example is an abstract collection class with a static fromArray() method. Every concrete collection subclass, such as a ProductCollection or OrderCollection, inherits this method unchanged and still correctly receives an instance of its own class when called, not of the abstract base class. That not only saves code but also prevents an entire class of bugs that arises when a child class forgets to override the factory method.
<?php
declare(strict_types=1);
abstract class Collection
{
/** @var array<int, mixed> */
protected array $items = [];
// Late Static Binding: returns an instance of the calling subclass
public static function fromArray(array $items): static
{
$collection = new static();
foreach ($items as $item) {
$collection->items[] = $item;
}
return $collection;
}
public function count(): int
{
return count($this->items);
}
}
final class ProductCollection extends Collection
{
public function firstSku(): ?string
{
return $this->items[0]->sku ?? null;
}
}
final class OrderCollection extends Collection
{
}
// Both calls reuse the same base implementation via Late Static Binding
$products = ProductCollection::fromArray([$product1, $product2]);
$orders = OrderCollection::fromArray([$order1]);
echo get_class($products); // ProductCollection — not Collection
4. Building ActiveRecord Patterns With static::
Another prime example for Late Static Binding is the ActiveRecord pattern, where model classes provide static finder methods like find(), findOrFail() or query(). These methods are defined in a shared base class, but need to address different tables and return different object types depending on the calling model class. Late Static Binding makes exactly that possible without every model having to maintain its own copy of this logic.
Importantly, not just instantiation but also metadata such as the table name can be resolved via Late Static Binding. A static method getTableName() that derives the table name from the class name by convention only works correctly if it uses static::class instead of self::class. This combination of object creation and metadata resolution through Late Static Binding is the foundation of practically every ORM-like library that relies on inheritance rather than composition.
<?php
declare(strict_types=1);
abstract class Model
{
protected static PdoConnection $connection;
// Derives the table name from the calling class via Late Static Binding
public static function getTableName(): string
{
$className = (new ReflectionClass(static::class))->getShortName();
return strtolower($className) . 's';
}
public static function find(int $id): static
{
$table = static::getTableName();
$row = static::$connection->fetchOne(
"SELECT * FROM {$table} WHERE id = :id",
['id' => $id]
);
// new static() instantiates the actual subclass, not Model itself
$instance = new static();
$instance->hydrate($row);
return $instance;
}
abstract protected function hydrate(array $row): void;
}
final class Customer extends Model
{
private int $id = 0;
private string $email = '';
protected function hydrate(array $row): void
{
$this->id = (int) $row['id'];
$this->email = (string) $row['email'];
}
}
$customer = Customer::find(42); // Late Static Binding resolves to Customer, table "customers"
5. Combining Late Static Binding With Abstract Classes
Abstract classes and Late Static Binding complement each other extremely well, because abstract classes by definition can never be instantiated themselves. A static factory method in an abstract base class using new static() therefore always works correctly, because static:: is resolved at runtime exclusively to concrete child classes. There is no case where new static() attempts to instantiate the abstract class itself, PHP prevents that with a fatal error anyway.
A clean design deliberately exploits this property: the abstract base class defines template methods that handle internal details such as validation or persistence, while Late Static Binding ensures that all static return values and metadata correctly point to the concrete child class. This pattern frequently shows up in combination with the template method pattern, where abstract methods define the behavior while Late Static Binding takes care of object creation and type information.
An important edge case: if static:: is called inside an abstract method that itself was not overridden, the resolution still works correctly as long as the original call went through a concrete child class. Late Static Binding remembers the calling class across the entire method call stack, not just for the immediately called method. That fundamentally distinguishes it from simply passing the class name as a parameter.
6. get_called_class() and Reflection in Detail
Alongside static:: itself, PHP provides get_called_class(), a function that delivers the same result as static::class, but also works in contexts where no class name literally appears in the code, for instance in generic utility functions. Both mechanisms are based on the same internal concept: PHP remembers at runtime which class a static method was actually called through, and makes that information available via Late Static Binding.
For deeper introspection, Late Static Binding is frequently combined with ReflectionClass. A call like new ReflectionClass(static::class) inside a base class method allows inspecting attributes, constants or methods of the actually calling child class, without the base class ever needing to know which child classes exist. This combination of Late Static Binding and reflection forms the backbone of many dependency injection containers and ORM libraries that need to read metadata from class annotations or attributes at runtime.
<?php
declare(strict_types=1);
abstract class SerializableEntity
{
// get_called_class() behaves identically to static::class here
public static function describe(): string
{
$calledClass = get_called_class();
$reflection = new ReflectionClass($calledClass);
$properties = array_map(
fn (ReflectionProperty $p): string => $p->getName(),
$reflection->getProperties()
);
return sprintf(
'%s has properties: %s',
$calledClass,
implode(', ', $properties)
);
}
}
final class InvoiceEntity extends SerializableEntity
{
private int $id;
private float $total;
}
echo InvoiceEntity::describe();
// "InvoiceEntity has properties: id, total" — resolved via Late Static Binding
7. Traits and Late Static Binding Together
Traits behave transparently with Late Static Binding: a trait's code is copied into the using class at compile time, which is why static:: inside a trait works exactly as if the code were written directly in the class that uses the trait. That makes traits an ideal tool for providing reusable factory logic or fluent interface building blocks that automatically point to the right class through Late Static Binding.
A practical example is a Singleton trait that provides a static getInstance() method via Late Static Binding. Every class that uses this trait automatically gets its own, correctly typed singleton instance, without the trait itself needing to know anything about the concrete class names. Importantly, the internal static property holding the instance exists separately per class, PHP automatically duplicates static properties of traits for each using class, which combined with Late Static Binding leads to a clean, class-specific singleton implementation.
<?php
declare(strict_types=1);
trait SingletonTrait
{
private static ?self $instance = null;
// Late Static Binding ensures each using class gets its own instance
public static function getInstance(): static
{
if (static::$instance === null) {
static::$instance = new static();
}
return static::$instance;
}
private function __construct()
{
}
}
final class ConfigRegistry
{
use SingletonTrait;
private array $values = [];
public function set(string $key, mixed $value): void
{
$this->values[$key] = $value;
}
}
$config = ConfigRegistry::getInstance(); // Late Static Binding via trait
8. Common Mistakes and Antipatterns
The most common mistake in working with Late Static Binding is unreflected use of self:: where static:: is actually needed. The symptom usually shows up late: a factory method works fine for the base class, but returns the wrong object instance for every child class. Since PHP does not throw an error here but silently returns the base class, such bugs often go unnoticed until type checks fail somewhere else in the code.
A second, more subtle mistake concerns static properties in combination with Late Static Binding. If a static property is declared in the base class and not redeclared in every child class, all child classes share the same storage for that property, even if access happens through static::. Late Static Binding correctly resolves the method call, but that does not change the fact that PHP manages static properties per class declaration, not per logical inheritance level. Anyone who needs class-specific static state must explicitly redeclare the property in every child class.
A third antipattern is overusing Late Static Binding for cases where dependency injection would actually be the cleaner solution. Static factory methods with new static() are convenient, but they significantly complicate mocking in tests because the concrete class stays hard-wired in the code. For objects with complex dependencies or high testing needs, a constructor-based approach with explicit dependency injection is often the better choice, while Late Static Binding plays to its strengths mostly with simple, stateless value objects and collections.
9. self:: vs. static:: vs. $this-> Compared
The choice between self::, static:: and $this-> has different consequences for inheritance and polymorphism in every concrete case. The following table summarizes the key differences to keep in mind when applying Late Static Binding in practice.
| Context | self:: | static:: (Late Static Binding) | $this-> |
|---|---|---|---|
| Binding happens | At compile time, to defining class | At runtime, to calling class | At runtime, to object instance |
| Works without object | Yes, usable statically | Yes, usable statically | No, requires instance |
| Polymorphic under inheritance | No, always base class | Yes, calling class | Yes, via object type |
| Typical use | Constants, private helpers | Factory methods, ActiveRecord | Fluent interfaces at instance level |
| Testability | Low, hard-wired | Medium, class stays hard-wired | High with DI construction |
This comparison shows: Late Static Binding via static:: is always the right choice whenever a static method should react polymorphically to the calling class. self:: remains useful for cases where the defining class is deliberately meant, such as private helper methods or class constants that should not be overridden. Anyone who uses both mechanisms consciously and deliberately avoids most of the mistakes that occur in practice with Late Static Binding.
Mironsoft
PHP architecture, OOP design, and Magento development
Clean PHP class hierarchies with correct Late Static Binding?
We audit existing PHP codebases for incorrect self:: instead of static:: usage, refactor factory methods and ActiveRecord patterns, and build testable, correctly inheriting class hierarchies for your project.
Code Audit
Static analysis for incorrect self::/static:: usage across inheritance hierarchies
Refactoring
Migrating factory methods and ActiveRecord patterns to correct Late Static Binding
Architecture Consulting
Guidance on choosing between inheritance with Late Static Binding and dependency injection
10. Summary
Late Static Binding solves a very concrete problem: static methods should behave polymorphically under inheritance, even though there is no object whose runtime type could be queried. With static:: instead of self::, PHP remembers at runtime which class a method was actually called through, and resolves subsequent references to that class rather than to the class where the code physically lives. This property is the foundation for factory methods, ActiveRecord patterns, and many forms of fluent interfaces that need to remain extensible through inheritance.
The biggest pitfalls arise when developers use self:: out of habit even though polymorphic behavior is desired, or when they confuse Late Static Binding with static properties, whose storage PHP manages per class regardless of the binding type. Anyone who has internalized these differences can build inheritance hierarchies that behave correctly and predictably, no matter how deep the concrete child class sits in the hierarchy.
Late Static Binding — The Key Points at a Glance
Core Principle
static:: resolves at runtime based on the calling class, self:: always resolves at compile time based on the defining class.
Main Use Case
Factory methods and ActiveRecord patterns where new static() needs to return the correct child class.
Tools
get_called_class() and static::class return the same value, combinable with ReflectionClass for metadata.
Limitation
Static properties are managed per class declaration, not automatically separated per child class by Late Static Binding.