Converting Jobs to Tasks

Derek Cameron
Derek Cameron

Introduction to Tasks

What is a task? In concreteCMS, tasks are the automation system that replaces the old job system. Under the hood the tasks are using symfony’s messenger system.

This system allows us to dispatch messages to a worker service that will handle the message. At first glance this may seem very similar to queued jobs in version 8 and below.

However, the difference is that tasks can accept user input from the dashboard and command line. It also allows for tasks that are run synchronously, or asynchronously.

From a developer perspective you can also have a messenger consumer that will handle the messages running on the server, meaning that the task will never interfere with your application front end. Even though users will get up to date information from the dashboard, the task will still be running on the server.

An Example Job

In concreteCMS, version 8 and below, we had a job system that was used to run tasks. Below is an example of a job that deletes all existing page drafts, from the erase page draft job package.

<?php

namespace Concrete\Package\EraseDraftpageJob\Job;

use QueueableJob;
use ZendQueue\Queue as ZendQueue;
use ZendQueue\Message as ZendQueueMessage;
use Exception;
use Concrete\Core\Page\Page;
use Core;

class EraseDraftpage extends QueueableJob
{
    public $jSupportsQueue = true;

    public function getJobName()
    {
        return t('Erase Draft Pages');
    }

    public function getJobDescription()
    {
        return t('This job will erase all draft pages. It would be useful for those who ended up having too many draft pages.');
    }

    public function start(ZendQueue $q)
    {
        $currentVersion = Core::make('config')->get('concrete.version');
        if (version_compare($currentVersion , '8.2.0', 'lt')) {
            $pageDrafts = Page::getDrafts();
        } else {
            $site = Core::make('site')->getSite();
            $pageDrafts = Page::getDrafts($site);
        }
        foreach ($pageDrafts as $pageDraft) {
            $q->send($pageDraft->getCollectionID());
        }
    }

    public function processQueueItem(ZendQueueMessage $msg)
    {
        $pageDraft = Page::getByID($msg->body);
        if ($pageDraft->isPageDraft()) {
            $pageDraft->delete();
        } else {
            throw new Exception(t('Error occurred while getting the Page object of pID: %s', $msg->body));
        }
    }

    public function finish(ZendQueue $q)
    {
        return t('Finished erasing draft pages.');
    }
}

Creating the TaskController

The first thing that we want to do to migrate this job is to create the task controller. A task controller is a specific class that will create the batch that we need to process all of the jobs. It is essentially the same as the start function of a queueable job in concreteCMS 8.x and below.

So to begin we first will need to create a new file called EraseDraftpageTaskController.php in the src/Concrete/Command/Task/Controller folder. This file will extend the AbstractController from the core task controller. So your file will look like this:

<?php
namespace Concrete\Package\EraseDraftpageJob\Command\Task\Controller;

use Concrete\Core\Command\Task\Controller\AbstractController;

class EraseDraftsController extends AbstractController {

}

Setting up the TaskController

Now that we have the basics your IDE might be warning you that you are missing a few things. Namely the getNamegetDescription, and getTaskRunner methods. The first two of these methods are similar to the ones in the queueable job. The last one is the one that will be used to create the batch. So let's copy the name and description from the queueable job, with one small change, let's change “job” to “task”.

So our file now looks like this:

<?php
namespace Concrete\Package\EraseDraftpageJob\Command\Task\Controller;

use Concrete\Core\Command\Task\Controller\AbstractController;

class EraseDraftsController extends AbstractController {
    public function getName(): string
    {
        return t('Erase Draft Pages');
    }

    public function getDescription(): string
    {
        return t('This task will erase all draft pages. It would be useful for those who ended up having too many draft pages.');
    }
}

For the task runner, we will need to add a few new classes. We need to add the InputInterfaceTaskRunnerInterface and TaskInterface classes to our use Statements.

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

Then let's add the getTaskRunner method with two parameters. The first parameter is called $task and is a TaskInterface type, the second is the $input and is a InputInterface. We need a return type of TaskRunnerInterface, currently there is three types of task runners, Concrete\Core\Command\Task\Runner\BatchProcessTaskRunnerConcrete\Core\Command\Task\Runner\ProcessTaskRunner and Concrete\Core\Command\Task\Runner\CommandTaskRunner. The one we will be using is the Concrete\Core\Command\Task\Runner\BatchProcessTaskRunner. But for now your function should look like this:

public function getTaskRunner(TaskInterface $task, InputInterface $input): TaskRunnerInterface 
{
$site = Core::make('site')->getSite();
            $pageDrafts = Page::getDrafts($site);
        }
        foreach ($pageDrafts as $pageDraft) {
            $q->send($pageDraft->getCollectionID());
        }
}

Next we will add some code to this function. So we will take some of our existing code and add it to the task runner. Since this is version 9 we don't need to use the facade Core or check the version of concreteCMS. Instead of the Core facade we can use the new app() helper function which is equal to \Core::make().

public function getTaskRunner(TaskInterface $task, InputInterface $input): TaskRunnerInterface 
{
            $site = app('site')->getSite();
            $pageDrafts = Page::getDrafts($site);
        foreach ($pageDrafts as $pageDraft) {
            $q->send($pageDraft->getCollectionID());
        }
}

Creating the Batch

Of course right now our code isn’t working correctly so we are going to add a few extra classes to our use statements. We need to add the PageBatchBatchProcessTaskRunner and DeletePageCommand classes like this:

use Concrete\Core\Page\Page;
use Concrete\Core\Command\Batch\Batch;
use Concrete\Core\Command\Task\Runner\BatchProcessTaskRunner;
use Concrete\Core\Page\Command\DeletePageCommand;

Now that we have added we can go ahead and create our batch of DeletePageCommands. Now you might be wondering what is the DeletePageCommand, well it does exactly what it says it is a command that will delete a page.

Inside of our getTaskRunner function we want to create a batch object. To create a batch object we call the static method create and it takes an argument of name and messages, but message is optional and we will leave it blank. So we will create a new batch object like this:

$batch = Batch::create(t('Erase Draft Pages'));

Now inside our foreach loop let's add a new DeletePageCommand object to the batch. The DeletePageCommand takes 1 required parameter and an optional parameter, a pageID and userID. Since we haven't got the userID yet we can just import the User class and use our app() helper to get the current user instance. If our user doesn't exist or is 0 we will pass null to the userID. First let's add the User class to our use statements and then get our User instance. So you will add the following to your file:

...
use Concrete\Core\User\User;
...
public function getTaskRunner(TaskInterface $task, InputInterface $input): TaskRunnerInterface 
{
...
    $user = app(User::class)->getUser();
    // Type cast the user to an int, return null if its 0
    $userID = ((int) $user->getUserID())?: null;
...

Now we can add the DeletePageCommand to the batch like this:

...
public function getTaskRunner(TaskInterface $task, InputInterface $input): TaskRunnerInterface 
{
...
    foreach ($pageDrafts as $pageDraft) {
        $batch->add(new DeletePageCommand($pageDraft->getCollectionID(),$userID));
    }
...

Our final piece for the getTaskRunner is to return the BatchProcessTaskRunner object. So we will add our $task, $batch, and $input to the constructor as well as a string that will be shown when starting the batch. Like this

...
    return new BatchProcessTaskRunner($task, $batch, $input, t('Starting to erase draft pages...'));
}
...

The Final Task Controller

With all of the above complete our task controller will look like the following:

<?php
namespace Concrete\Package\EraseDraftpageJob\Command\Task\Controller;


use Concrete\Core\Command\Task\Controller\AbstractController;
use Concrete\Core\Command\Task\Input\InputInterface;
use Concrete\Core\Command\Task\Runner\TaskRunnerInterface;
use Concrete\Core\Command\Task\TaskInterface;
use Concrete\Core\Page\Page;
use Concrete\Core\Command\Batch\Batch;
use Concrete\Core\Command\Task\Runner\BatchProcessTaskRunner;
use Concrete\Core\Page\Command\DeletePageCommand;
use Concrete\Core\User\User;


class EraseDraftsController extends AbstractController {

    public function getName(): string
    {
        return t('Erase Draft Pages');
    }

    public function getDescription(): string
    {
        return t('This task will erase all draft pages. It would be useful for those who ended up having too many draft pages.');
    }

    public function getTaskRunner(TaskInterface $task, InputInterface $input): TaskRunnerInterface
    {

        $user = app(User::class);
        $userID = ((int) $user->getUserID())?: null;
        $site = app('site')->getSite();
        $pageDrafts = Page::getDrafts($site);
        $batch = Batch::create(t('Erase Draft Pages'));

        foreach ($pageDrafts as $pageDraft) {
            $batch->add(new DeletePageCommand($pageDraft->getCollectionID(),$userID));
        }

        return new BatchProcessTaskRunner($task, $batch, $input, t('Started Erasing Draft Pages.'));
    }
}

Install our Task Controller

Now that we have a task controller we need to install it and register it with the Task Manager. To do this we will create a new file called tasks.xml in config/ in our package directory. Inside that xml file we will create a concrete5-cif tag, or concrete5 content import file, that contains a tasks tag and a single task tag containing our task handle and package handle. So our tasks.xml will look like the following:

<?xml version="1.0" encoding="UTF-8"?>
<concrete5-cif version="1.0">
    <tasks>
        <task handle="erase_drafts" package="erase_draftpage_job"/>
    </tasks>
</concrete5-cif>

Once we have done that we need to install the content file on upgrade or install. So we need to add the following line to both the install() and upgrade() functions:

$this->installContentFile('config/tasks.xml');

Register the Task Controller

Now our task can be installed but it needs to be registered with the Task Manager. To register our TaskController we need to extend the Manager and our task to its list of tasks. Inside our packages/controller.php we will add a new public method called on_start, this is called every time concreteCMS loads. So import our Manager class and then make it with $this->app->make(Manager::class); then call the extend method and pass in our task handle and a lambda function that returns our task controller.

...
use Concrete\Package\EraseDraftpageJob\Command\Task\Controller\EraseDraftsController;
use Concrete\Core\Command\Task\Manager;
...
    public function on_start()
    {
        $manager = $this->app->make(Manager::class);
        $manager->extend('erase_drafts', static function () {
            return new EraseDraftsController();
        });
    }

Once this is done let's bump our package version up so we can upgrade/install our task controller. (Please note that the original Job will not work in concrete version9, if you would like to update the package to version 9 check it out on github) So we change $pkgVersion to 1.1.0 like this:

...
    protected $pkgVersion = '1.1.0';
...
 

Then we can install/update our package and run the task.

Running The Task

Now to run the task head over to the dashboard and click on “System”, then under “Automation” click on “Tasks”. You should now see your task added at the bottom like thistask_before_run.pngSelect the task by clicking on it then click on “run task” to run the task. You will be redirected to the activity page where you should see your task running. It may appear like this if the task is still running:task_running.png

Now you are all ready to convert your own tasks to the new system.

In the next tutorial we will add options to the task and add a new Command and CommandHandler rather than relying on the core DeletePageCommand.