Skip to content

[RFC][DX][EventDispatcher] Add EventListener class to setup single-event listeners #33453

@yceruto

Description

@yceruto

Description
Thank to DI auto-configuration most people create an event listener through the EventSubscriberInterface, we could say it's the simplest and fastest way to achieve it so far:

class MyEventListener implements EventSubscriberInterface
{
    public function __invoke(MyEvent $event): void
    {
        // ...
    }

    public static function getSubscribedEvents(): array
    {
        return [
            MyEvent::class => '__invoke',
        ];
    }
}

Simple, right? but we could point out some things that at first glance look like duplicate information (although necessary of course):

  • on the project side it's preferable to use the FQCN of the event than an albitrary event name, so most of the time it will match the event parameter.
  • for single-event listeners, the method to be called seems too obvious as well: __invoke and '__invoke'.
  • the matter become cumbersome when you need to change the priority of the listener, most of the time you don't remember how to do it correctly, by resorting to the documentation or the PHPDoc block of the method.

That's why I would like to propose an alternative way (more intuitive) to configure it that could simplify a bit more this frequently task.

"Only applicable to single-event subscription" attuned to Single Responsibility Principle, this is what concretely I would like to add to the core:

abstract class EventListener implements EventSubscriberInterface
{
    final public static function getSubscribedEvents(): array
    {
        return [static::getEventName() => [static::getMethodName(), static::getPriority()]];
    }

    public static function getEventName(): string
    {
        $method = new \ReflectionMethod(static::class, static::getMethodName());

        if (0 === $method->getNumberOfParameters()) {
            throw new \LogicException(sprintf('Missing "%s" parameter in "%s::%s()" method.', Event::class, static::class, $method->getName()));
        }

        $parameter = $method->getParameters()[0];

        if (null === $type = $parameter->getType()) {
            throw new \LogicException(sprintf('Missing "%s" type-hint for "$%s" parameter in "%s::%s()" method.', Event::class, $parameter->getName(), static::class, $method->getName()));
        }

        if ($type->isBuiltin()) {
            throw new \LogicException(sprintf('The "$%s" parameter defined in "%s::%s()" method must be a class or subclass of "%s", "%s" given.', $parameter->getName(), static::class, $method->getName(), Event::class, $type->getName()));
        }

        return $type->getName();
    }

    public static function getMethodName(): string
    {
        return '__invoke';
    }

    public static function getPriority(): int
    {
        return 0;
    }
}

Basically, an abstract class that will auto-configure the subscribed event from the first parameter of the configured method.

Not sure about "Reflection" performance (that's why this RFC-Issue rather than a PoV-PR) I hope it's not a big deal as the class is intended to be used in userland code and it does not affect production (non-debug mode) thank to cached/compiled DI container. If the "Reflection" introspection is a problem, by making the getEventName() abstract could work to me too.

Example

This class will listen to the MyEvent::class event and it will be processed into __invoke() method (default configurable method):

class MyEventListener extends EventListener
{
    public function __invoke(MyEvent $event): void
    {
        // ...
    }
}

No magic involved since you'll have full control over the setup through getEventName(), getMethodName() and getPriority() methods if needed:

class ExceptionListener extends EventListener
{
    public function __invoke(ExceptionEvent $event): void
    {
        // ...
    }

    public static function getEventName(): string
    {
        return KernelEvents::EXCEPTION;
    }

    public static function getPriority(): int
    {
        return 32;
    }
}

I know :( this is another way of doing the same thing, but IMO is simpler than before because:

  • you won't have to worry about the configuration at all for ideal situation (perfect DX).
  • if configuration needed, it provides discoverable getter methods instead of hardcoded conventions (better DX again).
  • more acurrate error messages (DX++).

WDYT?

Metadata

Metadata

Assignees

No one assigned

    Labels

    DXDX = Developer eXperience (anything that improves the experience of using Symfony)EventDispatcherRFCRFC = Request For Comments (proposals about features that you want to be discussed)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions