← Back to blog

PHP 8 Attributes in Laravel Event Subscribers

| Laravel

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 $subscribe property 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:

  1. The $subscriberReflectionClass->getMethods() iterates over the listener's methods for the given subscriber class. In this case it gets handleUserLogin and handleUserLoogout methods from the UserEventSubscriber.
  2. The $listenerMethodAttributes gets the ListensTo::class attributes for the given listener.
  3. By iterating over the $listenerMethodAttributes you can instantiate the ListensTo attribute class using $listenerMethodAttribute->newInstance(). This returns an instance of App\Attributes\ListensTo so you can read the event's name.
  4. Finally, the event's name and its listener are added to $listeners and 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);
    }
}
  1. The $subscribers array holds all the subscribers. Do not mix it with Laravel's $subscribe property used to register the subscribers.
  2. The $eventDispatcher gets the event dispatcher from the container.
  3. You iterate over the $subscibers, resolve each one, and send it to the event dispatcher using the listen method.

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 subscribe method.
  • The ListensTo attribute replaces boilerplate wiring -- each listener method declares the event it handles directly, making the subscriber class self-documenting.
  • The ResolveListeners class uses the Reflection API -- it reads the ListensTo attributes at runtime and registers them with the event dispatcher automatically.
Share