Joomla plugin development tutorial showing code editor and plugin structure

Joomla Plugin Development: Create Your First Plugin

Marco Vasquez
Written By Marco Vasquez
Marcus Chen
Reviewed By Marcus Chen
Last Updated April 20, 2026

Joomla’s plugin system is the engine that lets us extend the core without touching core files. Because Joomla follows an event‑driven architecture, a plugin can react to a specific moment in the page life‑cycle, modify data, or inject new output. In this article we walk through joomla plugin development from the ground up, showing how to set up the environment, write a fully namespaced plugin, and test it on a local site. By the end you will have a working “shortcode replacer” plugin and a clear picture of the steps needed for any future joomla plugin project.

What Are Joomla Plugins and How Do They Work?

Joomla plugins belong to a family of extensions that differ from components and modules. While a component controls a full page and a module renders a block in a position, a plugin lives in the background and listens for events. Joomla implements the observer pattern: when a certain point in the execution is reached, the core fires an event name, and every plugin that has subscribed to that name receives a call.

Event / Observer Pattern

When Joomla reaches a hook such as onContentPrepare, it creates an event object and passes it through the dispatcher. All plugins that belong to the relevant group (for example content) and have declared interest in that event receive the object. The plugin can read or modify the data, then return a result. This design keeps the core clean and lets us add functionality without rewriting core code.

Difference from Components and Modules

Extension TypePrimary RoleTypical Location
ComponentHandles a complete request (e.g., com_content)`administrator/components/` and `components/`
ModuleRenders a block in a template position`modules/`
PluginReacts to events, can modify data or output`plugins///`

Plugins are the most flexible because they can work across the whole site, not just a single page or position.

Plugin Groups

Joomla ships with several predefined groups:

  • content – works with article processing (onContentPrepare, onContentAfterTitle, …)
  • system – runs on every request (onAfterInitialise, onBeforeRender, …)
  • user – handles user lifecycle (onUserLogin, onUserLogout, …)
  • authentication – participates in login verification
  • search, finder, quickicon, webservices, etc.

Choosing the correct group is the first step in any joomla plugin development project. Once you understand groups, the rest of joomla plugin development follows a predictable pattern.

Setting Up Your Development Environment

A reliable environment speeds up joomla plugin development and reduces friction when testing changes.

Local Joomla Installation

We recommend a stack that mirrors the production PHP version. XAMPP works well on Windows and macOS, while Docker gives us a clean, reproducible setup.

docker run -d -p 8080:80 \
  -v $(pwd)/joomla:/var/www/html \
  --name joomla-dev \
  joomla:4-apache

After the container starts, navigate to http://localhost:8080 and follow the Joomla installer. Make sure the database user has full privileges; you can read more about permissions in our Joomla File Permissions guide.

IDE Recommendations

A modern IDE helps us keep the code tidy. VS Code with the PHP Intelephense extension provides autocomplete, while PhpStorm offers deeper integration with Joomla’s XML schema validation.

Debug Mode and Error Reporting

Enable Joomla’s debug mode from System → Global Configuration → System → Debug System. Set Error Reporting to “Maximum”. This shows stack traces for any exception thrown by a plugin, which is invaluable when we are learning the event flow.

File Structure Overview

A namespaced plugin lives under plugins///. For our example we will use:

plugins/
└─ content/
   └─ shortcode/
      ├─ src/
      │  └─ Plugin/
      │     └─ Shortcode.php
      ├─ language/
      │  ├─ en-GB/
      │  │  ├─ en-GB.plg_content_shortcode.ini
      │  │  └─ en-GB.plg_content_shortcode.sys.ini
      ├─ shortcode.xml
      └─ services/
         └─ provider.php

The src/ folder holds the PHP classes, language/ contains translation files, services/ hosts the service provider, and the root XML file is the manifest.

Anatomy of a Joomla Plugin

The Manifest File (XML)

The manifest tells Joomla how to install the plugin and which files belong to it. Below is a minimal but complete example for our shortcode plugin.

<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" version="4.0" client="site" method="upgrade">
    <name>plg_content_shortcode</name>
    <author>Your Name</author>
    <creationDate>2026-03-15</creationDate>
    <license>GPLv2</license>
    <version>1.0.0</version>
    <description>Replaces custom shortcodes in article content.</description>

    <files>
        <filename>shortcode.xml</filename>
        <folder>src</folder>
        <folder>language</folder>
        <folder>services</folder>
    </files>

    <languages>
        <language tag="en-GB">language/en-GB/en-GB.plg_content_shortcode.ini</language>
        <language tag="en-GB">language/en-GB/en-GB.plg_content_shortcode.sys.ini</language>
    </languages>

    <config>
        <fields name="params">
            <fieldset name="basic">
                <field name="delimiter" type="text" default="[["
                       label="PLG_CONTENT_SHORTCODE_DELIMITER_LABEL"
                       description="PLG_CONTENT_SHORTCODE_DELIMITER_DESC" />
                <field name="end_delimiter" type="text" default="]]"
                       label="PLG_CONTENT_SHORTCODE_END_LABEL"
                       description="PLG_CONTENT_SHORTCODE_END_DESC" />
            </fieldset>
        </fields>
    </config>
</extension>

Key points:

  • type="plugin" tells Joomla this is a plugin.
  • client="site" limits it to the front‑end; use administrator for back‑end only.
  • lists the directories that will be copied.
  • registers translation files.
  • defines parameters that appear in the plugin manager.

The Service Provider

Since Joomla 4, plugins can register a service provider to inject dependencies. The provider lives in services/provider.php.

<?php
declare(strict_types=1);

namespace My\Plugin\Content;

use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;

class Provider implements ServiceProviderInterface
{
    public function register(Container $container): void
    {
        // Register the plugin class as a service.
        $container->set(
            Shortcode::class,
            fn (Container $c) => new Shortcode($c->get('config'), $c->get('dispatcher'))
        );
    }
}

The provider is referenced in the manifest via the tag (not shown in the minimal example but easy to add). This approach keeps the plugin class testable and isolates configuration.

The Extension Class

The core of the plugin lives in src/Plugin/Shortcode.php. It implements EventSubscriberInterface and, for Joomla 5 compatibility, ResultAwareInterface.

<?php
declare(strict_types=1);

namespace My\Plugin\Content;

use Joomla\Event\Event;
use Joomla\Event\SubscriberInterface;
use Joomla\Event\ResultAwareInterface;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Factory;

class Shortcode extends CMSPlugin implements SubscriberInterface, ResultAwareInterface
{
    protected $app;

    public function __construct($subject, array $config = [])
    {
        $this->app = Factory::getApplication();
        parent::__construct($subject, $config);
    }

    public static function getSubscribedEvents(): array
    {
        return [
            'onContentPrepare' => 'onContentPrepare',
        ];
    }

    public function onContentPrepare(Event $event): void
    {
        /** @var \Joomla\CMS\Table\Content $article */
        $article = $event->getArgument('article');
        $context = $event->getArgument('context');

        // Only process articles in the front‑end.
        if ($this->app->isClient('administrator')) {
            return;
        }

        $delimiter = $this->params->get('delimiter', '[[');
        $endDelimiter = $this->params->get('end_delimiter', ']]');

        $article->text = $this->replaceShortcodes($article->text, $delimiter, $endDelimiter);
    }

    private function replaceShortcodes(string $text, string $start, string $end): string
    {
        $pattern = '/' . preg_quote($start, '/') . '(.*?)' . preg_quote($end, '/') . '/';
        return preg_replace_callback($pattern, fn($matches) => $this->renderShortcode($matches[1]), $text);
    }

    private function renderShortcode(string $code): string
    {
        // Simple example: [year] → current year.
        if (trim($code) === 'year') {
            return (string) date('Y');
        }

        // Unknown code – return unchanged.
        return $code;
    }
}

Explanation of key sections:

  • getSubscribedEvents() tells Joomla that this class wants to listen to onContentPrepare.
  • The constructor receives the dispatcher and configuration via DI.
  • onContentPrepare() receives an Event object; we extract the article, check the client, then run our replacement logic.
  • ResultAwareInterface allows the plugin to return a result object if needed in Joomla 5; we keep it simple here.

Language Files

Translation files follow the naming pattern en-GB.plg_content_shortcode.ini for front‑end strings and en-GB.plg_content_shortcode.sys.ini for system strings (shown in the plugin manager). Example en-GB.plg_content_shortcode.ini:

PLG_CONTENT_SHORTCODE="Shortcode Replacer"
PLG_CONTENT_SHORTCODE_DELIMITER_LABEL="Opening delimiter"
PLG_CONTENT_SHORTCODE_DELIMITER_DESC="Characters that start a shortcode, e.g. [["
PLG_CONTENT_SHORTCODE_END_LABEL="Closing delimiter"
PLG_CONTENT_SHORTCODE_END_DESC="Characters that end a shortcode, e.g. ]]"

The sys.ini file contains only the plugin name and description, which appear in the extensions list.

Building Your First Content Plugin Step by Step

Below we walk through creating a “shortcode replacer” plugin that turns [[year]] into the current year. The steps match the file structure shown earlier.

1. Create the Folder

mkdir -p plugins/content/shortcode/src/Plugin
mkdir -p plugins/content/shortcode/language/en-GB
mkdir -p plugins/content/shortcode/services

2. Write the Manifest (shortcode.xml)

Copy the XML example from the previous section, adjusting the and as needed.

3. Add the Service Provider (services/provider.php)

Create the file with the code shown earlier. Ensure the namespace matches the folder path.

4. Write the Extension Class (src/Plugin/Shortcode.php)

Paste the class code. Remember to run the namespace My\Plugin\Content and to import the required Joomla classes.

5. Add Language Files

Create language/en-GB/en-GB.plg_content_shortcode.ini and language/en-GB/en-GB.plg_content_shortcode.sys.ini. The sys.ini can be a copy of the front‑end file with only the name and description.

6. Package and Install

Compress the shortcode folder into a ZIP archive. In the Joomla admin panel, go to Extensions → Manage → Install, upload the ZIP, and enable the plugin. For a quick walkthrough of the installation process, see our How to Install Joomla Extensions article.

7. Test the Plugin

Create a new article with the text:

Welcome! The current year is [[year]].

Save and view the article on the front‑end. The placeholder should be replaced by the actual year, e.g., “2026”. If the output does not change, enable Debug System and check the Error Log for any exceptions.

Understanding Joomla Plugin Events

Joomla fires many events across its life‑cycle. Knowing which one to use saves time and prevents unexpected side effects.

Common Events by Group

GroupEventWhen it firesTypical use case
content`onContentPrepare`Before article text is displayedModify article body, replace placeholders
content`onContentAfterTitle`After the title is renderedInsert custom HTML after the title
system`onAfterInitialise`Early in request, before routingInitialise global resources
system`onBeforeRender`Just before the final HTML is sentInject scripts or CSS globally
user`onUserLogin`After a user logs inLog activity, set custom session data
user`onUserLogout`After a user logs outClean up temporary data
authentication`onUserAuthenticate`During login verificationAdd external authentication sources
search`onSearch`When a search query is processedExtend the search index with custom data

Choosing the Right Event

  • Content manipulationonContentPrepare (most common for article text) or onContentAfterTitle if you need to work with the title only.
  • Site‑wide changesonBeforeRender or onAfterInitialise in the system group.
  • User‑related actionsonUserLogin / onUserLogout in the user group.

When you subscribe to an event, Joomla passes an Event object that contains arguments. The argument names are documented in the official API; you can also inspect them by dumping the event with var_dump($event->getArguments()).

For a full list, refer to the Joomla Official Plugin Documentation.

Plugin Development Best Practices

Writing maintainable plugins helps the community and reduces future bugs.

Namespace Compliance

All Joomla 4/5 plugins should use a PHP namespace that mirrors the folder structure. For our example the namespace is My\Plugin\Content. Avoid the old global class names like plgContentShortcode.

Joomla 4/5 Compatibility

  • Use the ResultAwareInterface if you plan to return a result object in Joomla 5.
  • Prefer the DI container over global functions (JFactory::getApplication() is still available but DI is preferred).
  • Test on both Joomla 4.4 and the latest Joomla 5 release.

Security

  • Input filtering – Use $this->app->input->getString('param') or the filter classes to sanitize user data.
  • Output escaping – When inserting HTML into article text, run it through HTMLHelper::_('string.escape', $html).
  • Permission checks – Verify the current user’s rights with $this->app->getIdentity()->authorise('core.edit', 'com_content') before altering content.

Performance Considerations

  • Only subscribe to events you need; unnecessary listeners add overhead.
  • Cache expensive calculations (e.g., external API calls) using Joomla’s cache API.
  • Keep regular plugin’s code lightweight; heavy logic belongs in a separate service class.

Using Dependency Injection

Inject services such as the database, logger, or configuration through the service provider. This makes unit testing straightforward because you can replace the real service with a mock.

Common Mistakes in Joomla Plugin Development

Even seasoned developers stumble over a few pitfalls when starting with joomla plugin development.

MistakeWhy it hurtsHow to avoid
Selecting the wrong plugin groupEvent never fires because the group doesn’t matchVerify the group in the manifest (`content`) and match it to the event you need
Forgetting the namespaceJoomla cannot autoload the class, leading to “class not found” errorsFollow the `My\Plugin\Content` pattern and run `composer dump-autoload` if you use Composer
Ignoring client contextPlugin runs in the administrator area and may cause duplicate outputCheck `$this->app->isClient(‘site’)` before processing
Missing language filesText strings appear as keys in the plugin managerProvide both front‑end and system language files, even if they contain only the name
Not registering the service providerDI container does not know about the plugin classAdd `services/provider.php` to the manifest

Next Steps After Your First Plugin

Now that we have completed our first joomla plugin development project, we can explore more advanced topics.

  • System plugins – Hook into the global request life‑cycle, useful for SEO meta tags or custom routing.
  • User plugins – Extend login, registration, or profile handling.
  • Authentication plugins – Connect to LDAP, OAuth, or custom token providers.
  • Testing and debugging – Use PHPUnit with Joomla’s testing framework; enable the Debug Console to view event dispatches.
  • Packaging for distribution – Follow the guidelines on the Joomla Extensions Directory (JED). Include a README, changelog, and proper versioning.
  • Further learning – Our Joomla 4 Tutorial covers MVC components, while the Joomla Template Overrides article shows how plugins can work together with template overrides.

FAQ

What is the difference between a Joomla plugin and a module?

A plugin reacts to events and can modify data anywhere in the site, while a module is a visual block placed in a template position. Plugins have no direct output unless they inject HTML into an event; modules always render a layout file.

Can I create a Joomla plugin without coding experience?

Joomla plugin development has a learning curve, but it is possible to start with a simple “Hello World” plugin using the Joomla Extension Builder, but most useful plugins require PHP knowledge. The steps outlined in this guide assume a basic familiarity with PHP and Joomla’s file structure.

How do I debug a Joomla plugin?

Enable Debug System and set Error Reporting to “Maximum”. Use JLog::add('Message', JLog::INFO, 'myplugin'); to write to the log, or insert var_dump() statements and view the output in the Debug Console. For deeper inspection, use Xdebug with your IDE.

What Joomla plugin events are most commonly used?

  • onContentPrepare – modify article text.
  • onAfterInitialise – set up global resources.
  • onBeforeRender – inject scripts or CSS.
  • onUserLogin / onUserLogout – handle user session changes.

These events cover the majority of use cases for site‑wide extensions.

Marco Vasquez
Written By

Marco Vasquez

Developer Relations

Marco is a full-stack developer and Joomla contributor with deep expertise in template development, module creation, and Joomla 5 architecture. He translates complex technical concepts into clear, actionable tutorials that developers at every level can follow.

Last Updated: April 20, 2026
🇬🇧 English | 🇸🇪 Svenska | 🇫🇮 Suomi | 🇫🇷 Français