Events keep controllers focused while letting features like notifications, audit logs and analytics plug in.
An event is a plain class describing something that happened, such as OrderShipped. Listeners react to it. This decouples the code that causes an action from everything that should follow: the shipping code doesn't need to know about emails, analytics or webhooks.
Create them with php artisan make:event OrderShipped and php artisan make:listener SendShipmentEmail --event=OrderShipped. Laravel discovers listeners automatically: any listener in app/Listeners whose handle method type-hints an event is registered for it. You can also register explicitly with Event::listen() in a service provider.
Fire an event with OrderShipped::dispatch($order) or event(new OrderShipped($order)).
- A listener that implements
ShouldQueueruns on the queue, so slow side effects don't delay the response. It supports the same options as jobs (tries, backoff, queue name). - Implement
ShouldDispatchAfterCommiton the event (orShouldQueueAfterCommiton a listener) so nothing reacts to data that might be rolled back.
Model events fire automatically: creating, created, updating, updated, saving, saved, deleting, deleted and more. Group handlers in an observer (php artisan make:observer PostObserver --model=Post) and attach it with the #[ObservedBy(PostObserver::class)] attribute on the model.
Be careful: model events don't fire for mass updates like Post::where(...)->update([...]), because no models are loaded. Overusing events makes control flow hard to follow, so keep them for genuine side effects.
Example
<?php
class OrderShipped implements ShouldDispatchAfterCommit
{
use Dispatchable, SerializesModels;
public function __construct(public Order $order) {}
}
// Listener: auto-discovered from the handle() type-hint
class SendShipmentEmail implements ShouldQueue
{
public function handle(OrderShipped $e): void { Mail::to($e->order->customer)->send(new ShipmentMail($e->order)); }
}
#[ObservedBy(OrderObserver::class)]
class Order extends Model {}
class OrderObserver
{
public function updated(Order $order): void
{
if ($order->wasChanged('status')) AuditLog::record($order, 'status'); // audit trail
}
}
DB::transaction(function () use ($order) {
$order->update(['status' => 'shipped']);
OrderShipped::dispatch($order); // delivered only after commit
});Remember
- Events describe what happened; listeners decide what follows
- Queued listeners keep side effects off the request path
- Observers handle model lifecycle events, but mass updates skip them
Watch out for
- Relying on an observer's updated hook while using query-builder mass updates, which fire no model events.