We’re jumping in mid-steam this week, so be sure to catch the first half of this article, as well as the initial article that kicked off the series.
PSR Autoloaders
With both the files
and classmap
autoloader, you may wonder why Composer needs any additional autoloaders. The classmap
autoloader seems like it would work for any PHP framework, library, or application built in PHP.
While classmaps are great for computers — they leave the PHP programmer holding the bag as far as sane and consistent class naming goes. Within an individual project a classmap autoloader and a little discipline on the part of the team will be enough to keep class names under control, but when each of these smart, sane class loading methodologies are mashed up into an individual project, it can look like chaos. The PSR autoloading standards are as much for humans as they are for computers. We’ll start by looking at PSR-0.
You can read the entire specification on the PHP-FIG site, but here’s a quick (non-comprehensive summary).
- Classes must use namespaces
- The top level namespace is an identifying vendor name (
Pulsestorm\Someclass
) - Namespace separators are turned into directory separators
- Underscores are turned into separators unless they’re in the vendor name
- Files have a
\.php
extension
This means a class like
Pulsestorm\Some\Class\Is_Here
would be autoload transformed into the file path
Pulsestorm/Some/Class/Is/Here.php
Nothing to rocket-sciencey in there, again, the real value of an autoloading standard is that it’s the same everywhere making it one bit of behind the scenes piping we can ignore while working on our projects.
While PSR-0 does a good job of laying out the class transformation, there’s one giant hold in the standard that Composer needs to solve, and that’s an include path. While the PSR-0 standard makes mention of a
/path/to/project/lib/vendor
folder, the standard itself doesn’t really address where to look for these files. Even if /path/to/project/lib/vendor
was a part of the standard, it doesn’t solve things for Composer, because Composer doesn’t have the liberty of dropping all its packages into the same top level folder.
It’s also worth noting that the PSR-0 standard is officially depreciated in favor of PSR-4, and that the Composer source is reminiscent of a battleground where a lot of the early PHP-FIG developers worked out their autoloading lessons. If you’re starting a new project, PSR-4 is the way to go. That said, the vast majority of projects out there are still using the PSR-0 standard, and learning how Composer handles this is time well spent.
Covering every facet of PSR-0 autoloading would be an endeavor plagued with mental overload, so consider this a guide through the most well worn paths. Don’t be afraid to branch off on your own if you’re curious about a weird looking bit of code deep in the bowels of Composer.
Composer’s PSR-0 Autoloading Implementation
As a package developer, if you want to add PSR-0 autoloading to your package, you’d add something like this to composer.json
.
#File: composer.json
"autoload": {
"psr-0": [
"Microsoft\\": "path/to/source"
]
},
That is, an object property named psr-0
will contain a javascript array ([]
) of key/value pairs. The key is the vendor namespace required by the PSR-0 standard (an unlikely “Microsoft
” above). The value is the path where you can find the PSR-0 named classes in your project. The path is relative from the base folder of your project. Although it’s a common convention to name this folder src
, that’s only a convention. It’s not part of the PSR-0 standard.
So, with the above composer.json
, if you had a class named
Microsoft\Some\Class\Here_It_Is
your project folder hierarchy might look like
.
..
composer.json
path/to/source/Microsoft/Some/Class/Here/It/Is.php
This is the most basic use of Composer’s psr-0
autoloader. The syntax is the same regardless of whether you’re using it from a project’s root Composer file, or using it in a package you’re intending to use for distribution.
This raises an important question: How does Composer’s autoloader know if your file is in the root project folder, or a vendor project folder
//root project folder
path/to/source/Microsoft/Some/Class/Here/It/Is.php
//vendor project folder vendor/project-namespace/project-name/path/to/source/Microsoft/Some/Class/Here/It/Is.php
That’s the question we’ll answer next.
Composer’s PSR-0 File Generation
When you (or Composer automatically) runs
$ composer dumpautoload
One of the files Composer generates is
vendor/composer/autoload_namespaces.php
This file is the main auto-generated file for Composer’s PSR-0 autoloader support. The name is a little misleading — if I was up on my Composer history I’d be able to tell you if the Composer team chose autoload_namespaces
because this file predates the PSR-0 standard, or if it’s named that because nobody anticipated a second autoloading PSR. Regardless of its naming, inside you’ll find an array that’s similar to the files
and classmap
style array
#File: vendor/composer/autoload_namespaces.php
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Whoops' => array($vendorDir . '/filp/whoops/src'),
'System' => array($vendorDir . '/phpseclib/phpseclib/phpseclib'),
'Symfony\\Component\\Yaml\\' => array($vendorDir . '/symfony/yaml'),
'Symfony\\Component\\Translation\\' => array($vendorDir . '/symfony/translation'),
'Symfony\\Component\\Security\\Core\\' => array($vendorDir . '/symfony/security-core'),
'Symfony\\Component\\Routing\\' => array($vendorDir . '/symfony/routing'),
'Symfony\\Component\\Process\\' => array($vendorDir . '/symfony/process'),
'Symfony\\Component\\HttpKernel\\' => array($vendorDir . '/symfony/http-kernel'),
//...
)
Composer generates this file by looking at each package’s psr-0
configuration section, and then taking the configured namespaces and adding them to this array as keys, and then combines the package’s folder with the configured folder.
For example, the filp/whoops
package has a psr-0
section that looks like the following
#File: vendor/filp/whoops/composer.json
"autoload": {
"psr-0": {
"Whoops": "src/"
},
//...
},
In turn, the generated composer file creates the following
'Whoops' => array($vendorDir . '/filp/whoops/src'),
Composer adds the configured namespace (Whoops
) as an array key, and the value is a combination of the package folder ($vendorDir . '/filp/whoops
) and the configured folder (src
).
Covering how Composer generates this file would be an article (if not an article series) unto itself. For the curious, the actual code generation happens in the Composer\Autoload\AutoloadGenerator
class
#File: composer/composer/src/Composer/Autoload/AutoloadGenerator.php
$autoloads = $this->parseAutoloads($packageMap, $mainPackage);
//...
foreach ($autoloads['psr-0'] as $namespace => $paths) {
$exportedPaths = array();
foreach ($paths as $path) {
$exportedPaths[] = $this->getPathCode($filesystem, $basePath, $vendorPath, $path);
}
$exportedPrefix = var_export($namespace, true);
$namespacesFile .= " $exportedPrefix => ";
$namespacesFile .= "array(".implode(', ', $exportedPaths)."),\n";
}
$namespacesFile .= ");\n";
I’d start with the parseAutoload
method and the getPathCode
method, and then work your way backwards up the call stack.
Important: This is a Composer source file, and not something that’s distributed into every Composer based project.
This section of the Composer core code recognizes when it’s parsing a vendor package’s composer.json
vs. when it’s parsing the root package’s composer.json
, and generate an appropriate path. For example, if we look at the the following autoload_namespace
file segment
'Illuminate' => array($vendorDir . '/laravel/framework/src'),
'Foo' => array($baseDir . '/lib'),
'File' => array($vendorDir . '/phpseclib/phpseclib/phpseclib'),
You’ll see the Foo
vendor namespace is associated with the $baseDir
rather than the $vendorDir
.
Composer’s PSR-0 File Autoloading
This, finally, brings us back to where we left off in the Laravel Composer Autoloading. Namely, the ClassLoader::findFileWithExtension
method.
#File: vendor/bin/ClassLoader.php
private function findFileWithExtension($class, $ext)
{
//...
}
You’ll recall this is the method Composer calls if it fails to find a class in the classmap
array. It is also the method where Composer implements its main autoloading logic for both PSR-0 and PSR-4. We’re going to skip ahead to the start of the PSR-0 autoloading, which happens in this block
#File: vendor/bin/ClassLoader.php
private function findFileWithExtension($class, $ext)
{
//...
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
//...
}
Nothing like a legacy conditional to start things off. Before we get to that though, the “logical path” the $logicalPathPsr0
references is the class name normalized into a file path. In other words, the logical PSR-0 path for Symfony\Foo\Baz\Bar
is Symfony/Foo/Baz/Bar.php
. The above code block implements that transformation logic. You’re also probably wondering about the $logicalPathPsr4
variable — the Composer code set this variable earlier. We’ll get to that code eventually, but for now all you need to know is the PSR-0 logical path is created by manipulating the previously set PSR-4 logical path.
Or is it? That conditional block sure confuses things a bit. Let’s take a closer look at it
#File: vendor/bin/ClassLoader.php
if (false !== $pos = strrpos($class, '\\')) {
//run this code if the class name contains
//a namespace seperator (`\`)
} else {
//if the class name does not contain a namespace
//seperator (`\`), run this other code
}
Remember how we mentioned a battlefield? In addition to supporting modern, PSR style namespaces, Composer also supports the older, “namespace-less” “PEAR style” classnames. PEAR was/is PHP’s original package and dependency management system system. Although PEAR is falling out of fashion, Composer still has support for PEAR style class names, and autoloading for these class names piggy backs on the PSR-0 autoloader.
Regardless of whether it’s a proper PSR-0 class or a rogue, old-school namespace-less path, Composer now has a logical path. Next up is actually loading the class definition file. That’s handled by the next code block
#File: vendor/bin/ClassLoader.php
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
To understand this five levels deep code block, we need to speak briefly about how Composer loads the autoload_namespace.php
file, and how Composer populates the prefixesPsr0
object property. The easiest place to start is by showing you a sample of what you might find in prefixesPsr0
array (size=11)
'W' =>
array (size=1)
'Whoops' =>
array (size=1)
0 => string '/path/to/vendor/filp/whoops/src' (length=77)
'S' =>
array (size=17)
'System' =>
array (size=1)
0 => string '/path/to/vendor/phpseclib/phpseclib/phpseclib' (length=91)
'Symfony\Component\Yaml\' =>
array (size=1)
0 => string '/path/to/vendor/symfony/yaml' (length=74)
'Symfony\Component\Translation\' =>
array (size=1)
0 => string '/path/to/vendor/symfony/translation' (length=81)
'Symfony\Component\Security\Core\' =>
array (size=1)
0 => string '/path/to/vendor/symfony/security-core' (length=83)
'Symfony\Component\Routing\' =>
array (size=1)
0 => string '/path/to/vendor/symfony/routing' (length=77)
'Symfony\Component\Process\' =>
array (size=1)
0 => string '/path/to/vendor/symfony/process' (length=77)
Rather than store the array from autoload_namespace.php
as it’s found, Composer stores the key/value namespace/path values indexed by the first namespace letter.
$prefixesPsr0[$first_letter_of_namespace][$namespace] = [$path1, $path2];
On the face of it, this may seem bananas. However, storing the paths like this is a simple algorithmic performance improvement. It means the PSR autoloader will only need to loop through a small subset of each namespace/path to look for a file.
How Composer splits up the array is beyond the scope of this article, but if you’re interested in researching this yourself start with this block in the getLoader
method
#File: vendor/composer/autoload_real.php
public static function getLoader()
{
//...
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
//...
}
And then move on to the definition of the set
method.
Jumping back to our autoloading block, let’s start with the outer conditional. We’ll also expand our view a little bit so you can see where Composer defined the $first
variable.
#File: vendor/bin/ClassLoader.php
private function findFileWithExtension($class, $ext)
{
//...
$first = $class[0];
//...
if (isset($this->prefixesPsr0[$first])) {
//...
}
//...
}
The call to isset($this->prefixesPsr0[$first])
checks if there’s an index for classes starting with this letter. If not, that means there zero chance the PSR-0 autoloader can handle this class, and the code block skips the rest of the checks. Going one block level deeper
#File: vendor/bin/ClassLoader.php
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
//...
}
}
Here, if there are classes defined for the first letter, Composer foreach
es over them, and checks each namespace prefix to see if our class name shares that namespace prefix (if (0 === strpos($class, $prefix))
).
Once Composer’s found a match, it foreach
es over the directories
#File: vendor/bin/ClassLoader.php
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
and uses the logical path combined with a directory to create a full file path. If that file path points to a file that exists, our work is done and we’ll return the path, ready to be included.
Composer PSR-0 Extras: Fallback
Phew! That, as they say, was a mouthful. So, we’re done, right?
Not quite.
There’s a few extra things we need to cover. If you’re the read ahead type, you probably noticed the findFileWithExtension
function kept going where we ended. Before we can talk about the next block of code in findFileWithExtension
, we need to jump back to composer.json
for a second, and talk about an alternate configuration.
You’ll recall the psr-0
section of composer.json
lets you configure a top level namespace prefix and point to a folder where PHP can find that namespace’s class definition files
#File: vendor/filp/whoops/composer.json
"autoload": {
"psr-0": {
"Whoops": "src/"
},
//...
},
If you’re looking at older packages, you may see an odd looking syntax here
#File: composer.json
#or
#File: vendor/namespace/name/composer.json
"autoload": {
"psr-0": {
"": "path/to/src/"
},
//...
},
That is, a psr-0
section of composer.json
with an empty string as the key, pointing to a path. This is another one of those battle ground scars we’ve been talking about.
The above syntax allows a package developer to indicate a fallback folder where any classes may be found. That is — the class transformation rules of PSR-0 will still apply to classes, but it doesn’t explicitly assign a namespace. In the above example if you tried to instantiate a class
Foo\Baz\Bar_Gru
Then Composer would look for it in
path/to/src/Foo/Baz/Bar/Gru.php
Whether this breaks the PSR standard is up for debate, as a configuration with a namespace
#File: composer.json
"autoload": {
"psr-0": {
"Foo\": "path/to/src/"
},
//...
},
would result in the exact same file being autoloaded. The empty string as a key syntax is known as the fallback directory.
With that out of the way, we can now jump back to the next code block in the findFileWithExtension
method
#File: vendor/bin/ClassLoader.php
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
The fallbackDirsPsr0
property is where Composer stores all these “empty string key” autoloader configurations. This code blocks runs through them all, looking for a class definition file. If found, Composer returns the path to the class definition file.
It may seem like sloppy or gross programming to include this as a second loop — until you consider the indexing that happens above. Since the “namespace” portion of this configuration is an empty string, there’s no first letter to index the class namespaces by. You’ll see this often in systems programming — lots of gross code to ensure the system continues to function as it always has.
Composer PSR-0 Extras: Include Paths
There’s one last code block in findFileWithExtension
, and that’s
#File: vendor/bin/ClassLoader.php
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
This code block is here to support a depreciated bit of Composer autoloader history. Your composer.json
file can have a top level configuration node named include_path
#File: composer.json
{
"include-path": ["path/to/your/lib/","another/path/to/a/lib"]
}
Composer will automatically add all the paths from the values array to PHP’s include path. Magento developers will be familiar with this sort of functionality, as Magento jiggers the include path to enable its code pool override system
#File: app/Mage.php
Mage::register('original_include_path', get_include_path());
if (defined('COMPILER_INCLUDE_PATH')) {
$appPath = COMPILER_INCLUDE_PATH;
set_include_path($appPath . PS . Mage::registry('original_include_path'));
//...
} else {
/**
* Set include path
*/
$paths = array();
$paths[] = BP . DS . 'app' . DS . 'code' . DS . 'local';
$paths[] = BP . DS . 'app' . DS . 'code' . DS . 'community';
$paths[] = BP . DS . 'app' . DS . 'code' . DS . 'core';
$paths[] = BP . DS . 'lib';
$appPath = implode(PS, $paths);
set_include_path($appPath . PS . Mage::registry('original_include_path'));
//...
}
Rather than forcing each framework developer to have this code in the framework bootstrap, Composer introduced the include_path
feature to let Composer do this work for developers.
After running dumpautoload
(or after Composer automatically runs this for you), Composer will build up a list of all the package’s include-path
s in the vendor/composer/include_paths.php
file
#File: vendor/composer/include_paths.php
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
$vendorDir . '/phpseclib/phpseclib/phpseclib',
$vendorDir . '/phpunit/php-text-template',
$vendorDir . '/phpunit/php-timer',
$vendorDir . '/phpunit/php-file-iterator',
$vendorDir . '/phpunit/phpunit',
$vendorDir . '/symfony/yaml',
$vendorDir . '/phpunit/php-code-coverage',
);
Then, in turn, the getLoader
initialization method loads these include paths, and adds them to PHP with the set_include_path
function.
#File: vendor/composer/autoload_real.php
$includePaths = require __DIR__ . '/include_paths.php';
array_push($includePaths, get_include_path());
set_include_path(join(PATH_SEPARATOR, $includePaths));
With that context set, lets come back to findFileWithExtension
‘s last code block
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
If you’re not familiar with stream_resolve_include_path
, it’s a function that lets you pass in a relative path like
stream_resolve_include_path('src/Path/To/File.php');
and returns the full path to the file, as though it were include
d
#include('src/Path/To/File.php');
#require('src/Path/To/File.php');
/full/path/to/src/Path/To/File.php
This is Composer’s last ditch effort to load a PSR-0 logical path using any configured include paths. We mention it here mainly for completeness. Modern versions of Composer ship with $this->useIncludePath
set to false, so this include path behavior is truly a legacy Composer behavior — although be aware that Composer will set the PHP include_path regardless of the useIncludePath
value.
With that out of the way, let’s make a run for it before some other feature lurches from the depths to pull us back down into the swamp.
Composer’s PSR-4 Autoloader
Finally, we’ve arrived at the modern day PSR-4 autoloader standard. PSR-4 is PHP-FIG’s attempt to apply lesons learned during the implementation of PSR-0 and come up with a better standard. We’re not going to cover every nuance of the standard — for our purposes the important bits are
- PSR-4 no longer turns underscores into directory separators
- PSR-4 considers the entire namespace an identifying prefix (vs. PSR-0’s vendor namespace/prefix)
- PSR-4 enshrines the Composer “This namespace goes in this folder” implementation as the standard
So, as a Composer user, adding a PSR-4 namespace is very similar to adding a PSR-0 namespace. The autoload
section of your composer.json
has a psr-4
section, and this section has a list of key/value, namespace/paths pairs. Consider the Monolog’s composer.json
#File: vendor/monolog/monolog/composer.json
"autoload": {
"psr-4": {"Monolog\\": "src/Monolog"}
},
This autoloader would look for the following classes
Monolog\Foo\Baz\Bar
Monolog\Foo\Baz_Bar
In the following directories (respectively)
src/Monolog/Foo/Baz/Bar.php
src/Monolog/Monolog/Foo/Baz_Bar.php
So, already we see a slight difference from PSR-0 in that the _
‘s have on the class name. Where PSR-4 gets really different is in a situation like this
#File: vendor/monolog/monolog/composer.json
"autoload": {
"psr-4": {
"Monolog\\": "src/Monolog"
"Monolog\\Test\\": "src/tests"
}
},
Here, the Monolog\Foo\Baz\Bar
and Monolog\Foo\Baz_Bar
classes would load from the same place — however, a Monolog\Test\Foo_Bar
class would load from the following path
src/tests/Foo_Bar.php
Unlike PSR-0, the entire namespace (Monolog\\Test
) is considered a prefix. This is a bit of a departure from traditional PHP autoloaders, which typically used the entire class name to create a file path. It remains to be seen if this will cause more confusion or less confusion among the PHP masses who use Composer, but don’t closely follow its development.
Composer’s PSR-4 Generated Files
When you run composer dumpautoload
(or Composer runs it automatically on your behalf), Composer generates the following file, which contains the PSR-4 namespace/file-path key/value pairs.
#File: vendor/composer/autoload_psr4.php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'),
);
Then, Composer loads this information into place in the getLoader
method
#File: vendor/composer/autoload_real.php
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}
If we take a quick look at the setPsr4
method
#File: vendor/composer/autoload_real.php
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
We see that, similar to PSR-0, Composer indexes the prefixes by the first letter. In a slight variation from PSR-4, the namespace prefixes are stored in one object property (prefixLengthsPsr4
) and the actual path information stored in a second property (prefixDirsPsr4
). You’ll also notice the length of the prefix path is saved. This will be important later.
Composer’s PSR-4 Autoloading
Finally, with the generating and loading out of the way, we can take a look at how the findFileWithExtension
method looks for PSR-4 files. First up is transforming a class name into a logical path
#File: vendor/bin/ClassLoader.php
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
//...
}
Thanks to PSR-4 dropping _
support, this transformation is a single string replacement, turning the class
Foo\Baz\Bar
Into the logical path
Foo/Baz/Bar.php
This may be a little confusing at first — didn’t we just say PSR-4 is more than a simple character swap out? This will make sense if we consider the next code block, which actually looks for the file paths.
#File: vendor/bin/ClassLoader.php
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) {
if (0 === strpos($class, $prefix)) {
foreach ($this->prefixDirsPsr4[$prefix] as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
return $file;
}
}
}
}
}
This pattern is very similar to the PSR-0 pattern, so we won’t review it in full. Like the PSR-0 path, Composer looks up the namespace prefixes by their index (the first letter), and then foreach
es over each directory looking for a file. The main difference is the transformation of the “logical” path into a full path with this (as a file_exists
parameter)
#File: composer.json
$file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length)
This is where the stored length of the namespace in the generated file becomes important. Let’s consider our logical path
Foo/Baz/Bar.php
If we configured our composer.json
with
#File: composer.json
"Foo\\"=>'path/to/src'
Then the string length of our namespace prefix (Foo\\
) would be 4
, which means the substr
call would look like this
substr('Foo/Baz/Bar.php', 4)
and the paths generated would be
#substr portion
Baz/Bar.php
#full path for a package or project (root package)
/path/to/project/vender/namespace/package-name/path/to/src/Baz/Bar.php
/path/to/project/path/to/src/Baz/Bar.php
However, if we configured out compser.json
with
#File: compser.json
"Foo\\Bar\\"=>'path/to/src'
Then the string length of our namespace prefix (Foo\\Bar\\
) would be 8
, which means the substr
call would look like this
substr('Foo/Baz/Bar.php', 8)
and the paths generated would be
#substr portion
Bar.php
#full path for a package or project (i.e. root package)
/path/to/project/vender/namespace/package-name/path/to/src/Bar.php
/path/to/project/path/to/src/Bar.php
This substr
method isn’t the most straight forward transformation, but it’s likely there for performance reasons. Again, what you see in systems level code may be ugly and confusing to the uninitiated, but it’s always best to assume it’s there for a good reason.
Finally, just like the PSR-0 autoloader, the PSR-4 autoloader supports the “fallback” syntax
#File: vendor/monolog/monolog/composer.json
"autoload": {
"psr-4": {
"": "path/to/fallback/dir"
}
},
The final PSR-4 code block handles loading class definitions from this directory of last resort.
#File: vendor/bin/ClassLoader.php
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
Autoloader Method Precedence
With that, we’ve reach the end of Composer’s many autoloaders. While it’s a bit much to take in on a systems level, in day-to-day development most programmers won’t need to touch a proverbial 90% of this system. Like we said, when an autoloader system works well, it’s invisible to developers. However, understanding how the autoloader works and interacts with your program can be a vital skill when you’re tracking down unexpected bugs, or when a library developer decides to “go rogue” and do something unexpected with their own autoloading classes.
Within the Composer autoloader, the last thing to keep in mind is which autoloading systems “win” over the others. Based on the current implementation of findFile
, Composer will
- First look to the classmap
- Then to PHP classes with PSR-4 autoloaders
- Then to PHP classes with PSR-0 autoloaders
- Then to “
.hh
” HHVM classes with PSR-4 autoloaders - Then to “
.hh
” HHVM classes with PSR-0 autoloaders
Also, within the PSR autoloaders, on the rare chance Composer can load a class in multiple directories, the namespace prefixed autoloaders will win out over the fallback “any namespace” autoloaders. Composer doesn’t have a defined behavior is to how prefixed autoloaders try to load the same class — which is another good reason to not steal the vendor namespace of another class library.
As to how Composer’s autoloader interacts with other PHP autoloaders (added via the files
autoloader or other means) that’s a question we’ll have to leave for next time, when we return to Laravel and its four autoloaders.