Generating an Autoloader for a Legacy PHP Codebase
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:
PHP twitter hivemind: anyone know of a script to look in an application, show all the classes mapped to file paths?
— Oscar Merida (@omerida) September 5, 2017
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.
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.
Seriously, no code needed: https://t.co/q7aeyCvcs7. Then composer dump-autoload
— Victor Bolshov (@crocodile2u) September 6, 2017
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.
zend-file has a ClassFileLocator that can do this, and we used to have a script that used it to create class maps.
— weierophinney (@mwop) September 6, 2017
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.
I'd expect get_declared_classes() + ReclectionClass::getFileName() to do the right thing. On my phone so I can't write a script
— Frederjack Cheese (@_phred) September 5, 2017
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.
The post Generating an Autoloader for a Legacy PHP Codebase appeared first on php[architect].