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 theadvertiseRoutines
method implemented in theTaskPluginTrait
. This tells Joomla to add task types based on the contents of theTASKS_MAP
constant of your plugin. -
onExecuteTask
. This is handled by thestandardRoutineHandler
method implemented in theTaskPluginTrait
. When a scheduled task type the plugin can handle is being executed, this method will call your respective plugin method, as defined in theTASKS_MAP
constant of your plugin. -
onContentPrepareForm
. This is handled by theenhanceTaskItemForm
method implemented in theTaskPluginTrait
. 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
where something
Task.taskName
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 formatPLG_TASK_
whereSOMETHING
_TASK_TASKNAME
something
is the name of our plugin andtaskName
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_
. The title of the task type, displayed to the user. Keep it very short and descriptive.SOMETHING
_TASK_TASKNAME
_TITLE -
PLG_TASK_
. A longer description of the task type, displayed to the user. Try to keep it under 50 words.SOMETHING
_TASK_TASKNAME
_DESC
-
-
method
. Required. The name of the plugin method which will be handling the task type. It's customary to name it after thetaskName
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 theforms
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).