Tasks

A task is work to do whose submission may be decoupled from its execution: generate a PDF, call an API, process a webhook.

Task dispatch is an optional application execution primitive. Kaly provides a minimal dispatch contract, Kaly\Task\TaskDispatcher, and direct execution through its Injector. Persistent queues, workers and transport-specific guarantees belong to optional implementations.

$this->tasks->dispatch(new GeneratePdf($documentId));

A task owns its behavior

A task is a plain invocable object. Its constructor carries the data, its __invoke() receives the services:

namespace App\Documents\Application\Task;

final readonly class GeneratePdf
{
    public function __construct(
        public string $documentId,
    ) {}

    public function __invoke(PdfGenerator $generator): void
    {
        $generator->generate($this->documentId);
    }
}

No interface, attribute or handler registry: __invoke() replaces them, and the task has no dependency on Kaly. Services are resolved by the Injector when the task runs, never stored in the task, so a durable engine can persist the data alone. A task tests without Kaly:

(new GeneratePdf('doc-123'))(new FakePdfGenerator());

When the work grows, keep the task as the entry point and delegate to an application service (__invoke(SupportConversationProcessor $processor)).

A task never receives the current HTTP context implicitly. Identity or authorization travels in the task data and is checked again when it runs.

Choosing an engine

Kaly registers no TaskDispatcher: the application chooses its engine in its composition, so a task meant for a durable queue never runs by accident inside an HTTP transaction.

$app->configure(fn(Definitions $di) => $di->bind(TaskDispatcher::class, DirectTaskDispatcher::class));

dispatch() submits the task under the guarantees of the engine. It promises neither deferred execution, nor durability, nor atomicity with the current transaction:

Dispatcher When dispatch() returns
Kaly\Task\DirectTaskDispatcher The task has completed, or its exception propagates unchanged
Kaly\Test\RecordingTaskDispatcher The task is queued in memory
Durable engine The engine documents its own guarantees

Durable engines include kaly-tasks (PostgreSQL) and Symfony Messenger.

Both Kaly dispatchers reject a non-invocable object and accept closures. A durable engine may restrict tasks further, eg to persistable data.

The direct dispatcher is a production engine for short work that needs no deferred durability. Inside a transaction, it runs the task before the commit.

Testing

RecordingTaskDispatcher records tasks without running them:

$tasks = new RecordingTaskDispatcher();
$useCase = new PublishDocument($tasks);

$useCase->publish('doc-123');

$this->assertEquals([new GeneratePdf('doc-123')], $tasks->tasks());

runAll() runs the queue in dispatch order until it is empty, including tasks dispatched by running tasks. Each task leaves the queue before it runs: on failure, the exception propagates and the following tasks stay pending.

$tasks->runAll($container->get(Injector::class));

Recording validates neither transactions nor recovery: test those with the real engine.

Symfony Messenger

Messenger can run the same tasks unchanged. One generic handler delegates every message of a dedicated bus to the Injector:

use Kaly\Di\Injector;
use Kaly\Task\TaskDispatcher;
use Symfony\Component\Messenger\Handler\HandlersLocator;
use Symfony\Component\Messenger\MessageBusInterface;

final readonly class MessengerTaskDispatcher implements TaskDispatcher
{
    public function __construct(
        private MessageBusInterface $bus,
    ) {}

    public function dispatch(object $task): void
    {
        $this->bus->dispatch($task);
    }
}

$handlers = new HandlersLocator([
    '*' => [static fn(object $task) => $injector->invoke($task)],
]);
  • The '*' handler takes every message: keep a bus for tasks only.
  • Messenger wraps a failure in HandlerFailedException, with the task exception as previous.
  • Messenger serializes with PHP serialize() by default: configure its serializer if stored tasks must be a controlled format.
  • Switching engines does not migrate tasks already waiting in the old one.

Kaly PHP framework — MIT

This site uses Just the Docs, a documentation theme for Jekyll.