CakePHP background processing

The easy way 🙂

Some might also already be in the #CakePHP coding world for some time.
They might remember an article from like 11 years ago: queue-deferred-execution-in-cakephp.
If not, maybe catch up on that one first, as that is the intro to this new post of 2024 now.
It explains the main reasons why a queue is a must have for every CakePHP app, even if not that big just yet.

11 years later

It started more as a demo tool for background processing and wasn’t quite meant as stable plugin for larger/enterprise CakePHP apps.
With more and more users that loved the easiness of the plugin, the dependency free usage that integrates perfectly into a CakePHP app, and the default features it ships with out of the box it seemed to slowly grow into the status quo, though.

So what has changed in 10+ years of building the Queue plugin, half a million downloads and thousands of users and apps later?

It is now officially out of any demo state, and seems to reliable do its job even on larger databases and codebases.
The Queue plugin now integrates even more seamlessly into any app.
Not just a requirement for sending emails properly, but for any kind of work offloading, like PDF generation or media processing.

It also moved away from the legacy "PHP serialization" of objects to a more safe and sane approach using JSON for encoding/decoding.
With the following article I will show case some of the new benefits of the now freshly released v8 version.

Main benefits

Just to quickly summarize:

  • Dependency free: all it needs is PHP and the database, as well as a cronjob (PHP server access).
  • Simple but powerful: Progress, retry, admin backend, cost management (resource usage in parallel), …
  • Integrates very easily into any CakePHP app, allows displaying e.g. a specific job and its status and progress along the entity it belongs to.
  • Failed jobs automatically schedule for retry, will not be discarded until manually told so.
  • Built in statistics to monitor the average execution time for certain (longer) jobs.
  • Easy to debug: Export a failed job, import it locally and run it there.

QueueScheduler

With this optional plugin on top you can easily make scheduled runs of Commands or shell scripts.
They will be put into the queue and run when it is time.
It can be fully controlled from the backend, so no server access is necessary, only admin access to the backend.

Check it out: QueueScheduler plugin

IDE autocomplete

For PHPStorm and IDEs that allow meta information to be used to make the framework experience smoother, the plugin provides an easy way to integrate with the
must-have IdeHelper plugin.

It scans all existing tasks and provides them as autocomplete on your createJob() call:

Config as typed object

Associative arrays with unknown or unclear keys became a code smell over time.
So naturally, with powerful IDEs and PHPStan/Psalm, this also became more visible.

The Queue plugin allows now configuration to be passed with a clear fluent interface for the object that builds the config:

$config = $queuedJobsTable->newConfig()
    ->setPriority(2)
    ->setReference('foo')
    ->setNotBefore('+1 hour');
$queuedJobsTable->createJob('OrderUpdateNotification', $data, $config);

All methods on that JobConfig object are fully autocompleted and typehinted.

DTO usage

Similar topic for the $data param, both as input and as output on the concrete task.
It now supports any object passed that can serialize itself into an associative array and therefore a valid payload to pass into the task.

You can, for example, use DTOs from the CakeDto plugin to handle the transfer process.

Set up a DTO per task in your dto.xml, e.g.

<dto name="OrderUpdateNotificationQueueData" immutable="true">
    <field name="orderId" type="int" required="true"/>
    <field name="type" type="string" required="true"/>
    ...
</dto>

Instead of a plain array you can now rely on a clean API for input:

$dataDto = OrderUpdateNotificationQueueDataDto::createFromArray([
    'orderId' => $order->id,
    'type' => 'orderConfirmationToCustomer',
]);
$this->fetchTable('Queue.QueuedJobs')->createJob('OrderUpdateNotification', $dataDto);

Any of the required fields not provided or fields not defined will throw a clear exception.

Same then for the counterpart within the task:

public function run(array $data, int $jobId): void {
    $dataDto = OrderUpdateNotificationQueueDataDto::createFromArray($data);

    $orderId = $dataDto->getOrderId();
    $order = $this->fetchTable('Orders')->get($orderId, contain: ['OrderItems']);
    $this->getMailer('OrderConfirmation')->send($dataDto->getType(), [$order]);
}

PHPStan together with tests can now fully monitor and assert necessary data.
No more chaos with associative arrays – instead you got maximum IDE/autocomplete and testing capability.

DIC support

If you use the Dependency Injection Container provided by CakePHP you can also use
it inside your tasks.

use Queue\Queue\ServicesTrait;

class MyCustomTask extends Task {

    use ServicesTrait;

    public function run(array $data, int $jobId): void {
        $myService = $this->getService(MyService::class);
        ...
    }
}

As you see here you have to add the ServicesTrait to your task which then allows you to use the $this->getService() method.

Live demo

Check out the sandbox examples. You play around with it live.

You need help?

As a freelancer I am available to some extend. You can hire me for your (CakePHP) project to

  • built in the Queue and background processing
  • evaluate or help getting it on track regarding modern development
  • enhancing plugins or custom solutions to existing plugins
  • upgrading apps to v5
  • consulting or other support

Feel free to reach out.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.