* @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"
}
}
}