joomla-plugin-development

Joomla 5/6 Plugin Development: Complete Guide

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "joomla-plugin-development" with this command: npx skills add nicolasflores9/skills/nicolasflores9-skills-joomla-plugin-development

Joomla 5/6 Plugin Development: Complete Guide

  1. Introduction

Plugins in Joomla 5/6 represent the most modern evolution of the framework. This knowledge set provides everything you need to create robust, efficient, and maintainable extensions following current patterns.

1.1 What You Will Learn

  • Modern Joomla 5/6 plugin architecture

  • SubscriberInterface and getSubscribedEvents() implementation

  • System, content, and user event handling

  • Event Classes with type hints

  • Dependency injection

  • PSR-4 namespaces and automatic autoloading

  • Security and performance best practices

  • Practical and complete examples

1.2 Prerequisites

  • Working Joomla 5 or 6 installation

  • Intermediate PHP knowledge

  • Familiarity with namespaces and composition

  • Administrator access to Joomla

  • Code editor (VS Code, PhpStorm, etc.)

  1. Modern Plugin Structure

2.1 Standard Directory Tree

Every modern plugin follows this structure:

plg_system_myexample/ ├── manifest.xml # Configuration and installation ├── services/ │ └── provider.php # Dependency injection ├── src/ │ ├── Extension/ │ │ └── MyExample.php # Main class │ ├── Event/ # (Optional) Custom event classes │ └── Helper/ # (Optional) Helper classes └── language/ └── en-GB/ ├── plg_system_myexample.ini # Frontend translations └── plg_system_myexample.sys.ini # System translations

Critical naming conventions:

  • Prefix: plg_ (always plugin)

  • Group: system , content , user , etc.

  • Name: lowercase without spaces

  • Example: plg_content_shortcodes = content plugin named "shortcodes"

2.2 manifest.xml File

The manifest is the installation entry point:

<?xml version="1.0" encoding="utf-8"?> <extension type="plugin" group="system"> <name>PLG_SYSTEM_MYEXAMPLE</name> <author>Your Name</author> <creationDate>2025-03-06</creationDate> <copyright>Copyright 2025</copyright> <license>GNU General Public License version 2 or later</license> <version>1.0.0</version> <description>PLG_SYSTEM_MYEXAMPLE_DESCRIPTION</description> <targetPlatform version="5.0" />

&#x3C;!-- CRITICAL: Defines PSR-4 namespace -->
&#x3C;namespace path="src">MyCompany\Plugin\System\MyExample&#x3C;/namespace>

&#x3C;!-- File declarations -->
&#x3C;files>
    &#x3C;file>manifest.xml&#x3C;/file>
    &#x3C;folder plugin="myexample">services&#x3C;/folder>
    &#x3C;folder>src&#x3C;/folder>
    &#x3C;folder>language&#x3C;/folder>
&#x3C;/files>

&#x3C;!-- Parameter configuration (optional) -->
&#x3C;config>
    &#x3C;fields name="params">
        &#x3C;fieldset name="basic">
            &#x3C;field
                name="enabled"
                type="checkbox"
                label="Enable plugin"
                default="1"
            />
        &#x3C;/fieldset>
    &#x3C;/fields>
&#x3C;/config>

</extension>

Critical elements:

  • <namespace path="src"> : Defines the namespace prefix. MUST match exactly in provider.php and Extension

  • <folder plugin="myexample"> : The attribute must be the plugin name (matches manifest.xml)

  • type="plugin" and group="..." : Identifies type and category

2.3 Service Provider (services/provider.php)

This file registers your plugin in the dependency injection container:

<?php namespace MyCompany\Plugin\System\MyExample;

use Joomla\CMS\Extension\PluginInterface; use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface;

class ServiceProvider implements ServiceProviderInterface { public function register(Container $container) { $container->set( PluginInterface::class, function (Container $c) { $plugin = new Extension( $c->get('dispatcher'), (array) PluginHelper::getPlugin('system', 'myexample') ); return $plugin; } ); } }

Key points:

  • The namespace MUST match exactly with manifest.xml

  • PluginHelper::getPlugin() retrieves the plugin configuration

  • The dispatcher is Joomla's event manager

  • The Extension class is registered as PluginInterface

  1. Extension Class: The Heart of the Plugin

The main class implements SubscriberInterface :

<?php namespace MyCompany\Plugin\System\MyExample;

use Joomla\CMS\Plugin\CMSPlugin; use Joomla\Event\SubscriberInterface; use Joomla\CMS\Event\System\AfterInitialiseEvent;

class Extension extends CMSPlugin implements SubscriberInterface { protected $autoloadLanguage = true; protected $allowLegacyListeners = false;

/**
 * Declares subscribed events
 * This static method is MANDATORY
 */
public static function getSubscribedEvents(): array
{
    return [
        'onAfterInitialise' => 'onAfterInitialise',
    ];
}

/**
 * Handles the onAfterInitialise event
 */
public function onAfterInitialise(AfterInitialiseEvent $event)
{
    // Your logic here
}

}

Important properties:

  • $autoloadLanguage = true : Automatically loads .ini language files

  • $allowLegacyListeners = false : Improves performance, disables Reflection-based lookup

  • Extends CMSPlugin which provides $params , $app , etc.

  1. SubscriberInterface and getSubscribedEvents()

4.1 Modern vs Legacy Pattern

Legacy (Joomla 3-4):

public function onContentPrepare($context, &$article, &$params, $page = 0) { // Logic }

Modern (Joomla 5+):

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

public function onContentPrepare(ContentPrepareEvent $event) { $article = $event->getArgument('0'); // Logic with type hints }

4.2 getSubscribedEvents() Formats

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

// With priorities (lower number = executes first) public static function getSubscribedEvents(): array { return [ 'onContentPrepare' => ['onContentPrepare', 5], 'onContentAfterTitle' => 'onContentAfterTitle', ]; }

// Multiple listeners for one event public static function getSubscribedEvents(): array { return [ 'onContentPrepare' => [ ['primaryHandler', 0], ['secondaryHandler', 10], ], ]; }

  1. Main Event Types

5.1 System Events

Triggered on every page load:

Event When it fires

onAfterInitialise

After Joomla initialization

onAfterRoute

After route resolution

onAfterDispatch

After component execution

onBeforeRender

Before page rendering

onBeforeCompileHead

Before compiling head tags

onAfterRender

After rendering

5.2 Content Events

Triggered during article read/write cycle:

Event Description

onContentPrepare

Before displaying articles

onContentAfterTitle

After the title

onContentBeforeSave

Pre-save validation

onContentAfterSave

Post-processing

onContentBeforeDelete

Pre-delete cleanup

onContentAfterDelete

Post-delete cleanup

5.3 User Events

User management:

Event Description

onUserBeforeSave

Before saving user

onUserAfterSave

After saving user

onUserLogin

After successful login

onUserLogout

After logout

  1. Event Classes (Joomla 5.2+)

Event Classes provide type safety and better structure:

// Legacy public function onContentPrepare($context, &$article, &$params, $page = 0) { $text = $article->text; }

// Modern with Event Class use Joomla\CMS\Event\Content\ContentPrepareEvent;

public function onContentPrepare(ContentPrepareEvent $event) { // Access by index $article = $event->getArgument('0');

// Or use specific methods (if available)
$article = $event->getArticle();

// Modify
$event->setArgument('0', $modifiedArticle);

}

Event Classes location:

  • \Joomla\CMS\Event\Content* for content events

  • \Joomla\CMS\Event\System* for system events

  • \Joomla\CMS\Event\User* for user events

  1. Dependency Injection

7.1 Accessing Basic Services

<?php namespace MyCompany\Plugin\Content\Example;

use Joomla\CMS\Plugin\CMSPlugin; use Joomla\Event\SubscriberInterface; use Joomla\DI\Traits\ContainerAwareTrait; use Joomla\Database\DatabaseInterface;

class Extension extends CMSPlugin implements SubscriberInterface { use ContainerAwareTrait;

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

public function onContentPrepare($event)
{
    // Access the container
    $container = $this->getContainer();

    // Get the database
    $db = $container->get(DatabaseInterface::class);

    // Execute query
    $query = $db->getQuery(true)
        ->select('*')
        ->from($db->quoteName('#__articles'));

    $db->setQuery($query);
    $results = $db->loadObjectList();
}

}

7.2 In Service Provider

<?php namespace MyCompany\Plugin\Content\Example;

use Joomla\CMS\Extension\PluginInterface; use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Database\DatabaseInterface;

class ServiceProvider implements ServiceProviderInterface { public function register(Container $container) { $container->set( PluginInterface::class, function (Container $c) { $plugin = new Extension( $c->get('dispatcher'), (array) PluginHelper::getPlugin('content', 'example') );

            // Inject the container
            $plugin->setContainer($c);

            return $plugin;
        }
    );
}

}

  1. Your First Plugin: "Hello World"

8.1 Minimal Structure

plg_system_helloworld/ ├── manifest.xml ├── services/ │ └── provider.php ├── src/ │ └── Extension/ │ └── Helloworld.php └── language/ └── en-GB/ ├── plg_system_helloworld.ini └── plg_system_helloworld.sys.ini

8.2 manifest.xml

<?xml version="1.0" encoding="utf-8"?> <extension type="plugin" group="system"> <name>PLG_SYSTEM_HELLOWORLD</name> <author>Your Name</author> <creationDate>2025-03-06</creationDate> <copyright>Copyright 2025</copyright> <license>GNU General Public License version 2 or later</license> <version>1.0.0</version> <description>PLG_SYSTEM_HELLOWORLD_DESCRIPTION</description> <targetPlatform version="5.0" />

&#x3C;namespace path="src">MyCompany\Plugin\System\Helloworld&#x3C;/namespace>

&#x3C;files>
    &#x3C;file>manifest.xml&#x3C;/file>
    &#x3C;folder plugin="helloworld">services&#x3C;/folder>
    &#x3C;folder>src&#x3C;/folder>
    &#x3C;folder>language&#x3C;/folder>
&#x3C;/files>

</extension>

8.3 services/provider.php

<?php namespace MyCompany\Plugin\System\Helloworld;

use Joomla\CMS\Extension\PluginInterface; use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface;

class ServiceProvider implements ServiceProviderInterface { public function register(Container $container) { $container->set( PluginInterface::class, function (Container $c) { $plugin = new Extension( $c->get('dispatcher'), (array) PluginHelper::getPlugin('system', 'helloworld') ); return $plugin; } ); } }

8.4 src/Extension/Helloworld.php

<?php namespace MyCompany\Plugin\System\Helloworld;

use Joomla\CMS\Plugin\CMSPlugin; use Joomla\Event\SubscriberInterface; use Joomla\CMS\Event\System\AfterInitialiseEvent;

class Extension extends CMSPlugin implements SubscriberInterface { protected $autoloadLanguage = true; protected $allowLegacyListeners = false;

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

public function onAfterInitialise(AfterInitialiseEvent $event)
{
    $app = $this->getApplication();
    $app->getLogger()->info('Hello World! The plugin is working.');
}

}

8.5 Language Files

language/en-GB/plg_system_helloworld.ini:

PLG_SYSTEM_HELLOWORLD="Hello World Plugin"

language/en-GB/plg_system_helloworld.sys.ini:

PLG_SYSTEM_HELLOWORLD="Hello World Plugin" PLG_SYSTEM_HELLOWORLD_DESCRIPTION="A sample plugin that displays 'Hello World'"

8.6 Installation

  • Create folder: plugins/system/helloworld

  • Copy all files

  • Go to Control Panel > Extensions > Plugins

  • Search for "Hello World Plugin"

  • Enable it

  • Verify in logs: logs/joomla.log

  1. Advanced Plugin: Content Shortcodes

9.1 Manifest.xml with Parameters

<?xml version="1.0" encoding="utf-8"?> <extension type="plugin" group="content" method="upgrade"> <name>PLG_CONTENT_SHORTCODES</name> <author>Your Name</author> <creationDate>2025-03-06</creationDate> <copyright>Copyright 2025</copyright> <license>GNU General Public License version 2 or later</license> <version>1.0.0</version> <description>PLG_CONTENT_SHORTCODES_DESCRIPTION</description> <targetPlatform version="5.0" />

&#x3C;namespace path="src">MyCompany\Plugin\Content\Shortcodes&#x3C;/namespace>

&#x3C;files>
    &#x3C;file>manifest.xml&#x3C;/file>
    &#x3C;folder plugin="shortcodes">services&#x3C;/folder>
    &#x3C;folder>src&#x3C;/folder>
    &#x3C;folder>language&#x3C;/folder>
&#x3C;/files>

&#x3C;config>
    &#x3C;fields name="params">
        &#x3C;fieldset name="basic">
            &#x3C;field
                name="process_shortcodes"
                type="checkbox"
                label="PLG_CONTENT_SHORTCODES_ENABLED"
                default="1"
            />
        &#x3C;/fieldset>
    &#x3C;/fields>
&#x3C;/config>

</extension>

9.2 Complete Extension Class

<?php namespace MyCompany\Plugin\Content\Shortcodes;

use Joomla\CMS\Plugin\CMSPlugin; use Joomla\Event\SubscriberInterface; use Joomla\CMS\Event\Content\ContentPrepareEvent; use Joomla\CMS\Factory;

class Extension extends CMSPlugin implements SubscriberInterface { protected $autoloadLanguage = true; protected $allowLegacyListeners = false;

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

public function onContentPrepare(ContentPrepareEvent $event)
{
    // Check if enabled
    if (!$this->params->get('process_shortcodes', true)) {
        return;
    }

    $article = $event->getArgument('0');

    if (!isset($article) || !isset($article->text)) {
        return;
    }

    // Process shortcodes
    $article->text = $this->processShortcodes($article->text);
}

protected function processShortcodes($text)
{
    $config = Factory::getConfig();

    // Replace {sitename}
    $text = str_replace(
        '{sitename}',
        $config->get('sitename'),
        $text
    );

    // Replace {siteurl}
    $text = str_replace(
        '{siteurl}',
        $config->get('live_site'),
        $text
    );

    // Replace {year}
    $text = str_replace(
        '{year}',
        date('Y'),
        $text
    );

    return $text;
}

}

9.3 Service Provider

<?php namespace MyCompany\Plugin\Content\Shortcodes;

use Joomla\CMS\Extension\PluginInterface; use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface;

class ServiceProvider implements ServiceProviderInterface { public function register(Container $container) { $container->set( PluginInterface::class, function (Container $c) { $plugin = new Extension( $c->get('dispatcher'), (array) PluginHelper::getPlugin('content', 'shortcodes') ); return $plugin; } ); } }

9.4 Language Files

language/en-GB/plg_content_shortcodes.ini:

PLG_CONTENT_SHORTCODES="Content Shortcodes" PLG_CONTENT_SHORTCODES_ENABLED="Process Shortcodes"

language/en-GB/plg_content_shortcodes.sys.ini:

PLG_CONTENT_SHORTCODES="Content Shortcodes" PLG_CONTENT_SHORTCODES_DESCRIPTION="Replace shortcodes like {sitename} with site configuration values"

  1. Best Practices

10.1 Security

// ALWAYS validate input use Joomla\CMS\Filter\InputFilter;

$filter = InputFilter::getInstance(); $safe_input = $filter->clean($_GET['data'], 'STRING');

// ALWAYS escape output use Joomla\CMS\HTML\HTMLHelper;

echo HTMLHelper::_('common.escape', $user_content);

// Check permissions $user = Factory::getUser(); if (!$user->authorise('core.manage', 'com_example')) { return; }

10.2 Performance

// Use cache $cache = Factory::getCache('system'); $key = 'plugin_example_data' . $article_id;

if ($data = $cache->get($key)) { return $data; }

// Processing $data = $this->expensiveOperation();

// Store in cache (3600 seconds = 1 hour) $cache->store($data, $key, '_system', 3600);

// Be selective with events - DO NOT subscribe to all of them public static function getSubscribedEvents(): array { return [ 'onContentPrepare' => 'onContentPrepare', // NO: 'onAfterRender', 'onBeforeRender', etc. ]; }

10.3 Namespace and PSR-4

CORRECT:

  • manifest.xml: <namespace path="src">MyCompany\Plugin\Content\Shortcodes</namespace>

  • services/provider.php: namespace MyCompany\Plugin\Content\Shortcodes;

  • src/Extension/Shortcodes.php: namespace MyCompany\Plugin\Content\Shortcodes;

INCORRECT:

  • Mismatching namespaces

  • Not including path="src" in manifest

  • Using incorrect paths in files

  1. Common Troubleshooting

11.1 Plugin does not appear in the list

Solutions:

  • Verify that manifest.xml is in the root of the plugin folder

  • Check that the XML is valid (no special characters)

  • Clear cache: Control Panel > System > Cache > Clear Cache

  • Verify folder permissions (755)

11.2 Event does not fire

Solutions:

  • Verify that getSubscribedEvents() correctly declares the event

  • Make sure the plugin is enabled

  • Check that $allowLegacyListeners = false

  • Check logs at logs/joomla.log

11.3 Namespace error

Solutions:

  • Match EXACTLY the namespace in manifest, provider, and Extension

  • Use semicolon at the end of namespace

  • Verify path in <folder plugin="pluginname">services</folder>

  • Clear autoload cache: administrator/cache/autoload_psr4.php

  1. Installation Verification

After installing a plugin, verify:

  • Control Panel > Extensions > Plugins

  • Search for the plugin by name

  • Verify it appears in the list

  • Enable it (green status)

  • Check logs at logs/joomla.log for messages

  • Test functionality according to the plugin type

  1. Additional Resources
  • Official Joomla Manual Documentation

  • Joomla Event Classes Documentation

  • API Reference

  • Joomla Community - Forum

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

joomla-template-overrides

No summary provided by upstream source.

Repository SourceNeeds Review
General

joomla-database-queries

No summary provided by upstream source.

Repository SourceNeeds Review
General

joomla-frontend-integration

No summary provided by upstream source.

Repository SourceNeeds Review
General

moodle5-theme

No summary provided by upstream source.

Repository SourceNeeds Review