Automated Tasks

Improvements?

Let us know by posting here.

Task Essentials

Websites often need scheduled tasks, like checking inactive users or updating sitemap.xml. In Concrete, these are achieved with Tasks. They can be initiated from the Dashboard or command line.

Built upon messages and the Concrete command bus, developers can craft custom tasks. Dive deeper for insights on task creation or the message dispatcher system.

Preparation

First, check the Tasks Editor Documentation to understand tasks and their advanced features. Familiarity with Concrete Package Bundling is essential since tasks are installed this way. Also, familiarize yourself with the Command and Command Handler System.

Creating a Task: Key Considerations

When developing your own task, first ensure it's appropriate for your need.

Console Commands or Tasks?

Ask yourself:

  • Can't access your Concrete site's shell?
  • Not a fan of the command line?
  • Need to run this command regularly or on a set schedule?
  • Will non-developers or editors need to execute this command?

If you answered Yes to these, then a Task is apt. Tasks offer a user-friendly interface for regular commands and features like scheduling. Otherwise, for one-off needs, consider a console command.

Types of Tasks

If you've settled on creating a task, identify its type. Tasks can be:

  1. Immediate Single Commands: Straightforward tasks, like "Clear Cache."
  2. Asynchronous Single Commands: The command initiates promptly but operates asynchronously. It's either triggered during Dashboard page loads or by a specific command worker. For instance, "Generate Sitemap XML."
  3. Batched Asynchronous Commands: A group of commands runs asynchronously using the same handler with varied parameters. Examples are "Rescan Files" and "Reindex Content."

Starting Simple

For a beginner's task, consider a basic, routine command. Although "Clear Cache" exists, how about "Clear Log"? Periodically clearing the Concrete log prevents it from overflowing. It's also resource-friendly, reducing concerns about needing a separate worker. Let's develop a task to clear the Concrete log.

Making a "Log Utilities" Package

We're creating a package named "Log Utilities" for managing Concrete log tasks.

To start, set up the "Log Utilities" package as guided in this documentation. The package's structure will be:

  • log_utilities
    • controller.php

In controller.php, add:

<?php

namespace Concrete\Package\LogUtilities;

use Concrete\Core\Package\Package;

class Controller extends Package
{
    protected $appVersionRequired = '9.0';
    protected $pkgVersion = '0.5';
    protected $pkgHandle = 'log_utilities';
    protected $pkgAutoloaderRegistries = array(
        'src' => '\Concrete\Documentation\LogUtilities'
    );

    public function getPackageDescription()
    {
        return t("Adds additional tasks to your Concrete site for working with logs.");
    }

    public function getPackageName()
    {
        return t("Log Utilities");
    }

    public function install()
    {
        $pkg = parent::install();
        // No operations for now.
    }
}

Remember, we're setting up a PHP namespace Concrete\Documentation\LogUtilities for future support classes.

Finally, here’s our package ready for install:

text

At this point, if we install the package we'll have a package with nothing in it. Let’s add a task to it.

Setting Up the "Log Utilities" Task

Use Content XML for Tasks

For task setup in your package, leverage Content XML. Modify install and add upgrade like this:

public function install()
{
    parent::install();
    $this->installContentFile('tasks.xml');
}

public function upgrade()
{
    parent::upgrade();
    $this->installContentFile('tasks.xml');
}

This ensures tasks.xml is processed during install and upgrades.

Your package should have:

  • log_utilities
    • controller.php
    • tasks.xml

The XML content is:

<?xml version="1.0" encoding="UTF-8"?>
<concrete5-cif version="1.0">
    <tasks>
        <task handle="clear_log" package="log_utilities"/>
    </tasks>
    <tasksets>
        <taskset handle="maintenance">
            <task handle="clear_log"/>
        </taskset>
    </tasksets>
</concrete5-cif>

This XML informs Concrete to set up a clear_log task within the log_utilities package. It's also grouped under the Maintenance task set.

Task Controller Setup

Each task requires a Controller file to define its behavior. Create one at packages/log_utilities/src/Command/Task/Controller/ClearLogController.php:

<?php
namespace Concrete\Documentation\LogUtilities\Command\Task\Controller;

use Concrete\Core\Command\Task\Input\InputInterface;
use Concrete\Core\Command\Task\Runner\CommandTaskRunner;
use Concrete\Core\Command\Task\Runner\TaskRunnerInterface;
use Concrete\Core\Command\Task\TaskInterface;
use Concrete\Core\Command\Task\Controller\AbstractController;

defined('C5_EXECUTE') or die("Access Denied.");

class ClearLogController extends AbstractController
{
    public function getName(): string
    {
        return t('Clear Log');
    }

    public function getDescription(): string
    {
        return t('Clears the Concrete database log table..');
    }

    public function getTaskRunner(TaskInterface $task, InputInterface $input): TaskRunnerInterface
    {
        // To be added.
    }
}

This controller specifies task details like its name and description.

Link Controller to Manager

To make the task recognizable, register its controller with the Concrete Task Manager. In your package controller, add:

use Concrete\Core\Command\Task\Manager as TaskManager;
use Concrete\Documentation\LogUtilities\Command\Task\Controller\ClearLogController;

Then, insert on_start:

public function on_start()
{
    $manager = $this->app->make(TaskManager::class);
    $manager->extend('clear_log', function () {
        return new ClearLogController();
    });
}

Activating the Package

You can now activate the package via Dashboard or command line:

Dashboard image

Once activated, access it in the Tasks Dashboard:

Tasks image

It's also command-line ready:

Command line image

Task Functionality

With setup complete, focus on the task's main function.

Task Runner Creation

To execute our task, we have to set up its getTaskRunner function. This will decide how the task runs. Choose one based on your need:

  • CommandTaskRunner: Runs immediately.
  • ProcessTaskRunner: Deferred execution.
  • BatchProcessTaskRunner: Runs many small tasks.

For a direct task like clearing logs, use:

return new CommandTaskRunner($task, $command, t('Log cleared successfully.'));

Here, $task is passed automatically. The message for success is "Log cleared successfully." The main thing is the $command, which pairs with a command handler.

Command Making

Create a command for clearing logs:

<?php
namespace Concrete\Documentation\LogUtilities\Logging\Command;

use Concrete\Core\Foundation\Command\Command;

defined('C5_EXECUTE') or die("Access Denied.");

class ClearLogCommand extends Command
{
}

This doesn’t need extra properties, just a name.

Command Handler Setup

Every command needs a handler. We're creating a file for it:

<?php
namespace Concrete\Documentation\LogUtilities\Logging\Command;

defined('C5_EXECUTE') or die("Access Denied.");

class ClearLogCommandHandler
{
    public function __invoke(ClearLogCommand $command)
    {
        // Logic goes here.
    }
}

Link Command to Task Runner

Add the command to the task runner:

use Concrete\Documentation\LogUtilities\Logging\Command\ClearLogCommand;

public function getTaskRunner(TaskInterface $task, InputInterface $input): TaskRunnerInterface
{
    $command = new ClearLogCommand();
    return new CommandTaskRunner($task, $command, t('Log cleared successfully.'));
}

Task Testing

Test the task in the Dashboard. It should execute when you click “Run Task”. If you run it from the command line, it should also work.

Finalize Handler Logic

The task reports it ran, but it didn't clear logs because the handler lacks logic. To actually clear logs, update the handler:

<?php
namespace Concrete\Documentation\LogUtilities\Logging\Command;

use Concrete\Core\Logging\Handler\DatabaseHandler;

defined('C5_EXECUTE') or die("Access Denied.");

class ClearLogCommandHandler
{
    public function __invoke(ClearLogCommand $command)
    {
        DatabaseHandler::clearAll();
    }
}

Run the task again. Now it should clear the logs.

Task Types: Asynchronous and Batch

Asynchronous Tasks

To change our log clearing task to run asynchronously:

  1. Switch to ProcessTaskRunner in ClearCacheController:
use Concrete\Core\Command\Task\Runner\ProcessTaskRunner;
  1. Modify the getTaskRunner method:
public function getTaskRunner(TaskInterface $task, InputInterface $input): TaskRunnerInterface
{
    $command = new ClearLogCommand();
    return new ProcessTaskRunner($task, $command, $input, t('Clearing the Concrete log...'));
}

Remember: - ProcessTaskRunner requires $input. - The displayed message means the task has started, not finished.

Run the command from the Dashboard. It starts like this:

And looks like this once running:

Differences: - We land on the Tasks Activity Dashboard since the task isn't instantly done. - We get a notification about the task starting. - "Clear Log" appears under Currently Running and later moves to process history.

Batch Tasks

Batch tasks are good for repeated actions on many items, such as:

  • Rescanning files.
  • Reindexing content.
  • Sending batch emails.

For instance, the “Generate Thumbnails” task handles missed thumbnail creations. It checks all files and makes commands for each thumbnail. The getTaskRunner for GenerateThumbnailsController is:

public function getTaskRunner(TaskInterface $task, InputInterface $input): TaskRunnerInterface
{
    $fileList = new FileList();
    $thumbnailTypes = Type::getVersionList();
    $batch = Batch::create();

    foreach ($fileList->getResults() as $file) {
        if ($file instanceof File) {
            foreach ($file->getFileVersions() as $fileVersion) {
                foreach ($thumbnailTypes as $thumbnailType) {
                    if ($fileVersion->getTypeObject()->supportsThumbnails()) {
                        $batch->add(new GeneratedThumbnailCommand((int)$file->getFileID(), (int)$fileVersion->getFileVersionID(), $thumbnailType->getHandle()));
                    }
                }
            }
        }
    }
    return new BatchProcessTaskRunner($task, $batch, $input, t('Thumbnail generation beginning...'));
}

Steps: - Use Batch::create() to start a new batch. - Add commands/messages to it with $batch->add(). - Add the batch to BatchProcessTaskRunner.

Once set up, Concrete CMS manages the rest:

Task Input and Output

One of the great features about tasks is the ability for tasks to prompt users for input, and to echo output at various times.

Defining Task Input

Getting Task Input can be done by implementing the getInputDefinition method in your task controller. This method must return null or an object of the type Concrete\Core\Command\Task\Input\Definition\Definition. The definition object defines a list of basic inputs for different types of use cases. These input types will display nicely in the Dashboard and work well within the console. Here’s an example of the getInputDefinition method within the ReindexContentController, which powers the “Reindex Content” task.

public function getInputDefinition(): ?Definition
{
    $definition = new Definition();
    $definition->addField(new BooleanField('clear', t('Clear Index'), t('Clear index before reindexing.')));
    $definition->addField(new BooleanField('rebuild', t('Rebuild Index'), t('Rebuild index attributes table by rescanning all keys.')));
    $definition->addField(new Field('after', t('After ID'), t('Reindex objects after a particular ID.')));
    $definition->addField(
        new SelectField(
            'object',
            t('Object to reindex.'),
            t('You must provide what type of object you want to reindex.'),
            [
                'pages' => t('Pages'),
                'files' => t('Files'),
                'users' => t('Users'),
                'express' => t('Express'),
            ],
            true
        )
    );
    return $definition;
}

This should do a decent job at showing off the various Task input fields that are available. We have

Once you provide the input definition, the console arguments and options and the input fields are automatically available to your command. Here’s what the task options look like for Reindex Content in the Dashboard:

and on the console:

Getting Data from Task Input

Showing fields in the Dashboard and on the command line is only half of the problem; we also need to retrieve data for these task options when starting our task. Fortunately, that’s pretty easy to do from within the getTaskRunner command. The getTaskRunner command always has access to the Concrete\Core\Command\Task\Input\Input object, from which you can retrieve fields and their data. Here’s how the Reindex Content task checks the clear, rebuild, and after definition fields.

The --clear option can be checked through the use of $input->hasField():

if ($input->hasField('clear')) {
    $batch->add(new ClearPageIndexCommand());
}

Same with the --rebuild command, which is also a BooleanField:

if ($input->hasField('rebuild')) {
    foreach($entities as $entity) {
        $batch->add(new RebuildEntityIndexCommand($entity->getId()));
    }
}

The Field and SelectField objects can be checked with hasField and with getField:

$object = (string) $input->getField('object')->getValue();
$after = null;
if ($input->hasField('after')) {
    $after = $input->getField('after')->getValue();
}

Once you have the value of the specified inputs, you can craft your batches, process or command task runners based on what the administrators have specified.

Task Output

Tasks also do a good job of reporting their activity, provided task logging is enabled. Tasks accomplish this through the of the Concrete\Core\Command\Task\Output\OutputAwareInterface. Any command handler used by a task that implements this interface and its corresponding trait (Concrete\Core\Command\Task\Output\OutputAwareTrait) will be able to echo out various things that occur during the running of a task. Here’s how the rescan file task command reports on how it’s operating:

<?php

namespace Concrete\Core\File\Command;

use Concrete\Core\Command\Task\Output\OutputAwareInterface;
use Concrete\Core\Command\Task\Output\OutputAwareTrait;

class RescanFileTaskCommandHandler extends RescanFileCommandHandler implements OutputAwareInterface
{

    use OutputAwareTrait;

    /**
     * @param RescanFileTaskCommand $command
     */
    public function __invoke(RescanFileCommand $command)
    {
        $this->output->write(t('Rescanning file ID: %s', $command->getFileID()));
        parent::__invoke($command);
    }


}

(The RescanFileTaskCommandHandler differs from the core RescanFileCommandHandler because it reports on its operation within a task context, using $this->output.)

Once you write your task handlers using $this->output, you can rely on their output showing in real-time in the Dashboard, within historical task logs, and on the console if the task is run from the command line.


A Note on Task Output and Custom Tasks

There is more in tasks than just the definitions of the input, the command and the handlers, etc. The support classes for the tasks themselves take care of injecting that output object into the command handlers, so if they are not run using the support classes (and instead just reference the command directly) it will not have that output.

There are two options for this - the first option is you can run the command directly and have no access to output. Since most commands in a command bus won’t generate output, this may be fine for your use case. Alternatively, you can use the support classes to instantiate the tasks with output and run them that way.

This second option is actually not quite as difficult as it may sound. Here is an example of the a new Dashboard page introduced in Concrete 9.2, the Site Health page. It presents a number of tasks in the special site health category.

These reports are just tasks. When you click on the Run report button, behind the scenes from this custom dashboard page a task is run.

The sub-tasks/commands that make up this report are even displayed in the page. You can see the code that runs the task at:

concrete/controllers/single_page/dashboard/welcome/health.php.

If you were to adapt it for a hard-coded custom task, here is an example of how you might do that:

use Concrete\Core\Command\Task\TaskService;
use Concrete\Core\Command\Task\Traits\DashboardTaskRunnerTrait;

// snip...

use DashboardTaskRunnerTrait; // This adds the `executeTask` method into the controller.

$task = $this->app->make(TaskService::class)->getByHandle('my_custom_task');
$controller = $task->getController();
$this->executeTask($task);

This will execute the task with the Dashboard task output present.

For those using versions of Concrete earlier than 9.2 and wondering what that DashboardTaskRunnerTrait is, the contents of it are included below. This code can be used directly in lieu of the trait - the trait was added to make it easier for developers going forward:

$controller = $task->getController();
$runner = $controller->getTaskRunner($task, new Input());
$handler = $this->app->make($runner->getTaskRunnerHandler());
$handler->boot($runner);

$contextFactory = $this->app->make(ContextFactory::class);
$context = $contextFactory->createDashboardContext($runner);

$handler->start($runner, $context);
$handler->run($runner, $context);