
September 07, 2026
14 min read
By Kokil Thapa | Last reviewed: September 2026
Laravel Events and Listeners Real Use Cases matter the moment a controller starts doing too much: sending mail, writing audit rows, calling payment APIs, and updating search indexes in one method. That pattern works in a tutorial, but on a production Laravel enterprise application it becomes brittle—every new requirement edits the same action, tests get slow, and deployments feel risky. Events and listeners give you a clean contract: the core business action fires one event; independent listeners handle side effects. This guide walks through patterns I use on live Laravel 12 and 13 projects—PHP 8.3+, queued listeners, and decisions about when not to reach for events at all.
What are Laravel events and listeners, and when should you use them?
An event is a plain PHP class—often a DTO carrying the data listeners need. A listener is a class with a handle() method that reacts to that event. Laravel's event dispatcher wires them together through EventServiceProvider or auto-discovery.
Use events when:
- One action triggers multiple independent reactions (email + SMS + analytics + webhook).
- Reactions may be added later without touching the original controller or service.
- You want to queue slow I/O (mail, HTTP calls, PDF generation) away from the request cycle.
- Third-party packages already emit events you can hook into (
Illuminate\Auth\Events\Registered, Cashier webhooks, etc.).
Skip events when the logic is a single, synchronous step tightly bound to the transaction—use a service method or an Eloquent observer instead. I've seen teams create events for every model save; that adds indirection without benefit. Save events for business milestones: order paid, booking confirmed, document approved.
Laravel 13.x (PHP 8.3 minimum) and Laravel 12 (PHP 8.2+) share the same event API. If you are still on Laravel 11, note it reached end of life in March 2026—plan an upgrade using a staged migration approach described in our Laravel 12 migration guide.
How do you register Laravel events and listeners in a production app?
Start with explicit registration while the team learns the pattern; switch to auto-discovery once conventions are stable.
Step 1: Create the event and listeners
php artisan make:event Orders/OrderPlaced
php artisan make:listener Orders/SendOrderReceipt --event=Orders/OrderPlaced
php artisan make:listener Orders/NotifyFulfillmentTeam --event=Orders/OrderPlaced --queued Your event carries the domain object—not raw request input:
<?php
namespace App\Events\Orders;
use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderPlaced
{
use Dispatchable, SerializesModels;
public function __construct(public Order $order) {}
} Step 2: Register in EventServiceProvider
protected $listen = [
\App\Events\Orders\OrderPlaced::class => [
\App\Listeners\Orders\SendOrderReceipt::class,
\App\Listeners\Orders\NotifyFulfillmentTeam::class,
\App\Listeners\Orders\PushOrderToAnalytics::class,
],
]; Laravel 11+ moved provider configuration into bootstrap/app.php, but EventServiceProvider still works when you register it. Alternatively, enable listener discovery:
public function shouldDiscoverEvents(): bool
{
return true;
} Discovery scans app/Listeners and maps listeners whose type-hinted handle() parameter matches the event class. That keeps registration DRY on larger codebases aligned with modern Laravel architecture.
Step 3: Dispatch from a service, not a fat controller
<?php
namespace App\Services;
use App\Events\Orders\OrderPlaced;
use App\Models\Order;
use Illuminate\Support\Facades\DB;
class CheckoutService
{
public function placeOrder(array $cartData): Order
{
return DB::transaction(function () use ($cartData) {
$order = Order::create([/* validated data */]);
$order->items()->createMany($cartData['items']);
OrderPlaced::dispatch($order);
return $order;
});
}
} Dispatch inside the transaction when listeners must see committed data, or after commit when listeners call external systems that should not run if the DB rolls back. Laravel provides DB::afterCommit() wrapping:
OrderPlaced::dispatch($order)->afterCommit(); On payment integrations—Stripe, Khalti, eSewa—I've burned hours debugging listeners that fired before the transaction committed. Default to afterCommit() for anything that sends money confirmations or external webhooks. Our Khalti integration guide covers callback timing in more detail.
What are real Laravel events and listeners use cases from production apps?
These patterns come from eCommerce, booking, and legal-tech portals I've shipped—not abstract demos.
1. Order placed: email, inventory, and analytics
On a Laravel eCommerce build similar to Nepal Gift Card, checkout dispatches OrderPlaced. Listeners handle:
- Customer receipt email — queued; uses the order's eager-loaded relations.
- Inventory decrement — synchronous inside the same request if stock must be reserved immediately.
- Analytics pixel / server-side event — queued; failures should not block checkout.
<?php
namespace App\Listeners\Orders;
use App\Events\Orders\OrderPlaced;
use App\Mail\OrderReceipt;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Mail;
class SendOrderReceipt implements ShouldQueue
{
public int $tries = 3;
public function handle(OrderPlaced $event): void
{
Mail::to($event->order->customer_email)
->send(new OrderReceipt($event->order));
}
} 2. Booking confirmed: SMS, calendar, and supplier notification
Trekking and appointment systems—like booking flows on Adventure Third Pole Trek—fire BookingConfirmed after payment verification. Listeners send SMS via a gateway, email the itinerary PDF, and POST to a supplier's webhook. Each channel is isolated; disabling SMS for a staging environment means skipping one listener, not commenting out controller code.
3. Document uploaded: virus scan, thumbnail, and audit trail
Legal-tech client portals need an immutable audit log when a user uploads a contract or affidavit. On portals in the same family as Mijar Law Associates, DocumentUploaded triggers:
- A queued listener that pushes the file to a scanning service.
- A listener that generates a preview thumbnail via Spatie Media Library conversions.
- A synchronous listener that writes an
audit_logsrow with user ID, IP, and document hash.
Audit logging stays synchronous so it exists before the user navigates away. Scanning can lag seconds without UX impact.
4. User registered: welcome email and CRM sync
Laravel ships Registered out of the box. Hook your own listener alongside SendEmailVerificationNotification:
use Illuminate\Auth\Events\Registered;
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
SyncUserToCrm::class,
AssignDefaultRole::class,
],
]; Keep CRM sync queued. A slow HubSpot or Zoho API must never delay registration response time.
5. Payment captured: ledger entry and fulfilment
Payment gateways return asynchronously. When your webhook controller verifies a Khalti or Stripe payload and marks an order paid, dispatch PaymentCaptured rather than duplicating fulfilment logic in the webhook and the admin panel. One event, one set of listeners—critical for Laravel payment integrations where the same order can be confirmed from multiple entry points.
6. Lead form submitted: notifications and spam scoring
Law-firm lead capture on sites like Court Marriage In Nepal dispatches LeadSubmitted. Listeners notify staff on Slack or email, persist UTM parameters, and optionally call a spam-scoring API. The form controller validates and creates the lead—nothing else.
How should you queue Laravel listeners for slow side effects?
Implement Illuminate\Contracts\Queue\ShouldQueue on any listener that touches the network, filesystem, or third-party APIs. Your queue driver in production should be Redis 8.x (or database queue for low-traffic sites).
<?php
namespace App\Listeners\Orders;
use App\Events\Orders\OrderPlaced;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class PushOrderToAnalytics implements ShouldQueue
{
use InteractsWithQueue;
public string $queue = 'analytics';
public int $tries = 5;
public array $backoff = [30, 60, 120];
public function handle(OrderPlaced $event): void
{
/* HTTP POST to analytics endpoint */
}
public function failed(OrderPlaced $event, \Throwable $e): void
{
report($e);
/* optional: dead-letter notification */
}
} Run dedicated workers per queue so a stuck analytics job does not block receipt emails:
php artisan queue:work redis --queue=default,mail,analytics --tries=3 Serializing Eloquent models in queued listeners uses SerializesModels. Laravel re-fetches the model when the job runs—if it was deleted, the job is discarded. Pass IDs explicitly when you need custom handling:
public function __construct(public int $orderId) {}
public function handle(): void
{
$order = Order::findOrFail($this->orderId);
} For long-running listener chains, consider whether you actually need event sourcing—most CRUD apps do not. Events plus queues cover 90% of decoupling needs without an event store.
Events vs observers vs jobs: which pattern fits your Laravel app?
Teams often confuse four overlapping tools. Here is how I choose on production codebases.
| Pattern | Best for | Trigger | Multiple handlers? | Queue built-in? |
|---|---|---|---|---|
| Events + Listeners | Business milestones with several independent reactions | Explicit dispatch() | Yes | Per-listener via ShouldQueue |
| Eloquent Observers | Model lifecycle hooks (creating, saving, deleted) | Automatic on model events | One observer class per model | Observer can implement ShouldQueue (Laravel 11+) |
| Jobs | Single background task with clear input/output | Explicit dispatch() | No (one job, one handle) | Yes, by design |
| Notifications | User-facing alerts across mail/SMS/Slack/database | $user->notify() | Channels within one notification | Via ShouldQueue |
Rule of thumb: if the trigger is "user completed checkout", use an event. If the trigger is "every time updated_at changes on Article", use an observer. If the trigger is "generate this one PDF tonight", dispatch a job. Notifications sit alongside listeners—you can call $order->user->notify(new OrderShipped($order)) from inside a listener.
Testing events and listeners
Use Event::fake() in feature tests to assert an event was dispatched without running listeners:
Event::fake([OrderPlaced::class]);
$this->post('/checkout', $payload);
Event::assertDispatched(OrderPlaced::class, function ($event) {
return $event->order->total === 1500;
}); Test listeners in isolation by instantiating the event with a factory-built model and calling handle() directly. That keeps unit tests fast compared to full HTTP cycles. When building Laravel API endpoints that dispatch events, fake events in API tests the same way.
Subscriber classes for cross-cutting concerns
When one class listens to many events—logging every auth event, for example—use an event subscriber:
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;
use Illuminate\Events\Dispatcher;
class AuthActivitySubscriber
{
public function handleLogin(Login $event): void { /* log */ }
public function handleLogout(Logout $event): void { /* log */ }
public function subscribe(Dispatcher $events): array
{
return [
Login::class => 'handleLogin',
Logout::class => 'handleLogout',
];
}
} Register subscribers in EventServiceProvider::$subscribe. Subscribers reduce mapping noise when you have a dozen auth or billing events.
Broadcasting and real-time UI
Events can implement ShouldBroadcast to push updates over websockets. That overlaps with dedicated broadcasting setup—see Laravel Broadcasting with Reverb and real-time features with websockets and Redis. Keep domain events separate from broadcast events when payloads differ; a listener can dispatch a broadcast-specific event if needed.
Common production mistakes
- Fat listeners — a listener that grows to 200 lines should become a service called by a thin listener.
- Recursive events — a listener that updates a model whose observer dispatches the same event causes infinite loops. Use
$model->saveQuietly()or guard with a flag. - Missing queue workers — queued listeners silently pile up in Redis if Supervisor is not running. Monitor queue depth after deploy.
- Passing huge payloads — serialize IDs, not entire object graphs. Use the JSON formatter tool to inspect queue payload size during debugging.
- Synchronous external calls — calling payment verify or SMS APIs in a non-queued listener adds seconds to every request.
Authorization still belongs in policies and gates—not listeners. Listeners assume the action already passed validation and auth checks. See our Laravel policies and gates guide for where those checks live.
For SEO-heavy Laravel sites, sitemap regeneration and cache busting also fit the event model: dispatch ArticlePublished and let a listener ping your search index and warm CDN caches—details in SEO for Laravel sites.
If you expose events to external consumers, pair internal domain events with a documented public API rather than leaking Eloquent models across boundaries—patterns covered in building RESTful APIs with Laravel and API development services.
On shared hosting without Redis, database queues work for low-volume mail listeners. Move to Redis 8.x and dedicated workers once daily order volume or document uploads exceed what a single cron-driven worker can drain in a minute. Linux server administration and proper Supervisor config matter as much as the PHP code.
Need structured logging of every dispatched event during local development? Temporarily register a wildcard listener in AppServiceProvider that logs class names—remove it before production. For payload inspection, the regex tester helps parse log lines when debugging listener failures.
Complex read models and audit history may eventually push you toward CQRS or event sourcing, but that is a separate architectural step—start with simple events and add complexity only when replay or temporal queries become a business requirement. Our CQRS pattern guide explains that threshold.
Official references: the Laravel events documentation covers registration and discovery, and the queues documentation explains ShouldQueue, retries, and failed-job handling. For PHP version requirements on Laravel 13, see the Laravel 13 release notes.
Key Takeaways
- Fire domain events at business milestones—order placed, booking confirmed, document uploaded—not at every Eloquent save.
- Queue any listener that sends mail, calls HTTP APIs, or generates files; use
afterCommit()when listeners depend on committed DB state. - Keep controllers and services thin: validate, persist, dispatch one event, return the response.
- Choose observers for model lifecycle hooks, jobs for single background tasks, and events when multiple independent reactions are needed.
- Test with
Event::fake()at the feature level and directhandle()calls at the unit level. - Monitor queue workers in production—queued listeners do nothing useful if Redis jobs sit unprocessed.
People Also Ask
Can Laravel listeners run synchronously and asynchronously in the same app?
Yes. Each listener class decides independently. Implement ShouldQueue on slow listeners; leave fast, critical listeners synchronous. Laravel runs synchronous listeners during the dispatch call and pushes queued ones to your configured driver.
What is the difference between Laravel events and webhooks?
Events are internal application signals. Webhooks are outbound HTTP calls to external systems. A listener often sends a webhook, but the event itself stays inside your Laravel app boundary.
Should I use Laravel events for logging and auditing?
For significant business actions, yes—an audit listener writing to an audit_logs table keeps a clear trail. For debug logging during development, use Laravel's log facade directly; not every log line needs an event.
Do Laravel packages use events I can listen to?
Many do. Cashier fires subscription events, Passport emits token events, and the framework itself exposes auth, mail, and notification hooks. Check the package docs for event class names before writing custom hooks.
Ship decoupled Laravel apps with confidence
Laravel Events and Listeners Real Use Cases come down to one habit: finish the core transaction, dispatch one well-named event, and let focused listeners handle everything else. That structure scales from a law-firm lead form to a multi-currency gift-card checkout without rewriting checkout every time marketing asks for another integration. If your controllers are already crowded with mail and API calls, refactoring to events is one of the highest-return architecture changes you can make on an existing Laravel 12 or 13 codebase.
Need help untangling a monolithic controller or wiring queued listeners on production infrastructure? Contact us to discuss your project, or browse the portfolio for examples of Laravel systems already running this pattern in Nepal and abroad. For ongoing work after launch, see support and maintenance options and related posts on the blog, including why Laravel fits Nepali businesses and about the developer behind these builds.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

