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:
- Clean up old data.
- Notify people of some specific event or situation on the server.
- Add external information to the website.
- Do some time-intensive tasks when the server is less busy, or spread them over a longer period of time.
- Produce periodic reports.
- Check and repair integrity of data.
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:
- Manifest file: the xml-file with general information about the plugin.
- Service provider: provider.php file in /services folder, to instantiate the plugin and inject the dependencies.
- Extension file: the php-file that contains the actual plugin. You put it in the /src/Extension folder. The file name of this php file is exactly the same as the class name, with the same capitalisation.
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:
- The onExecuteTask event is handled to execute the task. In the
$resultof the event the scheduler expects a$snapshotarray back, logging how the execution of the task went. - The onTaskOptionsList event is handled to give the scheduler an overview of all different task “types” (also called “options” or “routines”) this plugin can handle. That list of task options is sent back to the scheduler. An example of a core task plugin that handles multiple task options is the SiteStatus task plugin. In that one plugin you can handle three types of related tasks: set site offline, set site online or toggle site online/offline.

- The optional onContentPrepareForm event is used to enhance the form with parameters to handle the task event.
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:
- The task option has an id ‘mytask’.
- There is a language constant to display the human readable name of the task option.
- The ‘method’ key refers to the method that is called from the standardRoutineHandler to execute the ExecuteTaskEvent. This method should give a status back that is automatically processed in the standard methods of the trait.
- The optional ‘form’ key refers to the name of the form in which the task parameters are defined.
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:
OK= 0: exit code on success.NO_EXIT= -1: used when a routine does not return an exit code.INVALID_EXIT= -2: used when a routine returns an invalid (non-integer) exit code.RUNNING= 1: used when the routine just starts. This is not meant to be an exit code.NO_LOCK= 2: used on failure to acquire a pseudo-lock.NO_RUN= 3: used on failure to run the task.NO_RELEASE= 4: used on failure to release, lock, or update the record.KNOCKOUT= 5: used when a routine is either "knocked out" by an exception, or encounters an exception it cannot handle gracefully.WILL_RESUME= 123: used when a task needs to resume. Use this for long running tasks to split them into smaller batches. When the last batch is executed return Status::OK.TIMEOUT= 124: used when a task times out.NO_TASK= 125: used when a task does not exist.NO_ROUTINE= 127: used when a task option is missing.
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:
Joomla\CMS\User\UserFactoryAwareTrait, which implementsJoomla\CMS\User\UserFactoryAwareInterface, to do something with users in your plugin.Joomla\CMS\Mail\MailerFactoryAwareTrait, which implementsJoomla\CMS\Mail\MailerFactoryInterface, to use the Mailer (to email notifications).
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
- The initial Summer of Code 2021 article about the Task Scheduler project by Tushar Malik.
- Get to know the new Joomla! Task Scheduler, by Brendan Hedges, Joomla Community Magazine, March 2022.
- Scheduled Tasks, by Marc Dechèvre, Joomla Community Magazine, November 2023. Contains links to other resources.
- Tutorial: How to Create Joomla Task Scheduler Plugin by Joomla Fry.
General about plugins in developers manual
Articles in this series about custom plugins
- General Overview.
- Task Plugin (this article).
- Workflow Plugin (next month).
Some articles published on the Joomla Community Magazine represent the personal opinion or experience of the Author on the specific topic and might not be aligned to the official position of the Joomla Project
By accepting you will be accessing a service provided by a third-party external to https://magazine.joomla.org/

Comments