Applies to Joomla 5 and 6 · PHP 8.3 · developers Updated 16 Oct 2026

The open item that stands between CiviCRM and official Joomla 6 support (dev/joomla#61) is not about CiviCRM at all. It is about how Joomla plugins are written. This post explains the change with an example in the spirit of CiviCRM's user plugin: a plugin that reacts when Joomla users are saved or deleted.

The code below is a generic plugin written for this post, not CiviCRM's own code.

What changed in Joomla

Since Joomla 4, the CMS has been moving plugins to its event dispatcher. A modern plugin:

  • is a namespaced class, created by a service provider (services/provider.php),
  • implements SubscriberInterface and says which events it listens to in getSubscribedEvents(),
  • receives one event object per call, with typed getters, instead of a list of loose arguments.

The old style – a class named plgUserSomething whose public methods are matched to event names by reflection – has been kept working through backward-compatibility code and is deprecated. CiviCRM's three Joomla plugins (user, system, quick icon) are still written that way, which is why they are the work item for Joomla 6.

Before: a legacy user plugin

// plugins/user/contactsync/contactsync.php
defined('_JEXEC') or die;

use Joomla\CMS\Plugin\CMSPlugin;

class PlgUserContactsync extends CMSPlugin
{
    // Called after a Joomla user is saved.
    // $user is an array, $isNew a bool – positional arguments.
    public function onUserAfterSave($user, $isNew, $success, $msg)
    {
        if (!$success) {
            return;
        }

        $this->syncContact((int) $user['id'], $user['email'], $user['name']);
    }

    private function syncContact(int $userId, string $email, string $name): void
    {
        // create or update the matching CRM contact
    }
}

After: the same plugin for Joomla 5 and 6

Three files. The service provider:

// plugins/user/contactsync/services/provider.php
defined('_JEXEC') or die;

use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Example\Plugin\User\Contactsync\Extension\Contactsync;

return new class () implements ServiceProviderInterface {
    public function register(Container $container): void
    {
        $container->set(PluginInterface::class, function (Container $container) {
            $plugin = new Contactsync(
                $container->get(DispatcherInterface::class),
                (array) PluginHelper::getPlugin('user', 'contactsync')
            );
            $plugin->setApplication(Factory::getApplication());

            return $plugin;
        });
    }
};

The plugin class:

// plugins/user/contactsync/src/Extension/Contactsync.php
namespace Example\Plugin\User\Contactsync\Extension;

defined('_JEXEC') or die;

use Joomla\CMS\Event\User\AfterSaveEvent;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\SubscriberInterface;

final class Contactsync extends CMSPlugin implements SubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return ['onUserAfterSave' => 'onUserAfterSave'];
    }

    public function onUserAfterSave(AfterSaveEvent $event): void
    {
        if (!$event->getSavingResult()) {
            return;
        }

        $user = $event->getUser();   // array, as before
        $this->syncContact((int) $user['id'], $user['email'], $user['name']);
    }

    private function syncContact(int $userId, string $email, string $name): void
    {
        // create or update the matching CRM contact
    }
}

And the manifest declares the namespace and the folders:

<extension type="plugin" group="user" method="upgrade">
    <name>plg_user_contactsync</name>
    <namespace path="src">Example\Plugin\User\Contactsync</namespace>
    <files>
        <folder plugin="contactsync">services</folder>
        <folder>src</folder>
    </files>
</extension>

What to watch for when converting

  • Return values. Legacy methods sometimes returned true/false to signal a result. With event objects, results are added to the event (for events that collect results) or the plugin stops propagation – check the event class for the right method.
  • One event, one handler. List every event in getSubscribedEvents(). A method that exists but is not listed is never called.
  • Priorities. If your plugin must run before or after another one, subscribe with a priority: ['onAfterRoute' => ['onAfterRoute', Priority::HIGH]].
  • Generic events. Not every event has a concrete class. For those, accept Joomla\Event\Event and read arguments with $event->getArgument('name').
  • The application. Use $this->getApplication(), not Factory::getApplication(), inside the plugin.

How this applies to CiviCRM today

Until CiviCRM ships Joomla 6-ready plugins, a site on Joomla 6 needs the same behavior provided from outside the files CiviCRM installs – a small plugin of your own that does what the CiviCRM plugin would do, written the modern way. Keep it in its own extension and repository, so a CiviCRM update never touches it. More on that approach in Keep CiviCRM fixes out of core.

When CiviCRM's own plugins are updated, retire yours – and test that nothing is left running twice.

What CiviCRM's real plugin looks like today

For reference, the method signatures in civicrm-joomla's user plugin (October 2026), shortened:

// admin/plugins/civicrm/civicrm.php (civicrm-joomla, shortened)
class plgUserCivicrm extends _J3_to_J4_plgUserCivicrm
{
    function onUserAfterSave($user, $isnew, $success, $msg) { self::civicrmResetNavigation(); }
    function onUserAfterSaveGroup($var) { self::civicrmResetNavigation(); }
    function onUserAfterDelete($user, $succes, $msg) { CRM_Core_BAO_UFMatch::deleteUser($user['id']); }
    public function onUserLogin($user, $options = []) { /* admin only: reset navigation */ }
}

Four legacy listeners with positional arguments. Converting them is mechanical: AfterSaveEvent, AfterSaveGroupEvent, AfterDeleteEvent and LoginEvent carry the same data through getters. Joomla 6 still calls the legacy form, but Joomla has marked that support "will be removed in 7.0". That's the real deadline for this work.

Need this on a live site? We have run CiviCRM on Joomla 6 since early 2026, with the compatibility code in its own extension and tested through CiviCRM updates.

CiviCRM Joomla 6 upgrade →