Introduction

A job is a PHP class that encapsulates one unit of background work. Each job should do one thing — process this podcast, send this email, generate this report — so it's easy to retry, monitor, and reason about in isolation.

Key Concepts

  • ShouldQueue interface: Marks a job as queueable. Without it, dispatches run synchronously.
  • SerializesModels trait: Serializes Eloquent models by ID and reloads them fresh when the worker runs.
  • handle() method: Where the actual work lives. Supports dependency injection via the service container.
  • Constructor: Where you accept the data the job needs (models, scalars, arrays).

Real World Context

When a user uploads a podcast, the controller should return "uploaded!" in milliseconds — not wait for audio transcoding. A ProcessPodcast job lets the controller push the work to a queue and return immediately. The worker picks up the job a moment later, transcodes the audio, and updates the database.

Deep Dive

Generating Jobs

bash
php artisan make:job ProcessPodcast

This creates app/Jobs/ProcessPodcast.php:

php
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class ProcessPodcast implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Create a new job instance.
     */
    public function __construct(
        public Podcast $podcast
    ) {}

    /**
     * Execute the job.
     */
    public function handle(): void
    {
        // Process the podcast...
    }
}

Job Anatomy

Traits Explained

TraitPurpose
DispatchableEnables ::dispatch() method
InteractsWithQueueAccess to queue methods ($this->delete(), etc.)
QueueableQueue, connection, and delay configuration
SerializesModelsEloquent models are serialized and reloaded

ShouldQueue Interface

php
// With ShouldQueue: Job is queued
class SendEmail implements ShouldQueue { }

// Without ShouldQueue: Job runs immediately (synchronously)
class SendEmail { }

Passing Data to Jobs

php
<?php

namespace App\Jobs;

use App\Models\User;
use App\Models\Report;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class GenerateReport implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        public User $user,
        public string $reportType,
        public array $options = []
    ) {}

    public function handle(): void
    {
        $report = Report::generate($this->reportType, $this->options);

        $this->user->notify(new ReportReady($report));
    }
}

Model Serialization

Eloquent models are serialized by ID and reloaded:

php
// When dispatching
GenerateReport::dispatch($user, 'sales');
// User is serialized as: {"class": "App\\Models\\User", "id": 123}

// When processing
// User is fetched fresh: User::find(123)

Warning: If the model is deleted before processing, the job will fail.

The handle() Method

The handle() method contains your job logic:

php
public function handle(
    AudioProcessor $processor,  // Dependency injection works!
    Storage $storage
): void {
    $processedFile = $processor->process($this->podcast->audio_path);

    $storage->put(
        "podcasts/{$this->podcast->id}/processed.mp3",
        $processedFile
    );

    $this->podcast->update(['processed' => true]);
}

Complete Job Example

php
<?php

namespace App\Jobs;

use App\Models\Order;
use App\Services\PaymentGateway;
use App\Notifications\OrderConfirmation;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;

class ProcessOrder implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Number of retry attempts.
     */
    public int $tries = 3;

    /**
     * Timeout in seconds.
     */
    public int $timeout = 120;

    public function __construct(
        public Order $order
    ) {}

    public function handle(PaymentGateway $gateway): void
    {
        Log::info('Processing order', ['order_id' => $this->order->id]);

        // Charge the customer
        $charge = $gateway->charge(
            $this->order->user,
            $this->order->total
        );

        // Update order status
        $this->order->update([
            'status' => 'paid',
            'charge_id' => $charge->id,
        ]);

        // Send confirmation
        $this->order->user->notify(new OrderConfirmation($this->order));
    }

    /**
     * Handle job failure.
     */
    public function failed(?Throwable $exception): void
    {
        Log::error('Order processing failed', [
            'order_id' => $this->order->id,
            'error' => $exception->getMessage(),
        ]);

        $this->order->update(['status' => 'failed']);
    }
}

Common Pitfalls

  1. Passing entire collections to the constructor — SerializesModels serializes each model by ID, but a raw collection or query result gets serialized in full. Pass IDs or single models, not collections.
  2. Putting logic in the constructor — the constructor runs in the dispatching process, the handle() method runs in the worker. Heavy work in the constructor blocks the HTTP response.

Best Practices

  1. Type-hint dependencies in handle(), not the constructor — the constructor serializes its arguments, so you can't inject services there. Services go in handle() where Laravel's container resolves them on the worker.
  2. One job, one responsibility — a ProcessOrder job that charges, emails, and updates analytics is three retries tangled together. Split it into three small jobs and chain them.

Summary

  • Jobs implement ShouldQueue to run in the background.
  • The constructor accepts data; handle() does the work.
  • SerializesModels handles Eloquent serialization automatically.
  • Dependency injection works in handle() via the service container.
✓ Completed