Work In Progress

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

Chapter 3. Plugins

Plugins are the fundamental building blocks of Joomla!. They let us execute code when something interesting happens. Unsurprisingly, plugins are extremely powerful and the cornerstone of implementing complex features which alter or add features in Joomla without having to modify core files (“hack core”) as is usual in other CMS. This lets us have very powerful, easily maintainable, sites.

The many forms of a Joomla plugin

Joomla plugins have been around for a very long time. In fact, they've been around since before Joomla forked off Mambo in August 2005. They were called ‘mambots’ back then. Having such a fundamental feature for over two decades understandably means that there are many forms of plugins possible.

Legacy (Joomla 1.x to 3.x)

It might come to you as a surprise, but the original way plugins were implemented in Joomla 1.0 back in 2005 is still supported in Joomla 4. This support will be removed in Joomla 6, scheduled for release in 2025, two decades after it first appeared. Sure, there have been refinements but the core concept still applies.

[Important]Important

Even though Joomla uses this form of plugin for most of its core plugins in Joomla 4 (and possibly Joomla 5), this is a form of plugin which has been deprecated and will most likely go away in Joomla 6.

This form of plugin is only recommended if you are writing a version of your software which is meant to run on both Joomla 3 and Joomla 4 / 5, to facilitate people trying to migrate their sites over to a newer Joomla version.

If you are writing software native to Joomla 4 and beyond you should use the Joomla 4 with SubscriberInterface form of plugins explained further below.

The only exception to this rule are, at the time of this writing, plugins in the editors-xtd folder because they are not real, pure Joomla plugins. Their class is instantiated directly by Joomla and the onDisplay method called directly.

Legacy plugins consist of a single class which is named PlgTypeName where Type is the plugin type a.k.a. folder (e.g. system, user, console, …) and Name is the name of the plugin. For example, we could have PlgSystemExample for a system plugin named example and which lives in plugins/system/example/example.php. The class always extends from \Joomla\CMS\Plugin\CMSPlugin or one of its sub-classes typically defined in a component (e.g. finder plugins extend from \Joomla\Component\Finder\Administrator\Indexer\Adapter which extends from \Joomla\CMS\Plugin\CMSPlugin).

Any public method whose name starts with on is registered as an event listener. Therefore a method called onFooBar is registered as a legacy plugin event listener for an event called onFooBar.

There's a small caveat when it comes to Joomla 4 and 5. If the method accepts only one parameter which is either named $event OR is type-hinted as a class implementing \Joomla\Event\EventInterface then the method is registered as an event listener, a new type of listener. See Legacy vs Event Listener methods. This is a deliberate design choice which lets you write plugins which can simultaneously handle legacy events when running under Joomla 3 and modern events when running under Joomla 4 and 5. This lets you provide a version of your plugin which acts as a “bridge” for people migrating from Joomla 3 to 4 and 5: the same plugin can run on both versions of Joomla without breaking the site.

Joomla 4 classic

As noted earlier, Joomla 4 introduced Dependency Injection and service providers for all extensions, of course including plugins. It should come as no surprise then that the second plugin variant we get is similar to the legacy plugins but with namespaces and service providers.

The first difference you will notice is that the XML manifest goes from this:

<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="system" method="upgrade">
    <name>plg_system_example</name>
    <author>D.Veloper</author>
    <creationDate>2022-10</creationDate>
    <copyright>(C) 2022 Acme Inc</copyright>
    <license>GNU General Public License version 2 or later; see LICENSE.txt</license>
    <authorEmail>d.veloper@acme.com</authorEmail>
    <authorUrl>www.acme.com</authorUrl>
    <version>1.0.0</version>
    <description>PLG_SYSTEM_EXAMPLE_XML_DESCRIPTION</description>
    <files>
        <filename plugin="example">example.php</filename>
    </files>
    <languages>
        <language tag="en-GB">language/en-GB/plg_system_example.ini</language>
        <language tag="en-GB">language/en-GB/plg_system_example.sys.ini</language>
    </languages>
</extension>
            

to this:

<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="system" method="upgrade">
    <name>plg_system_example</name>
    <author>D.Veloper</author>
    <creationDate>2022-10</creationDate>
    <copyright>(C) 2022 Acme Inc</copyright>
    <license>GNU General Public License version 2 or later; see LICENSE.txt</license>
    <authorEmail>d.veloper@acme.com</authorEmail>
    <authorUrl>www.acme.com</authorUrl>
    <version>1.0.0</version>
    <description>PLG_SYSTEM_EXAMPLE_XML_DESCRIPTION</description>
    <namespace path="src">Acme\Plugin\System\Example</namespace>
    <files>
        <folder>services</folder>
        <folder plugin="example">src</folder>
    </files>
    <languages>
        <language tag="en-GB">language/en-GB/plg_system_example.ini</language>
        <language tag="en-GB">language/en-GB/plg_system_example.sys.ini</language>
    </languages>
</extension>
            

The affected lines are in bold type.

First of all, we have a <namespace> tag to declare our namespace. The namespace follows the convention MyCompany\Plugin\Type\Name where MyCompany is the vendor namespace prefix, Type is the plugin type a.k.a. folder (e.g. system, user, console, …) and Name is the name of the plugin. TheType must be written as Uppercasefirst i.e. the first letter is uppercase and all others are lowercase.

The second obvious change is that instead of a plugin file we have two folders, services and src.

The services folder contains a single file, provider.php. It's very similar to a component's service provider file:

<?php
defined('_JEXEC') || die;

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

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

                $app = Factory::getApplication();

                /** @var \Joomla\CMS\Plugin\CMSPlugin $plugin */
                $plugin = new Example($subject, $config);
                $plugin->setApplication($app);

                return $plugin;
            }
        );
    }
};

The important thing to note here is that the service provider is responsible for registering a Joomla\CMS\Extension\PluginInterface service which returns an object instance of our plugin class.

[Tip]Tip

Your plugin class can also implement the \Joomla\CMS\Extension\BootableExtensionInterface interface. If it does, its boot method will be called when Joomla loads your plugin, before any event is executed, and gives you access to the plugin's Dependency Injection container. While you shouldn't try to execute any significant amount of code at this point (Joomla has not finished booting up yet!) and you shouldn't use the DI container directly to pull resources (the service provider is meant to push them to the plugin instead) it may come in handy for these, uh, forbidden purposes.

Here's an example. There are some services which have a lengthy initialisation — for example, a foreign currency exchange service will need to periodically pull the currency exchange rates from a central bank's web site. Ideally, you'd want to move their initialisation outside the constructor and into a different method you need to call before doing something useful with your service. However, you may also run into chicken-and-egg situations trying to do that. If the server doesn't support cURL the service might throw an exception. I'd like to catch it so I can not offer this feature with unsatisfied server dependencies. But if I do that in the service provider I am also very likely trying to make a web request to the central bank's website to get the currency exchange rate at an inopportune moment where the application needs to finish loading as fast as possible.

While there are clever ways to work around that, you may find it far easier to get access to the container in the boot method, store a reference to the container in a private property of your plugin and pull an instance of your custom forex service when you need it. If it throws an exception you can implement your “this isn't possible on this server” logic. By instantiating your service through the provider only when needed you solved your chicken-and-egg problem.

A cleaner solution would of course be trying to check the server dependencies on service instantiation and set a flag in the service. When the service consumer (your plugin) tries to use the service you could throw an exception. This is an obvious solution in this simple problem. For more complex problems there might not be an equally obvious solution.

As I always say to aspiring developers, your goal is to deliver something useful and maintainable in a finite period of time. If it means writing something a CS professor would give a disapproving frown, so be it. You can revisit that implementation later, when you have time to burn. As Steve Jobs succinctly put it, “real artists ship”.

The plugin class, as with legacy plugins, extends from \Joomla\CMS\Plugin\CMSPlugin and works the same as a Legacy plugin's class.

You will not see any plugins of this type in the core and they are pretty rare in the wild, mostly from software — like older versions of mine — which started the conversion process to Joomla 4 before Joomla 4 was finished and before the modern event system was mature enough for general use.

Practically speaking, the only use case for this type of plugin is if you still have plugin events which pass around scalar variables by reference. For example, something like this:

public function onMyCustomEvent(string $foo, array &$bar);

The $bar variable is a scalar (array) passed by reference. It can be modified by the plugin event handler. This will not be possible using modern events.

If you want a clean solution you need to make changes in the code which calls this plugin event. Instead of passing around an array you'd have to pass a \Joomla\CMS\Object\CMSObject or \Joomla\Registry\Registry object created from the contents of the array. Your modern event handler can modify that object (objects are always passed by reference in all versions of PHP supported by Joomla 4 and beyond). Then your consumer code could convert back from an object to an array.

The clean solution is admittedly more convoluted than you might expect and requires changes in the consumer which might not be possible if it's not code under your control. If you do not feel confident implementing the cleaner solution, or if it's just not possible because third party code is using your events, you can create a Joomla 4 classic plugin with a legacy plugin event handler for your plugin event which won't give you a hard time with scalar variables passed by reference. It's not the best coding practice but it does take a while —and several refactoring passes— to migrate to a cleaner architecture. Even more so considering that Joomla 3 and these coding practice had been around for nearly a decade before Joomla 4 was released.

Joomla 4 with SubscriberInterface

You might remember from an earlier chapter talking about the basic services in a component's service provider that you can trigger plugins using modern events like so:

$event = new \Joomla\Event\Event('onSomething', [$param1, $param2]);
$this->getDispatcher()->dispatch($event->getName(), $event);
$results = $event->getArgument('result', []);

It follows reason that Joomla 4 offers a way to write plugins which only deal with these modern events. These events are faster and have many more tricks up their sleeves than the ‘dumb’ callback model implemented in Joomla 1.x, 2.x and 3.x.

Plugins of this type are just like Joomla 4 classic plugins with a twist. The plugin class implements the \Joomla\Event\SubscriberInterface. This changes the way Joomla registers event handlers. Implementing this interface requires you to implement the getSubscribedEvents method, defined in the interface. This returns an array mapping event names to the names of public methods of your plugin.

The implementation of the getSubscribedEvents method is pretty straightforward:

public static function getSubscribedEvents(): array
{
    return [
        'onSomething'     => 'doSomething',
        'onSomethingElse' => ['doSomething', \Joomla\Event\Priority::HIGH],
    ];
}

The keys of the array are the event names. The values are the names of the methods which handle each event.

But, wait a minute! That second item has an array value. What is that? Well, that's one of the benefits of using SubscriberInterface: you can tell Joomla about the priority you want for your event handler, meaning that the order plugins execute is not dictated only by their user-defined ordering in the backend of the site but also by the programmer's preferred priority.

By default, all event handlers are attached with Normal priority. They are then executed in the order they were attached, i.e. how the plugins were ordered in the backend Plugins management page of the site. This is the only option you get with legacy and classic plugins.

Plugins implementing the SubscriberInterface can optionally set a priority for each event handler. This is an integer. The higher the integer is, the earlier your event handler executes. Joomla first executes all event handlers with the highest priority number in the order they were attached. Then moves to the event handlers with the second highest priority number in the order they were attached and so on.

If you set your priority to high, as I did above, your event handler will be one of the first (if not the absolute first) to be executed. If, conversely, you set it to \Joomla\Event\Priority::MIN or even PHP_INT_MIN your event handler will be one of the last (if not the absolute last) to be executed. In the few cases where this is truly needed you no longer have to tell your users to reorder plugins for your plugin to work correctly and predictably; you can just set the priority.

[Caution]Caution

With great power comes great responsibility. Do NOT set the priority of your event handlers unless there is an absolute need to do so.

For example, a security plugin will need to guarantee that its plugin events execute first to avoid other plugins' security vulnerabilities being triggered by malicious requests. Full page HTML source code search & replace plugins will need to have their onAfterRender handler run as the very last to be able to replace HTML text right before it's potentially compressed and returned to the browser.

The vast majority of plugins should NOT set a priority. By setting a priority you make it very hard if not impossible for the user to change the execution order of your plugin which might be necessary to work around issues on their site. DO NOT TAKE AWAY THE USER'S AGENCY UNLESS THERE IS AN VERY WELL JUSTIFIED AND DOCUMENTED REASON.

Moreover, you should keep in mind that all methods handling events only accept one argument (by value, not reference!), an event object which implements the \Joomla\Event\EventInterface interface. We will talk about this in the next section on listener types.

Why bother? Performance!

When you are using SubscriberInterface in your plugins you can and should set the protected property $this->allowLegacyListeners = false; to tell Joomla! that your plugin only handles plugin events defined by the getSubscribedEvents method. Joomla will NOT use the super slow PHP ReflectionObject to find public methods whose name starts with on. It will instead only call the public static method getSubscribedEvents which is defined in the interface and implemented in your plugin class and register the event handlers it describes.

This is the key to massive performance improvement on your users' sites.

Not having to go through reflection saves a lot of time on every page load of the site. Moreover, events are self-contained objects being passed around which reduces the overhead of calling each event handler. These add up with the dozens to hundreds of plugins and event handlers running on a typical Joomla site, saving several dozens to a few hundreds of milliseconds of page load time. This is a significant performance improvement for the site. This is why I've been telling people since 2017, that modern events make Joomla 4 and later versions faster.

Legacy vs Event Listener methods

As we have already alluded to in the previous sections, there are two types of plugin event listeners.

Legacy plugin event listeners

Legacy plugin event listeners are nothing more than glorified callbacks. They are plain old methods which accept a number of parameters and possibly[10] return a value. For example, we could have something like this:

public onSomething(string $foo, array $bar): array

This simplicity is simultaneously the strength and the Achille's heel of legacy plugin event handlers.

  • Having an arbitrary number of parameters makes it really hard to know what is the canonical parameters list when you are calling a plugin event and when you are implementing a handler for it.

  • It's impossible to add new parameters, even optional ones, without a breaking change. If different handlers have a different number of parameters (or, worse, parameters order!) they expect you might get PHP warning or errors.

  • Neither the parameters nor the return values can be type-hinted. This makes it perfectly possible for a developer, core or third party, to pass the wrong data type to an event's argument e.g. an object where an array is expected. It also makes for inconsistent return values which need to be normalised and validated in consumer code, i.e. every time an event is called.

  • The aforementioned problems can result in bugs which are really hard to address. For example, a third party extension may cause a core or third party plugin to fail with a PHP error on PHP 8 by passing the wrong data type to it. Conversely, a third party plugin may cause core or third party components or modules to fail by returning an unexpected data type. Identifying who messed up is hard. Explaining to a client that it's not your code at fault but a third party plugin may be really hard, especially if that third party plugin works fine in the very limited subset of use cases it was developed for and tested on.

  • Running callbacks with an arbitrary number of arguments requires going through PHP's call_user_func_array() function which has more overhead than calling methods directly. This is only relevant on older PHP versions; the performance delta is nearly gone in PHP 8.

The only thing this simple callback arrangement has going for it is that, well, it's very simple to implement in the code which calls the plugin events, the core code which runs the plugin event handlers and the code which consumes the results of plugin event handlers — though the latter is debatable.

Modern event listeners

Joomla 4 introduced a whole new concepts: events.

Each event is its own object. It has arguments which can be anonymous or named, typically the former in generic events and the latter in concrete events. Events always have a named argument called result which lets the event handler not only return a result but also inspect what other results were returned by event handlers called before it (most concrete events have an addResult method to facilitate returning results). A concrete event class can implement type checks and validation of any (or all) of its arguments, including the results. It's no longer the Wild West.

Event handlers have the option to stop the processing of an event's handlers by calling the event object's stopPropagation method; this is useful when you want to run a number of plugins until you get at least one result. Think about avatars displayed in a forum. You may have a number of plugins which can return an avatar image: Gravatar (if the user has an account there), using an image source from a user field, Facebook (if the user has linked an account), a locally installed social network component, and a fall-back to a generated fake avatar. The first avatar found will be used, the rest will be discarded. If the rest are going to be discarded, why even bother running the code to check if they exist?! Instead, each avatar plugin would work like this. It runs its code. If there is no avatar to be reported, return. If there is an avatar to be reported use addResult to return it and call the event's stopPropagation method to prevent the other avatar plugins from running for no reason and wasting the user's time and your server's resources. This is something you just cannot do with legacy event listeners.

Also, as we mentioned earlier, modern event handlers can have prioritised event handlers with all the benefits we already mentioned.

Modern event handlers are fairly simple:

public function handleSomething(\Joomla\Event\Event $event): void

Note the type-hint to the $event argument. As written above, the method can be used to handle any event. In most cases you will need to use a concrete event class: either the concrete core event classes supplied in Joomla's libraries/src/Event directory and its subdirectories or a concrete event class provided by another Joomla extension.

Getting the arguments to the event depends on the event class itself. For generic events you'd need to do something like:

[$param1, $param2, $param3] = $event->getArguments();

That's because generic events do not have named arguments. This is the equivalent of having a legacy plugin event handler with the method signature:

public function onSomething($param1, $param2, $param3);

With concrete events, it depends on the event. In most cases you'd do either of the following:

// Named arguments without a getter method
$param1 = $event->getArgument('param1');
// Named arguments with getter methods
$param2 = $event->getParam1();

Any decent IDE will let you know if there is a getter in $event based on its type-hint. That's why it makes sense to type-hint your event handler methods correctly.

[Important]Important

If you are writing a plugin targeting Joomla! 4 and 5 or, more generally speaking, two different Joomla! versions where a core event is a generic event in one version and a concrete event in the next version you can use the following pattern:

[$param1, $param2] = array_values($event->getArguments());

For example, the onContentPrepareForm event changed from a generic event in Joomla! 4.3 to a concrete event in Joomla! 5.0.

In Joomla! 4 we could do:

[$form, $data] = $event->getArguments();

This no longer works on Joomla! 5. Both $form and $data will always be NULL. This is because the getArguments() method is written in a stupid way; it returns an integer-indexed array for generic events (and, generally speaking, unnamed arguments) but a string-indexed array for concrete events (and, generally speaking, named arguments).

I had warned the Joomla! project's maintainers back in 2021 that this would cause a MASSIVE backwards compatibility issue as soon as Joomla! moves to concrete events with named arguments. I had even contributed code to avoid that. Unfortunately, neither my warning was heeded for Joomla! 5, nor my code was used. Fortunately, they did (accidentally...) followed my advice of keeping the same order of arguments. This means that by passing the result of that method through PHP's array_values() we can safely ditch the string array indexes and "downgrade" the array back to integer-based indexing which allows us to use the array extraction pattern list($a, $b) = $event->getArguments (or, in more modern PHP, [$a, $b] = $event->getArguments()).

Therefore, the way to write our code in a way that works in both versions of Joomla!, with generic and concrete events, is this awkward monstrosity:

[$form, $data] = array_values($event->getArguments());

Of course, if you enjoy pain and suffering you could be tempted to write the same thing as follows (DON'T DO IT; IT WILL NOT WORK; READ BELOW):

if (version_compare(JVERSION, '4.999999.999999', 'lt') {
  [$form, $data] = $event->getArguments();
} else {
  $form = $event->getArgument('subject');
  $form = $event->getArgument('data');
}

Why checking against version 4.999999.999999 instead of 5.0.0? Because Joomla! 5 alpha, beta, and RC versions are lower than 5.0.0 stable. If you do version_compare(JVERSION, '5.0.0', 'ge') on Joomla! 5.0.0-beta1 it will return FALSE.

A more careful observer might wonder why not write something like if($event->hasArgument('subject') (feature detection instead of version detection). Well, my friends, all core Joomla! events extend Joomla's GenericEvent which ALWAYS has an argument called subject, even on Joomla! 4.0.0; it just wasn't used in the older versions. Therefore, trying to do it the "right way" by using feature detection would make your code not work on Joomla! 4 if the event was fired by Joomla! itself.

However, even the code detecting the Joomla! version WILL NOT WORK CORRECTLY because a developer can always call the onContentPrepareForm event with this code:

$event = new \Joomla\Event\Event('onContentPrepareForm', $arguments);
\Joomla\CMS\Factory::getApplication()
  ->getDispatcher()
  ->dispatch('onContentPrepareForm', $event);

This ends up using a generic event instead of the concrete event, and your code goes to hell.

This is something which could have easily been prevented by going through the application's triggerEvent method, only that method is deprecated and will go away. Therefore, Joomla's lack of foresight has led to a situation where bugs will be aplenty with Joomla extensions in the next several years to come. What a great way to convince Joomla! users that new major versions (and minor versions, come to think of it) are "safe" to update to. Sigh.

The return type of a modern event handler is always void. You do NOT return values directly. Instead, you add them to the result argument which is always an array:

$result = $this->getArgument('result') ?? [];
$result[] = $theResultIAmReturning;
$this->setArgument('result', $result);

Most concrete event classes implement the \Joomla\CMS\Event\Result\ResultAwareInterface which has the addResult method to return values more easily:

$this->addResult($theResultIAmReturning);

Finally, remember that events can be mutable, immutable or selectively immutable. A mutable event allows its handlers to use setArgument freely, modifying all of its arguments.

[Caution]Caution

Even though most core events are currently mutable, this will change in Joomla 5 and Joomla 6. Joomla is moving towards concrete event classes for all core events. These concrete classes will be selectively immutable: all of their arguments will be immutable except for the result argument; the latter only if it's an event which expects a result to be returned.

Therefore you MUST NOT rely on core events' arguments being mutable at the moment. Treat them as immutable or you will suffer in the future!

An immutable event does not allow any of its arguments to be set after it has been created. This does not mean that you cannot do anything with the arguments! Remember that argument values which are objects can be modified without needing to set the argument again. For example, this is a valid, working event handler:

	public function handleTableAfterLoad(\Joomla\CMS\Event\Table\AfterLoadEvent $event): void
{
    if (!$event->getArgument('result')) return;

    /** @var \Joomla\CMS\Table\TableInterface $table */
    $table = $event->getArgument('subject');

    $table->modified_by = \Joomla\CMS\Factory::getApplication()
        ->getIdentity()
        ->id;
}

The \Joomla\CMS\Event\Table\AfterLoadEvent concrete event class is an immutable event. However, it has a subject argument which contains a Joomla Table object. All objects in PHP are passed by reference; any change we make to it will persist when we return from our method. That's why we can change its modified_by property without returning any value from our event handler and despite the event itself being immutable.

Finally, we have selective immutable events. On the face of it, they are immutable events. However, they have setter methods for specific named arguments. Actually, the \Joomla\CMS\Event\Table\AfterLoadEvent is a selectively immutable event. It has setters for the result and row arguments. Every event implementing \Joomla\CMS\Event\Result\ResultAwareInterface is selectively immutable; there is always the addResult method to add to the results returned by the event.

Handling events is a bit more involved than using simple callback methods like you used to in legacy plugins. If you feel utterly lost, var_dump the $event variable and die();[11]. You will get a lot of insight as to what the event does and which data does it carry.



[10] Actually, they always have to return a value. It just so happens that the return value of methods typehinted to return void is NULL in PHP. At some point, in newer PHP versions, this will start throwing an error which is another reason legacy listeners will have to eventually go away.

[11] You can of course attach a PHP debugger and add a breakpoint to your event handler to better inspect what is going on. Even though this method is preferable I understand that not everyone knows how to do it. Plus, var_dump and die is easy and I use it too when I just want to get a quick idea of what's going on with a variable without having to temporarily disable my other breakpoints. All tools are useful when used appropriately.