Work In Progress

This book is currently work in progress. Some sections are not yet written. Thank you for your understanding!

Integration with Scheduled Tasks

Joomla 4.1 and later have an amazingly useful new feature called Scheduled Tasks. This is not really part of a component, it's just a plugin type, but since it is often used in conjunction with a component it makes sense to mention it here. You can read more about plugins in general in the Plugins chapter of this book.

Scheduled Tasks allow the user to define repeated, unattended tasks which run on a schedule. It's like CRON jobs but without necessarily needing a CRON server on the site's server. They can be triggered with CRON jobs, with a special URL (e.g. using web-based pseudo-CRON services such as WebCRON.org), or automatically based on the site's traffic in what is called Lazy Scheduling.

The best way to understand how a Scheduled Task plugin works is looking at the demo tasks plugin under plugins/task/demotasks folder on your site; it is part of Joomla itself.

You create a plugin which provides one or more scheduled tasks in the task plugins folder. The plugin extension must implement \Joomla\Event\SubscriberInterface. It also needs to make use of the \Joomla\Component\Scheduler\Administrator\Traits\TaskPluginTrait. The trait provides all the magic.

You need to handle three events:

  • onTaskOptionsList. This is set to be handled by the advertiseRoutines method implemented in the TaskPluginTrait. This tells Joomla to add task types based on the contents of the TASKS_MAP constant of your plugin.

  • onExecuteTask. This is handled by the standardRoutineHandler method implemented in the TaskPluginTrait. When a scheduled task type the plugin can handle is being executed, this method will call your respective plugin method, as defined in the TASKS_MAP constant of your plugin.

  • onContentPrepareForm. This is handled by the enhanceTaskItemForm method implemented in the TaskPluginTrait. It tells the Scheduled Tasks manager to use the XML forms which define each task type's options when the user creates a scheduled task with this task type.

The TASKS_MAP constant is the most important part of the plugin. It is an array with task definitions like this:

private const TASKS_MAP = [
  'exampleTask.foobar' => [
    'langConstPrefix' => 'PLG_TASK_EXAMPLE_TASK_FOOBAR',
    'method'          => 'foobar',
    'form'            => 'foobar',
  ],
];

Do note that in this example we define one task type. It is possible and most of the times desirable for a task plugin to define more than one task types. In this case the TASKS_MAP array will have more than one elements.

The outermost key for each entry, in our example exampleTask.foobar, tells Joomla what is the internal name of the task type we are defining. The convention is to use somethingTask.taskName where something is the name of our plugin and taskName is a fairly short name for the task type. This will be stored in the database; you must not change it between different versions of your task plugin.

In the innermost array we have three keys:

  • langConstPrefix. Required. The common prefix of the language strings used for this task type. The convention is to use the format PLG_TASK_SOMETHING_TASK_TASKNAME where something is the name of our plugin and taskName is a fairly short name for the task type. The plugin's language INI file will have to define at least two language strings:

    • PLG_TASK_SOMETHING_TASK_TASKNAME_TITLE. The title of the task type, displayed to the user. Keep it very short and descriptive.

    • PLG_TASK_SOMETHING_TASK_TASKNAME_DESC. A longer description of the task type, displayed to the user. Try to keep it under 50 words.

  • method. Required. The name of the plugin method which will be handling the task type. It's customary to name it after the taskName you used in the previous keys.

  • form. Optional. The filename, without the .xml extension, of the XML form file which adds the configuration parameters to the task type when the user is editing the definition of a task. This file is placed in the forms folder of the plugin.

The method which handles each task type has the following signature:

public function foobar(
  \Joomla\Component\Scheduler\Administrator\Event\ExecuteTaskEvent $event
): int

The $event parameter is a standard Joomla event object. To get the task definition from the event you need to do

$task = $event->getArgument('subject');

This gives you a \Joomla\Component\Scheduler\Administrator\Task\Task object.

To get the configuration parameters of the task, as a simple stdClass object, you can do:

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

Your method does NOT set a result value to the $event object; it's not an event handler per se. It instead returns an integer value which must be one of the constants defined in \Joomla\Component\Scheduler\Administrator\Task\Status. You should realistically only use the following constants as return values:

  • KNOCKOUT. An error occurred. The task execution will appear failed. The task will be rescheduled normally for its next execution according to its configured schedule.

  • OK. Success. The task execution will appear successful. The task will be rescheduled normally for its next execution according to its configured schedule.

  • WILL_RESUME. Temporary pause. The task has more work to do but halted execution to avoid a timeout. The task execution will appear to be ongoing. The task is rescheduled to resume execution as soon as possible i.e. the next time Joomla is asked to check whether any tasks need to execute. Please note that if your task is being resumed the following will return true:

    $resuming = $event
      ->getArgument('subject')
      ->get('last_exit_code', Status::OK) === Status::WILL_RESUME;

    Resumable tasks are great for long operations, like resizing hundreds of images or sending a newsletter to thousands of recipients, which might otherwise time out if you tried executing them in a single run. Split your work into small batches and/or check how much time has elapsed since you started doing some work. If a batch is complete and/or you have spent more than a configured maximum amount of time (typically useful values are 3, 5, 10, 20 and 30 seconds) return a Status::WILL_RESUME. Next time your task runs checks if the last_exit_code parameter is Status::WILL_RESUME and continue your work where you left off.

Earlier, we talked about having an XML form file to add parameters to the Scheduled Task type. The contents of the file look like this:

<?xml version="1.0" encoding="utf-8" ?>
<form>
  <fields name="params">
    <fieldset name="task_params">
      <field
        name="my_parameter"
        type="text"
        label="PLG_TASK_EXAMPLE_TASK_FOOBAR_MY_PARAMETER_LABEL"
        description="PLG_TASK_EXAMPLE_TASK_FOOBAR_MY_PARAMETER_DESC"
        default=""
      />
    </fieldset>
  </fields>
</form>        

The form tag has a fields tag whose name is params and inside it there is a fieldset tag whose name is task_params. The fields inside the fieldset are rendered in the task definition page.

There is a special case where you can have a fieldset tag outside the fields tag; if you want your task to only be available for execution when Scheduled Tasks are executed through a Joomla CLI application command. This can be very useful if you have a very long running operation you do not want to (or cannot reasonably) break into smaller sub-tasks to make it a resumable task. In this case, we will add another fieldset tag whose name is aside and place a special hidden field in it. See below, highlighted in bold type:

<?xml version="1.0" encoding="utf-8" ?>
<form>
  <fieldset name="aside">
    <field name="cli_exclusive" type="hidden" default="1" />
  </fieldset>
  <fields name="params">
    <fieldset name="task_params">
      <field
        name="my_parameter"
        type="text"
        label="PLG_TASK_EXAMPLE_TASK_FOOBAR_MY_PARAMETER_LABEL"
        description="PLG_TASK_EXAMPLE_TASK_FOOBAR_MY_PARAMETER_DESC"
        default=""
      />
    </fieldset>
  </fields>
</form>        

That's really all there is to it! Since this is a regular Joomla plugin and assuming your component is native Joomla 4 (therefore its classes can autoload from anywhere in Joomla) you can of course pass your component's MVCFactory instance to the plugin and use it to get access to your component's Models and Tables. Therefore, your tasks can reuse your component's code. Remember that you write good software by staying DRY (Don't Repeat Yourself — reuse your objects and traits instead of copying and pasting the same code all over the place).