0% прочитано

I added multiple EventServiceProviders in Laravel and one Listener was registered three times

While connecting a modular event, I noticed that Laravel had registered the default email verification listener three times. The cause was not the cache, but several module providers extending EventServiceProvider.

23 липня 2026 р. 6 хв читанняLaravel

How I noticed the duplicate listener

While wiring notifications after user registration, I added a separate domain event called UserRegistered and registered a listener for it in the Notification module. A quick check with php artisan event:list unexpectedly revealed a different issue: Laravel’s default email verification listener was registered not once, but three times.

After registering the custom event, I checked the list of events and listeners:

bash
php artisan event:list

The custom event was connected correctly:

text
App\Modules\Auth\Domain\Event\UserRegistered└── App\Modules\Notification\Application\Listener\    DispatchNotificationsAfterUserRegistrationListener

But a little further down I noticed three identical listeners for Laravel’s built-in Registered event:

text
Illuminate\Auth\Events\Registered├── Illuminate\Auth\Listeners\SendEmailVerificationNotification├── Illuminate\Auth\Listeners\SendEmailVerificationNotification└── Illuminate\Auth\Listeners\SendEmailVerificationNotification
If the Registered event had been dispatched in this state, the verification email could potentially have been processed more than once.

The first suspect: event cache

My first thought was stale event cache. I cleared the event cache and all bootstrap caches:

bash
php artisan event:clearphp artisan optimize:clear php artisan event:list \    | grep -A 4 "Illuminate\\\\Auth\\\\Events\\\\Registered"

But the result did not change. SendEmailVerificationNotification was still registered three times.

That meant the issue was happening during application bootstrapping, not because of stale cache.

Where the duplicate registration came from

In bootstrap/providers.php I had two module event providers registered:

php
return [    App\Providers\AppServiceProvider::class,     App\Modules\Media\Provider\MediaEventProvider::class,    App\Modules\Notification\Provider\NotificationEventServiceProvider::class,];

Both classes extended Laravel’s standard EventServiceProvider:

php
use Illuminate\Foundation\Support\Providers\EventServiceProvider; final class MediaEventProvider extends EventServiceProvider{    protected $listen = [        // Media events...    ];}
php
use Illuminate\Foundation\Support\Providers\EventServiceProvider; final class NotificationEventServiceProvider extends EventServiceProvider{    protected $listen = [        UserRegistered::class => [            DispatchNotificationsAfterUserRegistrationListener::class,        ],    ];}

Why the email verification listener was duplicated

The issue was not in the $listen arrays.

Each module provider inherited additional framework behavior from Laravel’s EventServiceProvider. While registering such a provider, Laravel also configured the default email verification listener.

Simplified, that behavior looked like this:

php
Registered::class    => SendEmailVerificationNotification::class;

As a result, the listener was registered:

  1. by Laravel’s default configuration;

  2. when MediaEventProvider was booted;

  3. when NotificationEventServiceProvider was booted.

That is exactly why event:list showed three identical entries.

A separate EventServiceProvider for modules

I created a dedicated base provider for module events. It keeps the ability to use $listen, but prevents repeated registration of Laravel’s global email verification listener.
php
<?php declare(strict_types=1); namespace App\Modules\Core\Provider; use Illuminate\Foundation\Support\Providers\EventServiceProvider; abstract class ModuleEventServiceProvider extends EventServiceProvider{    /**     * Module providers must not register Laravel's     * global email verification listener.     */    protected function configureEmailVerification(): void    {        // Intentionally left blank.    }}

After that, the module event providers started extending the new base class.

php
use App\Modules\Core\Provider\ModuleEventServiceProvider; final class MediaEventProvider extends ModuleEventServiceProvider{    protected $listen = [        MediaFolderItemsChangedEvent::class => [            SyncMediaFolderItemsCountListener::class,        ],         MediaUsageChangedEvent::class => [            SyncMediaUsageCountListener::class,        ],    ];}
php
use App\Modules\Core\Provider\ModuleEventServiceProvider; final class NotificationEventServiceProvider extends ModuleEventServiceProvider{    protected $listen = [        UserRegistered::class => [            DispatchNotificationsAfterUserRegistrationListener::class,        ],    ];}

Before and after

listener-three-times-fix

After changing the inheritance, I cleared the caches once again and checked the event list:

bash
php artisan optimize:clear php artisan event:list \    | grep -A 4 "Illuminate\\\\Auth\\\\Events\\\\Registered"

Do all service providers need to be changed?

No. The issue affected only module classes that extended Laravel’s EventServiceProvider.

A regular ServiceProvider does not automatically configure email verification, so those providers do not need to be changed.

php
use Illuminate\Support\ServiceProvider; final class NotificationServiceProvider extends ServiceProvider{    public function boot(): void    {        Notification::observe(NotificationObserver::class);    }}

Takeaway

This was one of those cases where a simple diagnostic command turned out to be more useful than a long search through the codebase. I was checking the new UserRegistered listener, and ended up finding the reason why Laravel’s default email verification flow could potentially run more than once.