How to implement an autoloader in a legacy php application

On this post,

PSR-0

There are many different autoloader recommendations in PHP land. The one we will be using to modernize our legacy application is based on something called PSR-01.

PSR-0 is a PHP Framework Interoperability Group2 recommendation for structuring your class files. The recommendation rises out of a long history of projects using the “class-to-file” naming convention from the days of PHP 4. Originating with Horde3 and PEAR4, the convention was adopted by early PHP 5 projects such as Solar5 and Zend Framework6, and later by projects such as Symfony27.

We use PSR-0 instead of the newer PSR-48 recommendation because we are dealing with legacy code, code that was probably developed before PHP 5.3 namespaces came into being. Code written before PHP 5.3 did not have access to namespace separators, so authors following the “class-to-file” naming convention would typically use underscores in class names as a “pseudo-namespace” separator. PSR-0 makes an allowance for older non-PHP-5.3 pseudo-namespaces, making it more suitable for our legacy needs, whereas PSR-4 does not.

Under PSR-0, the class name maps directly to a file system sub-path. Given a fully-qualified class name, any PHP 5.3 namespace separators are converted to directory separators, and underscores in the class portion of the name are also converted to directory separators. (Underscores in the namespace portion proper are not converted to directory separators.) The result is prefixed with a base directory location, and suffixed with .php, to create a file path where the class file may be found. For example, the fully-qualified class name \Foo\Bar\Baz_Dib would be found in a sub-path named Foo/Bar/Baz/Dib.php on a UNIX-style file system.

A Single Location For Classes

Before we implement a PSR-0 autoloader, we need to pick a directory location in the codebase to hold every class that will ever be used in the codebase. Some projects already have such a location; it may be called “includes,” “classes,” “src,” “lib,” or something similar.

If a location like that already exists, examine it carefully. Does it have only class files in it, or is it a combination of class files and other kinds of files? If it has anything besides class files in it, or if no such location exists, create a new directory location and call it “classes” (or some other properly descriptive name).

This directory will be the central location for all classes used throughout the project. Later, we will begin moving classes from their scattered locations in the project to this central location.

Add Autoloader Code

Once we have a central directory location for our class files, we need to set up an autoloader to look in that location for classes. We can create the autoloader as a static method, an instance method, an anonymous function, or a regular global function. (Which one we use is not as important as actually doing the autoloading.) Then we will register it with spl_autoload_register() early in our bootstrap or setup code, before any classes are called.

As A Global Function

Perhaps the most straightforward way to implement our new autoloader code is as a global function. Below, we find the autoloader code to use; the function name is prefixed with mlaphp_ to make sure it does not conflict with any existing function names.

// ... setup code ... setup.php
<?php
// Define an autoloader function in the global namespace
function mlaphp_autoloader($class)
{
    // Strip off any leading namespace separator from PHP 5.3
    $class = ltrim($class, '\\');

    // The eventual file path
    $subpath = '';

    // Check for a PHP 5.3 namespace separator
    $pos = strrpos($class, '\\');
    if ($pos !== false) {
        // Convert namespace separators to directory separators
        $ns = substr($class, 0, $pos);
        $subpath = str_replace('\\', DIRECTORY_SEPARATOR, $ns) . DIRECTORY_SEPARATOR;

        // Remove the namespace portion from the final class name
        $class = substr($class, $pos + 1);
    }

    // Convert underscores in the class name to directory separators
    $subpath .= str_replace('_', DIRECTORY_SEPARATOR, $class);

    // The path to the central class directory location
    $dir = '/path/to/app/classes';

    // Prefix with the central directory location and suffix with .php,
    // then require it
    $file = $dir . DIRECTORY_SEPARATOR . $subpath . '.php';
    require $file;
}

// Register the autoloader with SPL
spl_autoload_register('mlaphp_autoloader');
?>

Note that the $dir variable represents an absolute directory as the base path for our central class directory. As an alternative on PHP 5.3 and later, it is perfectly acceptable to use the DIR constant in that variable so the absolute path is no longer hard-coded, but is instead relative to the file where the function is located. For example:

<?php
 // go "up one directory" for the central classes location
 $dir = dirname(__DIR__) . '/classes';
?>

If you are stuck on PHP 5.2 for some reason, the DIR constant is not available. You can replace dirname(__DIR__) with dirname(dirname(__FILE__)) in that case.

As A Closure

If we are using PHP 5.3, we can create the autoloader code as a closure and register it with SPL in a single step:

// setup.php
<?php
// setup code
spl_autoload_register(function ($class) {
    // ... the same code as in the global function ...
});

// ... other setup code ...

As A Static or Instance Method

This is my preferred way of setting up an autoloader. Instead of using a function, we create the autoloader code in a class as an instance method or a static method. I recommend instance methods over static ones, but your situation will dictate which is more appropriate.

First, we create our autoloader class file in our central class directory location. If we are using PHP 5.3 or later, we should use a proper namespace; otherwise, we use underscores as pseudo-namespace separators.

The following is a PHP 5.3 example. Under versions earlier than PHP 5.3, we would omit the namespace declaration and name the class Mlaphp_Autoloader. Either way, the file should be in the sub-path Mlaphp/Autoloader.php.

/path/to/app/classes/Mlaphp/Autoloader.php

<?php
namespace Mlaphp;

class Autoloader
{
    // An instance method alternative
    public function load($class)
    {
        // Implementation goes here
    }
    
    // a static method alternative
    static public function loadStatic($class)
    {
        // ... the same code as in the global function ...
    }
}
?>

Then, in the setup or bootstrap file, require_once the class file, instantiate it if needed, and register the method with SPL. Note that we use the array-callable format here, where the first array element is either a class name or an object instance, and the second element is the method to call:

setup.php

<?php
// Require the autoloader class file
require_once '/path/to/app/classes/Mlaphp/Autoloader.php';

// STATIC OPTION: Register a static method with SPL
spl_autoload_register(['Mlaphp\Autoloader', 'loadStatic']);

// INSTANCE OPTION: Create the instance and register the method with SPL
$autoloader = new \Mlaphp\Autoloader();
spl_autoload_register([$autoloader, 'load']);

// ... other setup code ...
?>

Please pick either an instance method or a static method, not both. The one is not a fallback for the other.

Using The __autoload() Function

If we are stuck on PHP 5.0 for some reason, we can use the autoload() function in place of the SPL autoloader registry. There are drawbacks to doing things this way, but under PHP 5.0 it is our only alternative. We do not need to register it with SPL (in fact, we cannot, since SPL was not introduced until PHP 5.1). We will not be able to mix-and-match other autoloaders in this implementation; only one autoload() function is allowed. If an __autoload() function is already defined, we will need to merge this code with any code already existing in the function.

setup.php

<?php
// ... setup code ...

// Define an __autoload() function
function __autoload($class)
{
    // ... the global function code ...
}

// ... other setup code ...
?>

I strongly recommend against using this kind of implementation in PHP 5.1 and later.

Autoloader Priority

Regardless of how we implement our autoloader code, we need it to be available before any classes get called in the codebase. It cannot hurt to register the autoloader as one of the very first bits of logic in our codebase, probably in a setup or bootstrap script.

Common Questions
What If I Already Have An Autoloader?

Some legacy applications may already have a custom autoloader in place. If this is our situation, we have some options:

1. Usetheexistingautoloaderas-is.Thisisourbestoptionifthereisalreadyacentraldirectorylocation for the application class files.

2. Modify the existing autoloader to add PSR-0 behavior. This is a good option if the autoloader does not conform to PSR-0 recommendations.

3. Register the PSR-0 autoloader described in this chapter with SPL in addition to the existing autoloader. This is another good option when the existing autoloader does not conform to PSR-0 recommendations.

Other legacy codebases may have a third-party autoloader in place, such as Composer. If Composer is present, we can obtain its autoloader instance and add our central class directory location for autoloading like so:

<?php
// Get the registered Composer autoloader instance from the vendor/ subdirectory
$loader = require '/path/to/app/vendor/autoload.php';

// Add our central class directory location; do not use a class prefix as
// we may have more than one top-level namespace in the central location
$loader->add('', '/path/to/app/classes');
?>

With that, we can co-opt Composer for our own purposes, making our own autoloader code unnecessary.

What Are The Performance Implications Of Autoloading?

There is some reason to think that using autoloaders may cause a slight performance drag compared to using include, but the evidence is mixed and situation-dependent. If it is true that autoloading is comparatively slower, how big of a performance hit can be expected?

I assert that, when modernizing a legacy application, it is probably not an important consideration. Any performance drag incurred from autoloading is minuscule compared to the other possible performance issues in your legacy application, such as the database interactions.

In most legacy applications, or even in most modern ones, attempting to optimize performance on autoloading is a case of attempting to optimize on the wrong resource. There are other resources that are likely to be worse offenders, just ones that we don’t see or don’t think of.

If autoloading is the single worst performance bottleneck in your legacy application, then you are in fantastic shape. (In that case, you should return this book for a refund, and then tell me if you are hiring, because I want to work for you.)