Events & Listeners
๐ก Events & Listeners
Event and Listener classes instead of a @receiver-decorated function, and (in modern Laravel) auto-discovers listeners with no manual wiring step at all.
Creating an Event and a Listener
php artisan make:event OrderPlaced php artisan make:listener SendOrderConfirmation --event=OrderPlaced
// app/Events/OrderPlaced.php namespace App\Events; use App\Models\Order; use Illuminate\Foundation\Events\Dispatchable; class OrderPlaced { use Dispatchable; public function __construct( public Order $order // a plain, typed data-carrying property ) {} }
Compare this to Django's post_save receiver, which gets loose sender/instance/created keyword arguments โ Laravel's event is a genuine typed object, with exactly the data it declares and nothing implicit:
// app/Listeners/SendOrderConfirmation.php namespace App\Listeners; use App\Events\OrderPlaced; use Illuminate\Support\Facades\Mail; class SendOrderConfirmation { public function handle(OrderPlaced $event): void { Mail::to($event->order->customer_email)->send( new \App\Mail\OrderConfirmation($event->order) ); } }
๐ Dispatching: Application-Triggered, Not Automatic
Here's a real difference from Django's post_save: a Laravel event fires only where you explicitly dispatch it โ not automatically on every model save:
// app/Http/Controllers/OrderController.php use App\Events\OrderPlaced; public function store(StoreOrderRequest $request) { $order = Order::create($request->validated()); OrderPlaced::dispatch($order); // fires exactly here, and nowhere else return redirect()->route('orders.show', $order); }
Django's Automatic Signal vs Laravel's Manual Dispatch
Django
@receiver(post_save, sender=Order) def on_order_saved(sender, instance, created, **kwargs): if created: # fires automatically, EVERY time an Order is saved โ # including from the admin, a script, a migration...
Laravel
OrderPlaced::dispatch($order); // fires ONLY at this exact line โ a script or Tinker // session creating an Order directly never triggers it
Laravel does have model events (creating, created, saving, saved) that fire automatically on Eloquent lifecycle actions โ those are the closer parallel to post_save, and carry the exact same "fires on every save, no exceptions" trade-off Django's signals do (see this chapter's gotcha).
Registering Listeners: Auto-Discovery
Modern Laravel auto-discovers listeners in app/Listeners โ no manual registration step, unlike Django's apps.py ready() import requirement from the Django course:
Auto-Discovery (Laravel 11+)
A listener's handle() method type-hint tells Laravel which event it's for โ placing the class in app/Listeners is enough; nothing extra to wire up.
Manual Registration (Older Laravel)
Pre-11 Laravel required listing the event/listener pair in EventServiceProvider's $listen array โ the same "forgot to register it" trap as Django's apps.py gotcha.
โณ Queued Listeners
The cleanest built-in integration point in this course so far โ implementing one interface moves a listener onto the queue automatically:
Making a Listener Non-Blocking
namespace App\Listeners; use App\Events\OrderPlaced; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Support\Facades\Mail; class SendOrderConfirmation implements ShouldQueue { public function handle(OrderPlaced $event): void { Mail::to($event->order->customer_email)->send( new \App\Mail\OrderConfirmation($event->order) ); } } // implements ShouldQueue โ that's the ENTIRE change. No .delay() call, // no separate task file โ the SAME class runs on the queue instead of // synchronously, unlike the Django course's Celery chapter, which required // the signal receiver to explicitly call a separate task's .delay() method.
๐ป Coding Challenges
Challenge 1: Create an Event and Listener Pair
Generate a UserRegistered event carrying a User, and a SendWelcomeEmail listener that sends a welcome email, then dispatch it from a registration controller.
Goal: Practice the full generate-event, generate-listener, dispatch workflow.
Challenge 2: Make a Listener Queued
Take the SendWelcomeEmail listener from Challenge 1 and make it non-blocking by implementing ShouldQueue, and explain in one sentence what changes about when the email actually gets sent.
Goal: Practice the one-interface change that moves a listener onto the queue.
Challenge 3: Spot the Model Event Trap
Given an Eloquent model with a static::created() model event hook that sends a real welcome email, explain what happens when a test suite or a database seeder creates instances of that model, and propose a fix.
Goal: Practice recognizing the gotcha around automatic Eloquent model events firing in unintended contexts.
A model event hook (static::created(), or a creating/saving listener) fires on every save that model ever goes through โ a factory in a test, a php artisan db:seed run populating fake data, a Tinker session, all trigger it exactly the same as a real user action would. This is the same trade-off Django's post_save signal carries, and it's a real risk: a model event that sends an actual email or charges a payment can fire unexpectedly while seeding a development database. A manually-dispatched custom event (OrderPlaced::dispatch($order)) doesn't have this problem โ it only ever fires exactly where you call it โ which is one real argument for preferring explicit dispatch over an automatic model event for anything with a real-world side effect.
๐ฏ What's Next
Queued listeners got a preview of running work outside the request cycle โ the next chapter goes deeper: Queues, Laravel's queue system in full, compared to the Django course's Celery chapter.