From 46cabd7b3226c9db9a61f80a632887cd753a1f28 Mon Sep 17 00:00:00 2001
From: Oliver Bartsch <bo@cedev.de>
Date: Thu, 6 Jul 2023 14:59:12 +0200
Subject: [PATCH] [BUGFIX] Support union types for event listeners

Since #94345 it's possible to omit the "event"
identifier when configuring an event listener,
since the service can be resolved using reflection.
This however did until now not work when using
union types. This patch therefore adjusts the
corresponding compiler pass, making it possible
to use the same method for listing on multiple
events.

Resolves: #101264
Related: #94345
Releases: main, 12.4
Change-Id: I5668269104515811d3c916c832942ef532bdfa27
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/79821
Tested-by: core-ci <typo3@b13.com>
Reviewed-by: Oliver Bartsch <bo@cedev.de>
Tested-by: Oliver Bartsch <bo@cedev.de>
---
 .../backend/Configuration/Services.yaml       |  2 -
 .../ListenerProviderPass.php                  | 58 +++++++++++++------
 2 files changed, 40 insertions(+), 20 deletions(-)

diff --git a/typo3/sysext/backend/Configuration/Services.yaml b/typo3/sysext/backend/Configuration/Services.yaml
index 486008a713fc..bc5b234ca06c 100644
--- a/typo3/sysext/backend/Configuration/Services.yaml
+++ b/typo3/sysext/backend/Configuration/Services.yaml
@@ -188,10 +188,8 @@ services:
     tags:
       - name: event.listener
         identifier: 'typo3/cms-backend/failed-login-attempt-notification'
-        event: TYPO3\CMS\Core\Authentication\Event\LoginAttemptFailedEvent
       - name: event.listener
         identifier: 'typo3/cms-backend/failed-mfa-verification-notification'
-        event: TYPO3\CMS\Core\Authentication\Event\MfaVerificationFailedEvent
 
   TYPO3\CMS\Backend\Security\EmailLoginNotification:
     tags:
diff --git a/typo3/sysext/core/Classes/DependencyInjection/ListenerProviderPass.php b/typo3/sysext/core/Classes/DependencyInjection/ListenerProviderPass.php
index 6762012340c5..0651172eec30 100644
--- a/typo3/sysext/core/Classes/DependencyInjection/ListenerProviderPass.php
+++ b/typo3/sysext/core/Classes/DependencyInjection/ListenerProviderPass.php
@@ -79,30 +79,37 @@ final class ListenerProviderPass implements CompilerPassInterface
             $service = $container->findDefinition($serviceName);
             $service->setPublic(true);
             foreach ($tags as $attributes) {
-                $eventIdentifier = $attributes['event'] ?? $this->getParameterType($serviceName, $service, $attributes['method'] ?? '__invoke');
-                if (!$eventIdentifier) {
+                $eventIdentifiers = $attributes['event'] ?? $this->getParameterType($serviceName, $service, $attributes['method'] ?? '__invoke');
+                if (empty($eventIdentifiers)) {
                     throw new \InvalidArgumentException(
                         'Service tag "event.listener" requires an event attribute to be defined or the listener method must declare a parameter type.  Missing in: ' . $serviceName,
                         1563217364
                     );
                 }
-
-                $listenerIdentifier = $attributes['identifier'] ?? $serviceName;
-                $unorderedEventListeners[$eventIdentifier][$listenerIdentifier] = [
-                    'service' => $serviceName,
-                    'method' => $attributes['method'] ?? null,
-                    'before' => GeneralUtility::trimExplode(',', $attributes['before'] ?? '', true),
-                    'after' => GeneralUtility::trimExplode(',', $attributes['after'] ?? '', true),
-                ];
+                if (is_string($eventIdentifiers)) {
+                    $eventIdentifiers = [$eventIdentifiers];
+                }
+                foreach ($eventIdentifiers as $eventIdentifier) {
+                    $listenerIdentifier = $attributes['identifier'] ?? $serviceName;
+                    $unorderedEventListeners[$eventIdentifier][$listenerIdentifier] = [
+                        'service' => $serviceName,
+                        'method' => $attributes['method'] ?? null,
+                        'before' => GeneralUtility::trimExplode(',', $attributes['before'] ?? '', true),
+                        'after' => GeneralUtility::trimExplode(',', $attributes['after'] ?? '', true),
+                    ];
+                }
             }
         }
         return $unorderedEventListeners;
     }
 
     /**
-     * Derives the class type of the first argument of a given method.
+     * Derives the class type(s) of the first argument of a given method.
+     * Supporting union types, this method returns the class type(s) as list.
+     *
+     * @return string[]|null A list of class types or NULL on failure
      */
-    protected function getParameterType(string $serviceName, Definition $definition, string $method = '__invoke'): ?string
+    protected function getParameterType(string $serviceName, Definition $definition, string $method = '__invoke'): ?array
     {
         // A Reflection exception should never actually get thrown here, but linters want a try-catch just in case.
         try {
@@ -114,13 +121,28 @@ final class ListenerProviderPass implements CompilerPassInterface
             }
             $params = $this->getReflectionMethod($serviceName, $definition, $method)->getParameters();
             $rType = count($params) ? $params[0]->getType() : null;
-            if (!$rType instanceof \ReflectionNamedType) {
-                throw new \InvalidArgumentException(
-                    sprintf('Service "%s" registers method "%s" as an event listener, but does not specify an event type and the method does not type a parameter. Declare a class type for the method parameter or specify an event class explicitly', $serviceName, $method),
-                    1623881315,
-                );
+            if ($rType instanceof \ReflectionNamedType) {
+                return [$rType->getName()];
+            }
+            if ($rType instanceof \ReflectionUnionType) {
+                $types = [];
+                foreach ($rType->getTypes() as $type) {
+                    if ($type instanceof \ReflectionNamedType) {
+                        $types[] = $type->getName();
+                    }
+                }
+                if ($types === []) {
+                    throw new \InvalidArgumentException(
+                        sprintf('Service "%s" registers method "%s" as an event listener, but does not specify an event type and the method\'s first parameter does not contain a valid class type. Declare valid class types for the method parameter or specify the event classes explicitly', $serviceName, $method),
+                        1688646662,
+                    );
+                }
+                return $types;
             }
-            return $rType->getName();
+            throw new \InvalidArgumentException(
+                sprintf('Service "%s" registers method "%s" as an event listener, but does not specify an event type and the method does not type a parameter. Declare a class type for the method parameter or specify an event class explicitly', $serviceName, $method),
+                1623881315,
+            );
         } catch (\ReflectionException $e) {
             // The collectListeners() method will convert this to an exception.
             return null;
-- 
GitLab