By Herman Peeren on Wednesday, 20 May 2026
Category: May

Custom Plugins, part 2: Task Plugin

In episode 2 of our series about building custom plugins we will look at task plugins. With a task plugin you can schedule tasks that you want your website to do. In this article we’ll show how to create your own plugins to execute all kinds of tasks.

Let your site periodically do tasks

The Joomla Task Scheduler can periodically execute tasks. Those tasks are defined in task plugins. Tasks can be anything, like:

See an overview of existing possibilities by Marc Dechèvre’s 2023 Magazine article about Scheduled Tasks.

You can define your own tasks. It is not too difficult. This article shows you how.

General setup of a plugin

Almost every plugin has the same basic structure with the following files and folders as a minimum:

Most task plugins need a /language folder, with language ini-files. A task plugin also often has an xml file with a form definition, stored in a /forms folder.

This would be the setup for a MyTask task plugin:

plg_task_mytask
├─── forms
│     └─── myForm.xml
├─── language
│     └─── en-GB
│           ├─── plg_task_mytask.ini
│           └─── plg_task_mytask.sys.ini
├─── services
│     └─── provider.php
├─── src
│     └─── Extension
│           └─── MyTask.php
└─── mytask.xml

Some specific characteristics of task plugins

Plugins in current Joomla handle so called “events”. Events are objects that contain all data to and from the plugin. All types of events have their own characteristics: what data do they provide and, if anything, what data is expected back.

There are 3 events that play a role in most task plugins:

To make this simpler, there is a TaskPluginTrait that takes away much of the code that you would need to handle this all. In the getSubscribedEvents() method you then let the 3 events point to standard methods in the trait:

public static function getSubscribedEvents(): array
{
   return [
       'onTaskOptionsList'    => 'advertiseRoutines',
       'onExecuteTask'        => 'standardRoutineHandler',
       'onContentPrepareForm' => 'enhanceTaskItemForm'
   ];
}

In order to let those standard methods know where to find specific things to handle the task, there is a TASKS_MAP constant, containing all the info where to find everything for every task option:

private const TASKS_MAP = [
   'mytask' => [
       'langConstPrefix' => 'PLG_TASK_MYTASK',
       'method'          => 'myEventHandler',
       'form'            => 'myForm',
   ],
];

In the above:

If you have multiple task options, like in the SiteStatus task plugin, the TASKS_MAP array has multiple of those entries. As an example, here is the TASKS_MAP of the J2Commerce task plugin, with 4 task options:

    private const TASKS_MAP = [
        'j2commerce.removeNewOrders' => [
            'langConstPrefix' => 'PLG_TASK_J2COMMERCE_REMOVE_NEW_ORDERS',
            'method'          => 'removeNewOrders',
            'form'            => 'removeNewOrders',
        ],
        'j2commerce.processQueue' => [
            'langConstPrefix' => 'PLG_TASK_J2COMMERCE_PROCESS_QUEUE',
            'method'          => 'processQueue',
            'form'            => 'processQueue',
        ],
        'j2commerce.cleanupQueueLogs' => [
            'langConstPrefix' => 'PLG_TASK_J2COMMERCE_CLEANUP_QUEUE_LOGS',
            'method'          => 'cleanupQueueLogs',
            'form'            => 'cleanupQueueLogs',
        ],
        'j2commerce.updateCurrencyRates' => [
            'langConstPrefix' => 'PLG_TASK_J2COMMERCE_UPDATE_CURRENCY_RATES',
            'method'          => 'updateCurrencyRates',
            'form'            => 'updateCurrencyRates',
        ],
    ];

Now we have wired everything together, we can go for the real work!

Handling the task event

The handler method contains the code to execute the task. In the above TASKS_MAP  with a single task option it is the myEventHandler() method. The handler has the ExecuteTaskEvent as a parameter:

private function myEventHandler(ExecuteTaskEvent $event): int
{
    // And here it all happens!
}

But, hey, wait a moment… How is this possible? How can this be a private method? And why is there a return value? I thought that the handler method should be public and doesn’t return anything when implementing the SubscriberInterface? Yes, you are right, but remember how in the getSubscribedEvents() method we wired the onExecuteTask to the standardRoutineHandler() method from the TaskPluginTrait. And indeed, that standardRoutineHandler() method is public and returns nothing.

This private myEventHandler() method is called from the standardRoutineHandler() method, which takes care of processing the status (that integer that is returned) into the $snapshot array that is put in the $result of the event. Handling the event via the standardRoutineHandler() method from the TaskPluginTrait is typical for a task plugin.

If all is going well, the ExecuteTaskEvent handler returns an OK status constant:

   return Status::OK;

These Joomla\Component\Scheduler\Administrator\Task\Status constants are mapped to integers. Here are all possible values:

Form with parameters

You can add custom parameters to the ExecuteTaskEvent. If, as above, in the getSubscribedEvents() the onContentPrepareForm event points to the standard enhanceTaskItemForm() method, and in the TASKS_MAP the 'form'  key  refers to 'myForm', then you must have a myForm.xml form definition in the /forms folder.

The fields that you define in that form will be added to the ExecuteTaskEvent parameters.

In your ExecuteTaskEvent handler you can get the parameter value with
$event->getArgument('params')->nameOfParameter.

Dependency Injection

In part one of this series about custom plugins I already wrote how the plugin is instantiated and dependencies are injected into it in the service provider file. For most standard dependencies, like the database, users or the Mailer, we can use standard traits and setter injection. For other dependencies that are specific for your custom plugin, you can also use constructor injection, as we’ll show down here.

Setter injection

With setter injection in your service provider you first instantiate the plugin and then set the dependencies. For standard dependencies, there exist standard traits with setters and getters to put in your plugin.

For instance, if you need a database in your plugin, you typically use the Joomla\Database\DatabaseAwareTrait in your plugin:

final class MyTask extends CMSPlugin implements SubscriberInterface, DatabaseAwareInterface
{
    use DatabaseAwareTrait;
    use TaskPluginTrait;

This trait has a setDatabase() and a getDatabase() method. Best practice is to declare which interface is implemented when using such a trait; in this case Joomla\Database\DatabaseAwareInterface.

In your service provider, after instantiating your plugin, you set the database, and with that inject the database object into your plugin: 

    $plugin->setDatabase($container->get(DatabaseInterface::class));

If you want to use that database object in a method of your plugin, you simply call the getter:

    $db     = $this->getDatabase();

The getter and setter for the Application-object are already implemented in the parent Joomla\CMS\Plugin\CMSPlugin. You don’t need to add a trait to set or get the Application.

Some other much used objects for which standard traits with setters and getters are available in the core are:

If you have other dependencies, for which no standard trait exists, you can add getters and setters to your plugin in order to set your dependency in the service provider and to get it in a method of your plugin. Or you can use constructor injection (see next section).

Constructor injection

With constructor injection you add extra parameters to your plugin and inject the dependencies while instantiating the plugin in your service provider. In your plugin you then add a constructor to retrieve the extra parameters.

For instance, you want to add Joomla\Http\HttpFactory to your plugin in order to call some API of another site. In your service provider you add it as an extra parameter while instantiating your plugin:

    $plugin = new MyTask(
         (array) PluginHelper::getPlugin('task', 'mytask'),
         new HttpFactory()
    );

You see: normally a CMSPlugin only has one parameter, the $config array. We now added a second one, a new Joomla\Http\HttpFactory.

In your plugin you add a property HttpFactory:

/**
* The Http Factory to create a Http object
*
* @var    HttpFactory
*/
private $httpFactory;

And in the constructor of your plugin you initialise that property:

public function __construct(array $config, HttpFactory $httpFactory)
{
   parent::__construct($config);

   $this->httpFactory   = $httpFactory;
}

Since PHP 8.0 we have so called constructor promotion, so instead of the above two pieces of code we can simply write:

public function __construct(array $config, private HttpFactory $httpFactory)
{
   parent::__construct($config);
}

In a method of your plugin you can now use this HttpFactory with $this->httpFactory. For instance to do a GET request to some foreign API you can write:

    $response = $this->httpFactory->getHttp([])->get($url, $headers, $timeout);

This example is a line from the core Request task plugin. That plugin also adds the HttpFactory and another variable via constructor injection.

Some useful snippets for task handlers

If you want to see some examples of task handlers, you can look at the code of the core task plugins. Several third party developers also offer task plugins that are worth viewing, for instance in the Pro versions of Akeeba Backup and Admin Tools.

In that Admin Tools task plugin nine different task options are handled, where each of the task handlers are in a separate trait, stored in a SubTask folder; I think that word ‘subtask’ is interesting and more clear than ‘task option’,  ‘task type’, and ‘routine’ that are used in the core code. But I won’t dare to propose a new standard 🤣:

Some other task plugins are listed in Marc Dechèvre’s article about scheduled tasks, and some are listed in the Joomla Extensions Directory. For a free task plugin with several task options you can also look at the J2Commerce task plugin, with options to remove new orders, process a queue, clean up queue logs or update currency rates.


Here are some snippets that can be useful when writing your own task handler:

Use the database

// DatabaseDriver was injected (use DatabaseAwareTrait)
$db    = $this->getDatabase();
$query = $db->createQuery();
// … build and process the query …

 

Send a notification by email

// MailerFactory was injected (use MailerFactoryAwareTrait)
// Get a mailer object from that MailerFactory
$mailer = $this->getMailerFactory()->createMailer();

// … Create your message, from address, to address, etc …

// Build the e-mail message
$mailer->addRecipient($to_addr);
$mailer->setSender(array($from_addr, $from_name));
$mailer->setSubject($subject);
$mailer->setBody($message);
$mailer->IsHTML(1);

// Send notification email
$mailer->Send();

 

Use the model of some component

// Application was injected
$app = $this->getApplication(); 
/** @var MVCFactoryInterface $mvcFactory */
$mvcFactory = $app->bootComponent('com_somecomponent')->getMVCFactory();
/** @var SomeModel $someModel */
$someModel = $mvcFactory->createModel('SomeModel', 'Administrator', ['ignore_request' => true]);

$someModel->someMethod($settings);

 

Call a web service API

// HttpFactory was injected (via constructor injection)
// Get a Http object from that HttpFactory
$http = $this->httpFactory->getHttp([]);

// … prepare url, headers (token), requestbody etc …

// Example: GET request
$response = $http->get($url, $headers, $timeout);

 

Process a limited amount of a queue per batch

In this example we simply count down from a ‘max_times’ param, and don’t use the WILL_RESUME status constant. Such a count down is also used in the core CheckFiles task plugin (to resize a limited amount of images, set in the $numImages param).

$params    = $event->getArgument('params');
$maxTimes  = $params->max_times;

// … do some query to get the items-to-process from a queue …

// process $maxTimes items
foreach($items as $item) {

    // … process item, and if successful mark as done in queue … 

    --$maxTimes;
    // We do a limited number of items per execution
    if ($maxTimes == 0) {
         break;
    }
}

 

Generating plugins

The most specific part  of the custom code of your plugin is in the ExecuteTaskEvent handler in the plugin class under /Extension. The rest is more to wire everything together. In order to better focus on the real thing, I’m working on a plugin generator that generates the boilerplate code, including specific things for several types of plugins. The first one is the task plugin, accompanying this article. You will at least get a skeleton, but I try to incorporate as much as possible ready-made  functionality too. You can find it at github.com/HermanPeeren/plug-gen.  


Next month

Next episode will be about custom workflow plugins. For instance to enhance the current publishing workflow or to define a completely different workflow.

Resources

General about plugins in developers manual

 Articles in this series about custom plugins

  1. General Overview.
  2. Task Plugin (this article).
  3. Workflow Plugin (next month).
Leave Comments