See : PHP SIG / Packaging Tips / Autoloader

Common design: consumer autoloader

It is one of the most commonly used solution to implement autoloader in application, and in packaging. The application provides an autoloader which take care of all its dependencies. So it the application need A and B, if B need C, you need to manage A + B + C in autoloader.

Problem: is B change in dependency from C to D, your autoloader need to be fixed, and your application is probably broken.

This is the solution implemented in composer, but it only works because every library is bundled, and autoloader is generated according to the list of installed components, at installation time.

In a perfect world, everything will be PSR-0 / PSR-4 compliant, with an clean namespace, and a very simple autoloader will be able to manage everything installed in the system tree (/usr/share/php). But, no luck, perfection doesn't exists.

New design: provider autoloader

The idea is to provide an autoloader for each library (which will consume autoloader of its dependencies).

So it the application need A and B, you only need to include A + B autoloaders in the application one (and B autoloader will requires C or D). Exactly as the RPM world, you don't have to take care of dependencies tree.

Improvment: shared autoloader

When a lot of dependencies are used by an application, having a huge stack of autoloaders can be a bootleneck for performance.

So the idea is to use a single autoloader, shared and configured by each library.

Here is an example:

<?php
if (!isset($fedoraClassLoader) || !($fedoraClassLoader instanceof \Symfony\Component\ClassLoader\ClassLoader)) {
    if (!class_exists('Symfony\\Component\\ClassLoader\\ClassLoader', false)) {
        require_once '%{_datadir}/php/Symfony/Component/ClassLoader/ClassLoader.php';
    }
    $fedoraClassLoader = new \Symfony\Component\ClassLoader\ClassLoader();
    $fedoraClassLoader->register();
}
// This library
$fedoraClassLoader->addPrefix('Foo\\Bar\\', dirname(dirname(__DIR__)));
// Another library (dependency)
require_once '/usr/share/php/Foo/Baz/autoload.php';

If Foo/Baz autoloader use the same implementation, the single instance ($fedoraClassLoader) will be shared.

Initially we start using an instance of Symfony UniversalClassLoader, but as it is deprecated in Symfony 2.7, we switched to the more simple ClassLoader.

I think UniversalClassLoader was a better choice, behaviors are really different:

  • With ClassLoader, the first path added will have priority, with UniversalClassLoader  the last path will have priority (and the order can be very important in some stack, with circular dependencies)
  • With ClassLoader, there is no check to avoid duplicated path for a given prefix (I've tried to fix this, see PR #7, waiting for upstream feedback)
  • With ClassLoader, you can only add "prefix" (no distinction between prefix, namespace, PSR-0 or PSR-4 whe).

Notice: it probably only make sense to use this Symfony component when some other Symfony components are already in the dependency tree. If you don't want this dependency on Symfony, you can write your own autoloader, use the Zend Framework autoloader, or a simple classmap generated by phpab (theseer/autoload).

More examples, see the phpunit, phpcompatinfo or phpspec packages, which use and share some Symfony, Doctrine and other components, and mostly implement this new way.

Feedback: want to dicuss about PHP packaging, give feedback about this, join the PHP SIG Mailing list!

Great Thanks to Shawn Iwinski for his work on this feature, his tests, and lot of valuable discussions.