php[architect] logo

Want to check out an issue? Sign up to receive a special offer.

Generating an Autoloader for a Legacy PHP Codebase

Posted by on September 6, 2017

If you’ve inherited a legacy code base, you may find it does not use an autoloader and has an idiosyncratic directory and file hierarchy for its Classes, Interfaces or Traits. Worse yet, it might not use name spaces consistently or at all. So you can’t use a PSR-4—or even PSR-0—autoloader with your code.

Well, why do you need an autoloader anyway. You’re doing fine with require_once and include_once in your class files, right? For one, each require_once or include_once call causes the PHP engine to check if, well, the requested file has already been loaded. This is a small performance hit. More importantly, keeping track the classes you need to use is tedious and error prone. Why not offload this task to PHP and have it find the right files based on the classes you use and extend? Down the road, this could also help in introducing a Dependency Injection Container and facilitate writing smaller, less-tightly-coupled classes.

Luckily, this is a solved problem, as I found out after posting on twitter:

In this post, I’ll detail the three solutions I found: using Composer’s classmap autoloader, Symfony classmap generator (deprecated), or Zend Framework’s ClassFileLocator.

Working with Legacy Code?

Check out the September 2016 issue of the magazine for more, practical advice to modernize your legacy application.

Legacy Code of the Ancients – Sept. 2016

1. Use Composer

If you want a drop-in, no hassles solution this is it, suggested by @crocodile2u. Unless you have very specific needs, I’d recommend you use it.

You may already be aware that Composer provides a PSR-4 autoloader. Digging into the documentation linked by Victor, I learned how to make a classmap autoloader with Composer. You can configure it to look in one or more directories and it will scan for .php, .inc, and .hh files. It will add any classes, interfaces, and traits it finds to the classmap. You can configure it to exclude specific files or directories too.

{
    "autoload": {
        "classmap": ["../models", "../services"],
        "exclude-from-classmap": ["../services/public"]
    }
}

Then generate it with the following command. If Composer runs into any issues, like classes in different files with the same name, you’ll get a warning.

composer dump-autoload

To use the class map autoloader, load it in your script like any other:

require 'vendor/autoload.php';

If you inspect the generated autoloader class source in vendor/composer/autoload_classmap.php you’ll see something like:

// autoload_classmap.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    'Address' => $baseDir . '/../models/user/address.php',
    // ... 
    'User' => $baseDir . '/../models/user/user.php',
    'UserServices' => $baseDir . '/../private/user/userservices/service.php',
);

2. Symfony 3 Classmap Generator

For completeness sake, I’m including Symfony’s Classmap Generator, but you should be aware that it is deprecated:

The ClassLoader component was deprecated in Symfony 3.3 and it will be removed in 4.0. As an alternative, use any of the class loading optimizations provided by Composer.

Like any other Symfony component, you can install with composer:

composer require symfony/class-loader

Building the classmap array is more work than with using Composer. However, if you need to plug this into a custom autoloader this component is useful. Of course, you need to to implement any filtering or exclusion for directories and files on your own. Below is a quick script I wrote to mimic what Composer did earlier. If you’re code has similarly named classes, or other oddities, you’ll have to account for that on your own too.

<?php
require 'vendor/autoload.php';

use Symfony\Component\ClassLoader\ClassMapGenerator;

$dirs = ["/../models", "/../services"];

$all = [];
foreach ($dirs as $dir) {
    // Gets all the classes (keys) with their files (value) 
    // in this folder
    $map = ClassMapGenerator::createMap(realpath(__DIR__ . $dir));

    // Remove any in the public services dir
    $map = array_filter($map, function($item) {
       if (false === strpos($item, '/services/public')) {
           return true;
       }
       return false;
    });

    $all = array_merge($all, $map);
}

$base = realpath( __DIR__  . '/..') . '/';
$all = array_map(function($item) use ($base) {
    return str_replace($base, '', $item);
}, $all);

// export this as an array
var_export($all);

This outputs the map in a way you can include as PHP code—or copy and paste it.

array (
  'Address' => 'models/user/address.php',
  'User' => 'models/user/user.php',
  'UserServices' => 'services/private/user/userservices/service.php',

3. Zend ClassFileLocator

A third option, pointed out by @mwop, the Zend Framwork Project Lead, is to use ZF’s ClassFileLocator class from the zend-file component. It looks for files ending in .php and will find classes, abstract classes, interfaces, and traits.

Installing zend-file is straightforward:

composer require zendframework/zend-file

Usages is slightly different than Symfony’s component. From what I could tell you have to iterate through things instead of getting the entire array at once. But the end result is the same.

<?php
require 'vendor/autoload.php';

use Zend\File\ClassFileLocator;

$dirs = ["models", "services"];
$base = realpath(__DIR__ . '/..') .'/';
$map = [];

foreach ($dirs as $dir) {
    // Look for classes in the current directory
    $locator = new ClassFileLocator($base . $dir);

    // Check if we want this file
    foreach ($locator as $file) {
        if (false !== strpos($file, '/services/public/')) {
            continue;
        }
        foreach ($file->getClasses() as $class) {
            $map[$class] = str_replace(
                $base, '', $file->getRealPath()
            );
        }
    }
}

// export this as an array
var_export($map);

Conclusion

Now you can easily add an autoloader to your own legacy code base without much hassle. When I first posed this question, I was afraid I might have to write something from scratch, which wouldn’t be super difficult.

Luckily, helpful responses and some searches showed it was a solved problem. Furhtermore, if you dig into the source for these components you’ll see there are edge cases you wouldn’t consider at first, like looking for traits if your code predates PHP 5.4. If you need to add an autoloader to your application, you can see it’s not as difficult as you might think.


Oscar still remembers downloading an early version of the Apache HTTP server at the end of 1995, and promptly asking "Ok, what's this good for?" He started learning PHP in 2000 and hasn't stopped since. He's worked with Drupal, WordPress, Zend Framework, and bespoke PHP, to name a few. Follow him on Google+.

 

Leave a comment

Use the form below to leave a comment: