From 2ae82114ddb06edef0aa2b4307dfdb648e810080 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Sun, 15 Dec 2019 19:39:24 +0100 Subject: [PATCH] [EventDispatcher] Freeze events. --- .../DependencyInjection/Configuration.php | 1 + .../FrameworkExtension.php | 14 +++++ .../Resources/config/schema/symfony-1.0.xsd | 1 + .../DependencyInjection/ConfigurationTest.php | 1 + .../Fixtures/php/freeze_events.php | 5 ++ .../Fixtures/xml/freeze_events.xml | 10 +++ .../Fixtures/yml/freeze_events.yml | 2 + .../FrameworkExtensionTest.php | 27 ++++++++ .../Bundle/FrameworkBundle/composer.json | 1 + .../Debug/TraceableEventDispatcher.php | 16 ++++- .../RegisterListenersPass.php | 37 +++++++++-- .../EventDispatcher/EventDispatcher.php | 33 +++++++++- .../Exception/ExceptionInterface.php | 16 +++++ .../Exception/RuntimeException.php | 16 +++++ .../ListenerProvider/LazyListenerProvider.php | 55 +++++++++++++++++ .../SimpleListenerProvider.php | 40 ++++++++++++ .../RegisterListenersPassTest.php | 48 +++++++++++++++ .../Tests/EventDispatcherTest.php | 44 +++++++++++++ .../LazyListenerProviderTest.php | 61 +++++++++++++++++++ .../SimpleListenerProviderTest.php | 39 ++++++++++++ .../Component/EventDispatcher/composer.json | 2 +- src/Symfony/Contracts/Cache/composer.json | 2 +- .../ListenerProviderAwareInterface.php | 22 +++++++ .../Contracts/EventDispatcher/composer.json | 2 +- .../Contracts/HttpClient/composer.json | 2 +- src/Symfony/Contracts/Service/composer.json | 2 +- .../Contracts/Translation/composer.json | 2 +- src/Symfony/Contracts/composer.json | 2 +- 28 files changed, 489 insertions(+), 14 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/freeze_events.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/freeze_events.xml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/freeze_events.yml create mode 100644 src/Symfony/Component/EventDispatcher/Exception/ExceptionInterface.php create mode 100644 src/Symfony/Component/EventDispatcher/Exception/RuntimeException.php create mode 100644 src/Symfony/Component/EventDispatcher/ListenerProvider/LazyListenerProvider.php create mode 100644 src/Symfony/Component/EventDispatcher/ListenerProvider/SimpleListenerProvider.php create mode 100644 src/Symfony/Component/EventDispatcher/Tests/ListenerProvider/LazyListenerProviderTest.php create mode 100644 src/Symfony/Component/EventDispatcher/Tests/ListenerProvider/SimpleListenerProviderTest.php create mode 100644 src/Symfony/Contracts/EventDispatcher/ListenerProviderAwareInterface.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 8d6d0fd37c811..95e2372faa3ee 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -88,6 +88,7 @@ public function getConfigTreeBuilder() ->scalarNode('error_controller') ->defaultValue('error_controller') ->end() + ->booleanNode('freeze_kernel_events')->defaultValue(false)->end() ->end() ; diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 9f697213ea353..4d6d85e492249 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -69,6 +69,7 @@ use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; use Symfony\Component\HttpKernel\DependencyInjection\Extension; +use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Lock\Lock; use Symfony\Component\Lock\LockFactory; use Symfony\Component\Lock\LockInterface; @@ -223,6 +224,19 @@ public function load(array $configs, ContainerBuilder $container) $container->setParameter('kernel.trusted_hosts', $config['trusted_hosts']); $container->setParameter('kernel.default_locale', $config['default_locale']); $container->setParameter('kernel.error_controller', $config['error_controller']); + $container->setParameter( + 'event_dispatcher.freeze_events', + $config['freeze_kernel_events'] ? [ + KernelEvents::REQUEST, + KernelEvents::EXCEPTION, + KernelEvents::VIEW, + KernelEvents::CONTROLLER, + KernelEvents::CONTROLLER_ARGUMENTS, + KernelEvents::RESPONSE, + KernelEvents::TERMINATE, + KernelEvents::FINISH_REQUEST, + ] : [] + ); if (!$container->hasParameter('debug.file_link_format')) { $links = [ diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index 0c91768a66f42..617dcd7435fae 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -41,6 +41,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index a087559ede8e5..eebe9054a8528 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -501,6 +501,7 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor 'notification_on_failed_messages' => false, ], 'error_controller' => 'error_controller', + 'freeze_kernel_events' => false, 'secrets' => [ 'enabled' => true, 'vault_directory' => '%kernel.project_dir%/config/secrets/%kernel.environment%', diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/freeze_events.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/freeze_events.php new file mode 100644 index 0000000000000..c8e60c2eb13c3 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/freeze_events.php @@ -0,0 +1,5 @@ +loadFromExtension('framework', [ + 'freeze_kernel_events' => true, +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/freeze_events.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/freeze_events.xml new file mode 100644 index 0000000000000..7f6afae541c7c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/freeze_events.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/freeze_events.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/freeze_events.yml new file mode 100644 index 0000000000000..993494e8e21b0 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/freeze_events.yml @@ -0,0 +1,2 @@ +framework: + freeze_kernel_events: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 6e6b5bd066c27..b9913ae9102d9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -39,6 +39,7 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpClient\ScopingHttpClient; use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass; +use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Messenger\Transport\TransportFactory; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; @@ -1384,6 +1385,32 @@ public function testMailerWithSpecificMessageBus(): void $this->assertEquals(new Reference('app.another_bus'), $container->getDefinition('mailer.mailer')->getArgument(1)); } + public function testFreezeKernelEvents(): void + { + $container = $this->createContainerFromFile('freeze_events'); + + $this->assertSame( + [ + KernelEvents::REQUEST, + KernelEvents::EXCEPTION, + KernelEvents::VIEW, + KernelEvents::CONTROLLER, + KernelEvents::CONTROLLER_ARGUMENTS, + KernelEvents::RESPONSE, + KernelEvents::TERMINATE, + KernelEvents::FINISH_REQUEST, + ], + $container->getParameter('event_dispatcher.freeze_events') + ); + } + + public function testDontFreezeKernelEventsByDefault(): void + { + $container = $this->createContainerFromFile('full'); + + $this->assertSame([], $container->getParameter('event_dispatcher.freeze_events')); + } + protected function createContainer(array $data = []) { return new ContainerBuilder(new EnvPlaceholderParameterBag(array_merge([ diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 46183969a8457..7bcb572e6c6ea 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -73,6 +73,7 @@ "symfony/console": "<4.4", "symfony/dotenv": "<4.4", "symfony/dom-crawler": "<4.4", + "symfony/event-dispatcher": "<5.1", "symfony/http-client": "<4.4", "symfony/form": "<4.4", "symfony/lock": "<4.4", diff --git a/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php b/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php index 11dce4897bc65..e1e581446d197 100644 --- a/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php +++ b/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php @@ -11,6 +11,7 @@ namespace Symfony\Component\EventDispatcher\Debug; +use Psr\EventDispatcher\ListenerProviderInterface; use Psr\EventDispatcher\StoppableEventInterface; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -18,6 +19,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Contracts\EventDispatcher\ListenerProviderAwareInterface; use Symfony\Contracts\Service\ResetInterface; /** @@ -27,7 +29,7 @@ * * @author Fabien Potencier */ -class TraceableEventDispatcher implements EventDispatcherInterface, ResetInterface +class TraceableEventDispatcher implements EventDispatcherInterface, ListenerProviderAwareInterface, ResetInterface { protected $logger; protected $stopwatch; @@ -47,6 +49,10 @@ public function __construct(EventDispatcherInterface $dispatcher, Stopwatch $sto $this->wrappedListeners = []; $this->orphanedEvents = []; $this->requestStack = $requestStack; + + if (!$dispatcher instanceof ListenerProviderAwareInterface) { + @trigger_error(sprintf('Implementing %s without %s is deprecated since Symfony 5.1.', EventDispatcherInterface::class, ListenerProviderAwareInterface::class), E_USER_DEPRECATED); + } } /** @@ -91,6 +97,14 @@ public function removeSubscriber(EventSubscriberInterface $subscriber) return $this->dispatcher->removeSubscriber($subscriber); } + /** + * {@inheritdoc} + */ + public function setListenerProvider(string $eventName, ListenerProviderInterface $listenerProvider): void + { + $this->dispatcher->setListenerProvider($eventName, $listenerProvider); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php index 7820d35c31311..8ec651412d9e4 100644 --- a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php @@ -14,11 +14,14 @@ use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\Event as LegacyEvent; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\EventDispatcher\ListenerProvider\LazyListenerProvider; +use Symfony\Component\EventDispatcher\ListenerProvider\SimpleListenerProvider; use Symfony\Contracts\EventDispatcher\Event; /** @@ -30,16 +33,18 @@ class RegisterListenersPass implements CompilerPassInterface protected $listenerTag; protected $subscriberTag; protected $eventAliasesParameter; + protected $freezeEventsParameter; private $hotPathEvents = []; private $hotPathTagName; - public function __construct(string $dispatcherService = 'event_dispatcher', string $listenerTag = 'kernel.event_listener', string $subscriberTag = 'kernel.event_subscriber', string $eventAliasesParameter = 'event_dispatcher.event_aliases') + public function __construct(string $dispatcherService = 'event_dispatcher', string $listenerTag = 'kernel.event_listener', string $subscriberTag = 'kernel.event_subscriber', string $eventAliasesParameter = 'event_dispatcher.event_aliases', string $freezeEventsParameter = 'event_dispatcher.freeze_events') { $this->dispatcherService = $dispatcherService; $this->listenerTag = $listenerTag; $this->subscriberTag = $subscriberTag; $this->eventAliasesParameter = $eventAliasesParameter; + $this->freezeEventsParameter = $freezeEventsParameter; } public function setHotPathEvents(array $hotPathEvents, $tagName = 'container.hot_path') @@ -64,6 +69,13 @@ public function process(ContainerBuilder $container) } $definition = $container->findDefinition($this->dispatcherService); + $frozenEvents = []; + if ($container->hasParameter($this->freezeEventsParameter)) { + foreach ($container->getParameter($this->freezeEventsParameter) as $event) { + $frozenEvents[$event] = []; + } + } + foreach ($container->findTaggedServiceIds($this->listenerTag, true) as $id => $events) { foreach ($events as $event) { $priority = isset($event['priority']) ? $event['priority'] : 0; @@ -91,7 +103,11 @@ public function process(ContainerBuilder $container) } } - $definition->addMethodCall('addListener', [$event['event'], [new ServiceClosureArgument(new Reference($id)), $event['method']], $priority]); + if (isset($frozenEvents[$event['event']])) { + $frozenEvents[$event['event']][$priority][] = [new Reference($id), $event['method']]; + } else { + $definition->addMethodCall('addListener', [$event['event'], [new ServiceClosureArgument(new Reference($id)), $event['method']], $priority]); + } if (isset($this->hotPathEvents[$event['event']])) { $container->getDefinition($id)->addTag($this->hotPathTagName); @@ -119,8 +135,12 @@ public function process(ContainerBuilder $container) ExtractingEventDispatcher::$subscriber = $class; $extractingDispatcher->addSubscriber($extractingDispatcher); foreach ($extractingDispatcher->listeners as $args) { - $args[1] = [new ServiceClosureArgument(new Reference($id)), $args[1]]; - $definition->addMethodCall('addListener', $args); + if (isset($frozenEvents[$args[0]])) { + $frozenEvents[$args[0]][$args[2]][] = [new Reference($id), $args[1]]; + } else { + $args[1] = [new ServiceClosureArgument(new Reference($id)), $args[1]]; + $definition->addMethodCall('addListener', $args); + } if (isset($this->hotPathEvents[$args[0]])) { $container->getDefinition($id)->addTag($this->hotPathTagName); @@ -129,6 +149,15 @@ public function process(ContainerBuilder $container) $extractingDispatcher->listeners = []; ExtractingEventDispatcher::$aliases = []; } + + foreach ($frozenEvents as $event => $listeners) { + krsort($listeners, SORT_NUMERIC); + $providerId = sprintf('__%s.listener_provider.%s', $this->dispatcherService, $event); + $container->register($providerId, SimpleListenerProvider::class) + ->setArguments([array_merge(...array_values($listeners))]); + + $definition->addMethodCall('setListenerProvider', [$event, new Definition(LazyListenerProvider::class, [new ServiceClosureArgument(new Reference($providerId))])]); + } } private function getEventFromTypeDeclaration(ContainerBuilder $container, string $id, string $method): string diff --git a/src/Symfony/Component/EventDispatcher/EventDispatcher.php b/src/Symfony/Component/EventDispatcher/EventDispatcher.php index c0f839b9db905..106dd6e22f441 100644 --- a/src/Symfony/Component/EventDispatcher/EventDispatcher.php +++ b/src/Symfony/Component/EventDispatcher/EventDispatcher.php @@ -11,8 +11,11 @@ namespace Symfony\Component\EventDispatcher; +use Psr\EventDispatcher\ListenerProviderInterface; use Psr\EventDispatcher\StoppableEventInterface; use Symfony\Component\EventDispatcher\Debug\WrappedListener; +use Symfony\Component\EventDispatcher\Exception\RuntimeException; +use Symfony\Contracts\EventDispatcher\ListenerProviderAwareInterface; /** * The EventDispatcherInterface is the central point of Symfony's event listener system. @@ -29,11 +32,15 @@ * @author Jordan Alliot * @author Nicolas Grekas */ -class EventDispatcher implements EventDispatcherInterface +class EventDispatcher implements EventDispatcherInterface, ListenerProviderAwareInterface { private $listeners = []; private $sorted = []; private $optimized; + /** + * @var ListenerProviderInterface[] + */ + private $listenerProviders = []; public function __construct() { @@ -49,7 +56,9 @@ public function dispatch(object $event, string $eventName = null): object { $eventName = $eventName ?? \get_class($event); - if (null !== $this->optimized && null !== $eventName) { + if (isset($this->listenerProviders[$eventName])) { + $listeners = $this->listenerProviders[$eventName]->getListenersForEvent($event); + } elseif (null !== $this->optimized && null !== $eventName) { $listeners = $this->optimized[$eventName] ?? (empty($this->listeners[$eventName]) ? [] : $this->optimizeListeners($eventName)); } else { $listeners = $this->getListeners($eventName); @@ -140,6 +149,10 @@ public function hasListeners(string $eventName = null) */ public function addListener(string $eventName, $listener, int $priority = 0) { + if (isset($this->listenerProviders[$eventName])) { + throw new RuntimeException(sprintf('The "%s" event is frozen. You cannot attache listeners to it.', $eventName)); + } + $this->listeners[$eventName][$priority][] = $listener; unset($this->sorted[$eventName], $this->optimized[$eventName]); } @@ -149,6 +162,10 @@ public function addListener(string $eventName, $listener, int $priority = 0) */ public function removeListener(string $eventName, $listener) { + if (isset($this->listenerProviders[$eventName])) { + throw new RuntimeException(sprintf('The "%s" event is frozen. You cannot remove listeners from it.', $eventName)); + } + if (empty($this->listeners[$eventName])) { return; } @@ -209,6 +226,18 @@ public function removeSubscriber(EventSubscriberInterface $subscriber) } } + /** + * {@inheritdoc} + */ + public function setListenerProvider(string $eventName, ListenerProviderInterface $listenerProvider): void + { + if (isset($this->listeners[$eventName])) { + throw new RuntimeException(sprintf('There are already listeners attached to the "%s" event. You cannot set a listener provider for it.', $eventName)); + } + + $this->listenerProviders[$eventName] = $listenerProvider; + } + /** * Triggers the listeners of an event. * diff --git a/src/Symfony/Component/EventDispatcher/Exception/ExceptionInterface.php b/src/Symfony/Component/EventDispatcher/Exception/ExceptionInterface.php new file mode 100644 index 0000000000000..bf419aa4cf903 --- /dev/null +++ b/src/Symfony/Component/EventDispatcher/Exception/ExceptionInterface.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\Exception; + +interface ExceptionInterface +{ +} diff --git a/src/Symfony/Component/EventDispatcher/Exception/RuntimeException.php b/src/Symfony/Component/EventDispatcher/Exception/RuntimeException.php new file mode 100644 index 0000000000000..14382d3eb0196 --- /dev/null +++ b/src/Symfony/Component/EventDispatcher/Exception/RuntimeException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\Exception; + +final class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/EventDispatcher/ListenerProvider/LazyListenerProvider.php b/src/Symfony/Component/EventDispatcher/ListenerProvider/LazyListenerProvider.php new file mode 100644 index 0000000000000..fdc222380bc17 --- /dev/null +++ b/src/Symfony/Component/EventDispatcher/ListenerProvider/LazyListenerProvider.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\ListenerProvider; + +use Psr\EventDispatcher\ListenerProviderInterface; + +/** + * A lazy proxy for listener providers. + * + * @author Alexander M. Turek + */ +final class LazyListenerProvider implements ListenerProviderInterface +{ + private $factory; + + /** + * @var ListenerProviderInterface + */ + private $delegate; + + public function __construct(callable $factory) + { + $this->factory = $factory; + } + + /** + * {@inheritdoc} + */ + public function getListenersForEvent(object $event): iterable + { + if (!$this->delegate) { + $this->delegate = ($this->factory)(); + } + + return $this->delegate->getListenersForEvent($event); + } + + public function __call(string $name, array $arguments) + { + if (!$this->delegate) { + $this->delegate = ($this->factory)(); + } + + return $this->delegate->$name(...$arguments); + } +} diff --git a/src/Symfony/Component/EventDispatcher/ListenerProvider/SimpleListenerProvider.php b/src/Symfony/Component/EventDispatcher/ListenerProvider/SimpleListenerProvider.php new file mode 100644 index 0000000000000..0553ed059701b --- /dev/null +++ b/src/Symfony/Component/EventDispatcher/ListenerProvider/SimpleListenerProvider.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\ListenerProvider; + +use Psr\EventDispatcher\ListenerProviderInterface; + +/** + * A minimal listener provider that always returns all configured listeners. + * + * @author Alexander M. Turek + */ +final class SimpleListenerProvider implements ListenerProviderInterface +{ + private $listeners; + + /** + * @param iterable|callable[] $listeners + */ + public function __construct(iterable $listeners) + { + $this->listeners = $listeners; + } + + /** + * {@inheritdoc} + */ + public function getListenersForEvent(object $event): iterable + { + return $this->listeners; + } +} diff --git a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php index 5252664a9f998..7bfd1eb5f9cfd 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php @@ -14,11 +14,14 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass; use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\EventDispatcher\ListenerProvider\LazyListenerProvider; +use Symfony\Component\EventDispatcher\ListenerProvider\SimpleListenerProvider; class RegisterListenersPassTest extends TestCase { @@ -355,6 +358,51 @@ public function testOmitEventNameOnSubscriber(): void ]; $this->assertEquals($expectedCalls, $definition->getMethodCalls()); } + + public function testFrozenEvent(): void + { + $builder = new ContainerBuilder(); + $builder->setParameter('event_dispatcher.freeze_events', ['event']); + $eventDispatcherDefinition = $builder->register('event_dispatcher'); + $builder->register('my_event_listener', 'stdClass') + ->addTag('kernel.event_listener', ['event' => 'event', 'method' => 'onEarlyEvent', 'priority' => 42]) + ->addTag('kernel.event_listener', ['event' => 'event', 'method' => 'onLateEvent', 'priority' => -42]) + ->addTag('kernel.event_listener', ['event' => 'another_event', 'method' => 'onAnotherEvent']); + $builder->register('my_event_subscriber', SubscriberService::class) + ->addTag('kernel.event_subscriber'); + + $registerListenersPass = new RegisterListenersPass(); + $registerListenersPass->process($builder); + + $expectedProviderService = new Definition(SimpleListenerProvider::class, [[ + [new Reference('my_event_listener'), 'onEarlyEvent'], + [new Reference('my_event_subscriber'), 'onEvent'], + [new Reference('my_event_listener'), 'onLateEvent'], + ]]); + $this->assertEquals($expectedProviderService, $builder->getDefinition('__event_dispatcher.listener_provider.event')); + + $expectedCalls = [ + [ + 'addListener', + [ + 'another_event', + [new ServiceClosureArgument(new Reference('my_event_listener')), 'onAnotherEvent'], + 0, + ], + ], + [ + 'setListenerProvider', + [ + 'event', + new Definition( + LazyListenerProvider::class, + [new ServiceClosureArgument(new Reference('__event_dispatcher.listener_provider.event'))] + ), + ], + ], + ]; + $this->assertEquals($expectedCalls, $eventDispatcherDefinition->getMethodCalls()); + } } class SubscriberService implements EventSubscriberInterface diff --git a/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php b/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php index ea9fe8c1b6cdc..c1c9c5b896704 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php @@ -12,8 +12,10 @@ namespace Symfony\Component\EventDispatcher\Tests; use PHPUnit\Framework\TestCase; +use Psr\EventDispatcher\ListenerProviderInterface; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\EventDispatcher\Exception\RuntimeException; use Symfony\Contracts\EventDispatcher\Event; class EventDispatcherTest extends TestCase @@ -406,6 +408,48 @@ public function testMutatingWhilePropagationIsStopped() $this->assertTrue($testLoaded); } + + public function testListenerProvider(): void + { + $event = new Event(); + $calls = 0; + $provider = $this->createMock(ListenerProviderInterface::class); + $provider->expects($this->once()) + ->method('getListenersForEvent') + ->with($this->identicalTo($event)) + ->willReturn([static function () use (&$calls): void { + ++$calls; + }]) + ; + + $this->dispatcher->setListenerProvider('foo', $provider); + $this->assertSame($event, $this->dispatcher->dispatch($event, 'foo')); + $this->assertSame(1, $calls); + } + + public function testAddListenerToNestedDispatcher(): void + { + $this->dispatcher->setListenerProvider('foo', $this->createMock(ListenerProviderInterface::class)); + $this->expectException(RuntimeException::class); + + $this->dispatcher->addListener('foo', static function () {}); + } + + public function testRemoveListenerFromNestedDispatcher(): void + { + $this->dispatcher->setListenerProvider('foo', $this->createMock(ListenerProviderInterface::class)); + $this->expectException(RuntimeException::class); + + $this->dispatcher->removeListener('foo', static function () {}); + } + + public function testSetListenerProviderAfterAddingListeners(): void + { + $this->dispatcher->addListener('foo', static function () {}); + $this->expectException(RuntimeException::class); + + $this->dispatcher->setListenerProvider('foo', $this->createMock(ListenerProviderInterface::class)); + } } class CallableClass diff --git a/src/Symfony/Component/EventDispatcher/Tests/ListenerProvider/LazyListenerProviderTest.php b/src/Symfony/Component/EventDispatcher/Tests/ListenerProvider/LazyListenerProviderTest.php new file mode 100644 index 0000000000000..42872fa29611e --- /dev/null +++ b/src/Symfony/Component/EventDispatcher/Tests/ListenerProvider/LazyListenerProviderTest.php @@ -0,0 +1,61 @@ +createMock(ListenerProviderInterface::class); + $innerProvider + ->expects($this->exactly(2)) + ->method('getListenersForEvent') + ->with($this->identicalTo($expectedEvent)) + ->willReturn($expectedResult); + + $provider = new LazyListenerProvider(static function () use (&$calls, $innerProvider): ListenerProviderInterface { + ++$calls; + + return $innerProvider; + }); + + $this->assertSame(0, $calls); + $this->assertSame($expectedResult, $provider->getListenersForEvent($expectedEvent)); + $this->assertSame($expectedResult, $provider->getListenersForEvent($expectedEvent)); + $this->assertSame(1, $calls); + } + + public function testPassthrough(): void + { + $innerProvider = new class() implements ListenerProviderInterface { + private $calls = 0; + + public function getListenersForEvent(object $event): iterable + { + return []; + } + + public function increment(): int + { + return ++$this->calls; + } + }; + + $provider = new LazyListenerProvider(static function () use ($innerProvider): ListenerProviderInterface { + return $innerProvider; + }); + + $this->assertSame(1, $provider->increment()); + $this->assertSame(2, $provider->increment()); + } +} diff --git a/src/Symfony/Component/EventDispatcher/Tests/ListenerProvider/SimpleListenerProviderTest.php b/src/Symfony/Component/EventDispatcher/Tests/ListenerProvider/SimpleListenerProviderTest.php new file mode 100644 index 0000000000000..2308b3f925939 --- /dev/null +++ b/src/Symfony/Component/EventDispatcher/Tests/ListenerProvider/SimpleListenerProviderTest.php @@ -0,0 +1,39 @@ +assertSame([], $dispatcher->getListenersForEvent(new Event())); + } + + public function testArray(): void + { + $dispatcher = new SimpleListenerProvider([ + $one = static function (): void {}, + $two = static function (): void {}, + $three = static function (): void {}, + ]); + + $this->assertSame([$one, $two, $three], $dispatcher->getListenersForEvent(new Event())); + } + + public function testIterator(): void + { + $dispatcher = new SimpleListenerProvider(new \ArrayIterator([ + $one = static function (): void {}, + $two = static function (): void {}, + $three = static function (): void {}, + ])); + + $this->assertSame([$one, $two, $three], iterator_to_array($dispatcher->getListenersForEvent(new Event()))); + } +} diff --git a/src/Symfony/Component/EventDispatcher/composer.json b/src/Symfony/Component/EventDispatcher/composer.json index 72b8f1c2d228e..080c06d6d9336 100644 --- a/src/Symfony/Component/EventDispatcher/composer.json +++ b/src/Symfony/Component/EventDispatcher/composer.json @@ -17,7 +17,7 @@ ], "require": { "php": "^7.2.5", - "symfony/event-dispatcher-contracts": "^2" + "symfony/event-dispatcher-contracts": "^2.1" }, "require-dev": { "symfony/dependency-injection": "^4.4|^5.0", diff --git a/src/Symfony/Contracts/Cache/composer.json b/src/Symfony/Contracts/Cache/composer.json index c9cf8b538f5bb..1b4b55196b51f 100644 --- a/src/Symfony/Contracts/Cache/composer.json +++ b/src/Symfony/Contracts/Cache/composer.json @@ -28,7 +28,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" } } } diff --git a/src/Symfony/Contracts/EventDispatcher/ListenerProviderAwareInterface.php b/src/Symfony/Contracts/EventDispatcher/ListenerProviderAwareInterface.php new file mode 100644 index 0000000000000..d06065be77714 --- /dev/null +++ b/src/Symfony/Contracts/EventDispatcher/ListenerProviderAwareInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\EventDispatcher; + +use Psr\EventDispatcher\ListenerProviderInterface; + +interface ListenerProviderAwareInterface +{ + /** + * Registers a listener provider for an given event name. + */ + public function setListenerProvider(string $eventName, ListenerProviderInterface $listenerProvider): void; +} diff --git a/src/Symfony/Contracts/EventDispatcher/composer.json b/src/Symfony/Contracts/EventDispatcher/composer.json index f7ba8f119e1c8..6ccbc49c2f7cf 100644 --- a/src/Symfony/Contracts/EventDispatcher/composer.json +++ b/src/Symfony/Contracts/EventDispatcher/composer.json @@ -28,7 +28,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" } } } diff --git a/src/Symfony/Contracts/HttpClient/composer.json b/src/Symfony/Contracts/HttpClient/composer.json index e91f2e52cd2de..31eac967e719f 100644 --- a/src/Symfony/Contracts/HttpClient/composer.json +++ b/src/Symfony/Contracts/HttpClient/composer.json @@ -27,7 +27,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" } } } diff --git a/src/Symfony/Contracts/Service/composer.json b/src/Symfony/Contracts/Service/composer.json index cbd491b59591f..08539a1cc318b 100644 --- a/src/Symfony/Contracts/Service/composer.json +++ b/src/Symfony/Contracts/Service/composer.json @@ -28,7 +28,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" } } } diff --git a/src/Symfony/Contracts/Translation/composer.json b/src/Symfony/Contracts/Translation/composer.json index d185b3a4394a2..cacc4b3d16b7c 100644 --- a/src/Symfony/Contracts/Translation/composer.json +++ b/src/Symfony/Contracts/Translation/composer.json @@ -27,7 +27,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" } } } diff --git a/src/Symfony/Contracts/composer.json b/src/Symfony/Contracts/composer.json index 6eccaa85989b8..98f1dfaf1cd9f 100644 --- a/src/Symfony/Contracts/composer.json +++ b/src/Symfony/Contracts/composer.json @@ -47,7 +47,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" } } }