Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[EventDispatcher] add a way to call a listener before or after another one (WIP) #50687

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ public function __construct(
public ?string $method = null,
public int $priority = 0,
public ?string $dispatcher = null,
/** @param string|array{0: string, 1: string}|null $after */
public string|array|null $before = null,
/** @param string|array{0: string, 1: string}|null $after */
public string|array|null $after = null,
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

declare(strict_types=1);

namespace Symfony\Component\EventDispatcher\DependencyInjection;

use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

final class ListenerDefinition
{
public readonly int $priorityModifier;
public readonly string|null $beforeAfterService;
public readonly string|null $beforeAfterMethod;

public function __construct(
public readonly string $serviceId,
public readonly string $event,
public readonly string $method,
public readonly int $priority,
public readonly array $dispatchers,
public readonly string|array|null $before,
public readonly string|array|null $after,
)
{
if ($before && $after) {
throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": cannot use "after" and "before" at the same time.', $serviceId));
}

if (null === $before && null === $after) {
$this->priorityModifier = 0;
$this->beforeAfterMethod = null;
$this->beforeAfterService = null;

return;
}

$this->priorityModifier = null !== $before ? 1 : -1;

$beforeAfterDefinition = $before ?? $after;

if (\is_array($beforeAfterDefinition)) {
if (!array_is_list($beforeAfterDefinition) || 2 !== \count($beforeAfterDefinition)) {
throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": when declaring as an array, first item must be a service id and second item must be the method.', $this->serviceId));
}

$this->beforeAfterMethod = $beforeAfterDefinition[1];
$this->beforeAfterService = $beforeAfterDefinition[0];
} else {
$this->beforeAfterMethod = null;
$this->beforeAfterService = $beforeAfterDefinition;
}
}

public function withPriority(int $priority): self
{
return new self(
$this->serviceId,
$this->event,
$this->method,
$priority,
$this->dispatchers,
$this->before,
$this->after,
);
}

public function name(): string
{
return "{$this->serviceId}::{$this->method}";
}

public function printableBeforeAfterDefinition(): string|null
{
return match (true){
null !== $this->beforeAfterMethod => sprintf('%s::%s()', $this->beforeAfterService, $this->beforeAfterMethod),
null !== $this->beforeAfterService => $this->beforeAfterService,
default => null,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

declare(strict_types=1);

namespace Symfony\Component\EventDispatcher\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

final class ListenerDefinitionsIterator
{
private readonly array $listenerDefinitions;

/**
* @param list<ListenerDefinition> $listenerDefinitions
*/
public function __construct(array $listenerDefinitions, private readonly ContainerBuilder $container)
{
$this->listenerDefinitions = $listenerDefinitions;
}

/**
* @return array<string, list<ListenerDefinition>>
*/
public function iterate(): array
{
$listeners = [];

foreach ($this->listenerDefinitions as $listener) {
$listeners[$listener->serviceId] ??= [];
$listeners[$listener->serviceId][] = $listener->withPriority($this->getPriorityFor($listener));
}

return $listeners;
}

private function getPriorityFor(ListenerDefinition $listener, array $alreadyVisited = []): int
{
if ($alreadyVisited[$listener->name()] ?? false) {
throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": circular reference detected.', array_key_first($alreadyVisited)));
}

$alreadyVisited[$listener->name()] = true;

if (!$listener->beforeAfterService) {
return $listener->priority;
}

$beforeAfterListeners = $this->matchingBeforeAfterListeners($listener);

$beforeAfterListener = match (true) {
!$beforeAfterListeners => throw new InvalidArgumentException(
sprintf('Invalid before/after definition for service "%s": "%s" does not listen to the same event.', $listener->serviceId, $listener->printableBeforeAfterDefinition())
),
!$listener->beforeAfterMethod && count($beforeAfterListeners) === 1 => current($beforeAfterListeners),
!$listener->beforeAfterMethod && count($beforeAfterListeners) > 1 => throw new InvalidArgumentException(
sprintf('Invalid before/after definition for service "%s": "%s" has multiple methods. Please specify the "method" attribute.', $listener->serviceId, $listener->printableBeforeAfterDefinition())
),
$listener->beforeAfterMethod && !isset($beforeAfterListeners[$listener->beforeAfterMethod]) => throw new InvalidArgumentException(
sprintf('Invalid before/after definition for service "%s": method "%s" does not exist or is not a listener.', $listener->serviceId, $listener->printableBeforeAfterDefinition())
),
$listener->beforeAfterMethod && isset($beforeAfterListeners[$listener->beforeAfterMethod]) => $beforeAfterListeners[$listener->beforeAfterMethod],
default => new \LogicException('This should never happen')
};

if ($beforeAfterListener->dispatchers !== $listener->dispatchers) {
throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": "%s" is not handled by the same dispatchers.', $listener->serviceId, $listener->printableBeforeAfterDefinition()));
}

return $this->getPriorityFor($beforeAfterListener, $alreadyVisited) + $listener->priorityModifier;
}

/**
* @return array<string, ListenerDefinition>
*/
private function matchingBeforeAfterListeners(ListenerDefinition $listener): array
{
$beforeAfterService = $listener->beforeAfterService;

if (
$this->container->has($beforeAfterService)
&& (($def = $this->container->findDefinition($beforeAfterService))->hasTag('kernel.event_listener') || $def->hasTag('kernel.event_subscriber'))
) {
$listenersWithServiceId = array_filter(
$this->listenerDefinitions,
static fn(ListenerDefinition $listenerDefinition) => $listenerDefinition->serviceId === $beforeAfterService && $listenerDefinition->event === $listener->event
);

return array_combine(
array_map(static fn(ListenerDefinition $listenerDefinition) => $listenerDefinition->method, $listenersWithServiceId),
$listenersWithServiceId,
);
}

throw new InvalidArgumentException(sprintf('Invalid before/after definition for service "%s": "%s" is not a listener.', $listener->serviceId, $listener->printableBeforeAfterDefinition()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,56 @@ public function process(ContainerBuilder $container)
return;
}

$aliases = [];

if ($container->hasParameter('event_dispatcher.event_aliases')) {
$aliases = $container->getParameter('event_dispatcher.event_aliases');
}
$listerDefinitions = new ListenerDefinitionsIterator([
...iterator_to_array($this->collectListeners($container)),
...iterator_to_array($this->collectSubscribers($container)),
], $container
);

$globalDispatcherDefinition = $container->findDefinition('event_dispatcher');

foreach ($container->findTaggedServiceIds('kernel.event_listener', true) as $id => $events) {
foreach ($listerDefinitions->iterate() as $id => $listenerDefinitions) {
$noPreload = 0;

foreach ($events as $event) {
$priority = $event['priority'] ?? 0;
foreach ($listenerDefinitions as $listenerDefinition) {
$dispatcherDefinitions = [];
foreach ($listenerDefinition->dispatchers as $dispatcher) {
$dispatcherDefinitions[] = 'event_dispatcher' === $dispatcher ? $globalDispatcherDefinition : $container->findDefinition($dispatcher);
}

foreach ($dispatcherDefinitions as $dispatcherDefinition) {
$dispatcherDefinition->addMethodCall(
'addListener',
[
$listenerDefinition->event,
[new ServiceClosureArgument(new Reference($id)), $listenerDefinition->method],
$listenerDefinition->priority ?? 0,
]
);
}

if (isset($this->hotPathEvents[$listenerDefinition->event])) {
$container->getDefinition($id)->addTag('container.hot_path');
} elseif (isset($this->noPreloadEvents[$listenerDefinition->event])) {
++$noPreload;
}
}

if ($noPreload && \count($listenerDefinitions) === $noPreload) {
$container->getDefinition($id)->addTag('container.no_preload');
}
}
}

/**
* @return \Generator<string, list<ListenerDefinition>>
*/
private function collectListeners(ContainerBuilder $container): \Generator
{
$aliases = $this->getEventsAliases($container);

foreach ($container->findTaggedServiceIds('kernel.event_listener', true) as $id => $events) {
foreach ($events as $event) {
if (!isset($event['event'])) {
if ($container->getDefinition($id)->hasTag('kernel.event_subscriber')) {
continue;
Expand All @@ -93,29 +129,33 @@ public function process(ContainerBuilder $container)
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');
$event['dispatchers'] = [$event['dispatcher'] ?? 'event_dispatcher'];
$event['serviceId'] = $id;
unset($event['dispatcher']);

yield new ListenerDefinition(
serviceId: $id,
event: $event['event'],
method: $event['method'],
priority: $event['priority'] ?? 0,
dispatchers: $event['dispatchers'],
before: $event['before'] ?? null,
after: $event['after'] ?? null,
);
}
}
}

/**
* @return \Generator<string, list<array{serviceId: string, event: string, method: string, before?: string|array{0: string, 1: string}, after?: string|array{0: string, 1: string}, priority?: int, dispatchers: list<string>}>>
*/
private function collectSubscribers(ContainerBuilder $container): \Generator
{
$aliases = $this->getEventsAliases($container);

$extractingDispatcher = new ExtractingEventDispatcher();

Expand All @@ -133,43 +173,55 @@ public function process(ContainerBuilder $container)
}
$class = $r->name;

$dispatcherDefinitions = [];
$dispatchers = [];
foreach ($tags as $attributes) {
if (!isset($attributes['dispatcher']) || isset($dispatcherDefinitions[$attributes['dispatcher']])) {
if (!isset($attributes['dispatcher']) || \in_array($attributes['dispatcher'], $dispatchers, true)) {
continue;
}

$dispatcherDefinitions[$attributes['dispatcher']] = $container->findDefinition($attributes['dispatcher']);
$dispatchers[] = $attributes['dispatcher'];
}

if (!$dispatcherDefinitions) {
$dispatcherDefinitions = [$globalDispatcherDefinition];
if (!$dispatchers) {
$dispatchers[] = 'event_dispatcher';
}

$noPreload = 0;
sort($dispatchers);

ExtractingEventDispatcher::$aliases = $aliases;
ExtractingEventDispatcher::$subscriber = $class;
$extractingDispatcher->addSubscriber($extractingDispatcher);
foreach ($extractingDispatcher->listeners as $args) {
$args[1] = [new ServiceClosureArgument(new Reference($id)), $args[1]];
foreach ($dispatcherDefinitions as $dispatcherDefinition) {
$dispatcherDefinition->addMethodCall('addListener', $args);
}

if (isset($this->hotPathEvents[$args[0]])) {
$container->getDefinition($id)->addTag('container.hot_path');
} elseif (isset($this->noPreloadEvents[$args[0]])) {
++$noPreload;
}
}
if ($noPreload && \count($extractingDispatcher->listeners) === $noPreload) {
$container->getDefinition($id)->addTag('container.no_preload');
foreach ($extractingDispatcher->listeners as $listener) {
yield new ListenerDefinition(
serviceId: $id,
event: $listener[0],
method: $listener[1],
priority: $listener[2],
dispatchers: array_values(array_unique($dispatchers)),
before: null,
after: null,
);
}

$extractingDispatcher->listeners = [];
ExtractingEventDispatcher::$aliases = [];
}
}

/**
* @return array<string, string>
*/
private function getEventsAliases(ContainerBuilder $container): array
{
$aliases = [];

if ($container->hasParameter('event_dispatcher.event_aliases')) {
$aliases = $container->getParameter('event_dispatcher.event_aliases') ?? [];
}

return $aliases;
}

private function getEventFromTypeDeclaration(ContainerBuilder $container, string $id, string $method): string
{
if (
Expand Down
Loading