Listeners and Events in Laravel

Video thumbnail

Listeners and events in Laravel are one of those tools that, when you understand them well, completely change the way you structure an application. Not only do they make the code cleaner, but they also allow you to decouple logic, reuse behaviors, and avoid controllers bloated with responsibilities.

Laravel Events provide a simple implementation of the Observer design pattern, allowing you to subscribe to and listen for various actions occurring in your application. It's the ultimate tool for logic decoupling, enabling a primary action (such as user registration) to trigger multiple secondary tasks (such as sending an email, creating a profile, or notifying Slack) without overloading the original controller.

In this article, I'm going to explain what they are, how they really work, what changed from Laravel 11 onwards, and most importantly: when it makes sense to use them, with practical examples and a real-life case I encountered while working with authentication and sessions.

What are events and listeners in Laravel?

Laravel natively implements the observer pattern, which means that one part of the system can "notify" that something happened, and other parts can react to it without being directly coupled.

Event: what it represents and when it's fired

An event is simply a notification that something happened in the application.
Typical examples:

  • A user registered
  • A user logged in
  • An order was created
  • An important model was updated

The event contains no logic, only data related to what happened.

Listener: what it does and why it's key

A listener is the class that reacts to the event and executes a specific action:

  • Send an email
  • Send a notification
  • Update stock
  • Log information
  • Synchronize data

The key is that the event doesn't know who is listening, and the listener doesn't need to know where the event was fired.

Observer Pattern Applied in Laravel

Thanks to this pattern:

  • The code remains decoupled
  • You can add or remove behaviors without touching the main logic
  • A single event can have many listeners

This is exactly what Laravel does internally with authentication, registration, email verification, etc.

What are event listeners for in Laravel?

Listeners are used to move secondary logic out of the main application flow.

Decoupling business logic

Instead of doing this in a controller:

 
// save user // send email // notify admin // write to logs 

You can simply:

 
event(new UserRegistered($user));

And let each listener do its job.

Executing multiple actions from a single event

An event can trigger:

  • A listener that sends an email
  • Another that notifies the administrator
  • Another that updates metrics

All independent of each other.

Typical use cases

  • Welcome emails
  • Notifications (Slack, SMS, push)
  • Logging
  • Background processes
  • Data synchronization
  • Internal automations

How events and listeners work in Laravel (full flow)

In the Laravel ecosystem, the workflow is divided into two critical components:

  • Event: This class acts as a data container. It is triggered when something happens in the application.
  • Listen: This class contains the logic that should be executed when the event is triggered.

Benefits of implementing Events:

  • Modularity: Separate your code responsibilities following SOLID principles.
  • Scalability: Add new features to an existing process without modifying the original code.
  • Asynchronicity: Thanks to native integration with Laravel Queues, you can run listeners in the background to improve user response time.

In practice:

1. Firing an event

When something important happens:

 
event(new UserRegistered($user));
// or UserRegistered::dispatch($user);

2. Listening for the event

Laravel automatically looks for listeners associated with that event.

3. Executing the listener

The method is executed:

 
handle() 
// or __invoke()

Of the corresponding listener.

Types of Events in Laravel

1. System Events (Internal)

Laravel automatically triggers events throughout the application lifecycle. Some of the most useful include:

  • Auth Events: Registered, Login, Logout, Failed.
  • Database Events: QueryExecuted.
  • Eloquent Models: creating, updated, deleting (also known as Model Observers).

2. Personalized Events

You can generate your own events for specific business processes using the Artisan command

$ php artisan make:event OrderProcessed

Important changes since Laravel 11: listener auto-discovery

Here is a key point that many people overlook.

How it worked before (EventServiceProvider)

Before Laravel 11, you had to manually register everything in EventServiceProvider:

 
protected $listen = [
    UserRegistered::class => [
        SendWelcomeEmail::class,
    ],
];

Event Discovery: How auto-discovery works now

Since Laravel 11, this is no longer mandatory.

Laravel automatically scans the app/Listeners directory and:

  • Detects handle() methods
  • Or __invoke() methods
  • Uses the event's type-hint to associate them

In most cases, you don't need to touch the EventServiceProvider.

Which methods Laravel detects

  • public function handle(Event $event)
  • public function __invoke(Event $event)

Simple, clean, and without extra configuration.

Creating an event and its listener in Laravel (basic example)

Event Definition

 
namespace App\Events;
use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class UserRegistered
{
    use Dispatchable, SerializesModels;
    public function __construct(
        public readonly User $user
    ) {}
}

The event only carries data.

Listener Creation

 
php artisan make:listener SendWelcomeEmail --event=UserRegistered
 
namespace App\Listeners;
use App\Events\UserRegistered;
class SendWelcomeEmail
{
    public function handle(UserRegistered $event): void
    {
        // Mail::to($event->user)->send(...)
    }
}

Laravel detects this automatically.

Dispatching the event

 
event(new UserRegistered($user));

Or:

 
UserRegistered::dispatch($user);

Real-world case: using a listener on Laravel's login

Let's consider the following scenario:

  • A user adds products to the cart before authenticating
  • That data is saved in the database
  • Upon login, I needed to replicate the cart from the DB to the session

I could have:

  • Overridden the login method
  • Put logic in the controller
  • Touched Fortify

But I preferred something much more modular.

The problem: synchronizing data after authentication

Laravel already fires the event internally:

 
Illuminate\Auth\Events\Login

So it made no sense to reinvent anything.

Why use a listener instead of overriding the login

  • I don't couple business logic to the authentication process
  • I can change or remove the behavior without touching the login
  • The code remains reusable and clear

Listening to Laravel's Login event

 
php artisan make:listener LoginSuccessful --event=Illuminate\Auth\Events\Login
 
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
use App\Models\ShoppingCart;
use Illuminate\Support\Arr;
class LoginSuccessful
{
    public function handle(Login $event): void
    {
        $cartDB = ShoppingCart::where('user_id', auth()->id())->get();
        $cartSession = session('cart', []);
        foreach ($cartDB as $c) {
            if (Arr::exists($cartSession, $c->post->id)) {
                $cartSession[$c->post->id][1] = $c->count;
            } else {
                $cartSession[$c->post->id] = [$c->post, $c->count];
            }
        }
        session(['cart' => $cartSession]);
    }
}

Every time the user logs in, the cart is automatically synchronized.

Without touching the login. No hacks.

Listener vs direct callback: when to use each

Laravel also allows listening to events directly with a callback.

Dedicated Listener

✔ Cleaner
✔ Better scalability
✔ Ideal for reusable logic

Callback in AppServiceProvider

✔ Faster for simple scripts
✔ Fewer files

 
Event::listen(Login::class, function () {
    // quick logic
});

For small things it's fine, but when it grows, the dedicated listener wins.

Queued Listeners in Laravel

If the listener does something slow (emails, HTTP, integrations), it should not run in the main request.

When to use queued listeners

  • Sending emails
  • External notifications
  • Heavy processes

Implementing ShouldQueue

 
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendWelcomeEmail implements ShouldQueue
{
    use InteractsWithQueue;
    public function handle(UserRegistered $event): void
    {
        //
    }
}

This improves performance and user experience.

Events, queues, and database transactions

A common mistake: using queued listeners before a transaction commits.

The problem

The listener can run when the data does not yet exist in the DB.

Solution: ShouldHandleEventsAfterCommit

 
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;
class SendWelcomeEmail implements ShouldQueue, ShouldHandleEventsAfterCommit
{
    //
}

This way you ensure total consistency.

Common mistakes when using events and listeners in Laravel

  • Putting too much logic inside the listener
  • Using events for trivial things
  • Not considering queues
  • Ignoring transactions
  • Creating unnecessary events

Events are a tool, not a religion.

Frequently asked questions about events and listeners in Laravel

Is it mandatory to use EventServiceProvider in Laravel 11?
No, except for advanced configurations.

Can I use events without queues?
Yes, they are synchronous by default.

Listeners or direct logic in the controller?
If the logic is repeated or grows, use a listener.

What event is fired on login?
Illuminate\Auth\Events\Login

Conclusion

Listeners and events in Laravel are one of the best ways to maintain a clean, scalable, and easy-to-maintain architecture. With Laravel 11, their use is even simpler thanks to auto-discovery, and when you combine them with queues and good practices, they become an extremely powerful tool.

In my experience, using them to solve real problems (like synchronizing data after login) makes the difference between a quick fix and a well-designed solution.

Learn what events and listeners are in Laravel, how they work in Laravel, and when to use them with real-world examples, queues, and best practices.

I agree to receive announcements of interest about this Blog.

Andrés Cruz

ES En español
<script> window.addEventListener('scroll', function() { if (window.scriptsLoaded) return; loadThirdPartyScripts(); }, { once: true }); window.addEventListener('mousemove', function() { if (window.scriptsLoaded) return; loadThirdPartyScripts(); }, { once: true }); window.addEventListener('touchstart', function() { if (window.scriptsLoaded) return; loadThirdPartyScripts(); }, { once: true }); // Fallback if no interaction window.addEventListener('load', function() { setTimeout(function() { if (!window.scriptsLoaded) loadThirdPartyScripts(); }, 8000); }); function loadThirdPartyScripts() { if (window.scriptsLoaded) return; window.scriptsLoaded = true; console.log('Loading third party scripts...'); // Google Analytics var gtagScript = document.createElement('script'); gtagScript.src = 'https://www.googletagmanager.com/gtag/js?id=G-F22688T9RL'; gtagScript.async = true; document.head.appendChild(gtagScript); gtagScript.onload = function() { window.dataLayer = window.dataLayer || []; function gtag() { dataLayer.push(arguments); } gtag('js', new Date()); gtag('config', 'G-F22688T9RL'); }; // Google ADS const adScript = document.createElement('script'); adScript.src = "https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"; adScript.setAttribute('data-ad-client', 'ca-pub-5280469223132298'); adScript.async = true; document.head.appendChild(adScript); // Facebook Pixel (function(f, b, e, v, n, t, s) { if (f.fbq) return; n = f.fbq = function() { n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments) }; if (!f._fbq) f._fbq = n; n.push = n; n.loaded = !0; n.version = '2.0'; n.queue = []; t = b.createElement(e); t.async = !0; t.src = v; s = b.getElementsByTagName(e)[0]; s.parentNode.insertBefore(t, s); })(window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js'); fbq('init', '1643487712945352'); fbq('track', 'PageView'); } </script> <noscript> <img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1643487712945352&ev=PageView&noscript=1" /> </noscript>