Skip to main content

Plugins in Joomla

04 June 2026

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:

TypeRolePer pagePrefix
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 example plugins/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:

GroupPurposeExample
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 top

2. 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 / suffixMeaning
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
Back to top

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:

EventFires whenTypical 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.

Back to top

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.

Back to top

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 the services folder - marks the entry-point element name.

5.2 Where Plugins Are Recorded

Like every extension, each plugin is a row in the #__extensions table:

ColumnFor 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 top

6. 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)
GroupKey eventsUsed 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

GroupWhat it providesHooked 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 top

7. Working with Plugins

7.1 How to Read Any Plugin (in Order)

When you open an unfamiliar plugin, read its files in this order:

  1. *.xml manifest - group, element, params, files.
  2. services/provider.php - the entry point.
  3. src/Extension/*.php - the class and its getSubscribedEvents().
  4. The event methods - what it does when each event fires.
  5. 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:

ActionEffect
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:

  1. Zip it and install it through System → Install → Extensions.
  2. Enable it in the Plugins list.
  3. 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.

Back to top

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:

ConcernWhat 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 fields plugins. Each field type is a plugin.
  • Smart Search indexes content through finder plugins, one per content source.
  • Categories fire content events too, such as onContentPrepare and onContentBeforeSave, with the context com_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 top

9. 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 top

10. 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.
Back to top

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 top

12. 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, using SubscriberInterface and 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
Plugins in Joomla
Peter Martin

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