Photo by Gareth Harrison on Unsplash — https://unsplash.com/@gareth_harrison

Over the years I have had the chance to review hundreds of Joomla! plugins written by different developers, typically when they are causing a site to break in unexpected ways. It turns out, most plugins suffer a few very common and easily preventable problems.

You might wonder, do developers knowingly publish broken code? Far from it, they have tested it... but they have only done so in the very narrow use case they expect their plugins to be used. This is called “happy path testing” and is almost as bad as no testing at all. The problem is that when the plugin is used in any other context — the CLI application, non-HTML output, in cases where the output format may not be determined until after the component for the page has finished executing — they will cause unintended consequences, i.e. the site will break. Even worse, clients will start blaming the only innocent parties i.e. Joomla itself and third party developers whose software is written the right way and works perfectly fine. I should know, we are getting at least two tickets every week over at Akeeba Ltd about this kind of broken plugins.

Unfortunately, this is a chicken and egg problem. If you have never debugged a case where your plugin breaks the site you won't know why, how to test for it or how to write your plugin so it doesn't break other people's code and sites. If someone points the problems out to you it will seem like something trivial, obvious even. Hindsight is 20/20.

So let's see the common mistakes and how to avoid them through an example taken from a real world plugin which shall remain unnamed — its developer did contact me privately and we had a productive discussion. In fact, this interaction is what prompted this blog post.

A symphony of mistakes

I will use a very small part of a plugin I was debugging last week to show you four major mistakes you can possibly do as a Joomla plugin developer in five lines of code.

class plgSystemFoobar extends \Joomla\CMS\Plugin\CMSPlugin
{
	public function __construct(&$subject, $config)
	{
parent::__construct($subject, $config);
if (\Joomla\CMS\Factory::getApplication()->isClient('administrator')) return; $document = \Joomla\CMS\Factory::getDocument(); $document->addScript(\Joomla\CMS\Uri\Uri::root(true) . 'plugins/system/foobar/js/foobar.js'); } }

This looks like a deceptively simple system plugin. It adds a JavaScript file to every page load in the frontend. Right?

Well, that is its intention but not what it actually does. It also breaks Joomla 4's CLI and API applications, it breaks pages with non-HTML output, it forbids components from using non-HTML output and tries to load the JavaScript file from the wrong place. Four mistakes in five lines of code.

Do not execute plugin logic in the plugin constructor

Let's think about Joomla's lifetime. In broad terms, the request ends up getting handled by Joomla's index.php file. This spins up the Joomla! application, e.g. \Joomla\CMS\Application\SiteApplication for the frontend. The main entry point for the application object is the doExecute method. This method does a lot of initialisation before routing and dispatching the application, meaning that this initialisation takes place before Joomla has parsed SEF URLs or created a document object. In fact, Joomla will load all enabled system plugins before Joomla has figured out any basic facts about itself.

The developer of this plugin put his business logic in the constructor of the plugin which is executed at this early stage of the Joomla application initialisation. While isClient() will work, the rest of the code which tries to get the document object is wrong code which breaks site.

The plugin erroneously goes through the \Joomla\CMS\Factory::getDocument() method to get the document. This is deprecated in Joomla 4 and will be removed in Joomla 5. You are supposed to use the getDocument() method of the application object. Had the developer done that they'd have seen that they are getting null because the document has not been created yet.

In fact, all this code should be moved from the plugin object constructor to the onAfterDispatch method to work correctly.

Do not go through the \Joomla\CMS\Factory

The second problem with this plugin is the exact reason why the Factory's getDocument() method is deprecated.

Calling the factory's getDocument() will forcibly create a document object which will then be used by the application object. The document object is created based on the information in the request. However, as you might recall, at this point Joomla has not yet parsed the SEF route! This would also be true if this code was moved in the onAfterInitialise event, the earliest system plugin event triggered by Joomla.

Since the SEF URL has not been parsed Joomla cannot reliably know the type of document to use. Think for example about a URL like https://www.example.com/foobar.json which when gone through the SEF URL router will, among other things, set format=json in the request. This means that this request expects Joomla to create a \Joomla\CMS\Document\JsonDocument document object.

However, since format=json has not been set yet, Joomla will assume format=html when you call getDocument(), therefore it will create a \Joomla\CMS\Document\HtmlDocument document object which will be used by the application object as well. This will of course break the component which is handling the request with the fault lying solely on the broken plugin's author.

If I had a dime every time I have been accused of my components being buggy because a third party plugin developer did this I'd be a rich man.

You should only ever call TWO METHODS of the \Joomla\CMS\Factory in Joomla 4:

  • getContainer(). This returns Joomla's Dependency Injection container (DI Container, sometimes abbreviated as DIC).
  • getApplication(). This returns the current Joomla application object handling the request.

That's it. You do not get to use anything else! Everything else is either provided through the DI Container or through the application object itself.

To get the application's document you should do \Joomla\CMS\Factory::getApplication()->getDocument().

There is more to Joomla than HTML output

This is an absurdly common mistake. Developers seem to assume that Joomla will only ever generate HTML output. THIS HAS NOT BEEN THE CASE EVER SINCE JOOMLA 1.0 RELEASED IN 2005 FOR CHRIST'S SAKE! Seriously, people! This was even true in Mambo, Joomla's precursor. Joomla is not WordPress, it is perfectly capable of generating non-HTML output such as XML, JSON, RSS feeds, Atom feeds, raw binary output (e.g. images) and so on and so forth.

The developer of this plugin made the unreasonable assumption that his $document will always contain an HTMLDocument.

A URL like https://www.example.com/index.php?option=com_whatever&format=json — despite the two problems already mentioned above — would still populate $document with a JSONDocument object. However, JSONDocument does not have an addScript method. Therefore this plugin causes a PHP fatal error right away.

The correct way to do that is called “feature detection”:

$document = \Joomla\CMS\Factory::getApplication()->getDocument();

if (!($document instanceof \Joomla\CMS\Document\HtmlDocument))
{
	return;
}

If the document returned by our application is not an HTMLDocument we get the hell out of Dodge. Simple, isn't it?

There is more to Joomla than the frontend and backend

Now let's get to the biggest bug of them all: assuming that Joomla consists entirely of the frontend (site) and backend (administrator) application. This has not been true since Joomla 1.6, released in 2010 — that's twelve years ago at the time of this writing and I still see this bug!

This line is wrong:

if (\Joomla\CMS\Factory::getApplication()->isClient('administrator')) return;

Clearly, the developer wanted to write “if this is not the frontend of the site don't do anything”. Instead, what they actually wrote is “if this is the backend of the site — therefore it is the frontend of the site or the api application, or the console application, or any custom application which extends from Joomla's WebApplication class — don't do anything”. Oops.

You see, Joomla 4 has a number of applications shipped with it:

  • installation. This is the web installer when you build a new site. Third party code, like our symphony-of-mistakes system plugin, does not load in it and does not concern third party developers. It's been around since Joomla 1.0.
  • site. The frontend of the site. It's been around since Joomla 1.0.
  • administrator. The backend of the site. It's been around since Joomla 1.0.
  • api. The /api folder of your Joomla 4 site. Introduced in Joomla 4.0.
  • cli. The cli/joomla.php command line application. Introduced in Joomla 4.0.

Further to that, applications other than site and administrator have existing since Joomla 1.5.

From Joomla 1.5 onwards it's been possible to create your own custom application by extending JApplicationWeb. These custom application do load system plugins by default. They were used to create custom entry points for callbacks, e.g. in payment plugins for e-commerce components. They are no longer used as the reason for their existence has been made a moot point since the advent of com_ajax in Joomla 2.5.

From Joomla 1.6 and up until Joomla 5.0 will be released it's been possible for developer to create custom CLI applications by extending JApplicationCli. These applications do not load system plugins by default so they are unlikely to have broken.

This is why despite the fact that this has been an issue for at least 10 years plugin developers may have not bumped into this until Joomla 4 was released.

The correct way to do this is, of course, to check explicitly for the application you are running under:

if (!\Joomla\CMS\Factory::getApplication()->isClient('site')) return;

Do not load static resources directly and/or from your plugin's folder

This is a bonus round and not a bug which will break sites today, but it will break sites come Joomla 5.0 and a mild security concern :)

The developer of the extension chose to load their static JavaScript file using the deprecated addScript() method of the document and located the file in the plugin's folder structure. This is a two-in-one issue.

First of all, ever since Joomla 1.5 (released in 2007 — 15 years ago at the time of this writing) Joomla introduced the media folder where extensions are expected to place all publicly available static files, be they static files or user-generated content managed outside Joomla's Media Manager.

The developer of the extension should have placed his JavaScript file in the media/plg_system_foobar/js folder using the following section in their plugin's XML manifest:

<media folder="media" destination="plg_system_foobar">
<folder>js</folder>
<file>joomla.asset.json</file>
</media>

(We'll see what the joomla.asset.json file is in a moment)

It is a bad security practice mixing executable backend code (.php files) with frontend executable code (.js, .es6 etc) files and static media files (CSS, images, videos, ...). Joomla is moving towards placing all frontend accessible stuff into the media folder — even for templates, as of Joomla 4.1 — and will most likely start applying security controls to prevent web access to the plugin, component, module etc folders. You have been warned.

The next issue is that HTMLDocument's addScript() method has been deprecated. Joomla 4 has moved into using asset dependencies and a Web Asset Manager to, well, manage the asset dependencies!

Assets and their dependencies are declared in the joomla.asset.json file located in the extension's subdirectory under the media directory. So, the developer should've shipped a file media/plg_system_foobar/joomla.asset.json with the following contents:

{
	"$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json",
	"name": "plg_system_foobar",
	"version": "1.0.0",
	"description": "Foobar plugin",
	"license": "GPL-3.0-or-later",
	"assets": [
		{
			"name": "plg_system_foobar.foobar",
			"description": "Foobar JavaScript",
			"type": "script",
			"uri": "plg_system_foobar/foobar.js",
			"dependencies": [
				"core"
			],
			"attributes": {
				"defer": true
			}
		}
	]
}

This allows the developer to tell Joomla to add their script using this very simple piece of code: 

$document->getWebAssetManager()->useScript('plg_system_foobar.foobar');

Here's the kicker. This is safe even if you don't check the Document object type. Yeah, if $document is a JSONDocument which does not have a concept of web assets this code would still work. It's still a good idea to check the document type the way I've told you to avoid doing pointless work, or introducing future bugs.

Putting it all together

Let's put everything we learned together. Our five line plugin barely grew by a couple of lines and it no longer breaks the sites it is installed in. The changes are in bold type:

<?php
class plgSystemFoobar extends \Joomla\CMS\Plugin\CMSPlugin
{
public function onAfterDispatch()
{
if (!\Joomla\CMS\Factory::getApplication()->isClient('site')) return;

$document = \Joomla\CMS\Factory::getApplication()->getDocument();

if (!($document instanceof \Joomla\CMS\Document\HtmlDocument))
{
return;
}


$document->getWebAssetManager()->useScript('plg_system_foobar.foobar');
}
}

It's still short. It's still readable. It's (mostly) future-proof — well, it's still using the legacy CMSPlugin structure but that's another blog post for another day.

Hopefully, if you are a Joomla! plugin developer who read this article you can now write plugins which don't break other people's extensions and the sites they are installed in. Even better, you can understand if you are inadvertently breaking people's sites with your plugins and fix them. If you don't we'll find out and have you read this blog post 😉

No comments