• This site uses cookies. By continuing to use this site, you are agreeing to our use of cookies. Learn more.

XF 2.0 Composer autoload method

Jake B.

Well-known member
#1
This is what I'm using currently to autoload any classes I'm requiring through Composer for add-ons. It hasn't been thoroughly tested yet to make sure everything works properly if two add-ons are including the same packages, but from my brief testing it seems to function. For the most part it's pulled directly out of Composer so I can't imagine there being any major issues

Just create an app_setup event listener that points to this method:

Code:
public static function autoloadClasses()
{
    $composerDirectory = \XF::getAddOnDirectory() . '/VendorPrefix/AddOn/vendor/composer';
    $loader = \XF::$autoLoader;
    $map = require $composerDirectory . '/autoload_namespaces.php';
    foreach ($map as $namespace => $path) {
        $loader->set($namespace, $path);
    }

    $map = require $composerDirectory . '/autoload_psr4.php';
    foreach ($map as $namespace => $path) {
        $loader->setPsr4($namespace, $path);
    }

    $classMap = require $composerDirectory . '/autoload_classmap.php';
    if ($classMap) {
        $loader->addClassMap($classMap);
    }
}
obviously you'll replace VendorPrefix/AddOn with your Add-on's ID, you can probably also use __DIR__ instead if the file is in the root directory of your add-on, or __DIR__ . '/../' if it's in an Listeners directory. This will load namespaces, classmaps, etc directly from vendor/composer/* and register all of the classes for you automagically :)

If anyone more knowledgeable with the inner workings of XF's autoloader and composer sees any issues with this do let me know
 
Last edited:

Sim

Well-known member
#2
The way I structure my addons is to have a composer.json in the root of the addon and I run composer update from the root directory vendor folder containing the composer autoload files.

I then add the following Composer.php class to the root of my addon.

PHP:
<?php

namespace MyAddon;

class Composer
{
    public static function autoloadNamespaces(\XF\App $app, $prepend = false)
    {
        $namespaces = __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR . 'autoload_namespaces.php';

        if (!file_exists($namespaces))
        {
            $app->error()->logError('Missing vendor autoload files at %s', $namespaces);
        }
        else
        {
            $map = require $namespaces;

            foreach ($map as $namespace => $path) {
                \XF::$autoLoader->add($namespace, $path, $prepend);
            }
        }
    }

    public static function autoloadPsr4(\XF\App $app, $prepend = false)
    {
        $psr4 = __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR . 'autoload_psr4.php';

        if (!file_exists($psr4))
        {
            $app->error()->logError('Missing vendor autoload files at %s', $psr4);
        }
        else
        {
            $map = require $psr4;

            foreach ($map as $namespace => $path) {
                \XF::$autoLoader->addPsr4($namespace, $path, $prepend);
            }
        }
    }

    public static function autoloadClassmap(\XF\App $app)
    {
        $classmap = __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR . 'autoload_classmap.php';

        if (!file_exists($classmap))
        {
            $app->error()->logError('Missing vendor autoload files at %s', $classmap);
        }
        else
        {
            $map = require $classmap;

            if ($map)
            {
                \XF::$autoLoader->addClassMap($map);
            }
        }
    }

    public static function autoloadFiles(\XF\App $app)
    {
        $files = __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR . 'autoload_files.php';

        if (!file_exists($files))
        {
            $app->error()->logError('Missing vendor autoload files at %s', $files);
        }
        else
        {
            $includeFiles = require $files;

            foreach ($includeFiles as $fileIdentifier => $file)
            {
                if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
                    require $file;

                    $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
                }
            }
        }
    }
}

Finally, I add a code event listener for the app_setup event which executes the following code to actually perform the autoload:

PHP:
    public static function appSetup(App $app)
    {
        Composer::autoloadNamespaces($app);
        Composer::autoloadPsr4($app);
        Composer::autoloadClassmap($app);
        Composer::autoloadFiles($app);
    }
It's important to run all four autoloaders because different packages will utilise different autoload mechanisms. This is basically what the composer autoloader does itself.

You can see this example in operation in my Guzzle6 addon (also:
https://xenforo.com/community/resources/guzzle6-for-xenforo-2.5578/)
 

Jake B.

Well-known member
#3
Ah looks (for the most part) to be almost exactly what I did yours just seems more organized, and I apparently forgot to copy over the autoload_files.php part. How does the XF AutoLoader handle if two add-ons are using the same
package (or two separate versions of the same package)?
 

Jake B.

Well-known member
#4
Ended up making a bit of a change to what you have to be more generic, have added it to one of our core files that gets included with all add-ons:

PHP:
<?php

namespace ThemeHouse\Core;

class AutoLoad
{
    public static function autoloadComposerPackages($addOnId)
    {
        $composerDir = \XF::getAddOnDirectory() . '/' . $addOnId .'/vendor/composer';
        $app = \XF::app();

        self::autoloadNamespaces($composerDir, $app);
        self::autoloadPsr4($composerDir, $app);
        self::autoloadClassmap($composerDir, $app);
        self::autoloadFiles($composerDir, $app);
    }

    public static function autoloadNamespaces($composerDir, \XF\App $app, $prepend = false)
    {
        $namespaces = $composerDir . DIRECTORY_SEPARATOR . 'autoload_namespaces.php';

        if (!file_exists($namespaces)) {
            $app->error()->logError('Missing vendor autoload files at %s', $namespaces);
        } else {
            $map = require $namespaces;

            foreach ($map as $namespace => $path) {
                \XF::$autoLoader->add($namespace, $path, $prepend);
            }
        }
    }

    public static function autoloadPsr4($composerDir, \XF\App $app, $prepend = false)
    {
        $psr4 = $composerDir . DIRECTORY_SEPARATOR . 'autoload_psr4.php';

        if (!file_exists($psr4)) {
            $app->error()->logError('Missing vendor autoload files at %s', $psr4);
        } else {
            $map = require $psr4;

            foreach ($map as $namespace => $path) {
                \XF::$autoLoader->addPsr4($namespace, $path, $prepend);
            }
        }
    }

    public static function autoloadClassmap($composerDir, \XF\App $app)
    {
        $classmap = $composerDir . DIRECTORY_SEPARATOR . 'autoload_classmap.php';

        if (!file_exists($classmap)) {
            $app->error()->logError('Missing vendor autoload files at %s', $classmap);
        } else {
            $map = require $classmap;

            if ($map) {
                \XF::$autoLoader->addClassMap($map);
            }
        }
    }

    public static function autoloadFiles($composerDir, \XF\App $app)
    {
        $files = $composerDir . DIRECTORY_SEPARATOR . 'autoload_files.php';

        if (!file_exists($files)) {
            $app->error()->logError('Missing vendor autoload files at %s', $files);
        } else {
            $includeFiles = require $files;

            foreach ($includeFiles as $fileIdentifier => $file) {
                if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
                    require $file;

                    $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
                }
            }
        }
    }

}
and now in the app_setup listener it can just call \ThemeHouse\Core\AutoLoad::autoloadComposerPackages('ThemeHouse/AddOnId')
 

Sim

Well-known member
#5
How does the XF AutoLoader handle if two add-ons are using the same
package (or two separate versions of the same package)?

The magic is in the $prepend parameter to the add and addPsr4 methods.

PHP:
    /**
     * Registers a set of PSR-0 directories for a given prefix, either
     * appending or prepending to the ones previously set for this prefix.
     *
     * @param string       $prefix  The prefix
     * @param array|string $paths   The PSR-0 root directories
     * @param bool         $prepend Whether to prepend the directories
     */
    public function add($prefix, $paths, $prepend = false)
    {
        ...
By default, the autoloader appends the directories to any existing ones already found, so core vendor files take priority over addon vendor files.

So the XF autoloader (which by default is the Composer Autoloader from /src/vendor/composer/ClassLoader.php) will load its own autoload files first from /src/vendor/composer/autoload_namespaces.php and then /src/vendor/composer/autoload_psr4.php.

We then call the Composer autoloader again via our code event listener to load our addon's vendors autoload files - but it depends on how we set the $prepend variable as to what it does next.

If we leave $prepend set to the default value of false, it appends our new directories to any existing directories, which means that any classes autoloaded from the core vendor directory will take precedence. Similarly, any addons where their code event listener fires before ours and they utilise the class loader - will take priority over ours because ours will be added to the end of the list. This is where execution priority may become important.

Alternatively, if we set $prepend to true, our classes will be loaded first and will take priority.

So for my Guzzle6 library, I actually use the following app_setup code event listener code, because I need my vendor classes to take priority over the vendor classes loaded by the core:

PHP:
    public static function appSetup(App $app)
    {
        Composer::autoloadNamespaces($app, true); // prepend our vendor classes so they load first!!
        Composer::autoloadPsr4($app, true); // prepend our vendor classes so they load first!!
        Composer::autoloadClassmap($app);
        Composer::autoloadFiles($app);
    }
The key with the autoloader code is that it first builds a list of possible directory locations for the classes it has been asked to load, and then goes through them one by one to try and find a match. Before it loads a class, if first checks whether it has already been loaded, and if so, doesn't try to load it again.

So if you need to over-ride an existing vendor class with one from your addon, you want to be the first directory it finds with your class in it - hence being prepended before the beginning of the list rather than appended to the end of the list.