From 69898a263d33b5f2300c160a1c98c879786164f5 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Wed, 19 Jul 2023 09:00:17 +0200 Subject: [PATCH 1/3] [EventDispatcher] add a way to call a listener before or after another one --- .../Attribute/AsEventListener.php | 4 + ...BeforeAfterListenerDefinitionException.php | 81 +++++ .../RegisterListenersPass.php | 328 +++++++++++++++--- .../RegisterListenersPassTest.php | 275 +++++++++++++++ 4 files changed, 643 insertions(+), 45 deletions(-) create mode 100644 src/Symfony/Component/EventDispatcher/DependencyInjection/InvalidBeforeAfterListenerDefinitionException.php diff --git a/src/Symfony/Component/EventDispatcher/Attribute/AsEventListener.php b/src/Symfony/Component/EventDispatcher/Attribute/AsEventListener.php index bb931b82dc2b1..44e32eeb5125a 100644 --- a/src/Symfony/Component/EventDispatcher/Attribute/AsEventListener.php +++ b/src/Symfony/Component/EventDispatcher/Attribute/AsEventListener.php @@ -24,6 +24,10 @@ public function __construct( public ?string $method = null, public int $priority = 0, public ?string $dispatcher = null, + /** @var string|array{0: string, 1: string}|null */ + public string|array|null $before = null, + /** @var string|array{0: string, 1: string}|null */ + public string|array|null $after = null, ) { } } diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/InvalidBeforeAfterListenerDefinitionException.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/InvalidBeforeAfterListenerDefinitionException.php new file mode 100644 index 0000000000000..2cc4a5c2c2ce6 --- /dev/null +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/InvalidBeforeAfterListenerDefinitionException.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\DependencyInjection; + +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; + +/** + * @psalm-import-type BeforeAfterDefinition from RegisterListenersPass + */ +final class InvalidBeforeAfterListenerDefinitionException extends InvalidArgumentException +{ + private function __construct(string $errorServiceId, string $message) + { + parent::__construct(sprintf('Invalid before/after definition for service "%s": %s', $errorServiceId, $message)); + } + + public static function beforeAndAfterAtSameTime(string $errorServiceId): self + { + return new self($errorServiceId, 'cannot use "after" and "before" at the same time.'); + } + + public static function circularReference(string $errorServiceId): self + { + return new self($errorServiceId, 'circular reference detected.'); + } + + public static function arrayDefinitionInvalid(string $errorServiceId): self + { + return new self($errorServiceId, 'when declaring as an array, first item must be a service id or a class and second item must be the method.'); + } + + /** + * @param BeforeAfterDefinition $beforeAfterDefinition + */ + public static function notAListener(string $errorServiceId, string|array $beforeAfterDefinition): self + { + return new self($errorServiceId, sprintf('given definition "%s" is not a listener.', self::beforeAfterDefinitionToString($beforeAfterDefinition))); + } + + /** + * @param BeforeAfterDefinition $beforeAfterDefinition + */ + public static function notSameEvent(string $errorServiceId, string|array $beforeAfterDefinition): self + { + return new self($errorServiceId, sprintf('given definition "%s" does not listen to the same event.', self::beforeAfterDefinitionToString($beforeAfterDefinition))); + } + + /** + * @param BeforeAfterDefinition $beforeAfterDefinition + */ + public static function notSameDispatchers(string $errorServiceId, string|array $beforeAfterDefinition): self + { + return new self($errorServiceId, sprintf('given definition "%s" is not handled by the same dispatchers.', self::beforeAfterDefinitionToString($beforeAfterDefinition))); + } + + public static function ambiguousDefinition(string $errorServiceId, string $beforeAfterDefinition): self + { + return new self($errorServiceId, sprintf('given definition "%s" is ambiguous. Please specify the "method" attribute.', $beforeAfterDefinition)); + } + + /** + * @param BeforeAfterDefinition $beforeAfterDefinition + */ + private static function beforeAfterDefinitionToString(string|array $beforeAfterDefinition): string + { + if (\is_string($beforeAfterDefinition)) { + return $beforeAfterDefinition; + } + + return sprintf('%s::%s()', $beforeAfterDefinition[0], $beforeAfterDefinition[1]); + } +} diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php index 866f4e64ffc42..78149adeb6b7d 100644 --- a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php @@ -22,12 +22,30 @@ /** * Compiler pass to register tagged services for an event dispatcher. + * + * @psalm-type BeforeAfterDefinition = string|array{0: string, 1: string} + * @psalm-type ListenerDefinition = array{serviceId: string, event: string, method: string, before?: BeforeAfterDefinition, after?: BeforeAfterDefinition, priority?: int, dispatchers: list} + * @psalm-type AllListenersDefinition = array> */ class RegisterListenersPass implements CompilerPassInterface { private array $hotPathEvents = []; private array $noPreloadEvents = []; + /** + * $allListenersMap['event_name']['listener_service_id']['method'] => $listenerDefinition. + * + * @var array>> + */ + private array $allListenersMap = []; + + /** + * $listenerClassesMap['listener_FQCN']['event_name'][] => array{serviceId: string, method:string}. + * + * @var array>> + */ + private array $listenerClassesMap = []; + /** * @return $this */ @@ -57,20 +75,231 @@ public function process(ContainerBuilder $container) return; } - $aliases = []; + // collect all listeners, and prevent keys overriding for a very unlikely case where a service is both a listener and a subscriber + $allListenerDefinitions = array_merge_recursive( + iterator_to_array($this->collectListeners($container)), + iterator_to_array($this->collectSubscribers($container)), + ); - if ($container->hasParameter('event_dispatcher.event_aliases')) { - $aliases = $container->getParameter('event_dispatcher.event_aliases'); + $this->initializeListenersMaps($container, $allListenerDefinitions); + + $this->handleBeforeAfter($allListenerDefinitions, $container); + + $this->registerListeners($container, $allListenerDefinitions); + } + + /** + * @param AllListenersDefinition $allListenerDefinitions + */ + private function initializeListenersMaps(ContainerBuilder $container, array $allListenerDefinitions): void + { + foreach ($allListenerDefinitions as $listenerDefinitions) { + foreach ($listenerDefinitions as $listenerDefinition) { + $this->allListenersMap[$listenerDefinition['event']][$listenerDefinition['serviceId']][$listenerDefinition['method']] = $listenerDefinition; + + $listenerClass = $container->getDefinition($listenerDefinition['serviceId'])->getClass(); + + if ($listenerClass) { + $this->listenerClassesMap[$listenerClass][$listenerDefinition['event']] ??= []; + $this->listenerClassesMap[$listenerClass][$listenerDefinition['event']][] = [ + 'serviceId' => $listenerDefinition['serviceId'], + 'method' => $listenerDefinition['method'], + ]; + } + } + } + } + + /** + * @param AllListenersDefinition $allListenerDefinitions + */ + private function handleBeforeAfter(array &$allListenerDefinitions, ContainerBuilder $container): void + { + foreach ($allListenerDefinitions as &$listenerDefinitions) { + foreach ($listenerDefinitions as &$listenerDefinition) { + if (isset($listenerDefinition['before']) && isset($listenerDefinition['after'])) { + throw InvalidBeforeAfterListenerDefinitionException::beforeAndAfterAtSameTime($listenerDefinition['serviceId']); + } + + if (isset($listenerDefinition['before']) || isset($listenerDefinition['after'])) { + $listenerDefinition['priority'] = $this->computeBeforeAfterPriorities($container, $listenerDefinition); + + // register the new priority in listeners map + unset($listenerDefinition['before'], $listenerDefinition['after']); + $this->allListenersMap[$listenerDefinition['event']][$listenerDefinition['serviceId']][$listenerDefinition['method']] = $listenerDefinition; + } + } + } + } + + /** + * @param ListenerDefinition $listenerDefinition + * @param array $alreadyVisited + */ + private function computeBeforeAfterPriorities(ContainerBuilder $container, array $listenerDefinition, array $alreadyVisited = []): int + { + // Prevent circular references + $listenerName = sprintf('%s::%s', $listenerDefinition['serviceId'], $listenerDefinition['method']); + if ($alreadyVisited[$listenerName] ?? false) { + throw InvalidBeforeAfterListenerDefinitionException::circularReference($listenerDefinition['serviceId']); } + $alreadyVisited[$listenerName] = true; + if (isset($listenerDefinition['before']) || isset($listenerDefinition['after'])) { + ['serviceId' => $beforeAfterServiceId, 'method' => $beforeAfterMethod] = $this->normalizeBeforeAfter($container, $listenerDefinition); + + $beforeAfterListenerDefinition = $this->allListenersMap[$listenerDefinition['event']][$beforeAfterServiceId][$beforeAfterMethod]; + + $priority = $this->computeBeforeAfterPriorities($container, $beforeAfterListenerDefinition, $alreadyVisited); + + return isset($listenerDefinition['before']) ? $priority + 1 : $priority - 1; + } + + return $listenerDefinition['priority'] ?? 0; + } + + /** + * @param ListenerDefinition $listenerDefinition + * + * @return array{serviceId: string, method: string} + * + * before/after can be defined as: class-string, service-id, or array{class?: class-string, service?: service-id, method?: string} + * let's normalize it, and resolve the method if not given (or rise an exception if ambiguous) + */ + private function normalizeBeforeAfter(ContainerBuilder $container, array $listenerDefinition): array + { + $beforeAfterDefinition = $listenerDefinition['before'] ?? $listenerDefinition['after']; + $id = $listenerDefinition['serviceId']; + $event = $listenerDefinition['event']; + + $listenersForEvent = $this->allListenersMap[$event]; + + $beforeAfterMethod = null; + $normalizedBeforeAfter = null; + + if (\is_array($beforeAfterDefinition)) { + if (!array_is_list($beforeAfterDefinition) || 2 !== \count($beforeAfterDefinition)) { + throw InvalidBeforeAfterListenerDefinitionException::arrayDefinitionInvalid($id); + } + + $beforeAfterMethod = $beforeAfterDefinition[1]; + $beforeAfterServiceOrClass = $beforeAfterDefinition[0]; + } else { + $beforeAfterServiceOrClass = $beforeAfterDefinition; + } + + if (class_exists($beforeAfterServiceOrClass) && !$container->has($beforeAfterServiceOrClass)) { + if (!isset($this->listenerClassesMap[$beforeAfterServiceOrClass])) { + throw InvalidBeforeAfterListenerDefinitionException::notAListener($id, $beforeAfterDefinition); + } + + if (!isset($this->listenerClassesMap[$beforeAfterServiceOrClass][$event])) { + throw InvalidBeforeAfterListenerDefinitionException::notSameEvent($id, $beforeAfterDefinition); + } + + $listenersForClassAndEvent = $this->listenerClassesMap[$beforeAfterServiceOrClass][$event]; + + if (!$beforeAfterMethod) { + if (1 < \count($listenersForClassAndEvent)) { + throw InvalidBeforeAfterListenerDefinitionException::ambiguousDefinition($id, $beforeAfterServiceOrClass); + } + + $normalizedBeforeAfter = $listenersForClassAndEvent[0]; + } else { + foreach ($listenersForClassAndEvent as ['serviceId' => $serviceId, 'method' => $methodFromListenerDefinition]) { + if ($methodFromListenerDefinition === $beforeAfterMethod) { + $normalizedBeforeAfter = ['serviceId' => $serviceId, 'method' => $beforeAfterMethod]; + break; + } + } + + if (!isset($normalizedBeforeAfter)) { + throw InvalidBeforeAfterListenerDefinitionException::notAListener($id, $beforeAfterDefinition); + } + } + } elseif ( + $container->has($beforeAfterServiceOrClass) + && (($def = $container->findDefinition($beforeAfterServiceOrClass))->hasTag('kernel.event_listener') || $def->hasTag('kernel.event_subscriber')) + ) { + if (!isset($listenersForEvent[$beforeAfterServiceOrClass])) { + throw InvalidBeforeAfterListenerDefinitionException::notSameEvent($id, $beforeAfterDefinition); + } + + if (!$beforeAfterMethod) { + if (1 < \count($listenersForEvent[$beforeAfterServiceOrClass])) { + throw InvalidBeforeAfterListenerDefinitionException::ambiguousDefinition($id, $beforeAfterServiceOrClass); + } + + $beforeAfterMethod = array_key_first($listenersForEvent[$beforeAfterServiceOrClass]); + } else { + if (!isset($listenersForEvent[$beforeAfterServiceOrClass][$beforeAfterMethod])) { + throw InvalidBeforeAfterListenerDefinitionException::notAListener($id, $beforeAfterDefinition); + } + } + + $normalizedBeforeAfter = ['serviceId' => $beforeAfterServiceOrClass, 'method' => $beforeAfterMethod]; + } else { + throw InvalidBeforeAfterListenerDefinitionException::notAListener($id, $beforeAfterDefinition); + } + + if ($listenersForEvent[$normalizedBeforeAfter['serviceId']][$normalizedBeforeAfter['method']]['dispatchers'] !== $listenerDefinition['dispatchers']) { + throw InvalidBeforeAfterListenerDefinitionException::notSameDispatchers($id, $beforeAfterDefinition); + } + + return $normalizedBeforeAfter; + } + + /** + * @param AllListenersDefinition $allListenerDefinitions + */ + public function registerListeners(ContainerBuilder $container, array $allListenerDefinitions): void + { $globalDispatcherDefinition = $container->findDefinition('event_dispatcher'); - foreach ($container->findTaggedServiceIds('kernel.event_listener', true) as $id => $events) { + foreach ($allListenerDefinitions as $id => $listenerDefinitions) { $noPreload = 0; - foreach ($events as $event) { - $priority = $event['priority'] ?? 0; + foreach ($listenerDefinitions as $listenerDefinition) { + $dispatcherDefinitions = []; + foreach ($listenerDefinition['dispatchers'] as $dispatcher) { + $dispatcherDefinitions[] = 'event_dispatcher' === $dispatcher ? $globalDispatcherDefinition : $container->findDefinition($dispatcher); + } + foreach ($dispatcherDefinitions as $dispatcherDefinition) { + $dispatcherDefinition->addMethodCall( + 'addListener', + [ + $listenerDefinition['event'], + [new ServiceClosureArgument(new Reference($id)), $listenerDefinition['method']], + $listenerDefinition['priority'] ?? 0, + ] + ); + } + + if (isset($this->hotPathEvents[$listenerDefinition['event']])) { + $container->getDefinition($id)->addTag('container.hot_path'); + } elseif (isset($this->noPreloadEvents[$listenerDefinition['event']])) { + ++$noPreload; + } + } + + if ($noPreload && \count($listenerDefinitions) === $noPreload) { + $container->getDefinition($id)->addTag('container.no_preload'); + } + } + } + + /** + * @return \Generator> + */ + private function collectListeners(ContainerBuilder $container): \Generator + { + $aliases = $this->getEventsAliases($container); + + foreach ($container->findTaggedServiceIds('kernel.event_listener', true) as $id => $events) { + $listenersDefinition = []; + + foreach ($events as $event) { if (!isset($event['event'])) { if ($container->getDefinition($id)->hasTag('kernel.event_subscriber')) { continue; @@ -84,38 +313,36 @@ public function process(ContainerBuilder $container) if (!isset($event['method'])) { $event['method'] = 'on'.preg_replace_callback([ - '/(?<=\b|_)[a-z]/i', - '/[^a-z0-9]/i', - ], fn ($matches) => strtoupper($matches[0]), $event['event']); + '/(?<=\b|_)[a-z]/i', + '/[^a-z0-9]/i', + ], fn ($matches) => strtoupper($matches[0]), $event['event']); $event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']); if (null !== ($class = $container->getDefinition($id)->getClass()) && ($r = $container->getReflectionClass($class, false)) && !$r->hasMethod($event['method'])) { if (!$r->hasMethod('__invoke')) { throw new InvalidArgumentException(sprintf('None of the "%s" or "__invoke" methods exist for the service "%s". Please define the "method" attribute on "kernel.event_listener" tags.', $event['method'], $id)); } - $event['method'] = '__invoke'; } } - $dispatcherDefinition = $globalDispatcherDefinition; - if (isset($event['dispatcher'])) { - $dispatcherDefinition = $container->findDefinition($event['dispatcher']); - } - - $dispatcherDefinition->addMethodCall('addListener', [$event['event'], [new ServiceClosureArgument(new Reference($id)), $event['method']], $priority]); + $event['dispatchers'] = [$event['dispatcher'] ?? 'event_dispatcher']; + $event['serviceId'] = $id; + unset($event['dispatcher']); - if (isset($this->hotPathEvents[$event['event']])) { - $container->getDefinition($id)->addTag('container.hot_path'); - } elseif (isset($this->noPreloadEvents[$event['event']])) { - ++$noPreload; - } + $listenersDefinition[] = $event; } - if ($noPreload && \count($events) === $noPreload) { - $container->getDefinition($id)->addTag('container.no_preload'); - } + yield $id => $listenersDefinition; } + } + + /** + * @return \Generator> + */ + private function collectSubscribers(ContainerBuilder $container): \Generator + { + $aliases = $this->getEventsAliases($container); $extractingDispatcher = new ExtractingEventDispatcher(); @@ -133,43 +360,54 @@ public function process(ContainerBuilder $container) } $class = $r->name; - $dispatcherDefinitions = []; + $dispatchers = []; foreach ($tags as $attributes) { - if (!isset($attributes['dispatcher']) || isset($dispatcherDefinitions[$attributes['dispatcher']])) { + if (!isset($attributes['dispatcher']) || \in_array($attributes['dispatcher'], $dispatchers, true)) { continue; } - $dispatcherDefinitions[$attributes['dispatcher']] = $container->findDefinition($attributes['dispatcher']); + $dispatchers[] = $attributes['dispatcher']; } - - if (!$dispatcherDefinitions) { - $dispatcherDefinitions = [$globalDispatcherDefinition]; + if (!$dispatchers) { + $dispatchers[] = 'event_dispatcher'; } - $noPreload = 0; + sort($dispatchers); + ExtractingEventDispatcher::$aliases = $aliases; ExtractingEventDispatcher::$subscriber = $class; $extractingDispatcher->addSubscriber($extractingDispatcher); - foreach ($extractingDispatcher->listeners as $args) { - $args[1] = [new ServiceClosureArgument(new Reference($id)), $args[1]]; - foreach ($dispatcherDefinitions as $dispatcherDefinition) { - $dispatcherDefinition->addMethodCall('addListener', $args); - } - if (isset($this->hotPathEvents[$args[0]])) { - $container->getDefinition($id)->addTag('container.hot_path'); - } elseif (isset($this->noPreloadEvents[$args[0]])) { - ++$noPreload; - } - } - if ($noPreload && \count($extractingDispatcher->listeners) === $noPreload) { - $container->getDefinition($id)->addTag('container.no_preload'); - } + yield $id => array_map( + static fn (array $args) => [ + 'dispatchers' => array_values(array_unique($dispatchers)), + 'event' => $args[0], + 'method' => $args[1], + 'priority' => $args[2], + 'serviceId' => $id, + ], + $extractingDispatcher->listeners + ); + $extractingDispatcher->listeners = []; ExtractingEventDispatcher::$aliases = []; } } + /** + * @return array + */ + private function getEventsAliases(ContainerBuilder $container): array + { + $aliases = []; + + if ($container->hasParameter('event_dispatcher.event_aliases')) { + $aliases = $container->getParameter('event_dispatcher.event_aliases'); + } + + return $aliases; + } + private function getEventFromTypeDeclaration(ContainerBuilder $container, string $id, string $method): string { if ( diff --git a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php index c18d863a98c12..fae98259530aa 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php @@ -21,6 +21,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass; +use Symfony\Component\EventDispatcher\DependencyInjection\InvalidBeforeAfterListenerDefinitionException; use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\Tests\Fixtures\CustomEvent; @@ -503,6 +504,269 @@ public function testOmitEventNameOnSubscriber() ]; $this->assertEquals($expectedCalls, $definition->getMethodCalls()); } + + public function testBeforeAfterListener() + { + $container = new ContainerBuilder(); + $container->register('listener', GenericListener::class)->addTag('kernel.event_listener', ['event' => 'foo', 'priority' => 5]); + $container->register('before', InvokableListenerService::class)->addTag('kernel.event_listener', ['event' => 'foo', 'before' => 'listener']); + $container->register('after', InvokableListenerService::class)->addTag('kernel.event_listener', ['event' => 'foo', 'after' => GenericListener::class]); + $container->register('before_full_definition', InvokableListenerService::class)->addTag('kernel.event_listener', ['event' => 'foo', 'before' => ['listener', '__invoke']]); + $container->register('event_dispatcher'); + + $registerListenersPass = new RegisterListenersPass(); + $registerListenersPass->process($container); + + $definition = $container->getDefinition('event_dispatcher'); + $expectedCalls = [ + [ + 'addListener', + [ + 'foo', + [new ServiceClosureArgument(new Reference('listener')), '__invoke'], + 5, + ], + ], + [ + 'addListener', + [ + 'foo', + [new ServiceClosureArgument(new Reference('before')), '__invoke'], + 6, + ], + ], + [ + 'addListener', + [ + 'foo', + [new ServiceClosureArgument(new Reference('after')), '__invoke'], + 4, + ], + ], + [ + 'addListener', + [ + 'foo', + [new ServiceClosureArgument(new Reference('before_full_definition')), '__invoke'], + 6, + ], + ], + ]; + $this->assertEquals($expectedCalls, $definition->getMethodCalls()); + } + + public function testBeforeAfterListenerWithMultipleEvents() + { + $container = new ContainerBuilder(); + $container->register('listener', MultipleListeners::class) + ->addTag('kernel.event_listener', ['event' => 'foo', 'priority' => 0, 'method' => 'onEvent1']) + ->addTag('kernel.event_listener', ['event' => 'bar', 'priority' => 10, 'method' => 'onEvent2']) + ; + $container->register('before', InvokableListenerService::class) + ->addTag('kernel.event_listener', ['event' => 'foo', 'before' => 'listener', 'method' => '__invoke']) + ->addTag('kernel.event_listener', ['event' => 'bar', 'before' => MultipleListeners::class, 'method' => 'onEvent']) + ; + $container->register('event_dispatcher'); + + $registerListenersPass = new RegisterListenersPass(); + $registerListenersPass->process($container); + + $definition = $container->getDefinition('event_dispatcher'); + $expectedCalls = [ + [ + 'addListener', + [ + 'foo', + [new ServiceClosureArgument(new Reference('listener')), 'onEvent1'], + 0, + ], + ], + [ + 'addListener', + [ + 'bar', + [new ServiceClosureArgument(new Reference('listener')), 'onEvent2'], + 10, + ], + ], + [ + 'addListener', + [ + 'foo', + [new ServiceClosureArgument(new Reference('before')), '__invoke'], + 1, + ], + ], + [ + 'addListener', + [ + 'bar', + [new ServiceClosureArgument(new Reference('before')), 'onEvent'], + 11, + ], + ], + ]; + $this->assertEquals($expectedCalls, $definition->getMethodCalls()); + } + + public function testChainedBeforeAfterListener() + { + $container = new ContainerBuilder(); + + $container->register('listener_1', GenericListener::class)->addTag('kernel.event_listener', ['event' => 'foo', 'before' => 'listener_2']); + $container->register('listener_2', GenericListener::class)->addTag('kernel.event_listener', ['event' => 'foo', 'before' => 'listener_3']); + $container->register('listener_3', GenericListener::class)->addTag('kernel.event_listener', ['event' => 'foo']); + + $container->register('event_dispatcher'); + + $registerListenersPass = new RegisterListenersPass(); + $registerListenersPass->process($container); + + $definition = $container->getDefinition('event_dispatcher'); + $expectedCalls = [ + [ + 'addListener', + [ + 'foo', + [new ServiceClosureArgument(new Reference('listener_1')), '__invoke'], + 2, + ], + ], + [ + 'addListener', + [ + 'foo', + [new ServiceClosureArgument(new Reference('listener_2')), '__invoke'], + 1, + ], + ], + [ + 'addListener', + [ + 'foo', + [new ServiceClosureArgument(new Reference('listener_3')), '__invoke'], + 0, + ], + ], + ]; + $this->assertEquals($expectedCalls, $definition->getMethodCalls()); + } + + /** + * @dataProvider beforeAfterErrorsProvider + */ + public function testBeforeAfterErrors(string $expectedErrorMessage, array $erroneousTagDefinition) + { + $this->expectException(InvalidBeforeAfterListenerDefinitionException::class); + $this->expectExceptionMessage($expectedErrorMessage); + + $container = new ContainerBuilder(); + $container->register('listener', GenericListener::class)->addTag('kernel.event_listener', ['event' => 'foo']); + $container->register('error_listener', InvokableListenerService::class)->addTag('kernel.event_listener', $erroneousTagDefinition); + $container->register('event_dispatcher'); + + $registerListenersPass = new RegisterListenersPass(); + $registerListenersPass->process($container); + } + + public static function beforeAfterErrorsProvider(): iterable + { + yield [ + 'Invalid before/after definition for service "error_listener": cannot use "after" and "before" at the same time.', + ['event' => 'foo', 'before' => 'listener', 'after' => 'listener'], + ]; + + yield [ + 'Invalid before/after definition for service "error_listener": when declaring as an array, first item must be a service id or a class and second item must be the method.', + ['event' => 'foo', 'before' => []], + ]; + + yield [ + 'Invalid before/after definition for service "error_listener": given definition "stdClass" is not a listener.', + ['event' => 'foo', 'before' => 'stdClass'], + ]; + + yield [ + sprintf('Invalid before/after definition for service "error_listener": given definition "%s" does not listen to the same event.', GenericListener::class), + ['event' => 'bar', 'before' => GenericListener::class], + ]; + + yield [ + sprintf('Invalid before/after definition for service "error_listener": given definition "%s::foo()" is not a listener.', GenericListener::class), + ['event' => 'foo', 'before' => [GenericListener::class, 'foo']], + ]; + + yield [ + 'Invalid before/after definition for service "error_listener": given definition "listener" does not listen to the same event.', + ['event' => 'bar', 'before' => 'listener'], + ]; + + yield [ + 'Invalid before/after definition for service "error_listener": given definition "listener::foo()" is not a listener.', + ['event' => 'foo', 'before' => ['listener', 'foo']], + ]; + + yield [ + 'Invalid before/after definition for service "error_listener": given definition "event_dispatcher" is not a listener.', + ['event' => 'bar', 'before' => 'event_dispatcher'], + ]; + + yield [ + 'Invalid before/after definition for service "error_listener": given definition "listener" is not handled by the same dispatchers.', + ['event' => 'foo', 'before' => 'listener', 'dispatcher' => 'some_dispatcher'], + ]; + } + + /** + * @dataProvider beforeAfterAmbiguousProvider + */ + public function testBeforeAfterAmbiguous(string $expectedErrorMessage, array $ambiguousTagDefinition) + { + $this->expectException(InvalidBeforeAfterListenerDefinitionException::class); + $this->expectExceptionMessage($expectedErrorMessage); + + $container = new ContainerBuilder(); + $container->register('listener', MultipleListeners::class) + ->addTag('kernel.event_listener', ['event' => 'foo', 'method' => 'onEvent1']) + ->addTag('kernel.event_listener', ['event' => 'foo', 'method' => 'onEvent2']) + ; + + $container->register('error_listener', InvokableListenerService::class)->addTag('kernel.event_listener', $ambiguousTagDefinition); + $container->register('event_dispatcher'); + + $registerListenersPass = new RegisterListenersPass(); + $registerListenersPass->process($container); + } + + public static function beforeAfterAmbiguousProvider(): iterable + { + yield [ + 'Invalid before/after definition for service "error_listener": given definition "listener" is ambiguous. Please specify the "method" attribute.', + ['event' => 'foo', 'before' => 'listener'], + ]; + + yield [ + sprintf('Invalid before/after definition for service "error_listener": given definition "%s" is ambiguous. Please specify the "method" attribute.', MultipleListeners::class), + ['event' => 'foo', 'after' => MultipleListeners::class], + ]; + } + + public function testBeforeAfterCircularError() + { + $this->expectException(InvalidBeforeAfterListenerDefinitionException::class); + $this->expectExceptionMessage('Invalid before/after definition for service "listener_1": circular reference detected.'); + + $container = new ContainerBuilder(); + + $container->register('listener_1', GenericListener::class)->addTag('kernel.event_listener', ['event' => 'foo', 'before' => 'listener_2']); + $container->register('listener_2', GenericListener::class)->addTag('kernel.event_listener', ['event' => 'foo', 'before' => 'listener_3']); + $container->register('listener_3', GenericListener::class)->addTag('kernel.event_listener', ['event' => 'foo', 'before' => 'listener_1']); + + $container->register('event_dispatcher'); + + $registerListenersPass = new RegisterListenersPass(); + $registerListenersPass->process($container); + } } class SubscriberService implements EventSubscriberInterface @@ -559,6 +823,17 @@ public function __invoke(object $event): void } } +final class MultipleListeners +{ + public function onEvent1(): void + { + } + + public function onEvent2(): void + { + } +} + final class IncompleteSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents(): array From 5e9d11b6923f4df656fbde654ed130d625f853ff Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Wed, 2 Aug 2023 17:41:03 +0200 Subject: [PATCH 2/3] fix stof review --- .../Attribute/AsEventListener.php | 4 +- ...BeforeAfterListenerDefinitionException.php | 81 ----------- .../RegisterListenersPass.php | 134 ++++++++++-------- .../RegisterListenersPassTest.php | 7 +- 4 files changed, 80 insertions(+), 146 deletions(-) delete mode 100644 src/Symfony/Component/EventDispatcher/DependencyInjection/InvalidBeforeAfterListenerDefinitionException.php diff --git a/src/Symfony/Component/EventDispatcher/Attribute/AsEventListener.php b/src/Symfony/Component/EventDispatcher/Attribute/AsEventListener.php index 44e32eeb5125a..9bda2f3c24c88 100644 --- a/src/Symfony/Component/EventDispatcher/Attribute/AsEventListener.php +++ b/src/Symfony/Component/EventDispatcher/Attribute/AsEventListener.php @@ -24,9 +24,9 @@ public function __construct( public ?string $method = null, public int $priority = 0, public ?string $dispatcher = null, - /** @var string|array{0: string, 1: string}|null */ + /** @param string|array{0: string, 1: string}|null $after */ public string|array|null $before = null, - /** @var string|array{0: string, 1: string}|null */ + /** @param string|array{0: string, 1: string}|null $after */ public string|array|null $after = null, ) { } diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/InvalidBeforeAfterListenerDefinitionException.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/InvalidBeforeAfterListenerDefinitionException.php deleted file mode 100644 index 2cc4a5c2c2ce6..0000000000000 --- a/src/Symfony/Component/EventDispatcher/DependencyInjection/InvalidBeforeAfterListenerDefinitionException.php +++ /dev/null @@ -1,81 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\EventDispatcher\DependencyInjection; - -use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; - -/** - * @psalm-import-type BeforeAfterDefinition from RegisterListenersPass - */ -final class InvalidBeforeAfterListenerDefinitionException extends InvalidArgumentException -{ - private function __construct(string $errorServiceId, string $message) - { - parent::__construct(sprintf('Invalid before/after definition for service "%s": %s', $errorServiceId, $message)); - } - - public static function beforeAndAfterAtSameTime(string $errorServiceId): self - { - return new self($errorServiceId, 'cannot use "after" and "before" at the same time.'); - } - - public static function circularReference(string $errorServiceId): self - { - return new self($errorServiceId, 'circular reference detected.'); - } - - public static function arrayDefinitionInvalid(string $errorServiceId): self - { - return new self($errorServiceId, 'when declaring as an array, first item must be a service id or a class and second item must be the method.'); - } - - /** - * @param BeforeAfterDefinition $beforeAfterDefinition - */ - public static function notAListener(string $errorServiceId, string|array $beforeAfterDefinition): self - { - return new self($errorServiceId, sprintf('given definition "%s" is not a listener.', self::beforeAfterDefinitionToString($beforeAfterDefinition))); - } - - /** - * @param BeforeAfterDefinition $beforeAfterDefinition - */ - public static function notSameEvent(string $errorServiceId, string|array $beforeAfterDefinition): self - { - return new self($errorServiceId, sprintf('given definition "%s" does not listen to the same event.', self::beforeAfterDefinitionToString($beforeAfterDefinition))); - } - - /** - * @param BeforeAfterDefinition $beforeAfterDefinition - */ - public static function notSameDispatchers(string $errorServiceId, string|array $beforeAfterDefinition): self - { - return new self($errorServiceId, sprintf('given definition "%s" is not handled by the same dispatchers.', self::beforeAfterDefinitionToString($beforeAfterDefinition))); - } - - public static function ambiguousDefinition(string $errorServiceId, string $beforeAfterDefinition): self - { - return new self($errorServiceId, sprintf('given definition "%s" is ambiguous. Please specify the "method" attribute.', $beforeAfterDefinition)); - } - - /** - * @param BeforeAfterDefinition $beforeAfterDefinition - */ - private static function beforeAfterDefinitionToString(string|array $beforeAfterDefinition): string - { - if (\is_string($beforeAfterDefinition)) { - return $beforeAfterDefinition; - } - - return sprintf('%s::%s()', $beforeAfterDefinition[0], $beforeAfterDefinition[1]); - } -} diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php index 78149adeb6b7d..52337da6efc0c 100644 --- a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php @@ -22,30 +22,12 @@ /** * Compiler pass to register tagged services for an event dispatcher. - * - * @psalm-type BeforeAfterDefinition = string|array{0: string, 1: string} - * @psalm-type ListenerDefinition = array{serviceId: string, event: string, method: string, before?: BeforeAfterDefinition, after?: BeforeAfterDefinition, priority?: int, dispatchers: list} - * @psalm-type AllListenersDefinition = array> */ class RegisterListenersPass implements CompilerPassInterface { private array $hotPathEvents = []; private array $noPreloadEvents = []; - /** - * $allListenersMap['event_name']['listener_service_id']['method'] => $listenerDefinition. - * - * @var array>> - */ - private array $allListenersMap = []; - - /** - * $listenerClassesMap['listener_FQCN']['event_name'][] => array{serviceId: string, method:string}. - * - * @var array>> - */ - private array $listenerClassesMap = []; - /** * @return $this */ @@ -81,76 +63,106 @@ public function process(ContainerBuilder $container) iterator_to_array($this->collectSubscribers($container)), ); - $this->initializeListenersMaps($container, $allListenerDefinitions); + // $allListenersMap['event_name']['listener_service_id']['method'] => $listenerDefinition. + $allListenersMap = $this->initializeAllListenersMaps($allListenerDefinitions); + + // $listenerClassesMap['listener_FQCN']['event_name'][] => array{serviceId: string, method:string}. + $listenerClassesMap = $this->initializeListenersClassesMap($container, $allListenerDefinitions); - $this->handleBeforeAfter($allListenerDefinitions, $container); + $this->handleBeforeAfter($allListenerDefinitions, $container, $allListenersMap, $listenerClassesMap); $this->registerListeners($container, $allListenerDefinitions); } /** - * @param AllListenersDefinition $allListenerDefinitions + * @param array}>> $allListenerDefinitions + * + * @return array}>>> */ - private function initializeListenersMaps(ContainerBuilder $container, array $allListenerDefinitions): void + private function initializeAllListenersMaps(array $allListenerDefinitions): array { + $allListenersMap = []; + foreach ($allListenerDefinitions as $listenerDefinitions) { foreach ($listenerDefinitions as $listenerDefinition) { - $this->allListenersMap[$listenerDefinition['event']][$listenerDefinition['serviceId']][$listenerDefinition['method']] = $listenerDefinition; + $allListenersMap[$listenerDefinition['event']][$listenerDefinition['serviceId']][$listenerDefinition['method']] = $listenerDefinition; + } + } + + return $allListenersMap; + } + /** + * @param array}>> $allListenerDefinitions + * + * @return array>> + */ + private function initializeListenersClassesMap(ContainerBuilder $container, array $allListenerDefinitions): array + { + $listenerClassesMap = []; + + foreach ($allListenerDefinitions as $listenerDefinitions) { + foreach ($listenerDefinitions as $listenerDefinition) { $listenerClass = $container->getDefinition($listenerDefinition['serviceId'])->getClass(); if ($listenerClass) { - $this->listenerClassesMap[$listenerClass][$listenerDefinition['event']] ??= []; - $this->listenerClassesMap[$listenerClass][$listenerDefinition['event']][] = [ + $listenerClassesMap[$listenerClass][$listenerDefinition['event']] ??= []; + $listenerClassesMap[$listenerClass][$listenerDefinition['event']][] = [ 'serviceId' => $listenerDefinition['serviceId'], 'method' => $listenerDefinition['method'], ]; } } } + + return $listenerClassesMap; } /** - * @param AllListenersDefinition $allListenerDefinitions + * @param array}>> $allListenerDefinitions + * @param array}>>> $allListenersMap + * @param array>> $listenerClassesMap */ - private function handleBeforeAfter(array &$allListenerDefinitions, ContainerBuilder $container): void + private function handleBeforeAfter(array &$allListenerDefinitions, ContainerBuilder $container, array $allListenersMap, array $listenerClassesMap): void { foreach ($allListenerDefinitions as &$listenerDefinitions) { foreach ($listenerDefinitions as &$listenerDefinition) { if (isset($listenerDefinition['before']) && isset($listenerDefinition['after'])) { - throw InvalidBeforeAfterListenerDefinitionException::beforeAndAfterAtSameTime($listenerDefinition['serviceId']); + throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": cannot use "after" and "before" at the same time.', $listenerDefinition['serviceId'])); } if (isset($listenerDefinition['before']) || isset($listenerDefinition['after'])) { - $listenerDefinition['priority'] = $this->computeBeforeAfterPriorities($container, $listenerDefinition); + $listenerDefinition['priority'] = $this->computeBeforeAfterPriorities($container, $listenerDefinition, $allListenersMap, $listenerClassesMap); // register the new priority in listeners map unset($listenerDefinition['before'], $listenerDefinition['after']); - $this->allListenersMap[$listenerDefinition['event']][$listenerDefinition['serviceId']][$listenerDefinition['method']] = $listenerDefinition; + $allListenersMap[$listenerDefinition['event']][$listenerDefinition['serviceId']][$listenerDefinition['method']] = $listenerDefinition; } } } } /** - * @param ListenerDefinition $listenerDefinition - * @param array $alreadyVisited + * @param array{serviceId: string, event: string, method: string, before?: string|array{0: string, 1: string}, after?: string|array{0: string, 1: string}, priority?: int, dispatchers: list} $listenerDefinition + * @param array}>>> $allListenersMap + * @param array>> $listenerClassesMap + * @param array $alreadyVisited */ - private function computeBeforeAfterPriorities(ContainerBuilder $container, array $listenerDefinition, array $alreadyVisited = []): int + private function computeBeforeAfterPriorities(ContainerBuilder $container, array $listenerDefinition, array $allListenersMap, array $listenerClassesMap, array $alreadyVisited = []): int { // Prevent circular references $listenerName = sprintf('%s::%s', $listenerDefinition['serviceId'], $listenerDefinition['method']); if ($alreadyVisited[$listenerName] ?? false) { - throw InvalidBeforeAfterListenerDefinitionException::circularReference($listenerDefinition['serviceId']); + throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": circular reference detected.', $listenerDefinition['serviceId'])); } $alreadyVisited[$listenerName] = true; if (isset($listenerDefinition['before']) || isset($listenerDefinition['after'])) { - ['serviceId' => $beforeAfterServiceId, 'method' => $beforeAfterMethod] = $this->normalizeBeforeAfter($container, $listenerDefinition); + ['serviceId' => $beforeAfterServiceId, 'method' => $beforeAfterMethod] = $this->normalizeBeforeAfter($container, $listenerDefinition, $allListenersMap, $listenerClassesMap); - $beforeAfterListenerDefinition = $this->allListenersMap[$listenerDefinition['event']][$beforeAfterServiceId][$beforeAfterMethod]; + $beforeAfterListenerDefinition = $allListenersMap[$listenerDefinition['event']][$beforeAfterServiceId][$beforeAfterMethod]; - $priority = $this->computeBeforeAfterPriorities($container, $beforeAfterListenerDefinition, $alreadyVisited); + $priority = $this->computeBeforeAfterPriorities($container, $beforeAfterListenerDefinition, $allListenersMap, $listenerClassesMap, $alreadyVisited); return isset($listenerDefinition['before']) ? $priority + 1 : $priority - 1; } @@ -159,27 +171,29 @@ private function computeBeforeAfterPriorities(ContainerBuilder $container, array } /** - * @param ListenerDefinition $listenerDefinition + * @param array{serviceId: string, event: string, method: string, before?: string|array{0: string, 1: string}, after?: string|array{0: string, 1: string}, priority?: int, dispatchers: list} $listenerDefinition + * @param array}>>> $allListenersMap + * @param array>> $listenerClassesMap * * @return array{serviceId: string, method: string} * * before/after can be defined as: class-string, service-id, or array{class?: class-string, service?: service-id, method?: string} * let's normalize it, and resolve the method if not given (or rise an exception if ambiguous) */ - private function normalizeBeforeAfter(ContainerBuilder $container, array $listenerDefinition): array + private function normalizeBeforeAfter(ContainerBuilder $container, array $listenerDefinition, array $allListenersMap, array $listenerClassesMap): array { $beforeAfterDefinition = $listenerDefinition['before'] ?? $listenerDefinition['after']; $id = $listenerDefinition['serviceId']; $event = $listenerDefinition['event']; - $listenersForEvent = $this->allListenersMap[$event]; + $listenersForEvent = $allListenersMap[$event]; $beforeAfterMethod = null; $normalizedBeforeAfter = null; if (\is_array($beforeAfterDefinition)) { if (!array_is_list($beforeAfterDefinition) || 2 !== \count($beforeAfterDefinition)) { - throw InvalidBeforeAfterListenerDefinitionException::arrayDefinitionInvalid($id); + throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": when declaring as an array, first item must be a service id or a class and second item must be the method.', $id)); } $beforeAfterMethod = $beforeAfterDefinition[1]; @@ -188,20 +202,22 @@ private function normalizeBeforeAfter(ContainerBuilder $container, array $listen $beforeAfterServiceOrClass = $beforeAfterDefinition; } + $beforeAfterDefinitionAsString = \is_string($beforeAfterDefinition) ? $beforeAfterDefinition : sprintf('%s::%s()', $beforeAfterDefinition[0], $beforeAfterDefinition[1]); + if (class_exists($beforeAfterServiceOrClass) && !$container->has($beforeAfterServiceOrClass)) { - if (!isset($this->listenerClassesMap[$beforeAfterServiceOrClass])) { - throw InvalidBeforeAfterListenerDefinitionException::notAListener($id, $beforeAfterDefinition); + if (!isset($listenerClassesMap[$beforeAfterServiceOrClass])) { + throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": given definition "%s" is not a listener.', $id, $beforeAfterDefinitionAsString)); } - if (!isset($this->listenerClassesMap[$beforeAfterServiceOrClass][$event])) { - throw InvalidBeforeAfterListenerDefinitionException::notSameEvent($id, $beforeAfterDefinition); + if (!isset($listenerClassesMap[$beforeAfterServiceOrClass][$event])) { + throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": given definition "%s" does not listen to the same event.', $id, $beforeAfterDefinitionAsString)); } - $listenersForClassAndEvent = $this->listenerClassesMap[$beforeAfterServiceOrClass][$event]; + $listenersForClassAndEvent = $listenerClassesMap[$beforeAfterServiceOrClass][$event]; if (!$beforeAfterMethod) { if (1 < \count($listenersForClassAndEvent)) { - throw InvalidBeforeAfterListenerDefinitionException::ambiguousDefinition($id, $beforeAfterServiceOrClass); + throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": given definition "%s" is ambiguous. Please specify the "method" attribute.', $id, $beforeAfterServiceOrClass)); } $normalizedBeforeAfter = $listenersForClassAndEvent[0]; @@ -214,7 +230,7 @@ private function normalizeBeforeAfter(ContainerBuilder $container, array $listen } if (!isset($normalizedBeforeAfter)) { - throw InvalidBeforeAfterListenerDefinitionException::notAListener($id, $beforeAfterDefinition); + throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": given definition "%s" is not a listener.', $id, $beforeAfterDefinitionAsString)); } } } elseif ( @@ -222,35 +238,35 @@ private function normalizeBeforeAfter(ContainerBuilder $container, array $listen && (($def = $container->findDefinition($beforeAfterServiceOrClass))->hasTag('kernel.event_listener') || $def->hasTag('kernel.event_subscriber')) ) { if (!isset($listenersForEvent[$beforeAfterServiceOrClass])) { - throw InvalidBeforeAfterListenerDefinitionException::notSameEvent($id, $beforeAfterDefinition); + throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": given definition "%s" does not listen to the same event.', $id, $beforeAfterDefinitionAsString)); } if (!$beforeAfterMethod) { if (1 < \count($listenersForEvent[$beforeAfterServiceOrClass])) { - throw InvalidBeforeAfterListenerDefinitionException::ambiguousDefinition($id, $beforeAfterServiceOrClass); + throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": given definition "%s" is ambiguous. Please specify the "method" attribute.', $id, $beforeAfterServiceOrClass)); } $beforeAfterMethod = array_key_first($listenersForEvent[$beforeAfterServiceOrClass]); } else { if (!isset($listenersForEvent[$beforeAfterServiceOrClass][$beforeAfterMethod])) { - throw InvalidBeforeAfterListenerDefinitionException::notAListener($id, $beforeAfterDefinition); + throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": given definition "%s" is not a listener.', $id, $beforeAfterDefinitionAsString)); } } $normalizedBeforeAfter = ['serviceId' => $beforeAfterServiceOrClass, 'method' => $beforeAfterMethod]; } else { - throw InvalidBeforeAfterListenerDefinitionException::notAListener($id, $beforeAfterDefinition); + throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": given definition "%s" is not a listener.', $id, $beforeAfterDefinitionAsString)); } if ($listenersForEvent[$normalizedBeforeAfter['serviceId']][$normalizedBeforeAfter['method']]['dispatchers'] !== $listenerDefinition['dispatchers']) { - throw InvalidBeforeAfterListenerDefinitionException::notSameDispatchers($id, $beforeAfterDefinition); + throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": given definition "%s" is not handled by the same dispatchers.', $id, $beforeAfterDefinitionAsString)); } return $normalizedBeforeAfter; } /** - * @param AllListenersDefinition $allListenerDefinitions + * @param array}>> $allListenerDefinitions */ public function registerListeners(ContainerBuilder $container, array $allListenerDefinitions): void { @@ -313,9 +329,9 @@ private function collectListeners(ContainerBuilder $container): \Generator if (!isset($event['method'])) { $event['method'] = 'on'.preg_replace_callback([ - '/(?<=\b|_)[a-z]/i', - '/[^a-z0-9]/i', - ], fn ($matches) => strtoupper($matches[0]), $event['event']); + '/(?<=\b|_)[a-z]/i', + '/[^a-z0-9]/i', + ], fn ($matches) => strtoupper($matches[0]), $event['event']); $event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']); if (null !== ($class = $container->getDefinition($id)->getClass()) && ($r = $container->getReflectionClass($class, false)) && !$r->hasMethod($event['method'])) { @@ -338,7 +354,7 @@ private function collectListeners(ContainerBuilder $container): \Generator } /** - * @return \Generator> + * @return \Generator}>> */ private function collectSubscribers(ContainerBuilder $container): \Generator { @@ -402,7 +418,7 @@ private function getEventsAliases(ContainerBuilder $container): array $aliases = []; if ($container->hasParameter('event_dispatcher.event_aliases')) { - $aliases = $container->getParameter('event_dispatcher.event_aliases'); + $aliases = $container->getParameter('event_dispatcher.event_aliases') ?? []; } return $aliases; diff --git a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php index fae98259530aa..26965226e973c 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php @@ -21,7 +21,6 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass; -use Symfony\Component\EventDispatcher\DependencyInjection\InvalidBeforeAfterListenerDefinitionException; use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\Tests\Fixtures\CustomEvent; @@ -657,7 +656,7 @@ public function testChainedBeforeAfterListener() */ public function testBeforeAfterErrors(string $expectedErrorMessage, array $erroneousTagDefinition) { - $this->expectException(InvalidBeforeAfterListenerDefinitionException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage($expectedErrorMessage); $container = new ContainerBuilder(); @@ -722,7 +721,7 @@ public static function beforeAfterErrorsProvider(): iterable */ public function testBeforeAfterAmbiguous(string $expectedErrorMessage, array $ambiguousTagDefinition) { - $this->expectException(InvalidBeforeAfterListenerDefinitionException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage($expectedErrorMessage); $container = new ContainerBuilder(); @@ -753,7 +752,7 @@ public static function beforeAfterAmbiguousProvider(): iterable public function testBeforeAfterCircularError() { - $this->expectException(InvalidBeforeAfterListenerDefinitionException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid before/after definition for service "listener_1": circular reference detected.'); $container = new ContainerBuilder(); From 142df438fac3c82a066b28889a9c0210a7cf901a Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Wed, 20 Sep 2023 19:30:29 +0200 Subject: [PATCH 3/3] Use object oriented approach --- .../ListenerDefinition.php | 80 ++++++ .../ListenerDefinitionsIterator.php | 97 +++++++ .../RegisterListenersPass.php | 264 ++---------------- .../RegisterListenersPassTest.php | 78 +++--- 4 files changed, 247 insertions(+), 272 deletions(-) create mode 100644 src/Symfony/Component/EventDispatcher/DependencyInjection/ListenerDefinition.php create mode 100644 src/Symfony/Component/EventDispatcher/DependencyInjection/ListenerDefinitionsIterator.php diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/ListenerDefinition.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/ListenerDefinition.php new file mode 100644 index 0000000000000..c1e88a52618c4 --- /dev/null +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/ListenerDefinition.php @@ -0,0 +1,80 @@ +priorityModifier = 0; + $this->beforeAfterMethod = null; + $this->beforeAfterService = null; + + return; + } + + $this->priorityModifier = null !== $before ? 1 : -1; + + $beforeAfterDefinition = $before ?? $after; + + if (\is_array($beforeAfterDefinition)) { + if (!array_is_list($beforeAfterDefinition) || 2 !== \count($beforeAfterDefinition)) { + throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": when declaring as an array, first item must be a service id and second item must be the method.', $this->serviceId)); + } + + $this->beforeAfterMethod = $beforeAfterDefinition[1]; + $this->beforeAfterService = $beforeAfterDefinition[0]; + } else { + $this->beforeAfterMethod = null; + $this->beforeAfterService = $beforeAfterDefinition; + } + } + + public function withPriority(int $priority): self + { + return new self( + $this->serviceId, + $this->event, + $this->method, + $priority, + $this->dispatchers, + $this->before, + $this->after, + ); + } + + public function name(): string + { + return "{$this->serviceId}::{$this->method}"; + } + + public function printableBeforeAfterDefinition(): string|null + { + return match (true){ + null !== $this->beforeAfterMethod => sprintf('%s::%s()', $this->beforeAfterService, $this->beforeAfterMethod), + null !== $this->beforeAfterService => $this->beforeAfterService, + default => null, + }; + } +} diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/ListenerDefinitionsIterator.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/ListenerDefinitionsIterator.php new file mode 100644 index 0000000000000..7d57498ff258d --- /dev/null +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/ListenerDefinitionsIterator.php @@ -0,0 +1,97 @@ + $listenerDefinitions + */ + public function __construct(array $listenerDefinitions, private readonly ContainerBuilder $container) + { + $this->listenerDefinitions = $listenerDefinitions; + } + + /** + * @return array> + */ + public function iterate(): array + { + $listeners = []; + + foreach ($this->listenerDefinitions as $listener) { + $listeners[$listener->serviceId] ??= []; + $listeners[$listener->serviceId][] = $listener->withPriority($this->getPriorityFor($listener)); + } + + return $listeners; + } + + private function getPriorityFor(ListenerDefinition $listener, array $alreadyVisited = []): int + { + if ($alreadyVisited[$listener->name()] ?? false) { + throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": circular reference detected.', array_key_first($alreadyVisited))); + } + + $alreadyVisited[$listener->name()] = true; + + if (!$listener->beforeAfterService) { + return $listener->priority; + } + + $beforeAfterListeners = $this->matchingBeforeAfterListeners($listener); + + $beforeAfterListener = match (true) { + !$beforeAfterListeners => throw new InvalidArgumentException( + sprintf('Invalid before/after definition for service "%s": "%s" does not listen to the same event.', $listener->serviceId, $listener->printableBeforeAfterDefinition()) + ), + !$listener->beforeAfterMethod && count($beforeAfterListeners) === 1 => current($beforeAfterListeners), + !$listener->beforeAfterMethod && count($beforeAfterListeners) > 1 => throw new InvalidArgumentException( + sprintf('Invalid before/after definition for service "%s": "%s" has multiple methods. Please specify the "method" attribute.', $listener->serviceId, $listener->printableBeforeAfterDefinition()) + ), + $listener->beforeAfterMethod && !isset($beforeAfterListeners[$listener->beforeAfterMethod]) => throw new InvalidArgumentException( + sprintf('Invalid before/after definition for service "%s": method "%s" does not exist or is not a listener.', $listener->serviceId, $listener->printableBeforeAfterDefinition()) + ), + $listener->beforeAfterMethod && isset($beforeAfterListeners[$listener->beforeAfterMethod]) => $beforeAfterListeners[$listener->beforeAfterMethod], + default => new \LogicException('This should never happen') + }; + + if ($beforeAfterListener->dispatchers !== $listener->dispatchers) { + throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": "%s" is not handled by the same dispatchers.', $listener->serviceId, $listener->printableBeforeAfterDefinition())); + } + + return $this->getPriorityFor($beforeAfterListener, $alreadyVisited) + $listener->priorityModifier; + } + + /** + * @return array + */ + private function matchingBeforeAfterListeners(ListenerDefinition $listener): array + { + $beforeAfterService = $listener->beforeAfterService; + + if ( + $this->container->has($beforeAfterService) + && (($def = $this->container->findDefinition($beforeAfterService))->hasTag('kernel.event_listener') || $def->hasTag('kernel.event_subscriber')) + ) { + $listenersWithServiceId = array_filter( + $this->listenerDefinitions, + static fn(ListenerDefinition $listenerDefinition) => $listenerDefinition->serviceId === $beforeAfterService && $listenerDefinition->event === $listener->event + ); + + return array_combine( + array_map(static fn(ListenerDefinition $listenerDefinition) => $listenerDefinition->method, $listenersWithServiceId), + $listenersWithServiceId, + ); + } + + throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": "%s" is not a listener.', $listener->serviceId, $listener->printableBeforeAfterDefinition())); + } +} diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php index 52337da6efc0c..9dca515574b05 100644 --- a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php @@ -57,227 +57,20 @@ public function process(ContainerBuilder $container) return; } - // collect all listeners, and prevent keys overriding for a very unlikely case where a service is both a listener and a subscriber - $allListenerDefinitions = array_merge_recursive( - iterator_to_array($this->collectListeners($container)), - iterator_to_array($this->collectSubscribers($container)), + $listerDefinitions = new ListenerDefinitionsIterator([ + ...iterator_to_array($this->collectListeners($container)), + ...iterator_to_array($this->collectSubscribers($container)), + ], $container ); - // $allListenersMap['event_name']['listener_service_id']['method'] => $listenerDefinition. - $allListenersMap = $this->initializeAllListenersMaps($allListenerDefinitions); - - // $listenerClassesMap['listener_FQCN']['event_name'][] => array{serviceId: string, method:string}. - $listenerClassesMap = $this->initializeListenersClassesMap($container, $allListenerDefinitions); - - $this->handleBeforeAfter($allListenerDefinitions, $container, $allListenersMap, $listenerClassesMap); - - $this->registerListeners($container, $allListenerDefinitions); - } - - /** - * @param array}>> $allListenerDefinitions - * - * @return array}>>> - */ - private function initializeAllListenersMaps(array $allListenerDefinitions): array - { - $allListenersMap = []; - - foreach ($allListenerDefinitions as $listenerDefinitions) { - foreach ($listenerDefinitions as $listenerDefinition) { - $allListenersMap[$listenerDefinition['event']][$listenerDefinition['serviceId']][$listenerDefinition['method']] = $listenerDefinition; - } - } - - return $allListenersMap; - } - - /** - * @param array}>> $allListenerDefinitions - * - * @return array>> - */ - private function initializeListenersClassesMap(ContainerBuilder $container, array $allListenerDefinitions): array - { - $listenerClassesMap = []; - - foreach ($allListenerDefinitions as $listenerDefinitions) { - foreach ($listenerDefinitions as $listenerDefinition) { - $listenerClass = $container->getDefinition($listenerDefinition['serviceId'])->getClass(); - - if ($listenerClass) { - $listenerClassesMap[$listenerClass][$listenerDefinition['event']] ??= []; - $listenerClassesMap[$listenerClass][$listenerDefinition['event']][] = [ - 'serviceId' => $listenerDefinition['serviceId'], - 'method' => $listenerDefinition['method'], - ]; - } - } - } - - return $listenerClassesMap; - } - - /** - * @param array}>> $allListenerDefinitions - * @param array}>>> $allListenersMap - * @param array>> $listenerClassesMap - */ - private function handleBeforeAfter(array &$allListenerDefinitions, ContainerBuilder $container, array $allListenersMap, array $listenerClassesMap): void - { - foreach ($allListenerDefinitions as &$listenerDefinitions) { - foreach ($listenerDefinitions as &$listenerDefinition) { - if (isset($listenerDefinition['before']) && isset($listenerDefinition['after'])) { - throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": cannot use "after" and "before" at the same time.', $listenerDefinition['serviceId'])); - } - - if (isset($listenerDefinition['before']) || isset($listenerDefinition['after'])) { - $listenerDefinition['priority'] = $this->computeBeforeAfterPriorities($container, $listenerDefinition, $allListenersMap, $listenerClassesMap); - - // register the new priority in listeners map - unset($listenerDefinition['before'], $listenerDefinition['after']); - $allListenersMap[$listenerDefinition['event']][$listenerDefinition['serviceId']][$listenerDefinition['method']] = $listenerDefinition; - } - } - } - } - - /** - * @param array{serviceId: string, event: string, method: string, before?: string|array{0: string, 1: string}, after?: string|array{0: string, 1: string}, priority?: int, dispatchers: list} $listenerDefinition - * @param array}>>> $allListenersMap - * @param array>> $listenerClassesMap - * @param array $alreadyVisited - */ - private function computeBeforeAfterPriorities(ContainerBuilder $container, array $listenerDefinition, array $allListenersMap, array $listenerClassesMap, array $alreadyVisited = []): int - { - // Prevent circular references - $listenerName = sprintf('%s::%s', $listenerDefinition['serviceId'], $listenerDefinition['method']); - if ($alreadyVisited[$listenerName] ?? false) { - throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": circular reference detected.', $listenerDefinition['serviceId'])); - } - $alreadyVisited[$listenerName] = true; - - if (isset($listenerDefinition['before']) || isset($listenerDefinition['after'])) { - ['serviceId' => $beforeAfterServiceId, 'method' => $beforeAfterMethod] = $this->normalizeBeforeAfter($container, $listenerDefinition, $allListenersMap, $listenerClassesMap); - - $beforeAfterListenerDefinition = $allListenersMap[$listenerDefinition['event']][$beforeAfterServiceId][$beforeAfterMethod]; - - $priority = $this->computeBeforeAfterPriorities($container, $beforeAfterListenerDefinition, $allListenersMap, $listenerClassesMap, $alreadyVisited); - - return isset($listenerDefinition['before']) ? $priority + 1 : $priority - 1; - } - - return $listenerDefinition['priority'] ?? 0; - } - - /** - * @param array{serviceId: string, event: string, method: string, before?: string|array{0: string, 1: string}, after?: string|array{0: string, 1: string}, priority?: int, dispatchers: list} $listenerDefinition - * @param array}>>> $allListenersMap - * @param array>> $listenerClassesMap - * - * @return array{serviceId: string, method: string} - * - * before/after can be defined as: class-string, service-id, or array{class?: class-string, service?: service-id, method?: string} - * let's normalize it, and resolve the method if not given (or rise an exception if ambiguous) - */ - private function normalizeBeforeAfter(ContainerBuilder $container, array $listenerDefinition, array $allListenersMap, array $listenerClassesMap): array - { - $beforeAfterDefinition = $listenerDefinition['before'] ?? $listenerDefinition['after']; - $id = $listenerDefinition['serviceId']; - $event = $listenerDefinition['event']; - - $listenersForEvent = $allListenersMap[$event]; - - $beforeAfterMethod = null; - $normalizedBeforeAfter = null; - - if (\is_array($beforeAfterDefinition)) { - if (!array_is_list($beforeAfterDefinition) || 2 !== \count($beforeAfterDefinition)) { - throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": when declaring as an array, first item must be a service id or a class and second item must be the method.', $id)); - } - - $beforeAfterMethod = $beforeAfterDefinition[1]; - $beforeAfterServiceOrClass = $beforeAfterDefinition[0]; - } else { - $beforeAfterServiceOrClass = $beforeAfterDefinition; - } - - $beforeAfterDefinitionAsString = \is_string($beforeAfterDefinition) ? $beforeAfterDefinition : sprintf('%s::%s()', $beforeAfterDefinition[0], $beforeAfterDefinition[1]); - - if (class_exists($beforeAfterServiceOrClass) && !$container->has($beforeAfterServiceOrClass)) { - if (!isset($listenerClassesMap[$beforeAfterServiceOrClass])) { - throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": given definition "%s" is not a listener.', $id, $beforeAfterDefinitionAsString)); - } - - if (!isset($listenerClassesMap[$beforeAfterServiceOrClass][$event])) { - throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": given definition "%s" does not listen to the same event.', $id, $beforeAfterDefinitionAsString)); - } - - $listenersForClassAndEvent = $listenerClassesMap[$beforeAfterServiceOrClass][$event]; - - if (!$beforeAfterMethod) { - if (1 < \count($listenersForClassAndEvent)) { - throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": given definition "%s" is ambiguous. Please specify the "method" attribute.', $id, $beforeAfterServiceOrClass)); - } - - $normalizedBeforeAfter = $listenersForClassAndEvent[0]; - } else { - foreach ($listenersForClassAndEvent as ['serviceId' => $serviceId, 'method' => $methodFromListenerDefinition]) { - if ($methodFromListenerDefinition === $beforeAfterMethod) { - $normalizedBeforeAfter = ['serviceId' => $serviceId, 'method' => $beforeAfterMethod]; - break; - } - } - - if (!isset($normalizedBeforeAfter)) { - throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": given definition "%s" is not a listener.', $id, $beforeAfterDefinitionAsString)); - } - } - } elseif ( - $container->has($beforeAfterServiceOrClass) - && (($def = $container->findDefinition($beforeAfterServiceOrClass))->hasTag('kernel.event_listener') || $def->hasTag('kernel.event_subscriber')) - ) { - if (!isset($listenersForEvent[$beforeAfterServiceOrClass])) { - throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": given definition "%s" does not listen to the same event.', $id, $beforeAfterDefinitionAsString)); - } - - if (!$beforeAfterMethod) { - if (1 < \count($listenersForEvent[$beforeAfterServiceOrClass])) { - throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": given definition "%s" is ambiguous. Please specify the "method" attribute.', $id, $beforeAfterServiceOrClass)); - } - - $beforeAfterMethod = array_key_first($listenersForEvent[$beforeAfterServiceOrClass]); - } else { - if (!isset($listenersForEvent[$beforeAfterServiceOrClass][$beforeAfterMethod])) { - throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": given definition "%s" is not a listener.', $id, $beforeAfterDefinitionAsString)); - } - } - - $normalizedBeforeAfter = ['serviceId' => $beforeAfterServiceOrClass, 'method' => $beforeAfterMethod]; - } else { - throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": given definition "%s" is not a listener.', $id, $beforeAfterDefinitionAsString)); - } - - if ($listenersForEvent[$normalizedBeforeAfter['serviceId']][$normalizedBeforeAfter['method']]['dispatchers'] !== $listenerDefinition['dispatchers']) { - throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": given definition "%s" is not handled by the same dispatchers.', $id, $beforeAfterDefinitionAsString)); - } - - return $normalizedBeforeAfter; - } - - /** - * @param array}>> $allListenerDefinitions - */ - public function registerListeners(ContainerBuilder $container, array $allListenerDefinitions): void - { $globalDispatcherDefinition = $container->findDefinition('event_dispatcher'); - foreach ($allListenerDefinitions as $id => $listenerDefinitions) { + foreach ($listerDefinitions->iterate() as $id => $listenerDefinitions) { $noPreload = 0; foreach ($listenerDefinitions as $listenerDefinition) { $dispatcherDefinitions = []; - foreach ($listenerDefinition['dispatchers'] as $dispatcher) { + foreach ($listenerDefinition->dispatchers as $dispatcher) { $dispatcherDefinitions[] = 'event_dispatcher' === $dispatcher ? $globalDispatcherDefinition : $container->findDefinition($dispatcher); } @@ -285,16 +78,16 @@ public function registerListeners(ContainerBuilder $container, array $allListene $dispatcherDefinition->addMethodCall( 'addListener', [ - $listenerDefinition['event'], - [new ServiceClosureArgument(new Reference($id)), $listenerDefinition['method']], - $listenerDefinition['priority'] ?? 0, + $listenerDefinition->event, + [new ServiceClosureArgument(new Reference($id)), $listenerDefinition->method], + $listenerDefinition->priority ?? 0, ] ); } - if (isset($this->hotPathEvents[$listenerDefinition['event']])) { + if (isset($this->hotPathEvents[$listenerDefinition->event])) { $container->getDefinition($id)->addTag('container.hot_path'); - } elseif (isset($this->noPreloadEvents[$listenerDefinition['event']])) { + } elseif (isset($this->noPreloadEvents[$listenerDefinition->event])) { ++$noPreload; } } @@ -313,8 +106,6 @@ private function collectListeners(ContainerBuilder $container): \Generator $aliases = $this->getEventsAliases($container); foreach ($container->findTaggedServiceIds('kernel.event_listener', true) as $id => $events) { - $listenersDefinition = []; - foreach ($events as $event) { if (!isset($event['event'])) { if ($container->getDefinition($id)->hasTag('kernel.event_subscriber')) { @@ -346,10 +137,16 @@ private function collectListeners(ContainerBuilder $container): \Generator $event['serviceId'] = $id; unset($event['dispatcher']); - $listenersDefinition[] = $event; + yield new ListenerDefinition( + serviceId: $id, + event: $event['event'], + method: $event['method'], + priority: $event['priority'] ?? 0, + dispatchers: $event['dispatchers'], + before: $event['before'] ?? null, + after: $event['after'] ?? null, + ); } - - yield $id => $listenersDefinition; } } @@ -394,16 +191,17 @@ private function collectSubscribers(ContainerBuilder $container): \Generator ExtractingEventDispatcher::$subscriber = $class; $extractingDispatcher->addSubscriber($extractingDispatcher); - yield $id => array_map( - static fn (array $args) => [ - 'dispatchers' => array_values(array_unique($dispatchers)), - 'event' => $args[0], - 'method' => $args[1], - 'priority' => $args[2], - 'serviceId' => $id, - ], - $extractingDispatcher->listeners - ); + foreach ($extractingDispatcher->listeners as $listener) { + yield new ListenerDefinition( + serviceId: $id, + event: $listener[0], + method: $listener[1], + priority: $listener[2], + dispatchers: array_values(array_unique($dispatchers)), + before: null, + after: null, + ); + } $extractingDispatcher->listeners = []; ExtractingEventDispatcher::$aliases = []; diff --git a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php index 26965226e973c..0d511f3648528 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php @@ -509,7 +509,7 @@ public function testBeforeAfterListener() $container = new ContainerBuilder(); $container->register('listener', GenericListener::class)->addTag('kernel.event_listener', ['event' => 'foo', 'priority' => 5]); $container->register('before', InvokableListenerService::class)->addTag('kernel.event_listener', ['event' => 'foo', 'before' => 'listener']); - $container->register('after', InvokableListenerService::class)->addTag('kernel.event_listener', ['event' => 'foo', 'after' => GenericListener::class]); +// $container->register('after', InvokableListenerService::class)->addTag('kernel.event_listener', ['event' => 'foo', 'after' => GenericListener::class]); $container->register('before_full_definition', InvokableListenerService::class)->addTag('kernel.event_listener', ['event' => 'foo', 'before' => ['listener', '__invoke']]); $container->register('event_dispatcher'); @@ -534,14 +534,14 @@ public function testBeforeAfterListener() 6, ], ], - [ - 'addListener', - [ - 'foo', - [new ServiceClosureArgument(new Reference('after')), '__invoke'], - 4, - ], - ], +// [ +// 'addListener', +// [ +// 'foo', +// [new ServiceClosureArgument(new Reference('after')), '__invoke'], +// 4, +// ], +// ], [ 'addListener', [ @@ -563,7 +563,7 @@ public function testBeforeAfterListenerWithMultipleEvents() ; $container->register('before', InvokableListenerService::class) ->addTag('kernel.event_listener', ['event' => 'foo', 'before' => 'listener', 'method' => '__invoke']) - ->addTag('kernel.event_listener', ['event' => 'bar', 'before' => MultipleListeners::class, 'method' => 'onEvent']) +// ->addTag('kernel.event_listener', ['event' => 'bar', 'before' => MultipleListeners::class, 'method' => 'onEvent']) ; $container->register('event_dispatcher'); @@ -596,14 +596,14 @@ public function testBeforeAfterListenerWithMultipleEvents() 1, ], ], - [ - 'addListener', - [ - 'bar', - [new ServiceClosureArgument(new Reference('before')), 'onEvent'], - 11, - ], - ], +// [ +// 'addListener', +// [ +// 'bar', +// [new ServiceClosureArgument(new Reference('before')), 'onEvent'], +// 11, +// ], +// ], ]; $this->assertEquals($expectedCalls, $definition->getMethodCalls()); } @@ -676,42 +676,42 @@ public static function beforeAfterErrorsProvider(): iterable ]; yield [ - 'Invalid before/after definition for service "error_listener": when declaring as an array, first item must be a service id or a class and second item must be the method.', + 'Invalid before/after definition for service "error_listener": when declaring as an array, first item must be a service id and second item must be the method.', ['event' => 'foo', 'before' => []], ]; yield [ - 'Invalid before/after definition for service "error_listener": given definition "stdClass" is not a listener.', + 'Invalid before/after definition for service "error_listener": "stdClass" is not a listener.', ['event' => 'foo', 'before' => 'stdClass'], ]; - yield [ - sprintf('Invalid before/after definition for service "error_listener": given definition "%s" does not listen to the same event.', GenericListener::class), - ['event' => 'bar', 'before' => GenericListener::class], - ]; +// yield [ +// sprintf('Invalid before/after definition for service "error_listener": given definition "%s" does not listen to the same event.', GenericListener::class), +// ['event' => 'bar', 'before' => GenericListener::class], +// ]; +// +// yield [ +// sprintf('Invalid before/after definition for service "error_listener": given definition "%s::foo()" is not a listener.', GenericListener::class), +// ['event' => 'foo', 'before' => [GenericListener::class, 'foo']], +// ]; yield [ - sprintf('Invalid before/after definition for service "error_listener": given definition "%s::foo()" is not a listener.', GenericListener::class), - ['event' => 'foo', 'before' => [GenericListener::class, 'foo']], - ]; - - yield [ - 'Invalid before/after definition for service "error_listener": given definition "listener" does not listen to the same event.', + 'Invalid before/after definition for service "error_listener": "listener" does not listen to the same event.', ['event' => 'bar', 'before' => 'listener'], ]; yield [ - 'Invalid before/after definition for service "error_listener": given definition "listener::foo()" is not a listener.', + 'Invalid before/after definition for service "error_listener": method "listener::foo()" does not exist or is not a listener.', ['event' => 'foo', 'before' => ['listener', 'foo']], ]; yield [ - 'Invalid before/after definition for service "error_listener": given definition "event_dispatcher" is not a listener.', + 'Invalid before/after definition for service "error_listener": "event_dispatcher" is not a listener.', ['event' => 'bar', 'before' => 'event_dispatcher'], ]; yield [ - 'Invalid before/after definition for service "error_listener": given definition "listener" is not handled by the same dispatchers.', + 'Invalid before/after definition for service "error_listener": "listener" is not handled by the same dispatchers.', ['event' => 'foo', 'before' => 'listener', 'dispatcher' => 'some_dispatcher'], ]; } @@ -740,20 +740,20 @@ public function testBeforeAfterAmbiguous(string $expectedErrorMessage, array $am public static function beforeAfterAmbiguousProvider(): iterable { yield [ - 'Invalid before/after definition for service "error_listener": given definition "listener" is ambiguous. Please specify the "method" attribute.', + 'Invalid before/after definition for service "error_listener": "listener" has multiple methods. Please specify the "method" attribute.', ['event' => 'foo', 'before' => 'listener'], ]; - yield [ - sprintf('Invalid before/after definition for service "error_listener": given definition "%s" is ambiguous. Please specify the "method" attribute.', MultipleListeners::class), - ['event' => 'foo', 'after' => MultipleListeners::class], - ]; +// yield [ +// sprintf('Invalid before/after definition for service "error_listener": "%s" has multiple methods. Please specify the "method" attribute.', MultipleListeners::class), +// ['event' => 'foo', 'after' => MultipleListeners::class], +// ]; } public function testBeforeAfterCircularError() { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid before/after definition for service "listener_1": circular reference detected.'); + $this->expectExceptionMessage('Invalid before/after definition for service "listener_1::__invoke": circular reference detected.'); $container = new ContainerBuilder(); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy