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