Events, listeners and notifications
Domain events decoupling a workflow, model observers, queued listeners, and notifications across mail, database and broadcast channels.
Events and listeners
class OrderPlaced
{
use Dispatchable, SerializesModels;
public function __construct(public Order $order) {}
}
class ReserveStock implements ShouldQueue
{
public function handle(OrderPlaced $event): void
{
$event->order->items->each(fn ($item) => $item->reserve());
}
}
// dispatch after the transaction commits
DB::transaction(function () use ($order) {
$order->save();
OrderPlaced::dispatch($order)->afterCommit();
});- Queued listeners run outside the request, so dispatch
afterCommit()- otherwise a listener can act on a row that a later rollback erases. - Auto-discovery scans
app/Listeners; register explicit mappings inAppServiceProviderwhen the wiring is not obvious. - An event with several listeners is a fan-out, not a workflow. If the steps must be ordered, use a chain of jobs instead.
💡
Model observers fire on every save, including bulk operations through Eloquent. They are convenient for audit fields, and dangerous for anything with side effects that a mass update would multiply by a thousand rows.
Observers
class BookObserver
{
public function creating(Book $book): void
{
$book->slug ??= Str::slug($book->title);
$book->created_by ??= auth()->id();
}
public function updated(Book $book): void
{
if ($book->wasChanged('status')) {
Cache::forget("book:{$book->id}");
}
}
public function deleted(Book $book): void
{
$book->cover?->delete();
}
}
#[ObservedBy(BookObserver::class)]
class Book extends Model {}
// bulk updates bypass observers entirely
Book::where('status', 'draft')->update(['status' => 'archived']);| Hook | Fires | Typical use |
|---|---|---|
creating / created | Before/after the insert | Defaults, slugs, external ids |
updating / updated | Around an update | Cache invalidation, audit trail |
deleting / deleted | Around a delete | Cleaning up files and children |
restored | After a soft-delete restore | Re-indexing, notifications |
Notifications and broadcasts
class OrderShipped extends Notification implements ShouldQueue
{
public function __construct(public Order $order) {}
public function via(object $notifiable): array
{
return $notifiable->wantsSms()
? ['mail', 'database', 'vonage']
: ['mail', 'database'];
}
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject('Your order shipped')
->line("Tracking number: {$this->order->tracking}")
->action('Track it', url("/orders/{$this->order->id}"));
}
public function toDatabase(object $notifiable): array
{
return ['order_id' => $this->order->id, 'status' => 'shipped'];
}
}
$user->notify(new OrderShipped($order));
Notification::send($users, new OrderShipped($order));- The
databasechannel needs thenotificationstable:php artisan make:notifications-tablethen migrate. - Broadcast channels need driver configuration plus authentication on your private channels; publish to
PrivateChanneland authorize it inroutes/channels.php. - Queue notifications (
ShouldQueue) or the mail driver becomes part of the request latency.
FAQ
Events or direct method calls?
Direct calls when the caller genuinely owns the next step. Events when several unrelated concerns react to a fact - and be honest that the indirection costs discoverability. One level of events with a small number of listeners is usually the right amount.
Why did my listener not run?
Check that it is queued and a worker is running, that it fired inside a transaction with an
afterCommit dispatch, and that the class is discovered in app/Listeners. Then run php artisan event:list to see what the application actually registered.Related
Queues, jobs and scheduled tasks The service container, providers and facades
Last refreshed 2026-09-18.