Table of Contents
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.
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.
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 | |
---|---|
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
|
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.
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
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.MyCompany
\Plugin\Type\Name
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 | |
---|---|
Your plugin class can also implement the
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 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.
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 | |
---|---|
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.
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 | |
---|---|
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 In Joomla! 4 we could do: [$form, $data] = $event->getArguments(); This no longer works on Joomla! 5. Both 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
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 A more careful observer might wonder why not write something like
However, even the code detecting the Joomla! version WILL NOT WORK CORRECTLY because a developer can
always call the $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
|
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 | |
---|---|
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
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.