Repository: symfony/event-dispatcher Branch: 8.1 Commit: bd57b0a5c68c Files: 31 Total size: 118.8 KB Directory structure: gitextract_2owf2s03/ ├── .gitattributes ├── .github/ │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ └── close-pull-request.yml ├── .gitignore ├── Attribute/ │ └── AsEventListener.php ├── CHANGELOG.md ├── Debug/ │ ├── TraceableEventDispatcher.php │ └── WrappedListener.php ├── DependencyInjection/ │ ├── AddEventAliasesPass.php │ └── RegisterListenersPass.php ├── EventDispatcher.php ├── EventDispatcherInterface.php ├── EventSubscriberInterface.php ├── GenericEvent.php ├── ImmutableEventDispatcher.php ├── LICENSE ├── README.md ├── Tests/ │ ├── ChildEventDispatcherTest.php │ ├── Debug/ │ │ ├── TraceableEventDispatcherTest.php │ │ └── WrappedListenerTest.php │ ├── DependencyInjection/ │ │ └── RegisterListenersPassTest.php │ ├── EventDispatcherTest.php │ ├── Fixtures/ │ │ ├── CustomEvent.php │ │ ├── DummyEvent.php │ │ ├── TaggedInvokableListener.php │ │ ├── TaggedMultiListener.php │ │ └── TaggedUnionTypeListener.php │ ├── GenericEventTest.php │ └── ImmutableEventDispatcherTest.php ├── composer.json └── phpunit.xml.dist ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ /Tests export-ignore /phpunit.xml.dist export-ignore /.git* export-ignore ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ Please do not submit any Pull Requests here. They will be closed. --- Please submit your PR here instead: https://github.com/symfony/symfony This repository is what we call a "subtree split": a read-only subset of that main repository. We're looking forward to your PR there! ================================================ FILE: .github/workflows/close-pull-request.yml ================================================ name: Close Pull Request on: pull_request_target: types: [opened] jobs: run: runs-on: ubuntu-latest steps: - uses: superbrothers/close-pull-request@v3 with: comment: | Thanks for your Pull Request! We love contributions. However, you should instead open your PR on the main repository: https://github.com/symfony/symfony This repository is what we call a "subtree split": a read-only subset of that main repository. We're looking forward to your PR there! ================================================ FILE: .gitignore ================================================ vendor/ composer.lock phpunit.xml ================================================ FILE: Attribute/AsEventListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Attribute; /** * Service tag to autoconfigure event listeners. * * @author Alexander M. Turek */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class AsEventListener { /** * @param string|null $event The event name to listen to * @param string|null $method The method to run when the listened event is triggered * @param int $priority The priority of this listener if several are declared for the same event * @param string|null $dispatcher The service id of the event dispatcher to listen to */ public function __construct( public ?string $event = null, public ?string $method = null, public int $priority = 0, public ?string $dispatcher = null, ) { } } ================================================ FILE: CHANGELOG.md ================================================ CHANGELOG ========= 6.0 --- * Remove `LegacyEventDispatcherProxy` 5.4 --- * Allow `#[AsEventListener]` attribute on methods 5.3 --- * Add `#[AsEventListener]` attribute for declaring listeners on PHP 8 5.1.0 ----- * The `LegacyEventDispatcherProxy` class has been deprecated. * Added an optional `dispatcher` attribute to the listener and subscriber tags in `RegisterListenerPass`. 5.0.0 ----- * The signature of the `EventDispatcherInterface::dispatch()` method has been changed to `dispatch($event, string $eventName = null): object`. * The `Event` class has been removed in favor of `Symfony\Contracts\EventDispatcher\Event`. * The `TraceableEventDispatcherInterface` has been removed. * The `WrappedListener` class is now final. 4.4.0 ----- * `AddEventAliasesPass` has been added, allowing applications and bundles to extend the event alias mapping used by `RegisterListenersPass`. * Made the `event` attribute of the `kernel.event_listener` tag optional for FQCN events. 4.3.0 ----- * The signature of the `EventDispatcherInterface::dispatch()` method should be updated to `dispatch($event, string $eventName = null)`, not doing so is deprecated * deprecated the `Event` class, use `Symfony\Contracts\EventDispatcher\Event` instead 4.1.0 ----- * added support for invokable event listeners tagged with `kernel.event_listener` by default * The `TraceableEventDispatcher::getOrphanedEvents()` method has been added. * The `TraceableEventDispatcherInterface` has been deprecated. 4.0.0 ----- * removed the `ContainerAwareEventDispatcher` class * added the `reset()` method to the `TraceableEventDispatcherInterface` 3.4.0 ----- * Implementing `TraceableEventDispatcherInterface` without the `reset()` method has been deprecated. 3.3.0 ----- * The ContainerAwareEventDispatcher class has been deprecated. Use EventDispatcher with closure factories instead. 3.0.0 ----- * The method `getListenerPriority($eventName, $listener)` has been added to the `EventDispatcherInterface`. * The methods `Event::setDispatcher()`, `Event::getDispatcher()`, `Event::setName()` and `Event::getName()` have been removed. The event dispatcher and the event name are passed to the listener call. 2.5.0 ----- * added Debug\TraceableEventDispatcher (originally in HttpKernel) * changed Debug\TraceableEventDispatcherInterface to extend EventDispatcherInterface * added RegisterListenersPass (originally in HttpKernel) 2.1.0 ----- * added TraceableEventDispatcherInterface * added ContainerAwareEventDispatcher * added a reference to the EventDispatcher on the Event * added a reference to the Event name on the event * added fluid interface to the dispatch() method which now returns the Event object * added GenericEvent event class * added the possibility for subscribers to subscribe several times for the same event * added ImmutableEventDispatcher ================================================ FILE: Debug/TraceableEventDispatcher.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Debug; use Psr\EventDispatcher\StoppableEventInterface; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Stopwatch\Stopwatch; use Symfony\Contracts\Service\ResetInterface; /** * Collects some data about event listeners. * * This event dispatcher delegates the dispatching to another one. * * @author Fabien Potencier */ class TraceableEventDispatcher implements EventDispatcherInterface, ResetInterface { /** * @var \SplObjectStorage|null */ private ?\SplObjectStorage $callStack = null; private array $wrappedListeners = []; private array $orphanedEvents = []; private string $currentRequestHash = ''; public function __construct( private EventDispatcherInterface $dispatcher, protected Stopwatch $stopwatch, protected ?LoggerInterface $logger = null, private ?RequestStack $requestStack = null, protected readonly ?\Closure $disabled = null, ) { } public function addListener(string $eventName, callable|array $listener, int $priority = 0): void { $this->dispatcher->addListener($eventName, $listener, $priority); } public function addSubscriber(EventSubscriberInterface $subscriber): void { $this->dispatcher->addSubscriber($subscriber); } public function removeListener(string $eventName, callable|array $listener): void { if (isset($this->wrappedListeners[$eventName])) { foreach ($this->wrappedListeners[$eventName] as $index => $wrappedListener) { if ($wrappedListener->getWrappedListener() === $listener || ($listener instanceof \Closure && $wrappedListener->getWrappedListener() == $listener)) { $listener = $wrappedListener; unset($this->wrappedListeners[$eventName][$index]); break; } } } $this->dispatcher->removeListener($eventName, $listener); } public function removeSubscriber(EventSubscriberInterface $subscriber): void { $this->dispatcher->removeSubscriber($subscriber); } public function getListeners(?string $eventName = null): array { return $this->dispatcher->getListeners($eventName); } public function getListenerPriority(string $eventName, callable|array $listener): ?int { // we might have wrapped listeners for the event (if called while dispatching) // in that case get the priority by wrapper if (isset($this->wrappedListeners[$eventName])) { foreach ($this->wrappedListeners[$eventName] as $wrappedListener) { if ($wrappedListener->getWrappedListener() === $listener || ($listener instanceof \Closure && $wrappedListener->getWrappedListener() == $listener)) { return $this->dispatcher->getListenerPriority($eventName, $wrappedListener); } } } return $this->dispatcher->getListenerPriority($eventName, $listener); } public function hasListeners(?string $eventName = null): bool { return $this->dispatcher->hasListeners($eventName); } public function dispatch(object $event, ?string $eventName = null): object { if ($this->disabled?->__invoke()) { return $this->dispatcher->dispatch($event, $eventName); } $eventName ??= $event::class; $this->callStack ??= new \SplObjectStorage(); $currentRequestHash = $this->currentRequestHash = $this->requestStack && ($request = $this->requestStack->getCurrentRequest()) ? spl_object_hash($request) : ''; if (null !== $this->logger && $event instanceof StoppableEventInterface && $event->isPropagationStopped()) { $this->logger->debug(\sprintf('The "%s" event is already stopped. No listeners have been called.', $eventName)); } $this->preProcess($eventName); try { $this->beforeDispatch($eventName, $event); try { $e = $this->stopwatch->start($eventName, 'section'); try { $this->dispatcher->dispatch($event, $eventName); } finally { if ($e->isStarted()) { $e->stop(); } } } finally { $this->afterDispatch($eventName, $event); } } finally { $this->currentRequestHash = $currentRequestHash; $this->postProcess($eventName); } return $event; } public function getCalledListeners(?Request $request = null): array { if (null === $this->callStack) { return []; } $hash = $request ? spl_object_hash($request) : null; $called = []; foreach ($this->callStack as $listener) { [$eventName, $requestHash] = $this->callStack->getInfo(); if (null === $hash || $hash === $requestHash) { $called[] = $listener->getInfo($eventName); } } return $called; } public function getNotCalledListeners(?Request $request = null): array { try { $allListeners = $this->dispatcher instanceof EventDispatcher ? $this->getListenersWithPriority() : $this->getListenersWithoutPriority(); } catch (\Exception $e) { $this->logger?->info('An exception was thrown while getting the uncalled listeners.', ['exception' => $e]); // unable to retrieve the uncalled listeners return []; } $hash = $request ? spl_object_hash($request) : null; $calledListeners = []; if (null !== $this->callStack) { foreach ($this->callStack as $calledListener) { [, $requestHash] = $this->callStack->getInfo(); if (null === $hash || $hash === $requestHash) { $calledListeners[] = $calledListener->getWrappedListener(); } } } $notCalled = []; foreach ($allListeners as $eventName => $listeners) { foreach ($listeners as [$listener, $priority]) { if (!\in_array($listener, $calledListeners, true)) { if (!$listener instanceof WrappedListener) { $listener = new WrappedListener($listener, null, $this->stopwatch, $this, $priority); } $notCalled[] = $listener->getInfo($eventName); } } } uasort($notCalled, $this->sortNotCalledListeners(...)); return $notCalled; } public function getOrphanedEvents(?Request $request = null): array { if ($request) { return $this->orphanedEvents[spl_object_hash($request)] ?? []; } if (!$this->orphanedEvents) { return []; } return array_merge(...array_values($this->orphanedEvents)); } public function reset(): void { $this->callStack = null; $this->orphanedEvents = []; $this->currentRequestHash = ''; } /** * Proxies all method calls to the original event dispatcher. * * @param string $method The method name * @param array $arguments The method arguments */ public function __call(string $method, array $arguments): mixed { return $this->dispatcher->{$method}(...$arguments); } /** * Called before dispatching the event. */ protected function beforeDispatch(string $eventName, object $event): void { } /** * Called after dispatching the event. */ protected function afterDispatch(string $eventName, object $event): void { } private function preProcess(string $eventName): void { if (!$this->dispatcher->hasListeners($eventName)) { $this->orphanedEvents[$this->currentRequestHash][] = $eventName; return; } foreach ($this->dispatcher->getListeners($eventName) as $listener) { $priority = $this->getListenerPriority($eventName, $listener); $wrappedListener = new WrappedListener($listener instanceof WrappedListener ? $listener->getWrappedListener() : $listener, null, $this->stopwatch, $this); $this->wrappedListeners[$eventName][] = $wrappedListener; $this->dispatcher->removeListener($eventName, $listener); $this->dispatcher->addListener($eventName, $wrappedListener, $priority); $this->callStack[$wrappedListener] = [$eventName, $this->currentRequestHash]; } } private function postProcess(string $eventName): void { unset($this->wrappedListeners[$eventName]); $skipped = false; foreach ($this->dispatcher->getListeners($eventName) as $listener) { if (!$listener instanceof WrappedListener) { // #12845: a new listener was added during dispatch. continue; } // Unwrap listener $priority = $this->getListenerPriority($eventName, $listener); $this->dispatcher->removeListener($eventName, $listener); $this->dispatcher->addListener($eventName, $listener->getWrappedListener(), $priority); if (null !== $this->logger) { $context = ['event' => $eventName, 'listener' => $listener->getPretty()]; } if ($listener->wasCalled()) { $this->logger?->debug('Notified event "{event}" to listener "{listener}".', $context); } else { unset($this->callStack[$listener]); } if (null !== $this->logger && $skipped) { $this->logger->debug('Listener "{listener}" was not called for event "{event}".', $context); } if ($listener->stoppedPropagation()) { $this->logger?->debug('Listener "{listener}" stopped propagation of the event "{event}".', $context); $skipped = true; } } } private function sortNotCalledListeners(array $a, array $b): int { if (0 !== $cmp = strcmp($a['event'], $b['event'])) { return $cmp; } if (\is_int($a['priority']) && !\is_int($b['priority'])) { return 1; } if (!\is_int($a['priority']) && \is_int($b['priority'])) { return -1; } if ($a['priority'] === $b['priority']) { return 0; } if ($a['priority'] > $b['priority']) { return -1; } return 1; } private function getListenersWithPriority(): array { $result = []; $allListeners = new \ReflectionProperty(EventDispatcher::class, 'listeners'); foreach ($allListeners->getValue($this->dispatcher) as $eventName => $listenersByPriority) { foreach ($listenersByPriority as $priority => $listeners) { foreach ($listeners as $listener) { $result[$eventName][] = [$listener, $priority]; } } } return $result; } private function getListenersWithoutPriority(): array { $result = []; foreach ($this->getListeners() as $eventName => $listeners) { foreach ($listeners as $listener) { $result[$eventName][] = [$listener, null]; } } return $result; } } ================================================ FILE: Debug/WrappedListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Debug; use Psr\EventDispatcher\StoppableEventInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Stopwatch\Stopwatch; use Symfony\Component\VarDumper\Caster\ClassStub; /** * @author Fabien Potencier */ final class WrappedListener { private string|array|object $listener; private ?\Closure $optimizedListener; private string $name; private bool $called = false; private bool $stoppedPropagation = false; private string $pretty; private string $callableRef; private ClassStub|string $stub; private static bool $hasClassStub; public function __construct( callable|array $listener, ?string $name, private Stopwatch $stopwatch, private ?EventDispatcherInterface $dispatcher = null, private ?int $priority = null, ) { $this->listener = $listener; $this->optimizedListener = $listener instanceof \Closure ? $listener : (\is_callable($listener) ? $listener(...) : null); if (\is_array($listener)) { [$this->name, $this->callableRef] = $this->parseListener($listener); $this->pretty = $this->name.'::'.$listener[1]; $this->callableRef .= '::'.$listener[1]; } elseif ($listener instanceof \Closure) { $r = new \ReflectionFunction($listener); if ($r->isAnonymous()) { $this->pretty = $this->name = 'closure'; } elseif ($class = $r->getClosureCalledClass()) { $this->name = $class->name; $this->pretty = $this->name.'::'.$r->name; } else { $this->pretty = $this->name = $r->name; } } elseif (\is_string($listener)) { $this->pretty = $this->name = $listener; } else { $this->name = get_debug_type($listener); $this->pretty = $this->name.'::__invoke'; $this->callableRef = $listener::class.'::__invoke'; } if (null !== $name) { $this->name = $name; } self::$hasClassStub ??= class_exists(ClassStub::class); } public function getWrappedListener(): callable|array { return $this->listener; } public function wasCalled(): bool { return $this->called; } public function stoppedPropagation(): bool { return $this->stoppedPropagation; } public function getPretty(): string { return $this->pretty; } public function getInfo(string $eventName): array { $this->stub ??= self::$hasClassStub ? new ClassStub($this->pretty.'()', $this->callableRef ?? $this->listener) : $this->pretty.'()'; return [ 'event' => $eventName, 'priority' => $this->priority ??= $this->dispatcher?->getListenerPriority($eventName, $this->listener), 'pretty' => $this->pretty, 'stub' => $this->stub, ]; } public function __invoke(object $event, string $eventName, EventDispatcherInterface $dispatcher): void { $dispatcher = $this->dispatcher ?: $dispatcher; $this->called = true; $this->priority ??= $dispatcher->getListenerPriority($eventName, $this->listener); $e = $this->stopwatch->start($this->name, 'event_listener'); try { ($this->optimizedListener ?? $this->listener)($event, $eventName, $dispatcher); } finally { if ($e->isStarted()) { $e->stop(); } } if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) { $this->stoppedPropagation = true; } } private function parseListener(array $listener): array { if ($listener[0] instanceof \Closure) { foreach ((new \ReflectionFunction($listener[0]))->getAttributes(\Closure::class) as $attribute) { if ($name = $attribute->getArguments()['name'] ?? false) { return [$name, $attribute->getArguments()['class'] ?? $name]; } } } if (\is_object($listener[0])) { return [get_debug_type($listener[0]), $listener[0]::class]; } return [$listener[0], $listener[0]]; } } ================================================ FILE: DependencyInjection/AddEventAliasesPass.php ================================================ * * 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\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; /** * This pass allows bundles to extend the list of event aliases. * * @author Alexander M. Turek */ class AddEventAliasesPass implements CompilerPassInterface { public function __construct( private array $eventAliases, ) { } public function process(ContainerBuilder $container): void { $eventAliases = $container->hasParameter('event_dispatcher.event_aliases') ? $container->getParameter('event_dispatcher.event_aliases') : []; $container->setParameter( 'event_dispatcher.event_aliases', array_merge($eventAliases, $this->eventAliases) ); } } ================================================ FILE: DependencyInjection/RegisterListenersPass.php ================================================ * * 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\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Contracts\EventDispatcher\Event; /** * Compiler pass to register tagged services for an event dispatcher. */ class RegisterListenersPass implements CompilerPassInterface { private array $hotPathEvents = []; private array $noPreloadEvents = []; /** * @return $this */ public function setHotPathEvents(array $hotPathEvents): static { $this->hotPathEvents = array_flip($hotPathEvents); return $this; } /** * @return $this */ public function setNoPreloadEvents(array $noPreloadEvents): static { $this->noPreloadEvents = array_flip($noPreloadEvents); return $this; } public function process(ContainerBuilder $container): void { if (!$container->hasDefinition('event_dispatcher') && !$container->hasAlias('event_dispatcher')) { return; } $aliases = []; if ($container->hasParameter('event_dispatcher.event_aliases')) { $aliases = $container->getParameter('event_dispatcher.event_aliases'); } $globalDispatcherDefinition = $container->findDefinition('event_dispatcher'); foreach ($container->findTaggedServiceIds('kernel.event_listener', true) as $id => $events) { $noPreload = 0; $resolvedEvents = []; foreach ($events as $event) { if (!isset($event['event'])) { if ($container->getDefinition($id)->hasTag('kernel.event_subscriber')) { continue; } $event['method'] ??= '__invoke'; $eventNames = $this->getEventFromTypeDeclaration($container, $id, $event['method']); } else { $eventNames = [$event['event']]; } foreach ($eventNames as $eventName) { $event['event'] = $aliases[$eventName] ?? $eventName; $resolvedEvents[] = $event; } } foreach ($resolvedEvents as $event) { $priority = $event['priority'] ?? 0; if (!isset($event['method'])) { $event['method'] = 'on'.preg_replace_callback([ '/(?<=\b|_)[a-z]/i', '/[^a-z0-9]/i', ], static 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]); if (isset($this->hotPathEvents[$event['event']])) { $container->getDefinition($id)->addTag('container.hot_path'); } elseif (isset($this->noPreloadEvents[$event['event']])) { ++$noPreload; } } if ($noPreload && \count($events) === $noPreload) { $container->getDefinition($id)->addTag('container.no_preload'); } } $extractingDispatcher = new ExtractingEventDispatcher(); foreach ($container->findTaggedServiceIds('kernel.event_subscriber', true) as $id => $tags) { $def = $container->getDefinition($id); // We must assume that the class value has been correctly filled, even if the service is created by a factory $class = $def->getClass(); if (!$r = $container->getReflectionClass($class)) { throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); } if (!$r->isSubclassOf(EventSubscriberInterface::class)) { throw new InvalidArgumentException(\sprintf('Service "%s" must implement interface "%s".', $id, EventSubscriberInterface::class)); } $class = $r->name; $dispatcherDefinitions = []; foreach ($tags as $attributes) { if (!isset($attributes['dispatcher']) || isset($dispatcherDefinitions[$attributes['dispatcher']])) { continue; } $dispatcherDefinitions[$attributes['dispatcher']] = $container->findDefinition($attributes['dispatcher']); } if (!$dispatcherDefinitions) { $dispatcherDefinitions = [$globalDispatcherDefinition]; } $noPreload = 0; 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'); } $extractingDispatcher->listeners = []; ExtractingEventDispatcher::$aliases = []; } } /** * @return string[] */ private function getEventFromTypeDeclaration(ContainerBuilder $container, string $id, string $method): array { if ( null === ($class = $container->getDefinition($id)->getClass()) || !($r = $container->getReflectionClass($class, false)) || !$r->hasMethod($method) || 1 > ($m = $r->getMethod($method))->getNumberOfParameters() || !(($type = $m->getParameters()[0]->getType()) instanceof \ReflectionNamedType || $type instanceof \ReflectionUnionType) ) { throw new InvalidArgumentException(\sprintf('Service "%s" must define the "event" attribute on "kernel.event_listener" tags.', $id)); } $types = $type instanceof \ReflectionUnionType ? $type->getTypes() : [$type]; $names = []; foreach ($types as $type) { if (!$type instanceof \ReflectionNamedType || $type->isBuiltin() || Event::class === ($name = $type->getName()) ) { continue; } $names[] = $name; } if (!$names) { throw new InvalidArgumentException(\sprintf('Service "%s" must define the "event" attribute on "kernel.event_listener" tags.', $id)); } return $names; } } /** * @internal */ class ExtractingEventDispatcher extends EventDispatcher implements EventSubscriberInterface { public array $listeners = []; public static array $aliases = []; public static string $subscriber; public function addListener(string $eventName, callable|array $listener, int $priority = 0): void { $this->listeners[] = [$eventName, $listener[1], $priority]; } public static function getSubscribedEvents(): array { $events = []; foreach ([self::$subscriber, 'getSubscribedEvents']() as $eventName => $params) { $events[self::$aliases[$eventName] ?? $eventName] = $params; } return $events; } } ================================================ FILE: EventDispatcher.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher; use Psr\EventDispatcher\StoppableEventInterface; use Symfony\Component\EventDispatcher\Debug\WrappedListener; /** * The EventDispatcherInterface is the central point of Symfony's event listener system. * * Listeners are registered on the manager and events are dispatched through the * manager. * * @author Guilherme Blanco * @author Jonathan Wage * @author Roman Borschel * @author Bernhard Schussek * @author Fabien Potencier * @author Jordi Boggiano * @author Jordan Alliot * @author Nicolas Grekas */ class EventDispatcher implements EventDispatcherInterface { private array $listeners = []; private array $sorted = []; private array $optimized; public function __construct() { if (__CLASS__ === static::class) { $this->optimized = []; } } public function dispatch(object $event, ?string $eventName = null): object { $eventName ??= $event::class; if (isset($this->optimized)) { $listeners = $this->optimized[$eventName] ?? (empty($this->listeners[$eventName]) ? [] : $this->optimizeListeners($eventName)); } else { $listeners = $this->getListeners($eventName); } if ($listeners) { $this->callListeners($listeners, $eventName, $event); } return $event; } public function getListeners(?string $eventName = null): array { if (null !== $eventName) { if (empty($this->listeners[$eventName])) { return []; } if (!isset($this->sorted[$eventName])) { $this->sortListeners($eventName); } return $this->sorted[$eventName]; } foreach ($this->listeners as $eventName => $eventListeners) { if (!isset($this->sorted[$eventName])) { $this->sortListeners($eventName); } } return array_filter($this->sorted); } public function getListenerPriority(string $eventName, callable|array $listener): ?int { if (empty($this->listeners[$eventName])) { return null; } if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) { $listener[0] = $listener[0](); $listener[1] ??= '__invoke'; } foreach ($this->listeners[$eventName] as $priority => &$listeners) { foreach ($listeners as &$v) { if ($v !== $listener && \is_array($v) && isset($v[0]) && $v[0] instanceof \Closure && 2 >= \count($v)) { $v[0] = $v[0](); $v[1] ??= '__invoke'; } if ($v === $listener || ($listener instanceof \Closure && $v == $listener)) { return $priority; } } } return null; } public function hasListeners(?string $eventName = null): bool { if (null !== $eventName) { return !empty($this->listeners[$eventName]); } foreach ($this->listeners as $eventListeners) { if ($eventListeners) { return true; } } return false; } public function addListener(string $eventName, callable|array $listener, int $priority = 0): void { $this->listeners[$eventName][$priority][] = $listener; unset($this->sorted[$eventName], $this->optimized[$eventName]); } public function removeListener(string $eventName, callable|array $listener): void { if (empty($this->listeners[$eventName])) { return; } if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) { $listener[0] = $listener[0](); $listener[1] ??= '__invoke'; } foreach ($this->listeners[$eventName] as $priority => &$listeners) { foreach ($listeners as $k => &$v) { if ($v !== $listener && \is_array($v) && isset($v[0]) && $v[0] instanceof \Closure && 2 >= \count($v)) { $v[0] = $v[0](); $v[1] ??= '__invoke'; } if ($v === $listener || ($listener instanceof \Closure && $v == $listener)) { unset($listeners[$k], $this->sorted[$eventName], $this->optimized[$eventName]); } } if (!$listeners) { unset($this->listeners[$eventName][$priority]); } } } public function addSubscriber(EventSubscriberInterface $subscriber): void { foreach ($subscriber->getSubscribedEvents() as $eventName => $params) { if (\is_string($params)) { $this->addListener($eventName, [$subscriber, $params]); } elseif (\is_string($params[0])) { $this->addListener($eventName, [$subscriber, $params[0]], $params[1] ?? 0); } else { foreach ($params as $listener) { $this->addListener($eventName, [$subscriber, $listener[0]], $listener[1] ?? 0); } } } } public function removeSubscriber(EventSubscriberInterface $subscriber): void { foreach ($subscriber->getSubscribedEvents() as $eventName => $params) { if (\is_array($params) && \is_array($params[0])) { foreach ($params as $listener) { $this->removeListener($eventName, [$subscriber, $listener[0]]); } } else { $this->removeListener($eventName, [$subscriber, \is_string($params) ? $params : $params[0]]); } } } /** * Triggers the listeners of an event. * * This method can be overridden to add functionality that is executed * for each listener. * * @param callable[] $listeners The event listeners * @param string $eventName The name of the event to dispatch * @param object $event The event object to pass to the event handlers/listeners */ protected function callListeners(iterable $listeners, string $eventName, object $event): void { $stoppable = $event instanceof StoppableEventInterface; foreach ($listeners as $listener) { if ($stoppable && $event->isPropagationStopped()) { break; } $listener($event, $eventName, $this); } } /** * Sorts the internal list of listeners for the given event by priority. */ private function sortListeners(string $eventName): void { krsort($this->listeners[$eventName]); $this->sorted[$eventName] = []; foreach ($this->listeners[$eventName] as &$listeners) { foreach ($listeners as &$listener) { if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) { $listener[0] = $listener[0](); $listener[1] ??= '__invoke'; } $this->sorted[$eventName][] = $listener; } } } /** * Optimizes the internal list of listeners for the given event by priority. */ private function optimizeListeners(string $eventName): array { krsort($this->listeners[$eventName]); $this->optimized[$eventName] = []; foreach ($this->listeners[$eventName] as &$listeners) { foreach ($listeners as &$listener) { $closure = &$this->optimized[$eventName][]; if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) { $closure = static function (...$args) use (&$listener, &$closure) { if ($listener[0] instanceof \Closure) { $listener[0] = $listener[0](); $listener[1] ??= '__invoke'; } ($closure = $listener(...))(...$args); }; } else { $closure = $listener instanceof WrappedListener ? $listener : $listener(...); } } } return $this->optimized[$eventName]; } } ================================================ FILE: EventDispatcherInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as ContractsEventDispatcherInterface; /** * The EventDispatcherInterface is the central point of Symfony's event listener system. * Listeners are registered on the manager and events are dispatched through the * manager. * * @author Bernhard Schussek */ interface EventDispatcherInterface extends ContractsEventDispatcherInterface { /** * Adds an event listener that listens on the specified events. * * @param int $priority The higher this value, the earlier an event * listener will be triggered in the chain (defaults to 0) */ public function addListener(string $eventName, callable $listener, int $priority = 0): void; /** * Adds an event subscriber. * * The subscriber is asked for all the events it is * interested in and added as a listener for these events. */ public function addSubscriber(EventSubscriberInterface $subscriber): void; /** * Removes an event listener from the specified events. */ public function removeListener(string $eventName, callable $listener): void; public function removeSubscriber(EventSubscriberInterface $subscriber): void; /** * Gets the listeners of a specific event or all listeners sorted by descending priority. * * @return ($eventName is null ? array : array) */ public function getListeners(?string $eventName = null): array; /** * Gets the listener priority for a specific event. * * Returns null if the event or the listener does not exist. */ public function getListenerPriority(string $eventName, callable $listener): ?int; /** * Checks whether an event has any registered listeners. */ public function hasListeners(?string $eventName = null): bool; } ================================================ FILE: EventSubscriberInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher; /** * An EventSubscriber knows itself what events it is interested in. * If an EventSubscriber is added to an EventDispatcherInterface, the manager invokes * {@link getSubscribedEvents} and registers the subscriber as a listener for all * returned events. * * @author Guilherme Blanco * @author Jonathan Wage * @author Roman Borschel * @author Bernhard Schussek */ interface EventSubscriberInterface { /** * Returns an array of event names this subscriber wants to listen to. * * The array keys are event names and the value can be: * * * The method name to call (priority defaults to 0) * * An array composed of the method name to call and the priority * * An array of arrays composed of the method names to call and respective * priorities, or 0 if unset * * For instance: * * * ['eventName' => 'methodName'] * * ['eventName' => ['methodName', $priority]] * * ['eventName' => [['methodName1', $priority], ['methodName2']]] * * The code must not depend on runtime state as it will only be called at compile time. * All logic depending on runtime state must be put into the individual methods handling the events. * * @return array> */ public static function getSubscribedEvents(): array; } ================================================ FILE: GenericEvent.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher; use Symfony\Contracts\EventDispatcher\Event; /** * Event encapsulation class. * * Encapsulates events thus decoupling the observer from the subject they encapsulate. * * @author Drak * * @implements \ArrayAccess * @implements \IteratorAggregate */ class GenericEvent extends Event implements \ArrayAccess, \IteratorAggregate { /** * Encapsulate an event with $subject and $arguments. * * @param mixed $subject The subject of the event, usually an object or a callable * @param array $arguments Arguments to store in the event */ public function __construct( protected mixed $subject = null, protected array $arguments = [], ) { } /** * Getter for subject property. */ public function getSubject(): mixed { return $this->subject; } /** * Get argument by key. * * @throws \InvalidArgumentException if key is not found */ public function getArgument(string $key): mixed { if ($this->hasArgument($key)) { return $this->arguments[$key]; } throw new \InvalidArgumentException(\sprintf('Argument "%s" not found.', $key)); } /** * Add argument to event. * * @return $this */ public function setArgument(string $key, mixed $value): static { $this->arguments[$key] = $value; return $this; } /** * Getter for all arguments. */ public function getArguments(): array { return $this->arguments; } /** * Set args property. * * @return $this */ public function setArguments(array $args = []): static { $this->arguments = $args; return $this; } /** * Has argument. */ public function hasArgument(string $key): bool { return \array_key_exists($key, $this->arguments); } /** * ArrayAccess for argument getter. * * @param string $key Array key * * @throws \InvalidArgumentException if key does not exist in $this->args */ public function offsetGet(mixed $key): mixed { return $this->getArgument($key); } /** * ArrayAccess for argument setter. * * @param string $key Array key to set */ public function offsetSet(mixed $key, mixed $value): void { $this->setArgument($key, $value); } /** * ArrayAccess for unset argument. * * @param string $key Array key */ public function offsetUnset(mixed $key): void { if ($this->hasArgument($key)) { unset($this->arguments[$key]); } } /** * ArrayAccess has argument. * * @param string $key Array key */ public function offsetExists(mixed $key): bool { return $this->hasArgument($key); } /** * IteratorAggregate for iterating over the object like an array. * * @return \ArrayIterator */ public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->arguments); } } ================================================ FILE: ImmutableEventDispatcher.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher; /** * A read-only proxy for an event dispatcher. * * @author Bernhard Schussek */ class ImmutableEventDispatcher implements EventDispatcherInterface { public function __construct( private EventDispatcherInterface $dispatcher, ) { } public function dispatch(object $event, ?string $eventName = null): object { return $this->dispatcher->dispatch($event, $eventName); } public function addListener(string $eventName, callable|array $listener, int $priority = 0): never { throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); } public function addSubscriber(EventSubscriberInterface $subscriber): never { throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); } public function removeListener(string $eventName, callable|array $listener): never { throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); } public function removeSubscriber(EventSubscriberInterface $subscriber): never { throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); } public function getListeners(?string $eventName = null): array { return $this->dispatcher->getListeners($eventName); } public function getListenerPriority(string $eventName, callable|array $listener): ?int { return $this->dispatcher->getListenerPriority($eventName, $listener); } public function hasListeners(?string $eventName = null): bool { return $this->dispatcher->hasListeners($eventName); } } ================================================ FILE: LICENSE ================================================ Copyright (c) 2004-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ EventDispatcher Component ========================= The EventDispatcher component provides tools that allow your application components to communicate with each other by dispatching events and listening to them. Resources --------- * [Documentation](https://symfony.com/doc/current/components/event_dispatcher.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) in the [main Symfony repository](https://github.com/symfony/symfony) ================================================ FILE: Tests/ChildEventDispatcherTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Tests; use Symfony\Component\EventDispatcher\EventDispatcher; class ChildEventDispatcherTest extends EventDispatcherTest { protected function createEventDispatcher() { return new ChildEventDispatcher(); } } class ChildEventDispatcher extends EventDispatcher { } ================================================ FILE: Tests/Debug/TraceableEventDispatcherTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Tests\Debug; use PHPUnit\Framework\TestCase; use Symfony\Component\ErrorHandler\BufferingLogger; use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Stopwatch\Stopwatch; use Symfony\Contracts\EventDispatcher\Event; class TraceableEventDispatcherTest extends TestCase { public function testAddRemoveListener() { $dispatcher = new EventDispatcher(); $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); $tdispatcher->addListener('foo', $listener = static function () {}); $listeners = $dispatcher->getListeners('foo'); $this->assertCount(1, $listeners); $this->assertSame($listener, $listeners[0]); $tdispatcher->removeListener('foo', $listener); $this->assertCount(0, $dispatcher->getListeners('foo')); } public function testGetListeners() { $dispatcher = new EventDispatcher(); $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); $tdispatcher->addListener('foo', $listener = static function () {}); $this->assertSame($dispatcher->getListeners('foo'), $tdispatcher->getListeners('foo')); } public function testHasListeners() { $dispatcher = new EventDispatcher(); $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); $this->assertFalse($dispatcher->hasListeners('foo')); $this->assertFalse($tdispatcher->hasListeners('foo')); $tdispatcher->addListener('foo', $listener = static function () {}); $this->assertTrue($dispatcher->hasListeners('foo')); $this->assertTrue($tdispatcher->hasListeners('foo')); } public function testGetListenerPriority() { $dispatcher = new EventDispatcher(); $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); $tdispatcher->addListener('foo', static function () {}, 123); $listeners = $dispatcher->getListeners('foo'); $this->assertSame(123, $tdispatcher->getListenerPriority('foo', $listeners[0])); // Verify that priority is preserved when listener is removed and re-added // in preProcess() and postProcess(). $tdispatcher->dispatch(new Event(), 'foo'); $listeners = $dispatcher->getListeners('foo'); $this->assertSame(123, $tdispatcher->getListenerPriority('foo', $listeners[0])); } public function testGetListenerPriorityWhileDispatching() { $tdispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); $priorityWhileDispatching = null; $listener = static function () use ($tdispatcher, &$priorityWhileDispatching, &$listener) { $priorityWhileDispatching = $tdispatcher->getListenerPriority('bar', $listener); }; $tdispatcher->addListener('bar', $listener, 5); $tdispatcher->dispatch(new Event(), 'bar'); $this->assertSame(5, $priorityWhileDispatching); } public function testAddRemoveSubscriber() { $dispatcher = new EventDispatcher(); $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); $subscriber = new EventSubscriber(); $tdispatcher->addSubscriber($subscriber); $listeners = $dispatcher->getListeners('foo'); $this->assertCount(1, $listeners); $this->assertSame([$subscriber, 'call'], $listeners[0]); $tdispatcher->removeSubscriber($subscriber); $this->assertCount(0, $dispatcher->getListeners('foo')); } public function testGetCalledListeners() { $tdispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); $tdispatcher->addListener('foo', static function () {}, 5); $listeners = $tdispatcher->getNotCalledListeners(); $this->assertArrayHasKey('stub', $listeners[0]); unset($listeners[0]['stub']); $this->assertEquals([], $tdispatcher->getCalledListeners()); $this->assertEquals([['event' => 'foo', 'pretty' => 'closure', 'priority' => 5]], $listeners); $tdispatcher->dispatch(new Event(), 'foo'); $listeners = $tdispatcher->getCalledListeners(); $this->assertArrayHasKey('stub', $listeners[0]); unset($listeners[0]['stub']); $this->assertEquals([['event' => 'foo', 'pretty' => 'closure', 'priority' => 5]], $listeners); $this->assertEquals([], $tdispatcher->getNotCalledListeners()); } public function testGetNotCalledClosureListeners() { $instantiationCount = 0; $tdispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); $tdispatcher->addListener('foo', [static function () use (&$instantiationCount) { ++$instantiationCount; }, 'onFoo']); $tdispatcher->getNotCalledListeners(); $this->assertSame(0, $instantiationCount); } public function testClearCalledListeners() { $tdispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); $tdispatcher->addListener('foo', static function () {}, 5); $tdispatcher->dispatch(new Event(), 'foo'); $tdispatcher->reset(); $listeners = $tdispatcher->getNotCalledListeners(); $this->assertArrayHasKey('stub', $listeners[0]); unset($listeners[0]['stub']); $this->assertEquals([], $tdispatcher->getCalledListeners()); $this->assertEquals([['event' => 'foo', 'pretty' => 'closure', 'priority' => 5]], $listeners); } public function testDispatchAfterReset() { $tdispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); $tdispatcher->addListener('foo', static function () {}, 5); $tdispatcher->reset(); $tdispatcher->dispatch(new Event(), 'foo'); $listeners = $tdispatcher->getCalledListeners(); $this->assertArrayHasKey('stub', $listeners[0]); } public function testGetCalledListenersNested() { $tdispatcher = null; $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); $dispatcher->addListener('foo', static function (Event $event, $eventName, $dispatcher) use (&$tdispatcher) { $tdispatcher = $dispatcher; $dispatcher->dispatch(new Event(), 'bar'); }); $dispatcher->addListener('bar', static function (Event $event) {}); $dispatcher->dispatch(new Event(), 'foo'); $this->assertSame($dispatcher, $tdispatcher); $this->assertCount(2, $dispatcher->getCalledListeners()); } public function testItReturnsNoOrphanedEventsWhenCreated() { $tdispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); $events = $tdispatcher->getOrphanedEvents(); $this->assertSame([], $events); } public function testItReturnsOrphanedEventsAfterDispatch() { $tdispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); $tdispatcher->dispatch(new Event(), 'foo'); $events = $tdispatcher->getOrphanedEvents(); $this->assertCount(1, $events); $this->assertEquals(['foo'], $events); } public function testItDoesNotReturnHandledEvents() { $tdispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); $tdispatcher->addListener('foo', static function () {}); $tdispatcher->dispatch(new Event(), 'foo'); $events = $tdispatcher->getOrphanedEvents(); $this->assertSame([], $events); } public function testLogger() { $logger = new BufferingLogger(); $dispatcher = new EventDispatcher(); $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch(), $logger); $tdispatcher->addListener('foo', $listener1 = static function () {}); $tdispatcher->addListener('foo', $listener2 = static function () {}); $tdispatcher->dispatch(new Event(), 'foo'); $this->assertSame([ [ 'debug', 'Notified event "{event}" to listener "{listener}".', ['event' => 'foo', 'listener' => 'closure'], ], [ 'debug', 'Notified event "{event}" to listener "{listener}".', ['event' => 'foo', 'listener' => 'closure'], ], ], $logger->cleanLogs()); } public function testLoggerWithStoppedEvent() { $logger = new BufferingLogger(); $dispatcher = new EventDispatcher(); $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch(), $logger); $tdispatcher->addListener('foo', $listener1 = static function (Event $event) { $event->stopPropagation(); }); $tdispatcher->addListener('foo', $listener2 = static function () {}); $tdispatcher->dispatch(new Event(), 'foo'); $this->assertSame([ [ 'debug', 'Notified event "{event}" to listener "{listener}".', ['event' => 'foo', 'listener' => 'closure'], ], [ 'debug', 'Listener "{listener}" stopped propagation of the event "{event}".', ['event' => 'foo', 'listener' => 'closure'], ], [ 'debug', 'Listener "{listener}" was not called for event "{event}".', ['event' => 'foo', 'listener' => 'closure'], ], ], $logger->cleanLogs()); } public function testDispatchCallListeners() { $called = []; $dispatcher = new EventDispatcher(); $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); $tdispatcher->addListener('foo', static function () use (&$called) { $called[] = 'foo1'; }, 10); $tdispatcher->addListener('foo', static function () use (&$called) { $called[] = 'foo2'; }, 20); $tdispatcher->dispatch(new Event(), 'foo'); $this->assertSame(['foo2', 'foo1'], $called); } public function testDispatchNested() { $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); $loop = 1; $dispatchedEvents = 0; $dispatcher->addListener('foo', $listener1 = static function () use ($dispatcher, &$loop) { ++$loop; if (2 == $loop) { $dispatcher->dispatch(new Event(), 'foo'); } }); $dispatcher->addListener('foo', static function () use (&$dispatchedEvents) { ++$dispatchedEvents; }); $dispatcher->dispatch(new Event(), 'foo'); $this->assertSame(2, $dispatchedEvents); } public function testDispatchReusedEventNested() { $nestedCall = false; $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); $dispatcher->addListener('foo', static function (Event $e) use ($dispatcher) { $dispatcher->dispatch(new Event(), 'bar', $e); }); $dispatcher->addListener('bar', static function (Event $e) use (&$nestedCall) { $nestedCall = true; }); $this->assertFalse($nestedCall); $dispatcher->dispatch(new Event(), 'foo'); $this->assertTrue($nestedCall); } public function testListenerCanRemoveItselfWhenExecuted() { $eventDispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); $listener1 = static function ($event, $eventName, EventDispatcherInterface $dispatcher) use (&$listener1) { $dispatcher->removeListener('foo', $listener1); }; $eventDispatcher->addListener('foo', $listener1); $eventDispatcher->addListener('foo', static function () {}); $eventDispatcher->dispatch(new Event(), 'foo'); $this->assertCount(1, $eventDispatcher->getListeners('foo'), 'expected listener1 to be removed'); } public function testClearOrphanedEvents() { $tdispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); $tdispatcher->dispatch(new Event(), 'foo'); $events = $tdispatcher->getOrphanedEvents(); $this->assertCount(1, $events); $tdispatcher->reset(); $events = $tdispatcher->getOrphanedEvents(); $this->assertCount(0, $events); } } class EventSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return ['foo' => 'call']; } } ================================================ FILE: Tests/Debug/WrappedListenerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Tests\Debug; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\Debug\WrappedListener; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Stopwatch\Stopwatch; use Symfony\Component\Stopwatch\StopwatchEvent; class WrappedListenerTest extends TestCase { #[DataProvider('provideListenersToDescribe')] public function testListenerDescription($listener, $expected) { $wrappedListener = new WrappedListener($listener, null, new Stopwatch(), new EventDispatcher()); $this->assertStringMatchesFormat($expected, $wrappedListener->getPretty()); } public static function provideListenersToDescribe() { return [ [new FooListener(), 'Symfony\Component\EventDispatcher\Tests\Debug\FooListener::__invoke'], [[new FooListener(), 'listen'], 'Symfony\Component\EventDispatcher\Tests\Debug\FooListener::listen'], [['Symfony\Component\EventDispatcher\Tests\Debug\FooListener', 'listenStatic'], 'Symfony\Component\EventDispatcher\Tests\Debug\FooListener::listenStatic'], [['Symfony\Component\EventDispatcher\Tests\Debug\FooListener', 'invalidMethod'], 'Symfony\Component\EventDispatcher\Tests\Debug\FooListener::invalidMethod'], ['var_dump', 'var_dump'], [static function () {}, 'closure'], [\Closure::fromCallable([new FooListener(), 'listen']), 'Symfony\Component\EventDispatcher\Tests\Debug\FooListener::listen'], [\Closure::fromCallable(['Symfony\Component\EventDispatcher\Tests\Debug\FooListener', 'listenStatic']), 'Symfony\Component\EventDispatcher\Tests\Debug\FooListener::listenStatic'], [\Closure::fromCallable(static function () {}), 'closure'], [[#[\Closure(name: FooListener::class)] static fn () => new FooListener(), 'listen'], 'Symfony\Component\EventDispatcher\Tests\Debug\FooListener::listen'], ]; } public function testStopwatchEventIsStoppedWhenListenerThrows() { $stopwatchEvent = $this->createMock(StopwatchEvent::class); $stopwatchEvent->expects(self::once())->method('isStarted')->willReturn(true); $stopwatchEvent->expects(self::once())->method('stop'); $stopwatch = $this->createStub(Stopwatch::class); $stopwatch->method('start')->willReturn($stopwatchEvent); $dispatcher = $this->createStub(EventDispatcherInterface::class); $wrappedListener = new WrappedListener(static fn () => throw new \Exception(), null, $stopwatch, $dispatcher); try { $wrappedListener(new \stdClass(), 'foo', $dispatcher); } catch (\Exception $ex) { } } } class FooListener { public function listen() { } public function __invoke() { } public static function listenStatic() { } } ================================================ FILE: Tests/DependencyInjection/RegisterListenersPassTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\Compiler\AttributeAutoconfigurationPass; use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass; use Symfony\Component\DependencyInjection\ContainerBuilder; 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\Tests\Fixtures\CustomEvent; use Symfony\Component\EventDispatcher\Tests\Fixtures\DummyEvent; use Symfony\Component\EventDispatcher\Tests\Fixtures\TaggedInvokableListener; use Symfony\Component\EventDispatcher\Tests\Fixtures\TaggedMultiListener; use Symfony\Component\EventDispatcher\Tests\Fixtures\TaggedUnionTypeListener; class RegisterListenersPassTest extends TestCase { /** * Tests that event subscribers not implementing EventSubscriberInterface * trigger an exception. */ public function testEventSubscriberWithoutInterface() { $this->expectException(\InvalidArgumentException::class); $builder = new ContainerBuilder(); $builder->register('event_dispatcher'); $builder->register('my_event_subscriber', 'stdClass') ->addTag('kernel.event_subscriber'); $registerListenersPass = new RegisterListenersPass(); $registerListenersPass->process($builder); } public function testValidEventSubscriber() { $builder = new ContainerBuilder(); $eventDispatcherDefinition = $builder->register('event_dispatcher'); $builder->register('my_event_subscriber', 'Symfony\Component\EventDispatcher\Tests\DependencyInjection\SubscriberService') ->addTag('kernel.event_subscriber'); $registerListenersPass = new RegisterListenersPass(); $registerListenersPass->process($builder); $expectedCalls = [ [ 'addListener', [ 'event', [new ServiceClosureArgument(new Reference('my_event_subscriber')), 'onEvent'], 0, ], ], ]; $this->assertEquals($expectedCalls, $eventDispatcherDefinition->getMethodCalls()); } public function testAliasedEventSubscriber() { $builder = new ContainerBuilder(); $builder->setParameter('event_dispatcher.event_aliases', [AliasedEvent::class => 'aliased_event']); $builder->register('event_dispatcher'); $builder->register('my_event_subscriber', AliasedSubscriber::class) ->addTag('kernel.event_subscriber'); $eventAliasPass = new AddEventAliasesPass([CustomEvent::class => 'custom_event']); $eventAliasPass->process($builder); $registerListenersPass = new RegisterListenersPass(); $registerListenersPass->process($builder); $expectedCalls = [ [ 'addListener', [ 'aliased_event', [new ServiceClosureArgument(new Reference('my_event_subscriber')), 'onAliasedEvent'], 0, ], ], [ 'addListener', [ 'custom_event', [new ServiceClosureArgument(new Reference('my_event_subscriber')), 'onCustomEvent'], 0, ], ], ]; $this->assertEquals($expectedCalls, $builder->getDefinition('event_dispatcher')->getMethodCalls()); } public function testAbstractEventListener() { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('The service "foo" tagged "kernel.event_listener" must not be abstract.'); $container = new ContainerBuilder(); $container->register('foo', 'stdClass')->setAbstract(true)->addTag('kernel.event_listener', []); $container->register('event_dispatcher', 'stdClass'); $registerListenersPass = new RegisterListenersPass(); $registerListenersPass->process($container); } public function testAbstractEventSubscriber() { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('The service "foo" tagged "kernel.event_subscriber" must not be abstract.'); $container = new ContainerBuilder(); $container->register('foo', 'stdClass')->setAbstract(true)->addTag('kernel.event_subscriber', []); $container->register('event_dispatcher', 'stdClass'); $registerListenersPass = new RegisterListenersPass(); $registerListenersPass->process($container); } public function testEventSubscriberResolvableClassName() { $container = new ContainerBuilder(); $container->setParameter('subscriber.class', 'Symfony\Component\EventDispatcher\Tests\DependencyInjection\SubscriberService'); $container->register('foo', '%subscriber.class%')->addTag('kernel.event_subscriber', []); $container->register('event_dispatcher', 'stdClass'); $registerListenersPass = new RegisterListenersPass(); $registerListenersPass->process($container); $definition = $container->getDefinition('event_dispatcher'); $expectedCalls = [ [ 'addListener', [ 'event', [new ServiceClosureArgument(new Reference('foo')), 'onEvent'], 0, ], ], ]; $this->assertEquals($expectedCalls, $definition->getMethodCalls()); } public function testHotPathEvents() { $container = new ContainerBuilder(); $container->register('foo', SubscriberService::class)->addTag('kernel.event_subscriber', []); $container->register('event_dispatcher', 'stdClass'); (new RegisterListenersPass())->setHotPathEvents(['event'])->process($container); $this->assertTrue($container->getDefinition('foo')->hasTag('container.hot_path')); } public function testNoPreloadEvents() { $container = new ContainerBuilder(); $container->register('foo', SubscriberService::class)->addTag('kernel.event_subscriber', []); $container->register('bar')->addTag('kernel.event_listener', ['event' => 'cold_event']); $container->register('baz') ->addTag('kernel.event_listener', ['event' => 'event']) ->addTag('kernel.event_listener', ['event' => 'cold_event']); $container->register('event_dispatcher', 'stdClass'); (new RegisterListenersPass()) ->setHotPathEvents(['event']) ->setNoPreloadEvents(['cold_event']) ->process($container); $this->assertFalse($container->getDefinition('foo')->hasTag('container.no_preload')); $this->assertTrue($container->getDefinition('bar')->hasTag('container.no_preload')); $this->assertFalse($container->getDefinition('baz')->hasTag('container.no_preload')); } public function testEventSubscriberUnresolvableClassName() { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('You have requested a non-existent parameter "subscriber.class"'); $container = new ContainerBuilder(); $container->register('foo', '%subscriber.class%')->addTag('kernel.event_subscriber', []); $container->register('event_dispatcher', 'stdClass'); $registerListenersPass = new RegisterListenersPass(); $registerListenersPass->process($container); } public function testInvokableEventListener() { $container = new ContainerBuilder(); $container->setParameter('event_dispatcher.event_aliases', [AliasedEvent::class => 'aliased_event']); $container->register('foo', \get_class(new class { public function onFooBar() { } }))->addTag('kernel.event_listener', ['event' => 'foo.bar']); $container->register('bar', InvokableListenerService::class)->addTag('kernel.event_listener', ['event' => 'foo.bar']); $container->register('baz', InvokableListenerService::class)->addTag('kernel.event_listener', ['event' => 'event']); $container->register('zar', \get_class(new class { public function onFooBarZar() { } }))->addTag('kernel.event_listener', ['event' => 'foo.bar_zar']); $container->register('event_dispatcher', \stdClass::class); $registerListenersPass = new RegisterListenersPass(); $registerListenersPass->process($container); $definition = $container->getDefinition('event_dispatcher'); $expectedCalls = [ [ 'addListener', [ 'foo.bar', [new ServiceClosureArgument(new Reference('foo')), 'onFooBar'], 0, ], ], [ 'addListener', [ 'foo.bar', [new ServiceClosureArgument(new Reference('bar')), '__invoke'], 0, ], ], [ 'addListener', [ 'event', [new ServiceClosureArgument(new Reference('baz')), 'onEvent'], 0, ], ], [ 'addListener', [ 'foo.bar_zar', [new ServiceClosureArgument(new Reference('zar')), 'onFooBarZar'], 0, ], ], ]; $this->assertEquals($expectedCalls, $definition->getMethodCalls()); } public function testItThrowsAnExceptionIfTagIsMissingMethodAndClassHasNoValidMethod() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('None of the "onFooBar" or "__invoke" methods exist for the service "foo". Please define the "method" attribute on "kernel.event_listener" tags.'); $container = new ContainerBuilder(); $container->register('foo', \stdClass::class)->addTag('kernel.event_listener', ['event' => 'foo.bar']); $container->register('event_dispatcher', \stdClass::class); $registerListenersPass = new RegisterListenersPass(); $registerListenersPass->process($container); } public function testTaggedInvokableEventListener() { $container = $this->createContainerBuilder(); $container->register('foo', TaggedInvokableListener::class)->setAutoconfigured(true); $container->register('event_dispatcher', \stdClass::class); (new AttributeAutoconfigurationPass())->process($container); (new ResolveInstanceofConditionalsPass())->process($container); (new RegisterListenersPass())->process($container); $definition = $container->getDefinition('event_dispatcher'); $expectedCalls = [ [ 'addListener', [ CustomEvent::class, [new ServiceClosureArgument(new Reference('foo')), '__invoke'], 0, ], ], ]; $this->assertEquals($expectedCalls, \array_slice($definition->getMethodCalls(), 0, \count($expectedCalls))); } public function testTaggedMultiEventListener() { $container = $this->createContainerBuilder(); $container->register('foo', TaggedMultiListener::class)->setAutoconfigured(true); $container->register('event_dispatcher', \stdClass::class); (new AttributeAutoconfigurationPass())->process($container); (new ResolveInstanceofConditionalsPass())->process($container); (new RegisterListenersPass())->process($container); $definition = $container->getDefinition('event_dispatcher'); $expectedCalls = [ [ 'addListener', [ CustomEvent::class, [new ServiceClosureArgument(new Reference('foo')), 'onCustomEvent'], 0, ], ], [ 'addListener', [ 'foo', [new ServiceClosureArgument(new Reference('foo')), 'onFoo'], 42, ], ], [ 'addListener', [ 'bar', [new ServiceClosureArgument(new Reference('foo')), 'onBarEvent'], 0, ], ], [ 'addListener', [ 'baz', [new ServiceClosureArgument(new Reference('foo')), 'onBazEvent'], 0, ], ], ]; $this->assertEquals($expectedCalls, \array_slice($definition->getMethodCalls(), 0, \count($expectedCalls))); } public function testTaggedMethodUnionTypeEventListener() { $container = $this->createContainerBuilder(); $container->register('foo', TaggedUnionTypeListener::class)->setAutoconfigured(true); $container->register('event_dispatcher', \stdClass::class); (new AttributeAutoconfigurationPass())->process($container); (new ResolveInstanceofConditionalsPass())->process($container); (new RegisterListenersPass())->process($container); $definition = $container->getDefinition('event_dispatcher'); $expectedCalls = [ [ 'addListener', [ CustomEvent::class, [new ServiceClosureArgument(new Reference('foo')), 'onUnionEvent'], 0, ], ], [ 'addListener', [ DummyEvent::class, [new ServiceClosureArgument(new Reference('foo')), 'onUnionEvent'], 0, ], ], ]; $this->assertEquals($expectedCalls, \array_slice($definition->getMethodCalls(), 0, \count($expectedCalls))); } public function testAliasedEventListener() { $container = new ContainerBuilder(); $eventAliases = [AliasedEvent::class => 'aliased_event']; $container->setParameter('event_dispatcher.event_aliases', $eventAliases); $container->register('foo', InvokableListenerService::class)->addTag('kernel.event_listener', ['event' => AliasedEvent::class, 'method' => 'onEvent']); $container->register('bar', InvokableListenerService::class)->addTag('kernel.event_listener', ['event' => CustomEvent::class, 'method' => 'onEvent']); $container->register('event_dispatcher'); $customEventAlias = [CustomEvent::class => 'custom_event']; $eventAliasPass = new AddEventAliasesPass($customEventAlias); $eventAliasPass->process($container); $registerListenersPass = new RegisterListenersPass(); $registerListenersPass->process($container); $this->assertTrue($container->hasParameter('event_dispatcher.event_aliases')); $this->assertSame(array_merge($eventAliases, $customEventAlias), $container->getParameter('event_dispatcher.event_aliases')); $definition = $container->getDefinition('event_dispatcher'); $expectedCalls = [ [ 'addListener', [ 'aliased_event', [new ServiceClosureArgument(new Reference('foo')), 'onEvent'], 0, ], ], [ 'addListener', [ 'custom_event', [new ServiceClosureArgument(new Reference('bar')), 'onEvent'], 0, ], ], ]; $this->assertEquals($expectedCalls, $definition->getMethodCalls()); } public function testOmitEventNameOnTypedListener() { $container = new ContainerBuilder(); $container->setParameter('event_dispatcher.event_aliases', [AliasedEvent::class => 'aliased_event']); $container->register('foo', TypedListener::class)->addTag('kernel.event_listener', ['method' => 'onEvent']); $container->register('bar', TypedListener::class)->addTag('kernel.event_listener'); $container->register('event_dispatcher'); $registerListenersPass = new RegisterListenersPass(); $registerListenersPass->process($container); $definition = $container->getDefinition('event_dispatcher'); $expectedCalls = [ [ 'addListener', [ CustomEvent::class, [new ServiceClosureArgument(new Reference('foo')), 'onEvent'], 0, ], ], [ 'addListener', [ 'aliased_event', [new ServiceClosureArgument(new Reference('bar')), '__invoke'], 0, ], ], ]; $this->assertEquals($expectedCalls, $definition->getMethodCalls()); } public function testOmitEventNameOnUntypedListener() { $container = new ContainerBuilder(); $container->register('foo', InvokableListenerService::class)->addTag('kernel.event_listener', ['method' => 'onEvent']); $container->register('event_dispatcher'); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Service "foo" must define the "event" attribute on "kernel.event_listener" tags.'); $registerListenersPass = new RegisterListenersPass(); $registerListenersPass->process($container); } public function testOmitEventNameAndMethodOnUntypedListener() { $container = new ContainerBuilder(); $container->register('foo', InvokableListenerService::class)->addTag('kernel.event_listener'); $container->register('event_dispatcher'); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Service "foo" must define the "event" attribute on "kernel.event_listener" tags.'); $registerListenersPass = new RegisterListenersPass(); $registerListenersPass->process($container); } public function testOmitEventNameAndMethodOnGenericListener() { $container = new ContainerBuilder(); $container->register('foo', GenericListener::class)->addTag('kernel.event_listener'); $container->register('event_dispatcher'); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Service "foo" must define the "event" attribute on "kernel.event_listener" tags.'); $registerListenersPass = new RegisterListenersPass(); $registerListenersPass->process($container); } public function testOmitEventNameOnSubscriber() { $container = new ContainerBuilder(); $container->register('subscriber', IncompleteSubscriber::class) ->addTag('kernel.event_subscriber') ->addTag('kernel.event_listener') ->addTag('kernel.event_listener', ['event' => 'bar', 'method' => 'onBar']) ; $container->register('event_dispatcher'); $registerListenersPass = new RegisterListenersPass(); $registerListenersPass->process($container); $definition = $container->getDefinition('event_dispatcher'); $expectedCalls = [ [ 'addListener', [ 'bar', [new ServiceClosureArgument(new Reference('subscriber')), 'onBar'], 0, ], ], [ 'addListener', [ 'foo', [new ServiceClosureArgument(new Reference('subscriber')), 'onFoo'], 0, ], ], ]; $this->assertEquals($expectedCalls, $definition->getMethodCalls()); } private function createContainerBuilder(): ContainerBuilder { $container = new ContainerBuilder(); $container->setParameter('kernel.debug', true); $container->setParameter('kernel.project_dir', __DIR__); $container->setParameter('kernel.container_class', 'testContainer'); (new FrameworkExtension())->load([], $container); return $container; } } class SubscriberService implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ 'event' => 'onEvent', ]; } } class InvokableListenerService { public function __invoke() { } public function onEvent() { } } final class AliasedSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ AliasedEvent::class => 'onAliasedEvent', CustomEvent::class => 'onCustomEvent', ]; } } final class AliasedEvent { } final class TypedListener { public function __invoke(AliasedEvent $event): void { } public function onEvent(CustomEvent $event): void { } } final class GenericListener { public function __invoke(object $event): void { } } final class IncompleteSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ 'foo' => 'onFoo', ]; } public function onFoo(): void { } public function onBar(): void { } public function __invoke(CustomEvent $event): void { } } ================================================ FILE: Tests/EventDispatcherTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Tests; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Contracts\EventDispatcher\Event; class EventDispatcherTest extends TestCase { /* Some pseudo events */ private const preFoo = 'pre.foo'; private const postFoo = 'post.foo'; private const preBar = 'pre.bar'; private EventDispatcher $dispatcher; private TestEventListener $listener; protected function setUp(): void { $this->dispatcher = $this->createEventDispatcher(); $this->listener = new TestEventListener(); } protected function createEventDispatcher() { return new EventDispatcher(); } public function testInitialState() { $this->assertEquals([], $this->dispatcher->getListeners()); $this->assertFalse($this->dispatcher->hasListeners(self::preFoo)); $this->assertFalse($this->dispatcher->hasListeners(self::postFoo)); } public function testAddListener() { $this->dispatcher->addListener('pre.foo', [$this->listener, 'preFoo']); $this->dispatcher->addListener('post.foo', $this->listener->postFoo(...)); $this->assertTrue($this->dispatcher->hasListeners()); $this->assertTrue($this->dispatcher->hasListeners(self::preFoo)); $this->assertTrue($this->dispatcher->hasListeners(self::postFoo)); $this->assertCount(1, $this->dispatcher->getListeners(self::preFoo)); $this->assertCount(1, $this->dispatcher->getListeners(self::postFoo)); $this->assertCount(2, $this->dispatcher->getListeners()); } public function testGetListenersSortsByPriority() { $listener1 = new TestEventListener(); $listener2 = new TestEventListener(); $listener3 = new TestEventListener(); $listener4 = new TestEventListener(); $listener1->name = '1'; $listener2->name = '2'; $listener3->name = '3'; $listener4->name = '4'; $this->dispatcher->addListener('pre.foo', [$listener1, 'preFoo'], -10); $this->dispatcher->addListener('pre.foo', [$listener2, 'preFoo'], 10); $this->dispatcher->addListener('pre.foo', [$listener3, 'preFoo']); $this->dispatcher->addListener('pre.foo', $listener4->preFoo(...), 20); $expected = [ $listener4->preFoo(...), [$listener2, 'preFoo'], [$listener3, 'preFoo'], [$listener1, 'preFoo'], ]; $this->assertEquals($expected, $this->dispatcher->getListeners('pre.foo')); } public function testGetAllListenersSortsByPriority() { $listener1 = new TestEventListener(); $listener2 = new TestEventListener(); $listener3 = new TestEventListener(); $listener4 = new TestEventListener(); $listener5 = new TestEventListener(); $listener6 = new TestEventListener(); $this->dispatcher->addListener('pre.foo', $listener1, -10); $this->dispatcher->addListener('pre.foo', $listener2); $this->dispatcher->addListener('pre.foo', $listener3, 10); $this->dispatcher->addListener('post.foo', $listener4, -10); $this->dispatcher->addListener('post.foo', $listener5); $this->dispatcher->addListener('post.foo', $listener6, 10); $expected = [ 'pre.foo' => [$listener3, $listener2, $listener1], 'post.foo' => [$listener6, $listener5, $listener4], ]; $this->assertSame($expected, $this->dispatcher->getListeners()); } public function testGetListenerPriority() { $listener1 = new TestEventListener(); $listener2 = new TestEventListener(); $this->dispatcher->addListener('pre.foo', $listener1, -10); $this->dispatcher->addListener('pre.foo', $listener2); $this->assertSame(-10, $this->dispatcher->getListenerPriority('pre.foo', $listener1)); $this->assertSame(0, $this->dispatcher->getListenerPriority('pre.foo', $listener2)); $this->assertNull($this->dispatcher->getListenerPriority('pre.bar', $listener2)); $this->assertNull($this->dispatcher->getListenerPriority('pre.foo', static function () {})); } public function testDispatch() { $this->dispatcher->addListener('pre.foo', [$this->listener, 'preFoo']); $this->dispatcher->addListener('post.foo', $this->listener->postFoo(...)); $this->dispatcher->dispatch(new Event(), self::preFoo); $this->assertTrue($this->listener->preFooInvoked); $this->assertFalse($this->listener->postFooInvoked); $this->assertInstanceOf(Event::class, $this->dispatcher->dispatch(new Event(), 'noevent')); $this->assertInstanceOf(Event::class, $this->dispatcher->dispatch(new Event(), self::preFoo)); $event = new Event(); $return = $this->dispatcher->dispatch($event, self::preFoo); $this->assertSame($event, $return); } public function testDispatchForClosure() { $invoked = 0; $listener = static function () use (&$invoked) { ++$invoked; }; $this->dispatcher->addListener('pre.foo', $listener); $this->dispatcher->addListener('post.foo', $listener); $this->dispatcher->dispatch(new Event(), self::preFoo); $this->assertEquals(1, $invoked); } public function testStopEventPropagation() { $otherListener = new TestEventListener(); // postFoo() stops the propagation, so only one listener should // be executed // Manually set priority to enforce $this->listener to be called first $this->dispatcher->addListener('post.foo', [$this->listener, 'postFoo'], 10); $this->dispatcher->addListener('post.foo', $otherListener->postFoo(...)); $this->dispatcher->dispatch(new Event(), self::postFoo); $this->assertTrue($this->listener->postFooInvoked); $this->assertFalse($otherListener->postFooInvoked); } public function testDispatchByPriority() { $invoked = []; $listener1 = static function () use (&$invoked) { $invoked[] = '1'; }; $listener2 = static function () use (&$invoked) { $invoked[] = '2'; }; $listener3 = static function () use (&$invoked) { $invoked[] = '3'; }; $this->dispatcher->addListener('pre.foo', $listener1, -10); $this->dispatcher->addListener('pre.foo', $listener2); $this->dispatcher->addListener('pre.foo', $listener3, 10); $this->dispatcher->dispatch(new Event(), self::preFoo); $this->assertEquals(['3', '2', '1'], $invoked); } public function testRemoveListener() { $this->dispatcher->addListener('pre.bar', $this->listener); $this->assertTrue($this->dispatcher->hasListeners(self::preBar)); $this->dispatcher->removeListener('pre.bar', $this->listener); $this->assertFalse($this->dispatcher->hasListeners(self::preBar)); $this->dispatcher->removeListener('notExists', $this->listener); } public function testAddSubscriber() { $eventSubscriber = new TestEventSubscriber(); $this->dispatcher->addSubscriber($eventSubscriber); $this->assertTrue($this->dispatcher->hasListeners(self::preFoo)); $this->assertTrue($this->dispatcher->hasListeners(self::postFoo)); } public function testAddSubscriberWithPriorities() { $eventSubscriber = new TestEventSubscriber(); $this->dispatcher->addSubscriber($eventSubscriber); $eventSubscriber = new TestEventSubscriberWithPriorities(); $this->dispatcher->addSubscriber($eventSubscriber); $listeners = $this->dispatcher->getListeners('pre.foo'); $this->assertTrue($this->dispatcher->hasListeners(self::preFoo)); $this->assertCount(2, $listeners); $this->assertInstanceOf(TestEventSubscriberWithPriorities::class, $listeners[0][0]); } public function testAddSubscriberWithMultipleListeners() { $eventSubscriber = new TestEventSubscriberWithMultipleListeners(); $this->dispatcher->addSubscriber($eventSubscriber); $listeners = $this->dispatcher->getListeners('pre.foo'); $this->assertTrue($this->dispatcher->hasListeners(self::preFoo)); $this->assertCount(2, $listeners); $this->assertEquals('preFoo2', $listeners[0][1]); } public function testRemoveSubscriber() { $eventSubscriber = new TestEventSubscriber(); $this->dispatcher->addSubscriber($eventSubscriber); $this->assertTrue($this->dispatcher->hasListeners(self::preFoo)); $this->assertTrue($this->dispatcher->hasListeners(self::postFoo)); $this->dispatcher->removeSubscriber($eventSubscriber); $this->assertFalse($this->dispatcher->hasListeners(self::preFoo)); $this->assertFalse($this->dispatcher->hasListeners(self::postFoo)); } public function testRemoveSubscriberWithPriorities() { $eventSubscriber = new TestEventSubscriberWithPriorities(); $this->dispatcher->addSubscriber($eventSubscriber); $this->assertTrue($this->dispatcher->hasListeners(self::preFoo)); $this->dispatcher->removeSubscriber($eventSubscriber); $this->assertFalse($this->dispatcher->hasListeners(self::preFoo)); } public function testRemoveSubscriberWithMultipleListeners() { $eventSubscriber = new TestEventSubscriberWithMultipleListeners(); $this->dispatcher->addSubscriber($eventSubscriber); $this->assertTrue($this->dispatcher->hasListeners(self::preFoo)); $this->assertCount(2, $this->dispatcher->getListeners(self::preFoo)); $this->dispatcher->removeSubscriber($eventSubscriber); $this->assertFalse($this->dispatcher->hasListeners(self::preFoo)); } public function testEventReceivesTheDispatcherInstanceAsArgument() { $listener = new TestWithDispatcher(); $this->dispatcher->addListener('test', [$listener, 'foo']); $this->assertNull($listener->name); $this->assertNull($listener->dispatcher); $this->dispatcher->dispatch(new Event(), 'test'); $this->assertEquals('test', $listener->name); $this->assertSame($this->dispatcher, $listener->dispatcher); } /** * @see https://bugs.php.net/62976 * * This bug affects: * - The PHP 5.3 branch for versions < 5.3.18 * - The PHP 5.4 branch for versions < 5.4.8 * - The PHP 5.5 branch is not affected */ public function testWorkaroundForPhpBug62976() { $dispatcher = $this->createEventDispatcher(); $dispatcher->addListener('bug.62976', new CallableClass()); $dispatcher->removeListener('bug.62976', static function () {}); $this->assertTrue($dispatcher->hasListeners('bug.62976')); } public function testHasListenersWhenAddedCallbackListenerIsRemoved() { $listener = static function () {}; $this->dispatcher->addListener('foo', $listener); $this->dispatcher->removeListener('foo', $listener); $this->assertFalse($this->dispatcher->hasListeners()); } public function testGetListenersWhenAddedCallbackListenerIsRemoved() { $listener = static function () {}; $this->dispatcher->addListener('foo', $listener); $this->dispatcher->removeListener('foo', $listener); $this->assertSame([], $this->dispatcher->getListeners()); } public function testHasListenersWithoutEventsReturnsFalseAfterHasListenersWithEventHasBeenCalled() { $this->assertFalse($this->dispatcher->hasListeners('foo')); $this->assertFalse($this->dispatcher->hasListeners()); } public function testHasListenersIsLazy() { $called = 0; $listener = [static function () use (&$called) { ++$called; }, 'onFoo']; $this->dispatcher->addListener('foo', $listener); $this->assertTrue($this->dispatcher->hasListeners()); $this->assertTrue($this->dispatcher->hasListeners('foo')); $this->assertSame(0, $called); } public function testDispatchLazyListener() { $dispatcher = new TestWithDispatcher(); $called = 0; $factory = static function () use (&$called, $dispatcher) { ++$called; return $dispatcher; }; $this->dispatcher->addListener('foo', [$factory, 'foo']); $this->assertSame(0, $called); $this->dispatcher->dispatch(new Event(), 'foo'); $this->assertFalse($dispatcher->invoked); $this->dispatcher->dispatch(new Event(), 'foo'); $this->assertSame(1, $called); $this->dispatcher->addListener('bar', [$factory]); $this->assertSame(1, $called); $this->dispatcher->dispatch(new Event(), 'bar'); $this->assertTrue($dispatcher->invoked); $this->dispatcher->dispatch(new Event(), 'bar'); $this->assertSame(2, $called); } public function testRemoveFindsLazyListeners() { $test = new TestWithDispatcher(); $factory = static fn () => $test; $this->dispatcher->addListener('foo', [$factory, 'foo']); $this->assertTrue($this->dispatcher->hasListeners('foo')); $this->dispatcher->removeListener('foo', [$test, 'foo']); $this->assertFalse($this->dispatcher->hasListeners('foo')); $this->dispatcher->addListener('foo', [$test, 'foo']); $this->assertTrue($this->dispatcher->hasListeners('foo')); $this->dispatcher->removeListener('foo', [$factory, 'foo']); $this->assertFalse($this->dispatcher->hasListeners('foo')); } public function testPriorityFindsLazyListeners() { $test = new TestWithDispatcher(); $factory = static fn () => $test; $this->dispatcher->addListener('foo', [$factory, 'foo'], 3); $this->assertSame(3, $this->dispatcher->getListenerPriority('foo', [$test, 'foo'])); $this->dispatcher->removeListener('foo', [$factory, 'foo']); $this->dispatcher->addListener('foo', [$test, 'foo'], 5); $this->assertSame(5, $this->dispatcher->getListenerPriority('foo', [$factory, 'foo'])); } public function testGetLazyListeners() { $test = new TestWithDispatcher(); $factory = static fn () => $test; $this->dispatcher->addListener('foo', [$factory, 'foo'], 3); $this->assertSame([[$test, 'foo']], $this->dispatcher->getListeners('foo')); $this->dispatcher->removeListener('foo', [$test, 'foo']); $this->dispatcher->addListener('bar', [$factory, 'foo'], 3); $this->assertSame(['bar' => [[$test, 'foo']]], $this->dispatcher->getListeners()); } public function testMutatingWhilePropagationIsStopped() { $testLoaded = false; $test = new TestEventListener(); $this->dispatcher->addListener('foo', $test->postFoo(...)); $this->dispatcher->addListener('foo', [static function () use ($test, &$testLoaded) { $testLoaded = true; return $test; }, 'preFoo']); $this->dispatcher->dispatch(new Event(), 'foo'); $this->assertTrue($test->postFooInvoked); $this->assertFalse($test->preFooInvoked); $this->assertEquals(0, $this->dispatcher->getListenerPriority('foo', $test->postFoo(...))); $test->preFoo(new Event()); $this->dispatcher->dispatch(new Event(), 'foo'); $this->assertTrue($testLoaded); } public function testNamedClosures() { $listener = new TestEventListener(); $callback1 = $listener(...); $callback2 = $listener(...); $callback3 = (new TestEventListener())(...); $this->assertNotSame($callback1, $callback2); $this->assertNotSame($callback1, $callback3); $this->assertNotSame($callback2, $callback3); $this->dispatcher->addListener('foo', $callback1, 3); $this->dispatcher->addListener('foo', $callback2, 2); $this->dispatcher->addListener('foo', $callback3, 1); $this->assertSame(3, $this->dispatcher->getListenerPriority('foo', $callback1)); $this->assertSame(3, $this->dispatcher->getListenerPriority('foo', $callback2)); $this->dispatcher->removeListener('foo', $callback1); $this->assertSame(['foo' => [$callback3]], $this->dispatcher->getListeners()); } } class CallableClass { public function __invoke() { } } class TestEventListener { public string $name; public bool $preFooInvoked = false; public bool $postFooInvoked = false; /* Listener methods */ public function preFoo($e) { $this->preFooInvoked = true; } public function postFoo($e) { $this->postFooInvoked = true; if (!$this->preFooInvoked) { $e->stopPropagation(); } } public function __invoke() { } } class TestWithDispatcher { public ?string $name = null; public ?EventDispatcher $dispatcher = null; public bool $invoked = false; public function foo($e, $name, $dispatcher) { $this->name = $name; $this->dispatcher = $dispatcher; } public function __invoke($e, $name, $dispatcher) { $this->name = $name; $this->dispatcher = $dispatcher; $this->invoked = true; } } class TestEventSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return ['pre.foo' => 'preFoo', 'post.foo' => 'postFoo']; } } class TestEventSubscriberWithPriorities implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ 'pre.foo' => ['preFoo', 10], 'post.foo' => ['postFoo'], ]; } } class TestEventSubscriberWithMultipleListeners implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return ['pre.foo' => [ ['preFoo1'], ['preFoo2', 10], ]]; } } ================================================ FILE: Tests/Fixtures/CustomEvent.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Tests\Fixtures; final class CustomEvent { } ================================================ FILE: Tests/Fixtures/DummyEvent.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Tests\Fixtures; final class DummyEvent { } ================================================ FILE: Tests/Fixtures/TaggedInvokableListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Tests\Fixtures; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; #[AsEventListener] final class TaggedInvokableListener { public function __invoke(CustomEvent $event): void { } } ================================================ FILE: Tests/Fixtures/TaggedMultiListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Tests\Fixtures; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; #[AsEventListener(event: CustomEvent::class, method: 'onCustomEvent')] #[AsEventListener(event: 'foo', priority: 42)] #[AsEventListener(event: 'bar', method: 'onBarEvent')] final class TaggedMultiListener { public function onCustomEvent(CustomEvent $event): void { } public function onFoo(): void { } public function onBarEvent(): void { } #[AsEventListener(event: 'baz')] public function onBazEvent(): void { } } ================================================ FILE: Tests/Fixtures/TaggedUnionTypeListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Tests\Fixtures; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; final class TaggedUnionTypeListener { #[AsEventListener] public function onUnionEvent(CustomEvent|DummyEvent $event): void { } } ================================================ FILE: Tests/GenericEventTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Tests; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\GenericEvent; /** * Test class for Event. */ class GenericEventTest extends TestCase { private GenericEvent $event; private \stdClass $subject; protected function setUp(): void { $this->subject = new \stdClass(); $this->event = new GenericEvent($this->subject, ['name' => 'Event']); } public function testConstruct() { $this->assertEquals($this->event, new GenericEvent($this->subject, ['name' => 'Event'])); } /** * Tests Event->getArgs(). */ public function testGetArguments() { // test getting all $this->assertSame(['name' => 'Event'], $this->event->getArguments()); } public function testSetArguments() { $result = $this->event->setArguments(['foo' => 'bar']); $this->assertSame(['foo' => 'bar'], $this->event->getArguments()); $this->assertSame($this->event, $result); } public function testSetArgument() { $result = $this->event->setArgument('foo2', 'bar2'); $this->assertSame(['name' => 'Event', 'foo2' => 'bar2'], $this->event->getArguments()); $this->assertEquals($this->event, $result); } public function testGetArgument() { // test getting key $this->assertEquals('Event', $this->event->getArgument('name')); } public function testGetArgException() { $this->expectException(\InvalidArgumentException::class); $this->event->getArgument('nameNotExist'); } public function testOffsetGet() { // test getting key $this->assertEquals('Event', $this->event['name']); // test getting invalid arg $this->expectException(\InvalidArgumentException::class); $this->assertFalse($this->event['nameNotExist']); } public function testOffsetSet() { $this->event['foo2'] = 'bar2'; $this->assertSame(['name' => 'Event', 'foo2' => 'bar2'], $this->event->getArguments()); } public function testOffsetUnset() { unset($this->event['name']); $this->assertSame([], $this->event->getArguments()); } public function testOffsetIsset() { $this->assertArrayHasKey('name', $this->event); $this->assertArrayNotHasKey('nameNotExist', $this->event); } public function testHasArgument() { $this->assertTrue($this->event->hasArgument('name')); $this->assertFalse($this->event->hasArgument('nameNotExist')); } public function testGetSubject() { $this->assertSame($this->subject, $this->event->getSubject()); } public function testHasIterator() { $data = []; foreach ($this->event as $key => $value) { $data[$key] = $value; } $this->assertEquals(['name' => 'Event'], $data); } } ================================================ FILE: Tests/ImmutableEventDispatcherTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Tests; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\ImmutableEventDispatcher; use Symfony\Contracts\EventDispatcher\Event; /** * @author Bernhard Schussek */ class ImmutableEventDispatcherTest extends TestCase { public function testDispatchDelegates() { $innerDispatcher = $this->createMock(EventDispatcherInterface::class); $dispatcher = new ImmutableEventDispatcher($innerDispatcher); $event = new Event(); $resultEvent = new Event(); $innerDispatcher->expects($this->once()) ->method('dispatch') ->with($event, 'event') ->willReturn($resultEvent); $this->assertSame($resultEvent, $dispatcher->dispatch($event, 'event')); } public function testGetListenersDelegates() { $innerDispatcher = $this->createMock(EventDispatcherInterface::class); $dispatcher = new ImmutableEventDispatcher($innerDispatcher); $innerDispatcher->expects($this->once()) ->method('getListeners') ->with('event') ->willReturn(['result']); $this->assertSame(['result'], $dispatcher->getListeners('event')); } public function testHasListenersDelegates() { $innerDispatcher = $this->createMock(EventDispatcherInterface::class); $dispatcher = new ImmutableEventDispatcher($innerDispatcher); $innerDispatcher->expects($this->once()) ->method('hasListeners') ->with('event') ->willReturn(true); $this->assertTrue($dispatcher->hasListeners('event')); } public function testAddListenerDisallowed() { $dispatcher = new ImmutableEventDispatcher(new EventDispatcher()); $this->expectException(\BadMethodCallException::class); $dispatcher->addListener('event', static fn () => 'foo'); } public function testAddSubscriberDisallowed() { $dispatcher = new ImmutableEventDispatcher(new EventDispatcher()); $this->expectException(\BadMethodCallException::class); $subscriber = $this->createStub(EventSubscriberInterface::class); $dispatcher->addSubscriber($subscriber); } public function testRemoveListenerDisallowed() { $dispatcher = new ImmutableEventDispatcher(new EventDispatcher()); $this->expectException(\BadMethodCallException::class); $dispatcher->removeListener('event', static fn () => 'foo'); } public function testRemoveSubscriberDisallowed() { $dispatcher = new ImmutableEventDispatcher(new EventDispatcher()); $this->expectException(\BadMethodCallException::class); $subscriber = $this->createStub(EventSubscriberInterface::class); $dispatcher->removeSubscriber($subscriber); } } ================================================ FILE: composer.json ================================================ { "name": "symfony/event-dispatcher", "type": "library", "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Fabien Potencier", "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=8.4", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "require-dev": { "psr/log": "^1|^2|^3", "symfony/config": "^7.4|^8.0", "symfony/dependency-injection": "^7.4|^8.0", "symfony/error-handler": "^7.4|^8.0", "symfony/expression-language": "^7.4|^8.0", "symfony/framework-bundle": "^7.4|^8.0", "symfony/http-foundation": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3", "symfony/stopwatch": "^7.4|^8.0" }, "conflict": { "symfony/security-http": "<7.4", "symfony/service-contracts": "<2.5" }, "provide": { "psr/event-dispatcher-implementation": "1.0", "symfony/event-dispatcher-implementation": "2.0|3.0" }, "autoload": { "psr-4": { "Symfony\\Component\\EventDispatcher\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, "minimum-stability": "dev" } ================================================ FILE: phpunit.xml.dist ================================================ ./Tests/ ./ ./Resources ./Tests ./vendor