This article was published over 2 years ago. Some information may be outdated.
In Laravel, each event has its corresponding listener. For example, the OrderShipped event has a SendShipmentNotification listener. Simple and straightforward.
Sometimes you need to handle several events within the same listener class. For example, you need to handle both userLogin and userLogout events in the same listener class.
Event subscribers give you the ability to subscribe to multiple events from within the subscriber class itself.
Read more about event subscribers in Laravel.
Introduction to event subscribers
Event subscribers are straightforward. All you need is a class with a subscribe method that instructs Laravel where to find the listeners:
namespace App\Listeners;
class UserEventSubscriber
{
public function handleUserLogin($event) { // ... }
public function handleUserLogout($event) { // ... }
public function subscribe($events)
{
$events->listen(
'Illuminate\Auth\Events\Login',
[UserEventSubscriber::class, 'handleUserLogin']
);
$events->listen(
'Illuminate\Auth\Events\Logout',
[UserEventSubscriber::class, 'handleUserLogout']
);
}
}
Then register the event subscriber in the EventServiceProvider:
protected $subscribe = [
UserEventSubscriber::class,
];
Each time you need to add a new event, you have to open the UserEventSubscriber and modify the subscribe method to include the new event.
PHP 8 Attributes
PHP 8 introduced a feature called attributes.
Attributes add metadata to your code. For example:
class AboutPageController
{
#[Route('/about')]
public function show()
{
return view('pages.about');
}
}
The #[Route('/about')] is an attribute. It tells PHP that the show method has a Route attribute. That is all it does on its own.
Using the reflection API, you can extract the attributes and act on them.
For example, whenever you encounter the Route attribute, you create a route for the given parameter /about. So it makes sense to implement attributes in UserEventSubscriber:
namespace App\Listeners;
use Attributes\ListensTo;
class UserEventSubscriber
{
#[ListensTo(Illuminate\Auth\Events\Login::class)]
public function handleUserLogin($event) { // ... }
#[ListensTo(Illuminate\Auth\Events\Logout::class))
public function handleUserLogout($event) { // ... }
}
Here is how to do it.
PHP Attributes Subscribers
Create a class named ListensTo in App\Attributes as follows:
namespace App\Attributes;
use Attribute;
#[Attribute]
class ListensTo
{
public function __construct(public string $event)
{
}
}
This class does not do anything on its own -- it is just an attribute class.
Create another class named UserEventSubscriber in App\Subscribers:
namespace App\Subscribers;
use App\Attributes\ListensTo;
use Illuminate\Auth\Events\Login as LoginEvent;
use Illuminate\Auth\Events\Logout as LogoutEvent;
class UserEventSubscriber
{
#[ListensTo(LoginEvent::class)]
public function handleUserLogin(LoginEvent $event)
{
logger('Users logged in: '.$event->user->id);
}
#[ListensTo(LogoutEvent::class)]
public function handleUserLogout(LogoutEvent $event)
{
logger('Users logged out: '.$event->user->id);
}
}
Open up the EventServiveProvider and add a new property $subscibers:
use App\Subscribers\UserEventSubscriber;
class EventServiceProvider extends ServiceProvider
{
protected array $subscribers = [
UserEventSubscriber::class,
];
// ...
}
Do not mix Laravel's
$subscribeproperty with$subscribers. The latter holds all the attributed subscribers.
In the register method, you need to iterate over the $subscribers, read the ListensTo attribute, and send it to the event dispatcher.
Create a new class named ResolveListeners in the App\Support:
namespace App\Support;
use App\Attributes\ListensTo;
use ReflectionClass;
class ResolveListeners
{
public function resolve(string $subscriberClass): array
{
$subscriberReflectionClass = new ReflectionClass($subscriberClass);
$listeners = [];
foreach ($subscriberReflectionClass->getMethods() as $listenerMethod) {
// Only get the attribute "ListensTo"
$listenerMethodAttributes = $listenerMethod->getAttributes(ListensTo::class);
foreach ($listenerMethodAttributes as $listenerMethodAttribute) {
// Instantiate the "ListensTo" class so we can get the event name
/** @var ListensTo $listener */
$listener = $listenerMethodAttribute->newInstance();
$listeners[] = [
$listener->event,
[$subscriberClass, $listenerMethod->getName()]
];
}
}
return $listeners;
}
}
Here is how this class works:
- The
$subscriberReflectionClass->getMethods()iterates over the listener's methods for the given subscriber class. In this case it getshandleUserLoginandhandleUserLoogoutmethods from theUserEventSubscriber. - The
$listenerMethodAttributesgets theListensTo::classattributes for the given listener. - By iterating over the
$listenerMethodAttributesyou can instantiate theListensToattribute class using$listenerMethodAttribute->newInstance(). This returns an instance ofApp\Attributes\ListensToso you can read the event's name. - Finally, the event's name and its listener are added to
$listenersand returned.
Array
(
[0] => Array
(
[0] => Illuminate\Auth\Events\Login
[1] => Array
(
[0] => App\Subscribers\UserEventSubscriber
[1] => handleUserLogin
)
)
[1] => Array
(
[0] => Illuminate\Auth\Events\Logout
[1] => Array
(
[0] => App\Subscribers\UserEventSubscriber
[1] => handleUserLogout
)
)
)
The ResolveListeners is ready. Go back to the EventServiceProvider and resolve the events as follows:
namespace App\Providers;
use App\Subscribers\LoggerSubscriber;
use App\Subscribers\UserEventSubscriber;
use App\Support\ResolveListeners;
use Illuminate\Events\Dispatcher;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
protected array $subscribers = [
UserEventSubscriber::class,
LoggerSubscriber::class,
];
public function register()
{
/** @var Dispatcher $eventDispatcher */
$eventDispatcher = $this->app->make(Dispatcher::class);
foreach ($this->subscribers as $subscriber) {
foreach ($this->resolveListeners($subscriber) as [$event, $listener]) {
$eventDispatcher->listen($event, $listener);
}
}
}
private function resolveListeners(string $subscribeClass): array
{
return $this->app->make(ResolveListeners::class)->resolve($subscribeClass);
}
}
- The
$subscribersarray holds all the subscribers. Do not mix it with Laravel's$subscribeproperty used to register the subscribers. - The
$eventDispatchergets the event dispatcher from the container. - You iterate over the
$subscibers, resolve each one, and send it to the event dispatcher using thelistenmethod.
To test it out:
// routes/web.php
Route::get('/test_event', function() {
\Illuminate\Support\Facades\Auth::login(\App\Models\User::find(1));
\Illuminate\Support\Facades\Auth::logout();
});
Check the storage/logs/laravel.log file, and you should see the following entries:
[2021-02-21 14:03:02] local.DEBUG: User logged in: 1
[2021-02-21 14:03:02] local.DEBUG: User logged out: 1
Summary
- PHP 8 attributes add metadata to methods -- you can use them to declaratively map events to listener methods without maintaining a manual
subscribemethod. - The
ListensToattribute replaces boilerplate wiring -- each listener method declares the event it handles directly, making the subscriber class self-documenting. - The
ResolveListenersclass uses the Reflection API -- it reads theListensToattributes at runtime and registers them with the event dispatcher automatically.