1. Queues & Jobs
Some work is too slow to make a user wait for — sending an email, generating a report, calling a slow third-party API. A job represents that work as a class; dispatching it queues the work for later instead of running it inline:
php artisan make:job SendTaskDueSoonEmail
<?php
namespace App\Jobs;
use App\Models\Task;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Mail;
class SendTaskDueSoonEmail implements ShouldQueue
{
use Dispatchable, Queueable;
public function __construct(public Task $task) {}
public function handle(): void
{
Mail::to($this->task->user->email)->send(new \App\Mail\TaskDueSoon($this->task));
}
}
<?php
use App\Jobs\SendTaskDueSoonEmail;
SendTaskDueSoonEmail::dispatch($task);
// Returns instantly -- the actual email send happens later, off the request
ShouldQueue is what makes dispatch() push the job onto a
queue instead of running handle() immediately — the controller
returns its response to the user right away, without waiting for the email to
actually send.
2. Running a Worker
A queued job sits in storage (database, Redis, etc., configured in .env)
until a separate worker process picks it up and runs it:
# .env
QUEUE_CONNECTION=database
php artisan queue:table # generates the jobs table migration
php artisan migrate
php artisan queue:work # runs continuously, processing jobs as they arrive
In production, queue:work runs as a supervised, long-lived process
(via Supervisor or similar — covered in Week 28's deployment lesson), separate from
the web server entirely. If a job throws an exception, Laravel retries it a
configurable number of times, then moves it to a failed_jobs table for
inspection rather than losing it silently.
3. Events & Listeners
An event announces that something happened; a listener reacts to it. This decouples "a task was completed" from "here's everything that should happen as a result" — new reactions can be added without touching the code that completes the task:
php artisan make:event TaskCompleted
php artisan make:listener SendCompletionNotification --event=TaskCompleted
<?php
namespace App\Events;
use App\Models\Task;
class TaskCompleted
{
public function __construct(public Task $task) {}
}
<?php
namespace App\Listeners;
use App\Events\TaskCompleted;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendCompletionNotification implements ShouldQueue // runs on the queue too
{
public function handle(TaskCompleted $event): void
{
$event->task->user->notify(new \App\Notifications\TaskCompletedNotification($event->task));
}
}
<?php
use App\Events\TaskCompleted;
public function update(UpdateTaskRequest $request, Task $task)
{
$task->update($request->validated());
if ($task->is_done) {
TaskCompleted::dispatch($task);
}
return redirect()->route('tasks.index');
}
The controller only knows "a task was completed" — it has no idea what happens
next, and doesn't need to. A second listener (a Slack notification, an analytics
event) can be added later by registering it against the same
TaskCompleted event, with zero changes to TaskController.
4. Notifications
A notification is a message deliverable through multiple channels (email, database, Slack, SMS) from one class:
php artisan make:notification TaskCompletedNotification
<?php
namespace App\Notifications;
use App\Models\Task;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
class TaskCompletedNotification extends Notification
{
public function __construct(public Task $task) {}
public function via($notifiable): array
{
return ['mail', 'database']; // both an email and an in-app record
}
public function toMail($notifiable): MailMessage
{
return (new MailMessage)
->subject('Task completed!')
->line("You finished: {$this->task->title}");
}
public function toArray($notifiable): array
{
return ['task_id' => $this->task->id, 'title' => $this->task->title];
}
}
via() returning both 'mail' and 'database'
means one ->notify() call sends an email and stores an
in-app notification record, retrievable later via
$user->notifications — a single declaration covering both delivery
paths.
5. The Module Capstone
This week's exercise closes out Module 2. The target: a deployed, multi-feature task-tracker Laravel application, combining every piece from Weeks 9–16 into one coherent project.
✓ Breeze authentication (Week 13)
✓ Tasks with tags, migrations & Eloquent relationships (Week 11)
✓ Eager-loaded, scoped queries -- no N+1 (Week 12)
✓ Policy-enforced ownership on every task action (Week 13)
✓ File attachments via the Storage facade (Week 14)
✓ A versioned, Sanctum-secured JSON API with API Resources (Week 15)
✓ A Pest test suite covering auth, CRUD & ownership (Week 15)
✓ A queued job + event/notification for due-soon tasks (this week)
6. Module Capstone Exercise
Finish the task tracker as a deployed, tested Laravel app
Everything from Weeks 9–16, working together in one application.
Requirements:
- Confirm every item on the capstone checklist above is implemented and working.
- Add a
SendTaskDueSoonEmailjob, dispatched from a newphp artisan tasks:notify-due-soonArtisan command that finds tasks due within 24 hours and queues a notification for each. - Add a
TaskCompletedevent, dispatched when a task'sis_doneflips totrue, with a queued listener sending aTaskCompletedNotificationvia mail and database channels. - Set up a real queue worker locally (
QUEUE_CONNECTION=database, runphp artisan queue:work) and confirm jobs actually process, including at least one deliberately-failed job visible infailed_jobs. - Run the full Pest suite from Week 15 and confirm everything still passes after this week's additions.
Use Mail::fake() and Notification::fake() in Pest to assert a job or event actually queued the right mail/notification, without sending real email during test runs — Laravel's testing helpers exist specifically so async side effects stay verifiable.
7. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
What does ShouldQueue change about how SendTaskDueSoonEmail::dispatch($task) behaves?
What does ShouldQueue change about how SendTaskDueSoonEmail::dispatch($task) behaves?
Without ShouldQueue, dispatching a job runs its handle() method synchronously, right there in the request — the user waits for it to finish. With ShouldQueue, dispatching only pushes the job onto a queue and returns immediately; a separate worker process picks it up and runs handle() independently, off the request entirely.
Q2
Why dispatch a TaskCompleted event from the controller instead of calling the notification code directly?
Why dispatch a TaskCompleted event from the controller instead of calling the notification code directly?
It decouples "a task was completed" from "everything that should happen as a result." The controller stays focused on its actual job (updating the task) and has no knowledge of — or dependency on — how many listeners react to the event. A second listener can be registered against the same event later with zero changes to the controller, which wouldn't be true if the notification call were hardcoded directly into update().
Q3
What happens to a queued job that fails every one of its configured retry attempts?
What happens to a queued job that fails every one of its configured retry attempts?
It's moved into the failed_jobs table rather than being silently discarded — the job's data and the exception that caused the failure are both recorded, so a developer can inspect what went wrong and, if appropriate, manually retry it later with php artisan queue:retry. This is what stops a queue failure from becoming an invisible data-loss bug.
Q4
Why does via() returning ['mail', 'database'] only require writing one ->notify() call at the point of use?
Why does via() returning ['mail', 'database'] only require writing one ->notify() call at the point of use?
The notification class itself declares every delivery channel it should go through, along with a corresponding to*() method (toMail(), toArray() for the database channel) describing how the message looks on each one. Calling $user->notify(new TaskCompletedNotification($task)) triggers all of them together — the caller doesn't need to know or care how many channels are involved.