Last time we finished up our look at Laravel 4.2’s autoloader implementation. Like a lot of features in Laravel, (or any framework), once you pull out the microscope sharp edges begin to jut out everywhere.
However, unlike many other framework teams, the Laravel core team is willing to make shifts in their platform and application architecture. If you’re familiar with the internals of Laravel 4, looking at the internals of Laravel 3 may be a little disorienting. Similarly, the recent release of Laravel 5 presents some new wrinkles at the system level.
Today we’re going to examine changes to the autoloader in Laravel 5, and how they address some of the issues and edge cases we encountered when exploring Laravel 4.2’s autoloader.
Laravel 5 Registered Autoloaders
The first thing we’ll want to do is examine which autoloaders Laravel registers in a typical Laravel 5 HTTP request. One way to do this is to add the following route definition (note the new app/Http/routes.php
location in Laravel 5)
#File: app/Http/routes.php
Route::get('/show-autoloaders', function(){
foreach(spl_autoload_functions() as $callback)
{
if(is_string($callback))
{
echo '- ',$callback,"\n<br>\n";
}
else if(is_array($callback))
{
if(is_object($callback[0]))
{
echo '- ',get_class($callback[0]);
}
elseif(is_string($callback[0]))
{
echo '- ',$callback[0];
}
echo '::',$callback[1],"\n<br>\n";
}
else
{
var_dump($callback);
}
}
});
This code loops over each registered autoloader, and attempts to echo
back something sane, (based on the sort of callback registered). With the above code in place, if you load the following URL in your application
http://laravel5.example.com/index.php/show-autoloaders
You’ll see the following output
- Illuminate\Foundation\AliasLoader::load
- Composer\Autoload\ClassLoader::loadClass
- PhpParser\Autoloader::autoload
- Swift::autoload
So, right off the bat we see some familiar friends. The AliasLoader::load
method is still lazy loading Laravel’s aliases. The Composer ClassLoader::loadClass
method is still taking care of Composer’s four autoloading methodologies, and the Swift::autoload
method is still there, taking care of its legacy duties.
The first change that pops out is the addition of the PhpParser\Autoloader::autoload
method. However, more significant than that — Laravel’s native Illuminate\Support\ClassLoader::load
is gone! This is, by far, the most significant change to the autoloader architecture in Laravel 5. Instead of the dueling Composer and Illuminate autoloaders, Laravel’s thrown all its autoloading eggs in the Composer basket. We’ll talk more about the impact of this below, but first let’s get a quick explanation of the PhpParser\Autoloader::autoload
method out of the way.
PhpParser Autoloader
The PhpParser project is a third party library for creating an abstract syntax tree for PHP files, and is implemented in PHP itself. If that didn’t make sense, it’s a library that makes writing code analysis tools simpler. Laravel itself doesn’t use the PhpParser library for anything, but libraries that Laravel does use have PhpParser listed as a dependency, and this autoloader comes along for the ride.
If we look at the autoload section in the PhpParser composer.json
file
#File: vendor/nikic/php-parser/composer.json
"autoload": {
"files": ["lib/bootstrap.php"]
},
we see that PhpParser uses the files
autoloader type to include
the following file
#File: vendor/nikic/php-parser/lib/bootstrap.php
require __DIR__ . '/PhpParser/Autoloader.php';
PhpParser\Autoloader::register();
As you can see, unlike most of the other files
autoloaders we’ve seen, PhpParser actually uses the files
autoloader for its intended purpose — registering an autoloader. If we take a look at the PhpParse\Autoloader::autoload
method itself
#File: vendor/nikic/php-parser/lib/PhpParser/Autoloader.php
static public function autoload($class) {
if (0 === strpos($class, 'PhpParser\\')) {
$fileName = dirname(__DIR__) . '/' . strtr($class, '\\', '/') . '.php';
if (file_exists($fileName)) {
require $fileName;
}
} else if (0 === strpos($class, 'PHPParser_')) {
if (isset(self::$oldToNewMap[$class])) {
self::registerLegacyAliases();
}
}
}
we see a standard autoloader and a bit of legacy code handling.
The first conditional checks if the requested class is prefixed with PhpParser\\
, and if so, converts the namespace class name into a file path, and then require
s the file. This is the standard autoloader.
The second block of the conditional checks if the class is prefixed with PHPParser_
— i.e., if it’s a class in the global namespace. If so, and the class name exists as a key in self::$oldToNewMap
, the autoloader calls the registerLegacyAliases
method, which looks like this
#File: vendor/nikic/php-parser/lib/PhpParser/Autoloader.php
private static function registerLegacyAliases() {
foreach (self::$oldToNewMap as $old => $new) {
class_alias($new, $old);
}
}
This is a clever bit of refactoring help that’s probably best explained with an example. Let’s say there’s some old code out there that looks like this
$builder = new PHPParser_Builder;
When the PhpParser team wanted to modernize and use classes with namespaces, they could have easily changed
class PHPParser_Builder
{
}
to
namespace PHPParser;
class Builder
{
}
and left it at that.
However, this means anyone using old code like $builder = new PHPParser_Builder
would suddenly have a breaking application. That’s where registerLegacyAlias
comes into play
#File: vendor/nikic/php-parser/lib/PhpParser/Autoloader.php
private static function registerLegacyAliases() {
foreach (self::$oldToNewMap as $old => $new) {
class_alias($new, $old);
}
}
This method runs through every entry in the self::$oldToNewMap
array
#File: vendor/nikic/php-parser/lib/PhpParser/Autoloader.php
private static $oldToNewMap = array(
'PHPParser_Builder' => 'PhpParser\Builder',
'PHPParser_BuilderAbstract' => 'PhpParser\BuilderAbstract',
'PHPParser_BuilderFactory' => 'PhpParser\BuilderFactory',
//...
and registers an alias for the old-style class
#File: vendor/nikic/php-parser/lib/PhpParser/Autoloader.php
class_alias('PhpParser\Builder','PHPParser_Builder');
In plain english? If a user uses the old-style PHPParser_
classes, the PhpParser autoloader will automatically alias that old-style class to a new-style class, and the legacy code will continue to work without any changes. (PHP will automatically attempt to autoload the first argument you pass to class_alias
).
Nothing too out of the ordinary here — just another stand-alone pre-PSR autoloader doing its thing.
Composer Only Autoloading
The bigger change in Laravel is the move to Composer only autoloading. To get our heads around this, we’ll want to look at the stock Laravel application composer.json
autoloading sections
#File: composer.json
"autoload": {
"classmap": [
"database"
],
"psr-4": {
"App\\": "app/"
}
},
"autoload-dev": {
"classmap": [
"tests/TestCase.php"
]
},
as well as the Laravel framework composer.json
#File: vendor/laravel/framework/composer.json
"autoload": {
"classmap": [
"src/Illuminate/Queue/IlluminateQueueClosure.php"
],
"files": [
"src/Illuminate/Foundation/helpers.php",
"src/Illuminate/Support/helpers.php"
],
"psr-4": {
"Illuminate\\": "src/Illuminate/"
}
},
The first, and biggest change? Most classes in Laravel are now loaded with Composer’s psr-4
autoloader. For the framework code, this means Composer loads Illuminate\\
prefixed classes
#File: vendor/laravel/framework/composer.json
"psr-4": {
"Illuminate\\": "src/Illuminate/"
}
from the following folder
vendor/laravel/framework/src/Illuminate/
Previously, the Laravel framework code used a psr-0
autoloader to do the same thing.
More radical is the introduction of the App
namespace for application classes. Thanks to this bit of configuration
#File: composer.json
"psr-4": {
"App\\": "app/"
}
Composer will look for any class prefixed with App\\
in the app/
folder off the root of the project. This is combined with a major overhaul of how a Laravel 5 application works. When you create a Laravel 5 application
$ laravel new my-project
Laravel will automatically create a number of App\\
prefixed classes that make up your application. These changes could be an article in and of themselves, but here’s one example that demonstrates The New One True Way™.
In Laravel 4.2, if you wanted to register a new artisan
command class, you’d edit a simple PHP include file in your app
folder
#File: app/start/artisan.php
Artisan::add(new YourCommandClass);
In Laravel 5, you have an App\Commands\Kernel
class that manages your application configuration, and you register your commands by adding to the commands
array
#File: app/Http/Kernel.php
class Kernel extends ConsoleKernel {
//...
protected $commands = [
'App\Console\Commands\Inspire',
];
}
You’ll also notice Laravel now generates commands in the App\Console\Commands
namespace. In Laravel 4.2 the App\Console\Commands\Inspire
class would have been a global class named AppConsoleCommandsInspire
, or something similar.
By dumping both the Illuminate
and Composer classmap autoloaders, Laravel 5 solves many of the autoloader interaction problems we discussed last time.
Other Application Autoloading Changes
Let’s take another look at Laravel 5’s stock application autoloader sections in composer.json
#File: composer.json
"autoload": {
"classmap": [
"database"
],
"psr-4": {
"App\\": "app/"
}
},
"autoload-dev": {
"classmap": [
"tests/TestCase.php"
]
},
There’s two other things to make note of. First, the database/
folder (now off the root project folder, and not in the app/
folder) is still parsed by Composer’s classmap autoloader, and migrations/seeds classes still live in the global PHP namespace. This means the potential for autoloading conflicts still exists, although with only the database
folder under classmap
autoloading the potential surface area for conflicts is greatly reduced.
The other thing to note is Laravel’s base TestCase
class is still loaded via a classmap autoloader, but has been moved to the autoloader-dev
branch
#File: composer.json
"autoload-dev": {
"classmap": [
"tests/TestCase.php"
]
},
Without getting too deeply into Composer’s “dev” and “no-dev” concepts, this move into autoload-dev
ensures the TestCase
class will not be included when you or Composer calls dumpautoload
with the the --no-dev
option
composer dumpautoload --no-dev
This should only come up when you’re planning your application deployment. The short version is Composer’s “no-dev” mode ensure packages are only downloaded dependencies they need to run, and will not download the additional packages a developer would need to add new code and features to the package.
Laravel Framework Autoloader Changes
Let’s take another look at the new framework composer.json
autoload section
#File: vendor/laravel/framework/composer.json
"autoload": {
"classmap": [
"src/Illuminate/Queue/IlluminateQueueClosure.php"
],
"files": [
"src/Illuminate/Foundation/helpers.php",
"src/Illuminate/Support/helpers.php"
],
"psr-4": {
"Illuminate\\": "src/Illuminate/"
}
},
In addition to the psr-4
section, there’s still a classmap
and files
section. The classmap
autoloader hasn’t changed since 4.2 — it’s still that weird global IlluminateQueueClosure
class defined in a non-PSR compliant path at src/Illuminate/Queue/IlluminateQueueClosure.php
.
The files
autoloader is slightly different from Laravel 4.2, in that there’s now two files included. However, just as in the previous version, neither of these files defines any class autoloaders — instead, these files define a number of top level helper functions. Splitting the helpers into two files looks like an attempt to keep Laravel specific helper functions separated from generic helper function a programmer could use separate from Laravel.
Wrap Up
To an outside observer, the changes in Laravel’s autoloader may seem arbitrary and subject to the capricious whims of open source developers. However, after examining the autoloader implementation in detail, and understanding its shortcomings in Laravel 4, we can quickly understand why Laravel’s core developers made the changes.
This pattern applies to other changes in Laravel 5, as well as changes to any programming framework, language, or system. Knowing how to use your tools is always important, but understanding how your tools are built and how they fall short is one of the best ways to cope with an ever changing technology landscape.