Support #[AsEventListener] attribute on classes and methods

Created on 21 July 2023, over 1 year ago

Problem/Motivation

Event subscribers in Drupal are quite verbose.

symfony provides an easier way to register event listeners: The #[AsEventListener] attribute, which can be placed on service classes and methods.
https://symfony.com/doc/current/event_dispatcher.html#defining-event-lis...

Steps to reproduce

Proposed resolution

Let's support this in Drupal.

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet

πŸ“Œ Task
Status

Active

Version

11.0 πŸ”₯

Component
BaseΒ  β†’

Last updated 43 minutes ago

Created by

πŸ‡©πŸ‡ͺGermany donquixote

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @donquixote
  • Open on Drupal.org β†’
    Environment: PHP 8.2 & MySQL 8
    last update over 1 year ago
    Not currently mergeable.
  • last update over 1 year ago
    29,877 pass
  • πŸ‡¦πŸ‡ΊAustralia dpi Perth, Australia

    I took a look at this issue and gave an attempt to try to work with what we have, by supplementing ContainerAwareEventDispatcher with changes, and keeping our existing tag.

    But I realize that we also need to add a compiler pass to reflect on tagged services.

    So I agree that It might just be more straightforward to switch from \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher to \Symfony\Component\EventDispatcher\EventDispatcher and utilize Symfony's\Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass here.

    Since the associated MR came to the same conclusion, that swapping to Symfonys EventDispatcher is necessary, I think we should postpone this issue on our main EventDispatcher issue @ πŸ“Œ Replace ContainerAwareEventDispatcher with Symfony EventDispatcher Fixed

  • πŸ‡¦πŸ‡ΊAustralia dpi Perth, Australia
  • Status changed to Postponed 10 months ago
  • πŸ‡¦πŸ‡ΊAustralia dpi Perth, Australia
  • πŸ‡¦πŸ‡ΊAustralia dpi Perth, Australia

    It'd be valuable to also rely on ✨ Directory based automatic service creation Needs review to do automatic service creation for us.

  • Status changed to Active 2 months ago
  • πŸ‡ΊπŸ‡ΈUnited States moshe weitzman Boston, MA

    πŸ“Œ Replace ContainerAwareEventDispatcher with Symfony EventDispatcher Fixed was merged. Changing status per the most recent comments. Please update to a different status as needed.

  • First commit to issue fork.
  • Pipeline finished with Success
    2 months ago
    Total: 476s
    #295805
  • Rebased MR 4438. One thing that should be noted: using the AsEventListener attribute without the event property only works if the dispatched event name matches the event object FQCN, or if an alias mapping between the event name and the FQCN is set in the event_dispatcher.event_aliases container parameter. These examples (from Symfony docs) only work if the the dispatched event name is CustomEvent::class, or if the alias is set:

    namespace App\EventListener;
    
    use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
    
    #[AsEventListener]
    final class MyListener
    {
        public function __invoke(CustomEvent $event): void
        {
            // ...
        }
    }
    
    namespace App\EventListener;
    
    use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
    
    final class MyMultiListener
    {
        #[AsEventListener]
        public function onCustomEvent(CustomEvent $event): void
        {
            // ...
        }
    }
    

    For convenience, I've add a commit to the MR to register all the Symfony kernel event aliases. We can probably add follow up tasks to convert core event subscribers to use the AsEventListener attribute, as well as add aliases for other core events.

  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    Is there least 1 listener that can be converted to show this change works?

  • Is there least 1 listener that can be converted to show this change works?

    I mentioned converting core subscribers in a follow up, but I had a thought of whether there are benefits to doing it? I think having this feature is nice for DX going forward, but I don't know if there's an advantage to convert existing core code. I'll look into finding one to convert regardless, and the decision can be made from there.

  • Pipeline finished with Failed
    2 months ago
    Total: 742s
    #300360
  • Pipeline finished with Failed
    2 months ago
    Total: 492s
    #300391
  • Pipeline finished with Success
    2 months ago
    Total: 2367s
    #300402
  • OK, converted MaintenanceModeSubscriber. One thing I ran into is that AutowireTest was exempting MaintenanceModeSubscriber from needing a service alias declared, because it was one of many services implementing EventSubscriberInterface. However, with EventSubscriberInterface removed, needed to add an alias for MaintenanceModeSubscriber. I'm not sure that adding aliases for event subscriber services is worth it, but gonna push this forward for review.

  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    Awesome to see the replacement worked though!

  • πŸ‡ΊπŸ‡ΈUnited States moshe weitzman Boston, MA

    I reviewed the code and the draft CR. LGTM. Nice work.

  • πŸ‡ΊπŸ‡ΈUnited States moshe weitzman Boston, MA

    Actually, I am wondering why we have both:

    1. A service is declared in core.services.yml for maintenance_mode_subscriber
    2. Various #[EventListener] appear on various methods on the same file. Wouldn't the nicer usage be to omit the yml declared service and rely on Attribute based creation of Listeners? The MR suggests that all we are saving is the getSubscribedEvents() method. I had higher hopes than that :). Maybe we need more plumbing elsewhere in core for what I describe.
  • Automatic discovery of services without entry in a services.yml needs something like ✨ Directory based automatic service creation Needs review . There's no discovery of services in this MR, and it's probably out of scope for this issue.

    Since there are multiple handlers in MaintenanceModeSubscriber, either each method needs to have an attribute as done in the MR, or there needs to be multiple AsEventListener attributes on the class, each including the method property.

  • πŸ‡ΊπŸ‡ΈUnited States moshe weitzman Boston, MA

    Thanks. Once we have ✨ Directory based automatic service creation Needs review , we could add the RegisterListenersPass compiler pass and then get #[AsListener] classes registered as services and listeners. I'm not sure the intermediate step in this MR provides much value. If others think so, feel free to mark this RTBC.

  • The RegisterListenersPass already occurs within Drupal's Drupal\Core\DependencyInjection\Compiler\RegisterEventSubscribersPass

    That pass doesn't handle the #[AsEventListener] attribute, though. That is what this MR introduces, and the code to do so is copied from here: https://github.com/symfony/symfony/blob/9d89e05279631430f9037844fc4cc2ea...

    So even if or when ✨ Directory based automatic service creation Needs review gets in, the work here still needs to be done to support #[AsEventListener].

    Also see: https://stackoverflow.com/a/78338989

  • πŸ‡¦πŸ‡ΊAustralia kim.pepper πŸ„β€β™‚οΈπŸ‡¦πŸ‡ΊSydney, Australia

    Yes, I think this is still useful as it reduces boilerplate and makes the definition of listeners simpler. Going to set back to RTBC.

  • πŸ‡¬πŸ‡§United Kingdom longwave UK
       maintenance_mode_subscriber:
         class: Drupal\Core\EventSubscriber\MaintenanceModeSubscriber
         arguments: ['@maintenance_mode', '@config.factory', '@string_translation', '@url_generator', '@current_user', '@bare_html_page_renderer', '@messenger', '@event_dispatcher']
    +  Drupal\Core\EventSubscriber\MaintenanceModeSubscriber: '@maintenance_mode_subscriber'
    

    While this is annoying I think in the long run event subscribers should just be named after their class, ie.

       Drupal\Core\EventSubscriber\MaintenanceModeSubscriber:
         arguments: ['@maintenance_mode', '@config.factory', '@string_translation', '@url_generator', '@current_user', '@bare_html_page_renderer', '@messenger', '@event_dispatcher']
    

    Service names come with a BC promise (although I'm not sure how useful that is for event subscribers) so to make this switch we will have to alias the old name and deprecate it, which is out of scope of this issue, but worth considering if we decide to swap all services to use this attribute in a followup.

    Also I assume if we do ✨ Directory based automatic service creation Needs review this will have to happen anyway, because the definition will be gone from services.yml.

  • πŸ‡¬πŸ‡§United Kingdom longwave UK

    Alternatively we do this in AutowireTest:

          // Ignore proxy classes for autowiring purposes.
          if (str_contains($class, '\\ProxyClass\\')) {
            continue;
          }
    

    We could also ignore EventSubscriber classes the same way?

  • Pipeline finished with Success
    about 2 months ago
    Total: 1457s
    #311452
  • Added commit per #23 to ignore classes with '\\EventSubscriber\\' in the path.

  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    Appears to have MR conflicts. Surprised the bot didn't catch it

  • Pipeline finished with Success
    about 1 month ago
    Total: 988s
    #324204
  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    Sweet thanks for the quick turn around. Since was previously RTBC and previous feedback appears to be addressed restoring that.

  • It occurs to me that since πŸ“Œ OOP hooks using attributes and event dispatcher Needs review introduced a container compiler pass that essentially iterates through module files to find classes or methods with the Drupal\Core\Hook\Attribute\Hook attribute, and turns those into event listener services, if we:

    1. Made the Hook attribute a subclass of the Symfony\Component\EventDispatcher\Attribute\AsEventListener attribute class
    2. Changed the compiler pass to look for the AsEventListener attribute,

    we could have those general event listener services discovered in the same pass. And those event listeners wouldn't need to be declared in services.yml files.

    This wouldn't work for core event subscribers outside of modules, but it might be worth looking into?

  • Pipeline finished with Failed
    24 days ago
    Total: 518s
    #334066
  • Pipeline finished with Failed
    24 days ago
    Total: 1172s
    #334069
  • Pipeline finished with Success
    24 days ago
    Total: 1343s
    #334080
  • Created a new draft MR 10119 for a PoC of the approach I mentioned in #28 πŸ“Œ Support #[AsEventListener] attribute on classes and methods Active . It leverages a lot of what HookCollectorPass is already doing to also find module class with the AsEventListener attribute and registers them as event listener services. No entry in the module .services.yml is required. Note that discovery is limited to the EventSubscriber directory in a module. Also, these services are autowired, so any arguments in class constructors need to be able to be autowired or have the Autowire attribute applied to them.

    Putting back to NR for feedback on the PoC MR. I can also move this issue back to RTBC if this new work is better done in a followup.

  • The Needs Review Queue Bot β†’ tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

    This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

    Consult the Drupal Contributor Guide β†’ to find step-by-step guides for working with issues.

Production build 0.71.5 2024