Work In Progress

This book is currently work in progress. Some sections are not yet written. Thank you for your understanding!

Adding Composer dependencies

In many cases you will need to use third party libraries. The most common way to do that is through Composer. I will assume that you already know how to use Composer. If not, use your favourite search engine to look for “Composer tutorial for beginners” to get you started.

Joomla's Composer dependencies: can't touch this!

Joomla itself uses Composer for some third party dependencies in its core libraries and core components. You will find Joomla's Composer dependencies in your site's libraries/vendor folder. As you can see, even the Joomla Framework is pulled in as a Composer dependency along with several third party libraries.

[Caution]Caution

You MUST NEVER remove, upgrade, downgrade, modify, replace or otherwise interfere with the Composer dependencies provided by Joomla itself. Doing so is considered a core hack and will disqualify you from receiving support by third party developers, as well as getting your extensions unlisted from the Joomla Extensions Directory (JED). You will also be breaking Joomla and third party software with everything that entails to your reputation as a developer.

Adding a composer.json to your extension

The correct way to add Composer dependencies is to simply create a composer.json file in your repository's root and install your vendor folder alongside your extension's src folder. You can see an example of that in action in Akeeba SocialLogin's repository. The relevant part of the file is the following:

{
  "config": {
    "vendor-dir": "plugins/system/sociallogin/vendor"
    "platform": {
      "php": "7.4.0"
    }
  }
}

The vendor-dir parameter tells Composer where to place the Composer vendor folder. As you can see, in this collection of plugins we chose to put it in the system plugin, next to its src directory (plugins/system/sociallogin/src).

The platform block is optional but you SHOULD include it. It tells Composer to not look into the current PHP version it is executing in but instead install extensions valid for a specific PHP version. In this example it's for PHP 7.4.0. Set this to the minimum PHP version you are targeting in your extension.

[Tip]Tip

It is good practice to NOT include your composer.json and composer.lock files in the final distribution of your files to the user. You do not want them to mess around with your dependencies. Moreover, these files would be web accessible which would let an attacker very easily enumerate your dependencies and find old, vulnerable versions they can target in an attempt to attack the site.

You MUST commit your composer.json and composer.lock files to your repository.

You MUST NOT commit your vendor directory to your repository.

Including Composer dependencies in your runtime code

Once you install your dependencies you just have a vendor folder. Your dependencies are not loaded automatically as it is. You will need to somehow include your vendor/autoload.php file when your code executes. The best place to do that is in your service provider.

[Note]Note

The repository I am using as an example uses the latter option but that's only because I know that the Composer dependencies will not be used outside the context of my event handlers. If you have a component which uses the Composer dependencies in its Models and you reasonable expect said Models to be used outside the component itself you definitely need to load your dependencies in the service locator. Remember, the canonical way to get a Model object for a component is to boot the component to get its MVCFactory which means that the service provider file is included.

Let's say that you have the following services/provider.php file:

defined('_JEXEC') || die;

use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\Example\Extension\Example;

return new class implements ServiceProviderInterface {
	public function register(Container $container)
	{
		$container->set(
			PluginInterface::class,
			function (Container $container) {
				$subject = $container->get(DispatcherInterface::class);
				$config  = (array) PluginHelper::getPlugin('system', 'example');

				return new Example($subject, $config);
			}
		);
	}
};

You can load your Composer autoloader before returning the anonymous class (see the part in bold type):

defined('_JEXEC') || die;

use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\Example\Extension\Example;

require_once __DIR__ . '/../vendor/autoload.php'

return new class implements ServiceProviderInterface {
	public function register(Container $container)
	{
		$container->set(
			PluginInterface::class,
			function (Container $container) {
				$subject = $container->get(DispatcherInterface::class);
				$config  = (array) PluginHelper::getPlugin('system', 'example');

				return new Example($subject, $config)
            }
		);
	}
};

There is a caveat, though. If someone tries to use one of your classes outside your extension without booting your extension first it will fail. Well, they should never do that. That's not how Joomla 4 and beyond is meant to be used.

Dealing with namespace clashes and older dependency versions included with Joomla

There might be times where you have to deal with namespace clashes. For example, you might be pulling in a dependency which is already shipped as an older version in Joomla itself, a dependency which depends on something else which already exists in Joomla itself as an older version, or you might be pulling in a common dependency that other extensions also pull in. Namespaces are global in PHP. You cannot have a “private” container where you can override namespaced classes defined elsewhere.

First, let's get a simple example composer.json which pulls in a dependency already defined in Joomla:

{
  "name": "acme/example",
  "type": "project",
  "config": {
    "vendor-dir": "components/com_example/vendor",
    "platform": {
      "php": "7.2.5"
    }
  },
  "require": {
    "lcobucci/jwt": "^4.0"
  }
}

The lcobucci/jwt dependency already exists in Joomla 4 but it's in version 3.x of that library. We want to use version 4 of the library which is incompatible with version 3 but has the same namespace, Lcobucci\JWT. Here's the thing. If our dependency is loaded before Joomla's then Joomla's WebAuthn system plugin which depends on it will break. If Joomla loads its own dependency first then our extension breaks. This is an impasse, isn't it?

The solution to that is changing the namespaces of your dependencies, adding a prefix with PHP -Scoper . But first, a few things to make sure everything will work.

First, we need to install PHP-Scoper as a Composer global dependency. From the command line:

$ composer global require humbug/php-scoper

Your global vendor/bin directory is in your path. In most cases you have to make sure that your shell resource file (e.g. ~/.bashrc or ~/.zshrc) has a line like this:

export PATH="~/.composer/vendor/bin:$PATH"

On Windows, if you have installed Composer using its installer then this path is already added to your path. If not, you will have to add it yourself. If you had to add it to your path, close and reopen your terminal emulator or simply log out and log back in. Then we can check if PHP-Scoper is globally available from the command line

$ php-scoper -V
            PhpScoper version 0.17.6@b528b87

If you instead get an error that the command is not found please make sure that you have PHP installed somewhere it can be found by your shell and that you've added Composer's global vendor/bin directory in your path.

The next step is understanding how PHP-Scoper works. It processes a source directory and adds a namespace prefix (“scope”) to all classes within that directory. However, it does NOT overwrite the files in-place; it outputs them to a build directory.

We want to add a namespace prefix to everything in our vendor directory which is currently next to our extension's src directory. Here's what we are going to do. We are going to move the unprefixed, original vendor directory in our repository's root and have the prefixed, modified vendor directory next to our src directory.

So, the first thing to do is changing our composer.json file, removing the vendor-dir configuration key:

{
            "name": "acme/example",
            "type": "project",
            "config": {
            "platform": {
            "php": "7.2.5"
            }
            },
            "require": {
            "lcobucci/jwt": "^4.0"
            }
            }

Next up, delete the old vendor directory and reinstall the dependencies:

$ rm -rf components/com_example/vendor
            $ composer install

You now see a vendor folder in your repository's root. That's good!

We can now configure PHP-Scoper. Create the file scoper.inc.php in the repository's root:

declare(strict_types=1);

use Isolated\Symfony\Component\Finder\Finder;

return [
    'prefix' => 'Acme\\Example\\Dependencies',

	'finders' => [
		Finder::create()
		      ->files()
		      ->ignoreVCS(true)
		      ->notName('/LICENSE|.*\\.md|.*\\.dist|Makefile|composer\\.json|composer\\.lock/')
		      ->exclude([
			      'doc',
			      'test',
			      'test_old',
			      'tests',
			      'Tests',
			      'vendor-bin',
		      ])
		      ->in('vendor'),
		Finder::create()->append([
			'composer.json',
		]),
	],

	'exclude-files'      => [],
	'patchers'           => [],
	'exclude-namespaces' => [],
	'exclude-classes'    => [],
	'exclude-functions'  => [],
	'exclude-constants'  => [],

	'expose-global-constants' => true,
	'expose-global-classes'   => true,
	'expose-global-functions' => true,
	'expose-namespaces'       => [],
	'expose-classes'          => [],
	'expose-functions'        => [],
	'expose-constants'        => [],
];

It's mostly boilerplate, except for the line in bold. This line defines the prefix we're adding to all classes. As configured above, our dependency's namespace will change from Lcobucci\JWT to our prefixed, private namespace Acme\Example\Dependencies\Lcobucci\JWT.

Now run:

$ php-scoper add-prefix --output-dir components/com_example/dependencies

You will see that a new dependencies directory was created next to our extension's src directory.

However, we're not out of the woods just yet. The prefixed autoloader does not work just yet. We need to have Composer rebuild its autoloader:

$ composer dumpautoload -d components/com_example/dependencies \
            --optimize --classmap-authoritative

Finally, we need to change our service provider to load our dependencies from our scoped vendor dir:

defined('_JEXEC') || die;

use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\Example\Extension\Example;

require_once __DIR__ . '/../dependencies/vendor/autoload.php';

return new class implements ServiceProviderInterface {
  public function register(Container $container)
  {
    $container->set(
      PluginInterface::class,
      function (Container $container) {
        $subject = $container->get(DispatcherInterface::class);
        $config  = (array) PluginHelper::getPlugin('system', 'example');

        return new Example($subject, $config)
      }
    );
  }
};

You need to remember to run PHP-Scoper and dump the prefixed composer's autoloader every time you change the dependencies e.g. update or install a dependency.

Also remember to change the code in your extension to use the prefixed classes of the dependencies.

As for your repository management, you should add both vendor and the (prefixed) dependencies folders into your .gitignore file:

# Composer
vendor/*
# Scoped dependencies
components/com_example/dependencies/*