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
ShouldQueueinterface: Marks a job as queueable. Without it, dispatches run synchronously.SerializesModelstrait: 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
bashphp 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
| Trait | Purpose |
|---|---|
Dispatchable | Enables ::dispatch() method |
InteractsWithQueue | Access to queue methods ($this->delete(), etc.) |
Queueable | Queue, connection, and delay configuration |
SerializesModels | Eloquent 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:
phppublic 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
- Passing entire collections to the constructor —
SerializesModelsserializes each model by ID, but a raw collection or query result gets serialized in full. Pass IDs or single models, not collections. - 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
- Type-hint dependencies in
handle(), not the constructor — the constructor serializes its arguments, so you can't inject services there. Services go inhandle()where Laravel's container resolves them on the worker. - One job, one responsibility — a
ProcessOrderjob that charges, emails, and updates analytics is three retries tangled together. Split it into three small jobs and chain them.
Summary
- Jobs implement
ShouldQueueto run in the background. - The constructor accepts data;
handle()does the work. SerializesModelshandles Eloquent serialization automatically.- Dependency injection works in
handle()via the service container.