Plugins in Joomla
Plugins are the invisible workers of Joomla. They are event-driven code that runs in the background to change behaviour rather than to produce a page. A plugin has no URL, no module position, and often no visible output at all.
Yet plugins are how almost everything in Joomla gets extended without changing a single line of core code.
How Joomla reacts to events and extends itself without touching the core.
This article explains how Joomla plugins really work. It starts with the basics for website owners and administrators, then moves on to the technical details for developers. You will learn what a plugin is, how Joomla triggers it, how a modern plugin is built, and how to avoid the most common mistakes.
The one idea to take home is this: a plugin is a listener. It waits for something to happen, then reacts. Learn how one plugin subscribes to one event, and you understand how all of Joomla's extensibility works.
1. The Basics
1.1 What is a Plugin?
A plugin is one of Joomla's five extension types. Where a component builds the page and modules decorate it, a plugin reacts to things happening.
Think of it this way:
- A component answers the question: "What is this page?"
- A module answers: "What else appears around it?"
- A plugin answers: "What should happen when X occurs?"
A plugin does not own a URL and is not placed on a page. It waits for an event to fire, then runs. The code that fires the event never knows who, if anyone, is listening.
1.2 Where Plugins Sit in the Extension Family
Knowing the neighbours is what makes the word "plugin" meaningful. Joomla has five extension types:
| Type | Role | Per page | Prefix |
|---|---|---|---|
| Component | Main page content / application | exactly 1 | com_ |
| Module | Small boxes around the content | many | mod_ |
| Plugin | Event-driven behaviour in the background | many | plg_ |
| Template | Look, feel and page layout | 1 site + 1 admin | tpl_ |
| Language | Translations | 1 active | - |
If the component is the engine and modules are the dashboard, plugins are the sensors and relays wired throughout the machine: silent until their trigger fires.
1.3 What a Plugin Actually Is
A plugin has no place on the page. Instead, it hooks into the request lifecycle. Here is a simplified view of where plugins step in during a normal page request:
Browser request
|
v
System plugins fire ← onAfterInitialise, onAfterRoute ...
v
Component runs (com_content builds an article)
v
Content plugins fire ← onContentPrepare transforms the text
v
System plugins fire ← onBeforeRender, onAfterRender
v
Page sent to the browser
At each step, Joomla "announces" what it is about to do. Any plugin that is listening can step in and act.
1.4 The plg_ Naming Convention
Every plugin belongs to a group (a folder) and has an element (its own name):
plg_content_joomla
| |
| └── element (the plugin itself)
└── group (which events it relates to)
- The group decides where the plugin lives and which events it typically listens to.
- On disk it lives in
plugins/<group>/<element>/, for exampleplugins/content/joomla/.
1.5 The Plugin Groups You Already Use
Open the plugins/ folder and you will find one folder per group. Each is a family of plugins that serve a purpose:
| Group | Purpose | Example |
|---|---|---|
system |
Run on (almost) every request | plg_system_cache, plg_system_sef |
content |
Transform or react to article content | plg_content_joomla, plg_content_pagebreak |
authentication |
Verify a user's credentials | plg_authentication_joomla |
user |
React to user create/save/login/logout | plg_user_joomla |
editors |
Provide the WYSIWYG editor | plg_editors_tinymce |
editors-xtd |
Extra editor buttons (Article, Image) | plg_editorsxtd_article |
finder |
Smart Search indexing and search | plg_finder_content |
fields |
Custom field types | plg_fields_calendar |
webservices |
Expose a component's REST API | plg_webservices_content |
task |
Scheduled (cron-like) jobs | plg_task_checkfiles |
multifactorauth |
Two-factor authentication methods | plg_multifactorauth_totp |
quickicon |
Admin control-panel icons and checks | plg_quickicon_joomlaupdate |
The group is mainly a label for organisation and discovery. Technically, a plugin can listen to almost any event regardless of which folder it lives in.
Back to top2. How Plugins Are Triggered
2.1 The Publish and Subscribe Model
Plugins work through Joomla's event dispatcher. There are two sides, and they never reference each other directly:
PUBLISHER SUBSCRIBER
(core or a component) (your plugin)
dispatch('onContentPrepare') ----> onContentPrepare()
| |
| "something happened" | "I'll handle it"
└----------- decoupled ------------┘
- The publisher fires an event by name, then moves on.
- Every enabled plugin that subscribed to that name gets called, in order.
- Neither side knows about the other. This is called loose coupling.
This is the single most important pattern in Joomla. Once it clicks, the whole extension system starts to make sense.
2.2 An Event Has a Name, Data, and Sometimes a Result
When a component announces an event, it passes data along with it. A plugin can read that data, change it, or even stop the action:
// A component announces an event and passes data with it:
$event = new BeforeSaveEvent('onContentBeforeSave', [
'context' => 'com_content.article',
'subject' => $article, // the data plugins may read / change
'isNew' => $isNew,
]);
$this->getDispatcher()->dispatch($event->getName(), $event);
// A plugin can stop the action:
if ($somethingWrong) {
$event->addError('You shall not save.');
$event->stopPropagation(); // no later plugin runs
}
An event typically carries these things:
| The event carries... | Example |
|---|---|
| A name | onContentBeforeSave |
| A context | com_content.article - which thing |
| A subject / arguments | the article object, the form data |
| A way to return a result | transformed text, or false to abort |
2.3 The Naming Convention Reveals the Timing
Event names are deliberately readable. The verb inside the name tells you when it fires:
onContentBeforeSave ← just before the action (you can still cancel it)
onContentAfterSave ← just after the action (it already happened)
onUserLogin ← the moment it occurs
onAfterRoute ← a lifecycle checkpoint
| Prefix / suffix | Meaning |
|---|---|
on... |
Every event name starts with on |
...Before... |
Fires before - you can validate or abort |
...After... |
Fires after - react, log, notify |
onAfter... (system) |
A point in the request lifecycle |
3. The System Lifecycle Events
3.1 The Events That Fire on Nearly Every Request
system plugins are the most powerful, because they run on every page. They hang off the application lifecycle:
onAfterInitialise → app booted, session ready (cache, redirect, debug start here)
onAfterRoute → URL resolved to option/view/id (good place to alter the request)
onAfterDispatch → component has produced its output, not yet wrapped
onBeforeRender → template about to render (inject modules, late changes)
onAfterRender → full HTML built (string-level find/replace, for example SEF)
onBeforeRespond → response object ready, about to be sent
A surprising amount of Joomla's own behaviour - caching, SEF URLs, the debug bar, redirects, two-factor prompts - is implemented as ordinary system plugins listening to these events.
3.2 The Classic Content Events
content plugins, and any component that opts in, get these events around content:
| Event | Fires when | Typical use |
|---|---|---|
onContentPrepare |
Just before text is displayed | Replace {shortcodes}, embed media |
onContentBeforeSave / onContentAfterSave |
Around saving an item | Validate, sync, notify |
onContentBeforeDelete / onContentAfterDelete |
Around deleting an item | Clean up related data |
onContentChangeState |
Publish / unpublish / archive | Update caches, send alerts |
onContentPrepareForm |
A form is being built | Add fields to existing forms |
onContentPrepareData |
Data loaded into a form | Pre-fill custom data |
Because onContentPrepare runs on any text that has a context, one content plugin can affect articles, contacts, categories, and custom HTML modules alike.
4. Inside a Plugin (the Modern Structure)
4.1 The Joomla 4, 5 and 6 Folder Layout
Since Joomla 4, plugins are namespaced, PSR-4, dependency-injection-aware classes, just like components:
plugins/content/example/
├── example.xml ← manifest (group, element, files, params)
├── services/
│ └── provider.php ← DI registration (the entry point)
├── src/
│ └── Extension/
│ └── Example.php ← the plugin class (the listener)
├── language/ ← translation .ini files
└── tmpl/ (optional) ← layouts, if the plugin outputs HTML
Like components, the modern entry point is services/provider.php. There is no "magic" file-name loading anymore.
4.2 services/provider.php - Wiring the Plugin
The provider registers the plugin with the DI container and hands it any services it needs, such as the database or the application:
return new class () implements ServiceProviderInterface {
public function register(Container $container): void
{
$container->set(
PluginInterface::class,
function (Container $container) {
$dispatcher = $container->get(DispatcherInterface::class);
$plugin = new Example(
$dispatcher,
(array) PluginHelper::getPlugin('content', 'example')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};
The container injects the dependencies, so the plugin is testable and never reaches into global state.
4.3 The Plugin Class - Two Styles
Classic style (still supported): a public method per event, named after the event.
class Example extends CMSPlugin
{
public function onContentPrepare($context, &$article, &$params, $page = 0)
{
$article->text = str_replace('{hello}', 'Hello JUG!', $article->text);
}
}
Modern style (Joomla 5 and 6 preferred): implement SubscriberInterface and declare exactly which events you handle.
class Example extends CMSPlugin implements SubscriberInterface
{
public static function getSubscribedEvents(): array
{
return ['onContentPrepare' => 'transformText'];
}
public function transformText(ContentPrepareEvent $event): void
{
$article = $event->getItem();
$article->text = str_replace('{hello}', 'Hello JUG!', $article->text);
}
}
The method getSubscribedEvents() is explicit and fast. Joomla knows up front which events to route to you, instead of guessing from method names.
4.4 Reading and Writing Event Data
In the modern API you no longer use &$reference arguments. You use typed event objects with getters and setters:
public function transformText(ContentPrepareEvent $event): void
{
$context = $event->getContext(); // 'com_content.article'
$item = $event->getItem(); // the article (object)
$params = $event->getParams(); // registry of params
if ($context !== 'com_content.article') {
return; // not for me - bail early
}
$item->text .= '<p>Added by my plugin.</p>';
}
Joomla 6 moves toward immutable events. Read with getters, change data through the methods the event provides, and abort with stopPropagation(). Do not mutate the arguments directly.
5. The Manifest and the Database
5.1 example.xml - the Install Manifest
The manifest describes the plugin to the installer: its group, its files, and its options.
<extension type="plugin" group="content" method="upgrade">
<name>plg_content_example</name>
<version>1.0.0</version>
<namespace path="src">Joomla\Plugin\Content\Example</namespace>
<files>
<folder plugin="example">services</folder>
<folder>src</folder>
</files>
<config>
<fields name="params">
<fieldset name="basic">
<field name="greeting" type="text" default="Hello"
label="PLG_CONTENT_EXAMPLE_GREETING_LABEL" />
</fieldset>
</fields>
</config>
</extension>
Two attributes are unique to plugins:
group="content"- which group folder it installs into.plugin="example"on theservicesfolder - marks the entry-point element name.
5.2 Where Plugins Are Recorded
Like every extension, each plugin is a row in the #__extensions table:
| Column | For a plugin |
|---|---|
type |
plugin |
folder |
content (the group - plugins only) |
element |
example |
enabled |
1 / 0 |
ordering |
execution order within the group |
params |
JSON - the plugin's saved Options |
The folder column is what separates plugins from other extension types: it stores the group.
5.3 Ordering Matters
Multiple plugins can listen to the same event. They run in ordering order, which you set by dragging in the Plugins list:
onContentPrepare fires:
1. plg_content_loadmodule → expands
2. plg_content_pagebreak → handles page breaks
3. plg_content_emailcloak → obfuscates email addresses
4. plg_content_vote → appends the voting UI
- An earlier plugin can change data that the next one sees.
- An earlier plugin can call
stopPropagation()to prevent the rest from running.
Order bugs are real. If two plugins fight over the same text, ordering decides who wins.
Back to top6. The Specialised Plugin Groups
6.1 Authentication and User - the Login Pipeline
Logging in is not one step but a small pipeline of events. Different plugin groups handle different stages:
Login form submitted
v
onUserAuthenticate ← authentication plugins each try to verify
| (Joomla DB? LDAP? OAuth? first success wins)
v
onUserAuthorisation ← is this verified user allowed in?
v
onUserLogin / onUserAfterLogin ← user plugins react (set session, log)
| Group | Key events | Used for |
|---|---|---|
authentication |
onUserAuthenticate |
Who are you? (verify credentials) |
user |
onUserLogin, onUserAfterSave, onUserAfterDelete |
React to account lifecycle |
multifactorauth |
onUserMultifactorAuthenticate |
TOTP, WebAuthn, and similar |
This is how single sign-on, social login, and 2FA all slot in without changing com_users.
6.2 Editors, Fields and Web Services
| Group | What it provides | Hooked by |
|---|---|---|
editors |
The WYSIWYG editing area itself | the form/editor field |
editors-xtd |
Buttons under the editor (Article, Image, Module) | the toolbar |
fields |
A new custom field type | com_fields |
webservices |
Registers a component's REST API routes | the API application |
// A webservices plugin maps API routes to the component's API controllers:
public function onBeforeApiRoute(&$router)
{
$router->createCRUDRoutes('v1/content/articles', 'articles', ['component' => 'com_content']);
}
Enabling Web Services - Content is literally just enabling a plugin. That is what switches on the endpoint /api/index.php/v1/content/articles.
6.3 Task Plugins - Joomla's Cron
The Scheduled Tasks component (com_scheduler) is powered entirely by task plugins:
A trigger (web cron / CLI / lazy) fires
v
onExecuteScheduledTask
v
The matching task plugin runs its job
(delete old logs, rotate sessions, fetch a feed, send a digest ...)
- Each task plugin declares the routines it offers via
getSubscribedEvents(). - This is the modern, GUI-managed replacement for hand-written cron jobs.
A task plugin is "just a plugin" that happens to be triggered by the scheduler instead of by a page view.
Back to top7. Working with Plugins
7.1 How to Read Any Plugin (in Order)
When you open an unfamiliar plugin, read its files in this order:
*.xmlmanifest - group, element, params, files.services/provider.php- the entry point.src/Extension/*.php- the class and itsgetSubscribedEvents().- The event methods - what it does when each event fires.
tmpl/(if present) - any HTML it outputs.
Start at getSubscribedEvents(). It is the table of contents for what the plugin actually does.
7.2 Enabling, Ordering and Configuring
You manage all of this from System → Manage → Plugins:
| Action | Effect |
|---|---|
| Enable / Disable | Toggles the enabled flag - disabled plugins never fire |
| Drag to reorder | Sets ordering within the group |
| Open → Options | Edits the params JSON for that plugin |
| Filter by group | Find all content, system, user plugins |
A disabled plugin is invisible to the dispatcher. Its events simply never reach it.
7.3 Building Your Own - the Minimum
A bare-bones working content plugin needs surprisingly little:
plg_content_hello/
├── hello.xml ← manifest (group="content")
├── services/provider.php ← register with the DI container
├── src/Extension/Hello.php ← implements SubscriberInterface
└── language/en-GB/plg_content_hello.ini
final class Hello extends CMSPlugin implements SubscriberInterface
{
public static function getSubscribedEvents(): array
{
return ['onContentPrepare' => 'onContentPrepare'];
}
public function onContentPrepare(ContentPrepareEvent $event): void
{
$item = $event->getItem();
$item->text = str_replace('{hello}', 'Hello, JUG!', $item->text);
}
}
Then:
- Zip it and install it through
System → Install → Extensions. - Enable it in the Plugins list.
- Put
{hello}in any article and watch it become Hello, JUG!.
Tip: copy a tiny core plugin, such as plg_content_emailcloak, and rename everything. That is the fastest way to a correct skeleton.
8. Beyond the Basics
8.1 Plugins Versus the Alternatives - Pick the Right Tool
Plugins are powerful, but they are not always the right choice. Use this table to decide:
| You want to ... | Use a ... |
|---|---|
| Produce the main page content | Component |
| Show a reusable box on the page | Module |
| React to an event / change behaviour site-wide | Plugin |
| Add data fields to items | Custom Field (which is a fields plugin) |
| Change HTML output | Template override |
If your answer is "whenever X happens, do Y", that is almost always a plugin.
8.2 Performance Considerations
Because system plugins run on every request, they are a prime tuning target:
| Concern | What to do |
|---|---|
| Early exit | Check context and conditions first, then return fast |
| Cheap subscription | Prefer getSubscribedEvents() so unused events cost nothing |
| Avoid heavy boot work | Do not query the database in onAfterInitialise unless needed |
| Cache results | Reuse Joomla's cache for expensive lookups |
| Mind the count | Dozens of enabled system plugins add up - disable what you don't use |
A single badly written system plugin can slow down the entire site, because it runs on every page for every visitor.
8.3 Plugins and the Shared Services
Plugins are how Joomla's shared services stay decoupled and extensible:
- Custom Fields are delivered as
fieldsplugins. Each field type is a plugin. - Smart Search indexes content through
finderplugins, one per content source. - Categories fire content events too, such as
onContentPrepareandonContentBeforeSave, with the contextcom_content.categories. So the same plugin can enhance articles and their categories.
if ($event->getContext() === 'com_content.categories') {
// This same plugin also runs for category descriptions.
}
The shared category service is not a closed box. It emits the same events as articles, so plugins extend categories for free.
Back to top9. Common Mistakes and Pitfalls
Most plugin problems come from a handful of recurring mistakes. Watch out for these:
- Forgetting to enable the plugin. Nothing happens, and there is no error message. Always check the Plugins list first.
- Not bailing early on the wrong context. Your code then runs everywhere, including places you never intended.
- Heavy work in a system event. It runs on every page, which makes the whole site slow.
- Wrong ordering. Another plugin overwrites your changes because it runs after yours.
- Mutating event arguments directly in Joomla 6. Use getters and setters, and
stopPropagation(), instead. - Putting output HTML in the class instead of in a layout file under
tmpl/.
A simple habit prevents most of these: subscribe to a single event, check the context first, and return early when the event is not for you.
Back to top10. Best Practices
If you only remember a few things from this article, remember these:
- A plugin is an event listener. It changes behaviour, not the page.
- Subscribe to a single event, and always check the context before you act.
- Bail early to keep system plugins fast.
- Use
getSubscribedEvents()and typed event objects in modern plugins. - Mind the ordering when several plugins touch the same data.
- Pick the right tool: component for content, module for boxes, plugin for behaviour.
11. Quick Reference
WHAT IT IS An event listener (changes behaviour, not the page)
NAME plg_<group>_<element> (e.g. plg_content_joomla)
ON DISK plugins/<group>/<element>/
ENTRY POINT services/provider.php
THE CLASS src/Extension/*.php implements SubscriberInterface
SUBSCRIBE getSubscribedEvents() returns ['onEvent' => 'method']
RECORDED IN #__extensions (type=plugin, folder=group, ordering)
MANAGE System → Manage → Plugins
ORDER Drag in the list - sets execution order within the group
ABORT $event->stopPropagation()
EARLY EXIT if wrong context: return;
Back to top12. Summary
In Joomla, plugins are the quiet layer that ties everything together. They do not build pages and they do not sit in a position on the screen. Instead, they listen for events and react, which lets you extend almost any part of Joomla without changing core code.
Once you understand the publish-and-subscribe model, the rest follows naturally:
- System plugins hook the request lifecycle and run on every page.
- Content plugins wrap and transform content.
- Specialised groups cover authentication, users, editors, fields, web services, scheduled tasks, and two-factor login.
- Modern plugins are namespaced PSR-4 classes, wired by
services/provider.php, usingSubscriberInterfaceand typed events.
If you are planning a new feature, debugging unexpected behaviour, or wondering why a site is slow, plugins are often the place to look. They are small, but they shape how the whole system behaves.
If you need help building a custom plugin, tracking down a plugin that misbehaves, or making sure your event-driven code runs fast and safely, that is exactly the kind of advanced Joomla work I do every day.
Back to top

Joomla en Linux specialist voor snelle, veilige en schaalbare websites.


