Jobs & Queue

Introduction

Modern applications often need to perform tasks that are too slow to handle within a typical HTTP request — sending emails, generating PDFs, processing images, or calling external APIs. Slenix's job queue system allows you to defer these time-consuming tasks to a background worker, keeping your API responses fast and your users happy.

Jobs are plain PHP classes that extend Slenix\Supports\Queue\Job. The queue driver is file-based by default, requiring zero additional dependencies — no Redis, no database tables, no extensions beyond what Slenix already uses.


Creating a Job

Use the Celestial CLI to generate a new job class:

bash
php celestial make:job SendWelcomeEmail

This creates app/Jobs/SendWelcomeEmailJob.php:

php
<?php

declare(strict_types=1);

namespace App\Jobs;

use Slenix\Supports\Queue\Job;

class SendWelcomeEmailJob extends Job
{
    public int $tries   = 3;
    public int $timeout = 60;

    public function __construct(
        // Inject dependencies via constructor
    ) {}

    /**
     * Execute the job.
     */
    public function handle(): void
    {
        // Your job logic here
    }
}

All background logic goes inside handle(). Dependencies are injected through the constructor and serialised automatically with the job.


Writing a Job

A real-world job looks like this:

php
<?php

declare(strict_types=1);

namespace App\Jobs;

use Slenix\Supports\Queue\Job;
use App\Models\User;

class SendWelcomeEmailJob extends Job
{
    /**
     * Retry up to 3 times on failure.
     */
    public int $tries = 3;

    /**
     * Abort if the job runs for more than 60 seconds.
     */
    public int $timeout = 60;

    /**
     * Wait 10 seconds before retrying after a failure.
     */
    public int $retryAfter = 10;

    /**
     * Push this job onto the 'emails' channel.
     */
    public string $queue = 'emails';

    public function __construct(private User $user) {}

    public function handle(): void
    {
        (new Mail())->to($this->user->email)
            ->subject('Welcome to ' . env('APP_NAME'))
            ->message('<h1>Welcome, ' . $this->user->name . '!</h1>', true)
            ->send();
    }

    /**
     * Called when all retry attempts are exhausted.
     */
    public function failed(\Throwable $e): void
    {
        Log::error("Failed to send welcome email to {$this->user->email}: {$e->getMessage()}");
    }
}

Dispatching Jobs

After creating a job, dispatch it from anywhere in your application using the global dispatch() helper:

php
use App\Jobs\SendWelcomeEmailJob;

dispatch(new SendWelcomeEmailJob($user));

The job is queued immediately and the current request continues without waiting for handle() to run.

Specifying a Queue Channel

php
// Use the job's own $queue property (default)
dispatch(new SendWelcomeEmailJob($user));

// Override the channel at dispatch time
dispatch(new SendWelcomeEmailJob($user), queue: 'emails');

Delaying a Job

php
// Run 5 minutes from now
dispatch(new SendWelcomeEmailJob($user), delay: 300);

// Override both channel and delay
dispatch(new GenerateInvoiceJob($order), queue: 'pdfs', delay: 60);

Using the Queue Facade Directly

php
use Slenix\Supports\Queue\Queue;

Queue::push(new SendWelcomeEmailJob($user));
Queue::push(new SendWelcomeEmailJob($user), queue: 'emails', delay: 30);

Dispatching from a Controller

The most common pattern is dispatching jobs inside a controller after completing a database operation:

php
<?php

namespace App\Controllers;

use Slenix\Http\Request;
use Slenix\Http\Response;
use App\Models\User;
use App\Jobs\SendWelcomeEmailJob;
use App\Jobs\NotifyAdminJob;
use App\Jobs\GenerateAvatarJob;

class AuthController
{
    public function register(Request $request, Response $response): void
    {
        $user = User::create([
            'name'     => $request->input('name'),
            'email'    => $request->input('email'),
            'password' => hash_make($request->input('password')),
        ]);

        // All three run in the background — response is instant
        dispatch(new SendWelcomeEmailJob($user));
        dispatch(new NotifyAdminJob($user));
        dispatch(new GenerateAvatarJob($user), delay: 5);

        $response->status(201)->json([
            'success' => true,
            'message' => 'Account created successfully.',
            'data'    => $user,
        ]);
    }
}

Running the Worker

Jobs are not processed automatically. You must start a worker process that continuously polls the queue and executes pending jobs.

Start the default worker:

bash
php celestial queue:work

Start a worker for a specific channel:

bash
php celestial queue:work --queue=emails

Poll multiple channels in priority order:

bash
php celestial queue:work --queue=emails,pdfs,default

Process one job and exit (useful for cron jobs):

bash
php celestial queue:work --once

Exit automatically when the queue is empty:

bash
php celestial queue:work --stop-when-empty

Control the sleep interval between polls (default: 3 seconds):

bash
php celestial queue:work --sleep=5

Running Two Servers in Parallel

You typically run the HTTP server and the queue worker in separate terminals:

bash
# Terminal 1 — HTTP server
php celestial serve

# Terminal 2 — Queue worker
php celestial queue:work --queue=emails,default

Inspecting Failed Jobs

When a job fails on all retry attempts it is moved to the failed channel. You can list all failed jobs with:

bash
php celestial queue:failed

Clearing the Queue

Delete all pending jobs across all channels:

bash
php celestial queue:clear

Delete pending jobs from a specific channel:

bash
php celestial queue:clear --queue=emails

Queue Storage

Jobs are stored as files in storage/queue/{channel}/. Each file is a JSON document containing the serialised job payload, attempt count, and availability timestamp.

plaintext
storage/
└── queue/
    ├── default/
    │   └── 1700000000_a1b2c3d4.job
    ├── emails/
    │   └── 1700000060_e5f6g7h8.job
    └── failed/
        └── 1700000120_i9j0k1l2.job

You can customise the storage path via the .env file:

env
QUEUE_DRIVER=file
QUEUE_PATH=storage/queue

Job Properties Reference

PropertyTypeDefaultDescription
$triesint3Maximum number of attempts before marking as failed
$timeoutint60Seconds before the job is force-stopped
$retryAfterint5Seconds to wait before retrying after a failure
$queuestring'default'Queue channel this job belongs to
$delayint0Seconds to delay execution after dispatch

Worker Options Reference

OptionDescription
--queue=nameChannel(s) to poll, comma-separated
--sleep=NSeconds to sleep when queue is empty (default: 3)
--onceProcess one job and exit
--stop-when-emptyExit when the queue is empty
--max-jobs=NStop after processing N jobs