Late Static Binding in PHP verstehen: static:: statt self:: richtig einsetzen
AI generated
<?php
8.4
PHP · OOP · Vererbung · Design Patterns
Late Static Binding in PHP verstehen
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.

18 Min. Lesezeit static:: · self:: · get_called_class() · Vererbung PHP 8.x

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.

11. FAQ: Late Static Binding in PHP

1Was ist Late Static Binding genau?
Der Mechanismus, mit dem static:: zur Laufzeit anhand der tatsächlich aufrufenden Klasse aufgelöst wird, statt fest an die definierende Klasse gebunden zu sein. Seit PHP 5.3 verfügbar.
2Wann static:: statt self:: nutzen?
Immer wenn polymorphes Verhalten gewünscht ist, etwa bei Factory-Methoden und ActiveRecord-Findern, die die aufrufende Kindklasse berücksichtigen sollen.
3get_called_class() vs. static::class?
Beide liefern identische Ergebnisse. get_called_class() ist die Funktion, static::class die modernere direkte Syntax für dieselbe Late Static Binding Auflösung.
4Funktioniert es in Traits?
Ja, Trait-Code wird zur Kompilierzeit in die Klasse kopiert, static:: verhält sich innerhalb des Traits identisch wie direkt in der Klasse geschrieben.
5Warum liefert new static() die falsche Klasse?
Meist weil irgendwo im Aufrufstapel self:: statt static:: benutzt wurde. Sobald ein Zwischenschritt self:: nutzt, bricht die Auflösungskette ab dieser Stelle ab.
6Teilen Kindklassen statische Properties?
Ja, sofern nicht redeklariert. Statische Properties werden pro Klassendeklaration verwaltet, Late Static Binding betrifft nur Methoden und Instanziierung.
7Sicher mit abstrakten Klassen?
Ja, sogar ideal, da abstrakte Klassen nie direkt instanziiert werden können und new static() daher immer auf eine konkrete Kindklasse zeigt.
8Erschwert es Mocking in Tests?
Ja, statische Factory-Methoden sind schwerer zu mocken als Constructor-basierte Dependency Injection, weil die Klasse fest im Code verdrahtet bleibt.
9Kombinierbar mit Reflection?
Ja, ein sehr verbreitetes Muster: new ReflectionClass(static::class) erlaubt Introspektion der aufrufenden Kindklasse ohne konkrete Klassennamen zu kennen.
10Alternative zu Interfaces?
Nein, unterschiedliche Probleme: Interfaces definieren einen Vertrag über Klassen hinweg, Late Static Binding löst polymorphes Verhalten bei statischen Aufrufen innerhalb einer Hierarchie.