laika-framework

Queue

The Queue module of laikait/laika-engine runs slow work — sending mail, generating reports, calling third-party APIs — in a background worker process instead of the web request. It supports JSON, database and Redis drivers, delayed jobs, retries with backoff, and failed-job tracking.

use App\Job\SendWelcomeEmail;
use Laika\Engine\Worker\Queue;

Queue::driver()->push(new SendWelcomeEmail($userId), queue: 'emails', delay: 10);
php worker emails

This page covers everyday use. Driver internals, stuck jobs, worker hooks and custom drivers are in Queue Drivers & Operations.

Choosing a Driver

Set in lf-config/queue.php:

return [
    'driver'          => 'json',    // 'json' | 'database' | 'redis'
    'connection'      => 'default', // used by the database driver / database failed-job store
    'failed_driver'   => null,      // 'database' | 'json'; null = 'database' for the database driver, else 'json'
    'reserve_timeout' => 90,        // redis only
    'max_tries'       => 3,         // redis only
];

Always set driver. The web app (Queue::driver()) and the worker choose the driver separately, and if the key is missing they fall back to different ones — json and database respectively — so pushed jobs would never be run.

Driver Storage Good for Notes
json lf-storage/queues/jobs.json Development, one server No setup. Claiming a job is locked, so workers never share one. No recovery of a job whose worker crashed.
database laika_queue_jobs table, via the Model module Small production setups One worker per queue. Create the tables first (below).
redis Redis, from lf-config/redis.php Production, several workers Requires ext-redis. Atomic pop, stalled-job recovery.

Failed jobs go to a separate store, chosen by failed_driver — database (laika_failed_jobs) or json (lf-storage/queues/failed.json). There is no Redis failed-job store.

connection names a connection in lf-config/database.php, and may contain letters only — analytics works, analytics_db and db2 throw DriverException.

Creating the Database Tables

The database driver and the database failed-job store need their tables, and nothing creates them for you — not php laika app:migrate (since laika-queue 1.1.1), and not first use. Create them once, with a user that has CREATE rights; a custom command is a good place:

(new \Laika\Engine\Queue\Schema\QueueModelSchema())->up();      // laika_queue_jobs
(new \Laika\Engine\Queue\Schema\FailedJobModelSchema())->up();  // laika_failed_jobs

With no argument both follow queue.connection. They use createIfNotExists, so running them again is harmless.

The drivers also have an install() method. It drops the table first, deleting every pending or failed job — use it for a fresh install or a test, never on a live queue.

Defining a Job

php laika job:make SendWelcomeEmail

Job names — and the --queue value — may contain letters and underscores only.

namespace App\Job;

use Laika\Engine\Queue\Abstracts\Job;

class SendWelcomeEmail extends Job
{
    public int $maxTries = 3;
    protected int|array|null $backoffStrategy = [10, 30, 60]; // seconds before retry 1, 2, 3...

    public function __construct(protected int $userId) {}

    public function handle(): void
    {
        $user = (new \App\Model\UsersModel())->find($this->userId);
        // ... send the email; throw to fail this attempt ...
    }

    public function failed(\Throwable $e): void
    {
        if ($this->tries >= $this->maxTries) {
            error_log("Welcome email to {$this->userId} failed for good: {$e->getMessage()}");
        }
    }
}

The job object is serialized into the queue, so constructor properties are available in handle(). Pass IDs, not models or connections — anything that can’t be serialized (a PDO, a closure) fails.

Property / method Default  
public int $maxTries 3 Attempts before the job moves to the failed store
public int $retryAfter 60 Base delay for exponential backoff
protected int\|array\|null $backoffStrategy null int: n × attempt seconds. array: seconds per attempt, the last repeated; an empty array means exponential. null: retryAfter × 2^(attempt-1), capped at 3600.
protected bool $jitter true ±20% random jitter, applied to every strategy
public int $tries — Attempts so far, including the current one (set by the worker)
public string $id — Set by the driver when the job is pushed
abstract public function handle(): void   The work. Throw to fail the attempt.
public function failed(\Throwable $e): void   Called after every failed attempt — compare $this->tries with $this->maxTries to tell a retry from a final failure

Pushing a Job

Laika\Engine\Worker\Queue::driver() builds the driver configured in lf-config/queue.php:

use Laika\Engine\Worker\Queue;

$queue = Queue::driver();

$queue->push(new SendWelcomeEmail($userId));                        // 'default' queue, now
$queue->push(new SendWelcomeEmail($userId), queue: 'emails');       // another queue
$queue->push(new GenerateReport($month), queue: 'reports', delay: 300); // in 5 minutes

$queue->size('emails');   // pending jobs, including delayed ones
Driver method  
push(Job $job, string $queue = 'default', int $delay = 0): string Returns the job id
size(string $queue = 'default'): int Jobs waiting, delayed ones included
pop(string $queue = 'default'): ?Job Used by the worker
ack(), release(), delete() Used by the worker to finish, retry or drop a job
install(): void Creates the driver’s storage — for the database driver, by dropping and recreating the table

Build drivers with Queue::driver() rather than new DatabaseDriver(...)/new RedisDriver(...): it registers the database connection and gives the Redis driver the ability to reconnect in a forked worker.

Running the Worker

php worker                 # the 'default' queue
php worker emails          # one named queue
php laika queue:work emails  # the same, through the CLI

Signals (Linux/macOS with ext-pcntl): SIGTERM/SIGINT finish the current job and stop; SIGUSR2 pauses; SIGCONT resumes. Each job runs in a forked child, which is what makes the timeout possible; it needs ext-pcntl and ext-posix.

On Windows there’s no pcntl: no graceful stop, no pause, no per-job timeout — the worker runs jobs inline, in its own process. Fine for development. Run it as php worker; the old worker.bat is no longer generated.

See Deployment → Queue Worker for supervisor and systemd configuration.

Failed Jobs

php laika queue:failed                   # list failed jobs
php laika queue:failed --queue=emails
php laika queue:retry 9f86d081884c7d65…  # push one back by id, attempts reset
php laika queue:retry --all              # push all back
php laika queue:retry --all --queue=emails
php laika queue:flush                    # delete all failed jobs (asks first)
php laika queue:flush --hours=48         # delete those older than 48 hours

Trusted Job Classes

Job payloads are restored with unserialize(). To prevent PHP object injection, only trusted classes may be unserialized — anything else throws.

The worker (and queue:retry) trusts every class in the jobs resource automatically — by default every Job subclass in lf-app/Job/, plus any job directory an installed package declares. A job class that lives elsewhere must be registered before the worker processes it — for example in a hook file:

\Laika\Engine\Queue\Abstracts\Job::registerTrustedClasses([\Acme\Billing\ChargeJob::class]);

A job the worker can’t restore — untrusted, renamed or deleted — blocks its queue on the json and database drivers. See A Job That Can’t Be Restored.

Concurrency

Write jobs to be idempotent (safe to run twice) wherever you can.

CLI Reference

Command Description
php laika job:make <name> [--queue=default] [--max=3] Create a job class. --max sets $maxTries.
php laika job:list List job classes
php laika job:remove <name> Delete a job class
php laika job:rename --old=<name> --new=<name> Rename a job class
php laika queue:work [queue] Run a worker in the foreground (same as php worker [queue])
php laika queue:failed [--queue=<name>] List failed jobs
php laika queue:retry <id> / --all [--queue=<name>] Retry failed jobs
php laika queue:flush [--hours=N] Delete failed jobs

See Also