Features

Queues & Jobs

Process background tasks with retries and failure handling

Offload slow operations from HTTP requests: emails, reports, image processing, webhooks, bulk imports.

Quick Start

1. Create a Job

namespace App\Jobs;

use Glueful\Queue\Job;

class SendWelcomeEmailJob extends Job
{
    public function handle(): void
    {
        $data = $this->getData();
        $userId = $data['userId'];
        $user = db()->table('users')->find($userId);

        // Send email
        mail($user->email, 'Welcome!', 'Thanks for joining...');
    }
}

2. Dispatch the Job

use Glueful\Queue\QueueManager;
use App\Jobs\SendWelcomeEmailJob;

$queue = service(QueueManager::class);
$queue->push(SendWelcomeEmailJob::class, ['userId' => $userId]);

Why class names + data

  • Deterministic serialization: only the job class name and a plain array are enqueued, making payloads easy to serialize and version safely.
  • Safer and portable: avoids serializing arbitrary PHP objects; drivers store compact payloads (often JSON) that work across processes and connections.
  • Reliable retries/delays: workers rehydrate the job with the data and run handle(), ensuring consistent behavior on every attempt.
  • Clear contracts: jobs read inputs via getData(), keeping state explicit and idempotent-friendly.

3. Run Workers

php glueful queue:work

When to Use Jobs

Use jobs for:

  • Email sending
  • Image/video processing
  • Report generation
  • API webhooks
  • Bulk operations
  • Cache warming
  • Data imports

Keep in HTTP response:

  • Critical user feedback
  • Small, fast operations
  • Required synchronous validation

Configuration

config/queue.php:

return [
    'default' => env('QUEUE_CONNECTION', 'database'),

    'connections' => [
        'database' => [
            'driver' => 'database',
            'table' => 'queue_jobs',
            'queue' => 'default',
            'retry_after' => 90,
            'failed_table' => 'queue_failed_jobs',
        ],
        'redis' => [
            'driver' => 'redis',
            'host' => env('REDIS_HOST', '127.0.0.1'),
            'port' => env('REDIS_PORT', 6379),
            'database' => env('REDIS_DB', 0),
            'prefix' => env('REDIS_QUEUE_PREFIX', 'glueful:queue:'),
            'queue' => env('REDIS_QUEUE', 'default'),
            'retry_after' => 90,
        ],
    ],

    'failed' => [
        'driver' => env('QUEUE_FAILED_DRIVER', 'database'),
        'database' => 'default',
        'table' => 'queue_failed_jobs',
    ],
];

Job Basics

Simple Job

use Glueful\Queue\Job;

class ProcessImageJob extends Job
{
    public function handle(): void
    {
        $data = $this->getData();
        $imagePath = $data['imagePath'];

        // Resize image
        $image = Image::load($imagePath);
        $image->resize(800, 600);
        $image->save();
    }
}

Dispatch

$queue->push(ProcessImageJob::class, ['imagePath' => $path]);

Delayed Jobs

Schedule jobs for later:

// Run in 5 minutes (300 seconds)
$queue->later(300, RecalculateMetricsJob::class, ['accountId' => $accountId]);

// Run tomorrow
$queue->later(86400, SendReportJob::class, ['userId' => $userId]);

Priority Queues

Use different queues for different priorities:

// High priority
$queue->push(CriticalPaymentJob::class, ['data' => $data], 'payments_high');

// Normal priority
$queue->push(SendEmailJob::class, ['user' => $user], 'emails');

// Low priority
$queue->push(GenerateReportJob::class, ['id' => $id], 'reports_low');

Start workers by priority:

# Process high priority first
php glueful queue:work --queue=payments_high,emails,reports_low

Retries & Failures

Automatic Retries

Jobs retry automatically on failure:

class SendWebhookJob extends Job
{
    public function getMaxAttempts(): int
    {
        return 3; // Retry up to 3 times
    }

    public function handle(): void
    {
        $data = $this->getData();
        $response = $this->httpClient->post($data['webhookUrl'], $data['payload'] ?? []);

        if (method_exists($response, 'failed') ? $response->failed() : !(method_exists($response, 'successful') && $response->successful())) {
            throw new \Exception('Webhook failed');
        }
    }
}

Custom Retry Logic

class ImportDataJob extends Job
{
    public function handle(): void
    {
        try {
            $this->importData();
        } catch (TemporaryException $e) {
            // Retry this job
            throw $e;
        } catch (PermanentException $e) {
            // Don't retry, just log
            logger()->error('Import failed permanently', [$e]);
        }
    }
}

Idempotent Jobs

Make jobs safe to retry:

class GenerateInvoicePdfJob extends Job
{
    public function handle(): void
    {
        // Check if already generated
        if ($this->alreadyGenerated($this->invoiceId)) {
            return; // Safe no-op
        }

        $pdf = $this->generatePdf($this->invoiceId);
        $this->storePdf($pdf);
    }

    private function alreadyGenerated($id): bool
    {
        return file_exists("invoices/{$id}.pdf");
    }
}

Bulk Operations

Dispatch many jobs efficiently:

// Warm cache for 1000 users
$userIds = range(1, 1000);

$jobs = array_map(
    fn($id) => [
        'job' => WarmUserCacheJob::class,
        'data' => ['userId' => $id],
    ],
    $userIds
);

$queue->bulk($jobs, 'cache_warmup');

Job Chaining

Process jobs in sequence:

// Process video, then notify user
$queue->push(ProcessVideoJob::class, ['videoId' => $videoId]);
$queue->later(600, NotifyUserJob::class, ['userId' => $userId, 'message' => 'Video ready!']);

Common Patterns

Email Queue

class SendWelcomeEmailJob extends Job
{
    public function handle(): void
    {
        $data = $this->getData();
        $template = view('emails.welcome', [
            'name' => $data['name']
        ]);

        mail($data['email'], 'Welcome!', $template);
    }
}

Image Processing

class OptimizeImageJob extends Job
{
    public function handle(): void
    {
        $data = $this->getData();
        $image = Image::load($data['path']);

        // Generate thumbnails
        $image->resize(200, 200)->save($data['path'] . '.thumb.jpg');
        $image->resize(800, 600)->save($data['path'] . '.medium.jpg');

        // Optimize original
        $image->optimize()->save($data['path']);
    }
}

Webhook Delivery

class DeliverWebhookJob extends Job
{
    public function getMaxAttempts(): int
    {
        return 5;
    }

    public function handle(): void
    {
        $data = $this->getData();
        $response = $this->httpClient->post($data['url'], [
            'event' => $data['event'],
            'data' => $data['data'] ?? [],
            'timestamp' => time()
        ]);

        if (!(method_exists($response, 'successful') && $response->successful())) {
            throw new \Exception('Webhook delivery failed');
        }
    }
}

Report Generation

class GenerateMonthlyReportJob extends Job
{
    public function handle(): void
    {
        $data = $this->getData();
        $reportData = $this->gatherData();
        $pdf = $this->generatePdf($reportData);

        // Store report
        Storage::put("reports/monthly-{$data['month']}.pdf", $pdf);

        // Notify user
        $queue = service(\Glueful\Queue\QueueManager::class);
        $queue->push(NotifyReportReadyJob::class, ['userId' => $data['userId']]);
    }
}

Monitoring

Track job metrics:

// Queue depth
$pending = db()->table('queue_jobs')->count();

// Failed jobs
$failed = db()->table('queue_failed_jobs')->count();

// Job age
$oldest = db()->table('queue_jobs')
    ->orderBy('created_at', 'asc')
    ->first();

Failed Jobs

Use the FailedJobProvider to inspect, retry, and delete failed jobs:

use Glueful\Queue\Failed\FailedJobProvider;

$provider = new FailedJobProvider();

// List recent failed jobs
$failed = $provider->all(limit: 50);

// Retry a specific failed job
$provider->retry($uuid);

// Forget (delete) a failed job
$provider->forget($uuid);

// Retry all retryable failed jobs
$provider->retryAll();

Best Practices

Keep Jobs Small

// ✅ Good - focused, retryable
class SendEmailJob extends Job
{
    public function handle(): void
    {
        mail($this->email, $this->subject, $this->body);
    }
}

// ❌ Bad - too many responsibilities
class ProcessUserRegistrationJob extends Job
{
    public function handle(): void
    {
        // Send email
        // Update analytics
        // Notify admin
        // Create account
        // Generate invoice
        // ... (too much)
    }
}

Make Idempotent

// ✅ Good - safe to retry
public function handle(): void
{
    if ($this->alreadyProcessed()) {
        return;
    }
    $this->process();
}

// ❌ Bad - creates duplicates on retry
public function handle(): void
{
    $this->createRecord(); // Creates duplicate!
}

Throw on Failure

// ✅ Good - allows retry
public function handle(): void
{
    if (!$this->apiCall()) {
        throw new \Exception('API failed');
    }
}

// ❌ Bad - hides failures
public function handle(): void
{
    try {
        $this->apiCall();
    } catch (\Exception $e) {
        // Silently fails
    }
}

Troubleshooting

Jobs not processing?

  • Ensure worker is running: php glueful queue:work
  • Check queue connection in .env

Jobs failing?

  • Check queue_failed_jobs table
  • Review error messages
  • Ensure job is idempotent

Queue growing?

  • Scale workers
  • Optimize job performance
  • Split into priority queues

Jobs timing out?

  • Increase retry_after in config
  • Break large jobs into smaller ones

Scaling Workers

Single Worker

php glueful queue:work

Multiple Workers

# Terminal 1
php glueful queue:work --queue=high

# Terminal 2
php glueful queue:work --queue=default

# Terminal 3
php glueful queue:work --queue=low

Supervisor (Production)

/etc/supervisor/conf.d/queue-worker.conf:

[program:queue-worker]
command=php /path/to/glueful queue:work --queue=default
autostart=true
autorestart=true
user=www-data
numprocs=4

Next Steps