Categories


Archives


Recent Posts


Categories


Composer Autoloader Features: Part 2

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

Updated for Magento 2! No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

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).

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 foreaches 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 foreaches 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-paths 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 included

#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

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 foreaches 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

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.

Originally published February 1, 2015
Series Navigation<< Composer Autoloader Features: Part 1Registering Laravel’s Autoloaders >>

Copyright © Alana Storm 1975 – 2023 All Rights Reserved

Originally Posted: 1st February 2015

email hidden; JavaScript is required