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 |
---|---|
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 |
---|---|
It is good practice to NOT include your
You MUST commit your You MUST NOT commit your |
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 |
---|---|
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/*