PHP: Using class_alias to maintain BC while moving/renaming classes

Sometimes you want to rename a class or move it to a different namespace. But as soon as it is used anywhere outside the package, this is breaking backwards compatibility and should not be done lightheartedly.

Luckily there is a way in PHP to have both, the old class and the new class, while deprecating the old one: class_alias().

How to use class_alias() without messing up class autoloading

Let’s say, the old class is OldClass and we want to rename it to NewClass.

First, we rename the class and move it from OldClass.php to a new file NewClass.php.

Now add the following line below the class in the same file (NewClass.php):

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

Note: You can use the ::class constant for nonexistent classes, it just resolves the class name to a fully qualified class name (FQN) with the rules explained in Name resolution rules, without actually looking up the class.

Now we still need the file OldClass.php because it is loaded when a the OldClass class is used. There we trigger the autoloader for the NewClass class using class_exists():

\class_exists(NewClass::class);

You might ask: Why not “class_alias” in the old file which then autoloads the new file? The problem is that type hints do not trigger the autoloader. Similar to the ::class constant, type hints are resolved to a class name which is compared to the class name of the passed object, without actually looking for the type hinted class! That’s why it is important to have the alias in place as soon as the real class is loaded.

Otherwise passing a NewClass instance to the following function would fail because “NewClass” != “OldClass” and the alias is not defined yet:

function oldFunction(OldClass $old)

For the same reason it is a bad idea to keep the old class as subclass of the new one! With a class alias, both class names are treated as the same class. If the deprecated class extends the original class, a type hint against the deprecated class in client code will throw an error if an object of the new class is passed. Also it does not work for abstract classes, final classes, interfaces and traits.

Alternative location for “class_alias()”

Redditor mglaman suggested another approach, that works well if the package is loaded via composer: Collect the aliases in a separate file, deprecated.php and include it additionally with composer:

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

This way you have a nice overview of deprecated code and the new classes don’t contain legacy code.

@deprecated Warning in IDE

It is a good practice to mark deprecated code with the @deprecated docblock annotation. IDEs can recognize those and show a notice if you use deprecated methods or classes. Unfortunately, for alisases this is not possible, but found an IDE friendly workaround:

Add this code after the \class_alias() call:

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

Alternatively, you could collect these pseudo classes in a separate file, like “.ide.php”, which you never load

Note: Don’t be confused by the “class_” function names. The technique does not only work for classes, but just as well for interfaces and traits.

5 Replies to “PHP: Using class_alias to maintain BC while moving/renaming classes”

  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.

Leave a Reply

Your email address will not be published. Required fields are marked *