PHP: class_alias verwenden um Klassen rückwärtskompatibel zu verschieben/umzubenennen

Manchmal möchte man eine Klasse umbenennen oder sie in einen anderen Namespace verschieben. Aber sobald sie irgendwo außerhalb des Packages verwendet wurde, ist das eine nicht abwärtskompatible Änderung und sollte nicht leichtfertig vorgenommen werden.

Zum Glück gibt es in PHP eine Möglichkeit, beide Klassen gleichzeitig zu nutzen, die alte und die neue, um die alte als “deprecated” zu markieren aber weiterhin nutzen zu können: class_alias().

Wie man class_alias() nutzt, ohne Probleme mit dem Autoloading zu bekommen

Sagen wir, die alte Klasse ist OldClass und wir wollen sie zu NewClass umbenennen.

Als erstes benennen wir die Klasse um und schieben sie von OldClass.php in eine neue Datei NewClass.php.

Dann fügen wir die folgende Zeile unterhalb der Klasse in der selben Datei ein (NewClass.php):

\class_alias(NewClass::class, OldClass::class);

Hinweis: Die ::class Konstante kann auch für nicht-existierende Klassen verwendet werden. Sie löst lediglich den “vollständig qualifizierten Namen” (FQN) auf, mit den Regeln, die in Regeln für Namensauflösung erklärt sind, ohne tatsächlich die Klasse zu laden.

Jetzt brauchen wir immer noch die Datei OldClass.php da sie geladen wird, wenn die Klasse OldClass verwendet wird. Dort triggern wir den Autoloader für die Klasse NewClass mittels class_exists():

\class_exists(NewClass::class);

Du fragst dich vielleicht: Warum nicht “class_alias” direkt in der alten Datei aufrufen, dadurch würde doch auch die neue Datei geladen? Das Problem ist, dass Type Hints den Autoloader nicht triggern. Ähnlich wie ::class Konstanten werden Type Hints nur zu einem Klassennamen aufgelöst, der mit der Klasse des übergebenen Objekts verglichen wird, ohne die Klasse aus dem Type Hint zu laden. Deshalb ist es wichtig, den Alias definiert zu haben, sobald die echte Klasse geladen wurde.

Andernfalls führt folgender Code zu einem Fehler, wenn eine NewClass Instanz übergeben wird, weil “NewClass” != “OldClass” und der Alias noch nicht definiert ist:

function oldFunction(OldClass $old)

Aus dem selben Grund ist es übrigens keine gute Idee, die alte Klasse als Kindklasse der neuen zu behalten! Mit einem class alias werden beide Klassennamen als ein und die selbe Klasse behandelt. Wenn die alte Klasse von der neuen Klasse erbt, wird ein Type Hint gegen die alte Klasse in Client-Code einen Fehler werfen, wenn eine Instanz der neuen Klasse übergeben wird. Außerdem funktioniert es nicht für abstrakte Klassen, Interfaces und Traits.

Alternative Stelle für “class_alias()”

Redditor mglaman schlug einen anderen Ansatz vor, der gut funktioniert, wenn das Package mit Composer geladen wird: Die Aliase in einer separaten Datei sammeln, z.B. deprecated.php und dieese Datei zusätzlich mit Composer inkludieren:

{
    "autoload": {
        "files": ["src/deprecated.php"]
    }
}

Auf diese Weise hat man auch eine schöne Übersicht von veraltetem Code und die neuen Klassen enthalten keinen Legacy Code.

@deprecated Warnung in IDE

Es ist eine bewährte Praxis, veralteten Code mit @deprecated zu markieren. IDEs können diese Annotation erkennen und eine Warnung zeigen, wenn er genutzt wird. Leider ist dies für Klassen und Methoden, aber nicht für Klassen-Aliase möglich. Doch ich habe einen IDE freundlichen Workaround gefunden:

Füge diesen Code nach dem \class_alias() Aufruf ein:

if (! \class_exists(OldClass::class)) {     // äquivalent zu "if(false)"
    /** @deprecated this is an alias for NewClass */
    class OldClass {}
}

Alternativ könnten diese Pseudo-Klassen auch in einer separaten Datei gesammelt werden, z.B. “.ide.php”, die im Code nicht genutzt wird.

Hinweis: Lass dich nicht von den “class_” Funktionsnamen verwirren, die Technik funktioniert nicht nur für Klassen sondern genausogut für Interfaces und Traits.

5 Replies to “PHP: class_alias verwenden um Klassen rückwärtskompatibel zu verschieben/umzubenennen”

  1. Ist es damit dann nicht mehr nötig, alle Funktionsaufrufe durch den Typ der neuen Klasse zu ersetzen ?

    Also nach

    \class_alias(NewClass::class, OldClass::class);

    kann ich weiter

    function foo(OldClass ObjInstance)

    benutzen, oder muß ich alle Funktionen anpassen ?

    1. Richtig, das ist der Sinn dahinter, auch wenn man langfristig die Aufrufe vermutlich ersetzen möchte. So verschafft man sich für das Anpassen Zeit, und kann den Alias zu einem späteren Zeitpunkt löschen, wenn er nicht mehr benötigt wird.

    2. Grundsätzlich geht das, aber das Problem mit TypeHints ist, sie triggern kein Autoloading. Daher muss “OldClass” auf jeden Fall bekannt gemacht werden.

  2. Seitdem PHPStorm class_alias() erkennt, funktioniert das gekapselte @deprecated nicht mehr. Für PHPStorm sind das dann zwei verschiedene Implementationen.

Comments are closed.