Resource icon

Using Composer Packages in XenForo 2 Addons Tutorial 1.0.0

No permission to download
Compatible XF 2.x versions
2.0
Additional requirements
You must have Composer installed
Composer is a tool for dependency management in PHP. It allows you to declare the libraries your project depends on and it will manage (install/update) them for you.

XenForo 2 uses Composer behind the scenes to include certain packages it uses - however, there is no way to automatically include your own packages for addons using this core Composer support. There is no composer.json file for XenForo 2 we can edit to add our own packages.

However, the framework provided by XenForo 2 has extension points we can hook in to which allow us to include packages in our addons and have them autoloaded by the XenForo autoloader. This tutorial describes how to do so and provides some code you may use in your own addons to help achieve this.

Note that there are some caveats here - dependency management can be complex, and it is entirely possible for you to introduce unexpected bugs in your addon, other addons, or even in the XenForo core by following this tutorial and including composer packages. Care must be taken and you would be well advised to avoid this approach if you are not already familiar with Composer dependency management.

Assumptions made in this tutorial:
  1. you have Composer installed on your development server and you are familiar with how to use it
  2. you understand how Composer packages work
  3. all examples assume a Linux environment running bash, you will need to translate these for use on Windows or other platforms yourself
About this tutorial

This tutorial runs through a basic example of adding a Composer package to an addon. The addon itself is installable so you can see all of the code in operation and examine how it works. We will install the Carbon API extension for DateTime objects.

All code in this tutorial is licensed under the MIT license, which essentially allows you to use (or modify!) the code for any purpose (including commercial purposes) free of charge, with the only condition being that you include the relevant copyright notice and permission notice.

See the LICENSE file in the addon root for full license and copyright information. Alternatively, you can view the license file in the Git repository for the tutorial code: LICENSE.

Just FYI, Composer itself and the Carbon package we will install also use the MIT license.

Getting Started

I have created a basic addon called ComposerTutorial. My addon.json file contains nothing specific to using Composer, so don't worry about the contents of that file.

XenForo is installed on my dev server at /srv/www/xenforo2 - I will refer to this as the "XenForo root". My addon is installed at /srv/www/xenforo2/src/addons/ComposerTutorial - I will refer to this as my "addon root".

The first step to adding a Composer package is to create a composer.json file in the root of your addon. You can either do this by hand, or use the composer require command to do it for you.

Bash:
$ cd /srv/www/xenforo2/src/addons/ComposerTutorial
$ composer require nesbot/carbon

Using version ^1.32 for nesbot/carbon
./composer.json has been created
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 3 installs, 0 updates, 0 removals
  - Installing symfony/polyfill-mbstring (v1.8.0): Loading from cache
  - Installing symfony/translation (v4.1.3): Downloading (100%)
  - Installing nesbot/carbon (1.32.0): Downloading (100%)
symfony/translation suggests installing symfony/config ()
symfony/translation suggests installing symfony/yaml ()
symfony/translation suggests installing psr/log-implementation (To use logging capability in translator)
Writing lock file
Generating autoload files
We now have a basic Composer file created at /srv/www/xenforo2/src/addons/ComposerTutorial/composer.json:

JSON:
{
    "require": {
        "nesbot/carbon": "^1.32"
    }
}
While there are dozens of fields available in the composer.json Schema - you actually only need the require field to make Composer work.

If you created your composer.json file by hand, you would instead run the composer update command to install the package and its dependencies.

Bash:
$ cd /srv/www/xenforo2/src/addons/ComposerTutorial
$ composer update
The other thing we now have is a vendor folder containing the installed packages and all dependencies.

Bash:
$ ls -1p /srv/www/xenforo2/src/addons/ComposerTutorial/vendor/
autoload.php
composer/
nesbot/
symfony/
The autoload.php file is auto-generated by the composer commands and is for running the autoloader in a stand-alone project. This won't be suitable for using in XenForo, so we will ignore this file.

The composer directory is where the generated autoloader files are - it's worth familiarising yourself with the contents of this directory to understand how Composer finds and loads classes. We will be using some of the files in this directory to identify the classes and files we need to pass to the XenForo autoloader.

The nesbot directory is where the Carbon package is installed.

The symfony directory is where some Carbon dependency packages are installed (symfony/polyfill-mbstring and symfony-translation)

Finally, we have a composer.lock file that was created in our addon root directory. This file is auto-generated by the Composer commands and contains a list of the dependencies (and their specific versions!) that were resolved from composer.json the first time the packages were installed. You should consider the composer.lock file a "version-specific point-in-time snapshot" of the dependencies.

While you are developing your addon, you may run the composer update command from your addon root to update the dependencies, which will cause the composer.lock file to also be updated if new versions have been released. It is important to test that new versions have not included any breaking changes - although if using semantic versioning, this should (in theory) not occur.

However, in a production environment, it is important to only ever run the composer install command if you need to install packages and dependencies (which you shouldn't need to do if we have packaged the vendor folder with the addon - more discussion on this later). The install command does not resolve dependencies anew from the composer.json file, instead it relies on the composer.lock file to install exactly the versions specified. This means that we can be sure that the package versions installed in production are exactly those we have tested and found to work in development/test environments.

Either way, we'll cover installation in more detail later. For now, just recognise that the composer.lock file is important and should be checked into your source code control alongside your composer.json file.

So now that we have our package and dependencies installed, we need to get XenForo to autoload everything.

XenForo Autoloader

As mentioned, XenForo uses composer behind the scenes - although they hide the composer.json file from us, because we don't need to install or update any core packages and dependencies ourselves - their upgrade process takes care of that for us. See the src/vendor directory under the XenForo root to see the packages used by XenForo.

So with XenForo already running an autoloader, we cannot simply call the autoload.php file from our addon's vendor directory - we need to hook into the XenForo autoloader and add our own dependencies to theirs.

I've created a class to help achieve this. I copy this class to each of my addons which use Composer packages.

You can find this file in the addon root in the published addon: Composer.php - alternatively, view it in our Git repository: Composer.php

PHP:
<?php namespace ComposerTutorial;

/**
* Copyright (c) Simon Hampel
* Based on code used by Composer, which is Copyright (c) Nils Adermann, Jordi Boggiano
*/

// TODO: the namespace will need to be changed when copying this file to a new addon

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';

        // note that autoload_files.php is only generated if there is actually a 'files' directive somewhere in the dependency chain
        if (file_exists($files))
        {
            $includeFiles = require $files;

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

                    $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
                }
            }
        }
    }
}
This class provides four functions to look through the four different files which are auto-generated by Composer, identifies the classes and files and then adds them to the XenForo autoload queue.

I usually put this in the addon root - but it can go anywhere below your addon root, provided that you adjust the namespaces in this file and the initialisation functions (discussed later) appropriately.

Order of Class Loading

Note that the optional $prepend parameter can be used to change that behaviour of the first two functions.

When resolving a class during execution, the Composer autoloader simply runs through the autoload queue to find the first match for the class being called. If there are duplicate class definitions (for example if multiple addons use the same dependency, or if an addon uses the same depenency as the core XenForo packages), then only the first match is used.

By default, classes are appended to the autoload queue, meaning that the first classes added to the queue (the XenForo core dependencies!) will be given priority. This is generally a good thing, as it prevents you from clobbering the core dependencies - but be sure you check the versions being used by the core to ensure your package is not receiving a substantially different version of a class from the core rather than from your addon dependency packages.

If another addon is initialised before yours, its dependencies will be added to the queue before yours. If both addons contains the same classes - it is their versions which will be loaded, not yours. If this causes problems with other addons, you should be able to adjust the class loading queue order by adjusting the order of execution for the listener we'll set up in the next step. Assuming there are no significant bugs and the dependency versions are not significantly different, this should not cause issues - but this is where unexpected bugs can be introduced, so care is required.

In certain (rare) circumstances, you may need to replace one of the dependency classes provided in the XenForo core. You can set the $prepend parameter to true when calling the function (see next section), to have the autoloader prepend the dependencies to the autoload queue. This means that your addon packages will be loaded before the XenForo core packages.

For my Guzzle 6 addon, I needed to do exactly this because I was replacing core functionality - XenForo v2.0 ships with Guzzle v5.3 and so if I appended my classes to the autoloader queue, the core Guzzle 5.3 versions of the classes would always be found first. By appending my dependencies to the front of the queue, I was able to have Guzzle 6 classes loaded instead. This is a rare occurrence and not generally recommended as it does impact on core functionality and could break things unexpectedly.

Initialising the Composer Autoloader

We can use a code event listener to initialise our addon and add our dependencies to the XF autoloader queue. I create the following Listener.php file in the root of my addon:

PHP:
<?php namespace ComposerTutorial;

use XF\App;

class Listener
{
    public static function appSetup(App $app)
    {
        Composer::autoloadNamespaces($app);
        Composer::autoloadPsr4($app);
        Composer::autoloadClassmap($app);
        Composer::autoloadFiles($app);
    }
}
This function simply calls each of the four autoload functions (order matters!) from our Composer class to load the dependencies into the autoload queue.

It is here that we can use that $prepend parameter we mentioned earlier. To over-ride the core dependencies with your own, simply call Composer::autoloadNamespaces($app, true); and Composer::autoloadPsr4($app, true);. Again, this is not recommended other than in exceptional circumstances.

Now, we simply add the code event listener to execute the appSetup function we defined above:
  • Listen to event: app_setup
  • Event hint:
  • Execute callback: ComposerTutorial\Listener :: appSetup (namespace would be adjusted to suit your own listener namespace)
  • Callback execution order: 10 (note - this is where we adjust the execution order to get our dependencies to load before another addon's - use with caution! I recommend leaving this set to 10 unless absolutely necessary)
  • Enable callback execution: checked
  • Description: Autoload Composer packages
  • Add-on: Composer Tutorial (obviously, select your own addon from the list)
... and we're done!

Let's test this by adding a test tool to the admin area - see source code for details.

To run the test after installing the addon - go to the Admin > Tools > Test Composer Tutorial page and you'll see some output generated by Carbon.

Packaging the Addon

Once the addon is ready for release, we can simply package it up and publish it.

However, there are several different approaches here.

The "purist" approach would be to not check your vendor folder in to source control, but instead rely on the person installing the addon to run composer install from the addon root on the production server to install the dependencies specified in the composer.lock file. However, while this may work well in an automated deployment environment, it is a pain to have to do this manually and forgetting to do so will break your forum. Similarly, if there are issues connecting to the source of the packages such that the install command fails, your forum could be offline until those issues are resolved. The other downside to this approach is the requirement that Composer be installed on your production server, which may not be desirable.

The "pragmatic" approach is to check the entire vendor folder into source control, so that when the addon is deployed, it is complete and ready to use. This is the approach I take.

However, there are some things we should do before packaging our addon.

By default, Composer installs development dependencies (eg unit testing tools) specified in your require-dev block in composer.json, which are unnecessary in a production environment. Of course, if you don't use require-dev, this isn't an issue.

Similarly, there are some optimisations we can (and should!) do to our code for running in production environments. These optimisations can be handled for us by composer - we just need to execute the commands before packaging our addon.

Fortunately, the XenForo devs have been kind enough to provide us with a lovely little tool to run custom commands when building our addon.

Simply create a build.json file in the root of your addon with the following instructions:

JSON:
{
    "exec": [
        "composer install --working-dir=_build/upload/src/addons/{addon_id}/ --no-dev --optimize-autoloader"
    ]
}
This tells the XenForo build process to execute a composer install (not an update!) to ensure we have all dependencies installed as per the current (tested!) version of composer.lock, and includes three composer install options:
  • --working-dir=_build/upload/src/addons/{addon_id}/: this tells composer that we actually want to optimise the build files, not our development files. These files only exist during the build process and are removed once the release zip file has been generated.
  • --no-dev: Skip installing packages listed in require-dev. The autoloader generation skips the autoload-dev rules
  • --optimize-autoloader: Convert PSR-0/4 autoloading to classmap to get a faster autoloader. This is recommended especially for production, but can take a bit of time to run so it is currently not done by default
So now, when we swap back to our XenForo root and run the addon build command, it will optimise our autoloader code for us ready for production use.

Bash:
$ cd /srv/www/xenforo2
$ php cmd.php xf-addon:build-release ComposerTutorial

Performing add-on export.

Exporting data for Composer Tutorial to /srv/www/xenforo2/src/addons/ComposerTutorial/_data.
25/25 [============================] 100%
Written successfully.
Attempting to validate addon.json file...
JSON file validates successfully!

Building release ZIP.
Loading composer repositories with package information
Installing dependencies from lock file
Nothing to install or update
Generating optimized autoload files

Writing release ZIP to /srv/www/xenforo2/src/addons/ComposerTutorial/_releases.

Release written successfully.
You can see the output of the composer commands along with the build output.

I have attached the built addon to this tutorial so you can install it, look at the code and experiment with other packages.

Alternatively, clone the git repository for the tutorial code - XenForo Composer Tutorial
Author
Sim
Downloads
13
First release
Last update
Rating
5.00 star(s) 1 ratings

More resources from Sim

Latest reviews

Excellent tutorial. Works perfectly, I've implemented it in one of my projects. Everything is clearly explained, for all skill levels.

This saves me from having to manually change the namespace and manually keep a 3rd party library up to date.

Thanks!
Top