static:: statt self:: richtig einsetzen
Wer in PHP Vererbungshierarchien mit Factory-Methoden oder ActiveRecord-Mustern baut, stößt unweigerlich auf den Unterschied zwischen self:: und static::. Late Static Binding löst genau das Problem, dass self:: immer an die Klasse gebunden bleibt, in der die Methode definiert wurde, statt an die Klasse, über die sie tatsächlich aufgerufen wurde.
Inhaltsverzeichnis
- 1. Was Late Static Binding wirklich löst
- 2. Warum self:: an seine Grenzen stößt
- 3. Late Static Binding in Factory-Methoden
- 4. ActiveRecord-Muster mit static:: aufbauen
- 5. Late Static Binding mit abstrakten Klassen kombinieren
- 6. get_called_class() und Reflection im Detail
- 7. Traits und Late Static Binding im Zusammenspiel
- 8. Typische Fehler und Antipatterns
- 9. self:: vs. static:: vs. $this-> im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was Late Static Binding wirklich löst
Late Static Binding ist ein Mechanismus in PHP, der seit Version 5.3 verfügbar ist und das Schlüsselwort static:: von der lexikalischen Bindung von self:: entkoppelt. Der Kern des Problems: Wenn eine Methode innerhalb einer Basisklasse self::method() aufruft, bezieht sich dieser Aufruf immer auf genau die Klasse, in der die Methode geschrieben wurde, unabhängig davon, über welche Kindklasse sie tatsächlich aufgerufen wurde. Late Static Binding ersetzt diese starre Bindung durch eine, die zur Laufzeit anhand der aufrufenden Klasse aufgelöst wird.
Der Name ist Programm: "Late" bedeutet, dass die Bindung erst spät, also zur Laufzeit beim tatsächlichen Aufruf, erfolgt, statt früh beim Schreiben des Codes festgelegt zu werden. Das klingt zunächst nach einem akademischen Detail, hat aber massive praktische Auswirkungen überall dort, wo Vererbungshierarchien mit statischen Methoden kombiniert werden, etwa bei Factory-Methoden, ActiveRecord-Implementierungen oder Fluent Interfaces mit statischen Einstiegspunkten. Ohne Late Static Binding müsste jede Kindklasse ihre eigene Factory-Methode neu implementieren, nur um die richtige Klasse zu instanziieren.
In diesem Artikel arbeiten wir uns systematisch durch Late Static Binding: von der historischen Motivation über konkrete Anwendungsfälle in Factories und ActiveRecord-Mustern bis zu den Fallstricken, die selbst erfahrene PHP-Entwickler regelmäßig übersehen. Am Ende hast du ein belastbares mentales Modell dafür, wann static:: die richtige Wahl ist und wann self:: tatsächlich das gewünschte Verhalten liefert.
2. Warum self:: an seine Grenzen stößt
Um Late Static Binding wirklich zu verstehen, muss man zuerst verstehen, was self:: tatsächlich tut. self:: ist eine kompilierzeitliche Referenz auf die Klasse, in der der Code physisch steht. Wird eine Methode in einer Basisklasse mit self::create() geschrieben und diese Basisklasse von einer Kindklasse geerbt, ruft self::create() immer noch die Methode der Basisklasse auf, selbst wenn die Kindklasse eine eigene create()-Methode überschrieben hat. Das ist kein Bug, sondern exakt die Semantik, die vor PHP 5.3 fest verdrahtet war.
Das Problem wird konkret, sobald man polymorphes Verhalten bei statischen Methodenaufrufen erwartet. Bei Instanzmethoden löst $this->method() dank virtueller Methodentabellen ganz natürlich polymorph auf: Der Aufruf landet immer bei der überschriebenen Methode der tatsächlichen Objektklasse. Bei statischen Aufrufen mit self:: existiert diese Polymorphie nicht, weil es kein Objekt gibt, dessen Laufzeittyp befragt werden könnte. Late Static Binding wurde exakt eingeführt, um diese Lücke zu schließen, ohne die generelle Semantik von self:: für alle anderen Fälle zu verändern.
<?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-Methoden
Der klassische Anwendungsfall für Late Static Binding ist die statische Factory-Methode. Eine Basisklasse definiert eine create()-Methode, die ein neues Objekt der aufrufenden Klasse zurückgeben soll, nicht der Basisklasse selbst. Ohne Late Static Binding müsste jede Kindklasse diese Methode duplizieren, nur um new static() durch den korrekten Klassennamen zu ersetzen. Mit Late Static Binding reicht eine einzige Implementierung in der Basisklasse, die für beliebig viele Kindklassen korrekt funktioniert.
Diese Technik ist besonders wertvoll in Bibliotheken und Frameworks, die Erweiterbarkeit über Vererbung anbieten. Ein häufiges Beispiel ist eine abstrakte Collection-Klasse mit einer statischen fromArray()-Methode. Jede konkrete Collection-Unterklasse, etwa eine ProductCollection oder OrderCollection, erbt diese Methode unverändert und erhält trotzdem beim Aufruf korrekt eine Instanz der eigenen Klasse zurück, nicht der abstrakten Basisklasse. Das spart nicht nur Code, sondern verhindert auch eine ganze Klasse von Bugs, die entstehen, wenn eine Kindklasse die Factory-Methode vergisst zu überschreiben.
<?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. ActiveRecord-Muster mit static:: aufbauen
Ein weiteres Paradebeispiel für Late Static Binding ist das ActiveRecord-Muster, bei dem Modellklassen statische Finder-Methoden wie find(), findOrFail() oder query() bereitstellen. Diese Methoden werden in einer gemeinsamen Basisklasse definiert, müssen aber je nach aufrufender Modellklasse unterschiedliche Tabellen ansprechen und unterschiedliche Objekttypen zurückgeben. Late Static Binding macht genau das möglich, ohne dass jedes Modell seine eigene Kopie dieser Logik pflegen muss.
Wichtig dabei ist, dass nicht nur das Instanziieren, sondern auch Metadaten wie der Tabellenname über Late Static Binding aufgelöst werden können. Eine statische Methode getTableName(), die per Konvention aus dem Klassennamen ableitet, funktioniert nur korrekt, wenn sie mit static::class statt self::class arbeitet. Diese Kombination aus Objekterzeugung und Metadatenauflösung über Late Static Binding ist das Fundament praktisch jeder ORM-ähnlichen Bibliothek, die auf Vererbung statt auf Composition setzt.
<?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. Late Static Binding mit abstrakten Klassen kombinieren
Abstrakte Klassen und Late Static Binding ergänzen sich hervorragend, weil abstrakte Klassen per Definition niemals selbst instanziiert werden. Eine statische Factory-Methode in einer abstrakten Basisklasse mit new static() funktioniert deshalb immer korrekt, weil static:: zur Laufzeit ausschließlich auf konkrete Kindklassen aufgelöst wird. Es gibt keinen Fall, in dem new static() versucht, die abstrakte Klasse selbst zu instanziieren, PHP verhindert das ohnehin durch einen Fatal Error.
Ein sauberes Design nutzt diese Eigenschaft gezielt aus: Die abstrakte Basisklasse definiert Template-Methoden, die interne Details wie Validierung oder Persistenz regeln, während Late Static Binding sicherstellt, dass alle statischen Rückgabewerte und Metadaten korrekt auf die konkrete Kindklasse zeigen. Dieses Muster taucht häufig in Kombination mit dem Template-Method-Pattern auf, bei dem abstrakte Methoden das Verhalten definieren, während Late Static Binding die Objekterzeugung und Typinformation übernimmt.
Ein wichtiger Randfall: Ruft man static:: innerhalb einer abstrakten Methode auf, die selbst nicht überschrieben wurde, funktioniert die Auflösung trotzdem korrekt, solange der ursprüngliche Aufruf über eine konkrete Kindklasse erfolgte. Late Static Binding merkt sich die aufrufende Klasse über den gesamten Methodenaufrufstapel hinweg, nicht nur für die unmittelbar aufgerufene Methode. Das unterscheidet es fundamental von einer einfachen Parameterübergabe des Klassennamens.
6. get_called_class() und Reflection im Detail
Neben static:: selbst stellt PHP mit get_called_class() eine Funktion bereit, die dasselbe Ergebnis liefert wie static::class, aber auch in Kontexten funktioniert, in denen kein Klassenname literal im Code steht, etwa in generischen Utility-Funktionen. Beide Mechanismen basieren auf demselben internen Konzept: PHP merkt sich zur Laufzeit, über welche Klasse eine statische Methode tatsächlich aufgerufen wurde, und stellt diese Information über Late Static Binding zur Verfügung.
Für tiefergehende Introspektion kombiniert man Late Static Binding häufig mit der ReflectionClass. Ein Aufruf wie new ReflectionClass(static::class) innerhalb einer Basisklassenmethode erlaubt es, Attribute, Konstanten oder Methoden der tatsächlich aufrufenden Kindklasse zu inspizieren, ohne dass die Basisklasse überhaupt wissen muss, welche Kindklassen existieren. Diese Kombination aus Late Static Binding und Reflection bildet das Rückgrat vieler Dependency-Injection-Container und ORM-Bibliotheken, die Metadaten aus Klassenannotationen oder Attributen zur Laufzeit auslesen müssen.
<?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 und Late Static Binding im Zusammenspiel
Traits verhalten sich bei Late Static Binding transparent: Der Code eines Traits wird zur Kompilierzeit in die verwendende Klasse hineinkopiert, weshalb static:: innerhalb eines Traits genauso funktioniert, als stünde der Code direkt in der Klasse, die den Trait einbindet. Das macht Traits zu einem idealen Werkzeug, um wiederverwendbare Factory-Logik oder Fluent-Interface-Bausteine bereitzustellen, die über Late Static Binding automatisch auf die richtige Klasse zeigen.
Ein praktisches Beispiel ist ein Singleton-Trait, der eine statische getInstance()-Methode über Late Static Binding bereitstellt. Jede Klasse, die diesen Trait einbindet, erhält automatisch ihre eigene, korrekt typisierte Singleton-Instanz, ohne dass der Trait selbst irgendetwas über die konkreten Klassennamen wissen muss. Wichtig ist dabei, dass die interne statische Property, die die Instanz hält, pro Klasse getrennt existiert, PHP dupliziert statische Properties von Traits automatisch für jede einbindende Klasse, was mit Late Static Binding zusammen zu einer sauberen, klassenspezifischen Singleton-Implementierung führt.
<?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. Typische Fehler und Antipatterns
Der häufigste Fehler im Umgang mit Late Static Binding ist die unreflektierte Verwendung von self::, wo eigentlich static:: benötigt wird. Das Symptom zeigt sich meist erst spät: Eine Factory-Methode funktioniert für die Basisklasse einwandfrei, liefert aber bei jeder Kindklasse die falsche Objektinstanz zurück. Da PHP hier keinen Fehler wirft, sondern still die Basisklasse zurückgibt, bleiben solche Bugs häufig unentdeckt, bis Typprüfungen an anderer Stelle im Code fehlschlagen.
Ein zweiter, subtilerer Fehler betrifft statische Properties in Kombination mit Late Static Binding. Wird eine statische Property in der Basisklasse deklariert und nicht in jeder Kindklasse neu deklariert, teilen sich alle Kindklassen denselben Speicher für diese Property, selbst wenn der Zugriff über static:: erfolgt. Late Static Binding löst zwar den Methodenaufruf korrekt auf, ändert aber nichts an der Tatsache, dass PHP statische Properties pro Klassendeklaration und nicht pro logischer Vererbungsebene verwaltet. Wer klassenspezifische statische Zustände braucht, muss die Property explizit in jeder Kindklasse redeklarieren.
Ein dritter Antipattern ist der übermäßige Einsatz von Late Static Binding für Fälle, in denen eigentlich Dependency Injection die sauberere Lösung wäre. Statische Factory-Methoden mit new static() sind praktisch, erschweren aber das Mocken in Tests erheblich, weil die konkrete Klasse fest im Code verdrahtet bleibt. Für Objekte mit komplexen Abhängigkeiten oder hohem Testbedarf ist ein Constructor-basierter Ansatz mit expliziter Dependency Injection oft die bessere Wahl, während Late Static Binding seine Stärken vor allem bei einfachen, zustandslosen Werteobjekten und Collections ausspielt.
9. self:: vs. static:: vs. $this-> im Vergleich
Die Wahl zwischen self::, static:: und $this-> hat in jedem konkreten Fall unterschiedliche Konsequenzen für Vererbung und Polymorphie. Die folgende Tabelle fasst die wichtigsten Unterschiede zusammen, die bei der praktischen Anwendung von Late Static Binding zu beachten sind.
| Kontext | self:: | static:: (Late Static Binding) | $this-> |
|---|---|---|---|
| Bindung erfolgt | Zur Kompilierzeit, an definierende Klasse | Zur Laufzeit, an aufrufende Klasse | Zur Laufzeit, an Objektinstanz |
| Funktioniert ohne Objekt | Ja, statisch nutzbar | Ja, statisch nutzbar | Nein, benötigt Instanz |
| Polymorph bei Vererbung | Nein, immer Basisklasse | Ja, aufrufende Klasse | Ja, über Objekttyp |
| Typischer Einsatz | Konstanten, private Helper | Factory-Methoden, ActiveRecord | Fluent Interfaces auf Instanzebene |
| Testbarkeit | Gering, hart verdrahtet | Mittel, Klasse bleibt fest verdrahtet | Hoch bei DI-Konstruktion |
Diese Gegenüberstellung zeigt: Late Static Binding über static:: ist immer dann die richtige Wahl, wenn eine statische Methode polymorph auf die aufrufende Klasse reagieren soll. self:: bleibt sinnvoll für Fälle, in denen bewusst immer die definierende Klasse gemeint ist, etwa bei privaten Hilfsmethoden oder Klassenkonstanten, die nicht überschrieben werden sollen. Wer beide Mechanismen bewusst und gezielt einsetzt, vermeidet die meisten Fehler, die im Zusammenhang mit Late Static Binding in der Praxis auftreten.
Mironsoft
PHP-Architektur, OOP-Design und Magento-Entwicklung
Saubere PHP-Klassenhierarchien mit korrektem Late Static Binding?
Wir prüfen bestehende PHP-Codebasen auf self:: statt static:: Fehlnutzung, refaktorieren Factory-Methoden und ActiveRecord-Muster und bauen testbare, korrekt vererbende Klassenhierarchien für euer Projekt.
Code-Audit
Statische Analyse auf fehlerhafte self::/static::-Nutzung in Vererbungshierarchien
Refactoring
Factory-Methoden und ActiveRecord-Muster auf korrektes Late Static Binding umstellen
Architektur-Beratung
Entscheidungshilfe zwischen Vererbung mit Late Static Binding und Dependency Injection
10. Zusammenfassung
Late Static Binding löst ein sehr konkretes Problem: Statische Methoden sollen sich bei Vererbung polymorph verhalten, obwohl es kein Objekt gibt, dessen Laufzeittyp befragt werden könnte. Mit static:: statt self:: merkt sich PHP zur Laufzeit, über welche Klasse eine Methode tatsächlich aufgerufen wurde, und löst nachfolgende Referenzen auf diese Klasse auf, statt auf die Klasse, in der der Code physisch steht. Diese Eigenschaft ist die Grundlage für Factory-Methoden, ActiveRecord-Muster und viele Formen von Fluent Interfaces, die über Vererbung erweiterbar bleiben sollen.
Die größten Stolperfallen entstehen, wenn Entwickler self:: aus Gewohnheit einsetzen, obwohl polymorphes Verhalten gewünscht ist, oder wenn sie Late Static Binding mit statischen Properties verwechseln, deren Speicher PHP unabhängig von der Bindungsart pro Klasse verwaltet. Wer diese Unterschiede verinnerlicht hat, kann Vererbungshierarchien bauen, die sich korrekt und vorhersagbar verhalten, egal wie tief die konkrete Kindklasse in der Hierarchie steht.
Late Static Binding — Das Wichtigste auf einen Blick
Grundprinzip
static:: löst zur Laufzeit anhand der aufrufenden Klasse auf, self:: immer zur Kompilierzeit anhand der definierenden Klasse.
Haupt-Anwendungsfall
Factory-Methoden und ActiveRecord-Muster, bei denen new static() die korrekte Kindklasse zurückgeben muss.
Werkzeuge
get_called_class() und static::class liefern denselben Wert, kombinierbar mit ReflectionClass für Metadaten.
Grenze
Statische Properties werden pro Klassendeklaration verwaltet, nicht automatisch durch Late Static Binding pro Kindklasse getrennt.