[DependencyInjection][EventDispatcher] Order tagged services and listeners with before/after constraints - #66012
Open
nicolas-grekas wants to merge 1 commit into
Open
Conversation
nicolas-grekas
force-pushed
the
tagged-before-after
branch
2 times, most recently
from
September 11, 2026 13:31
c15687b to
d8fb14b
Compare
nicolas-grekas
force-pushed
the
tagged-before-after
branch
2 times, most recently
from
September 11, 2026 14:08
2c1b10a to
595aec6
Compare
nicolas-grekas
added a commit
that referenced
this pull request
Sep 11, 2026
…its keys (nicolas-grekas)
This PR was merged into the 8.2 branch.
Discussion
----------
[EventDispatcher] Allow getSubscribedEvents() to name its keys
| Q | A
| ------------- | ---
| Branch? | 8.2
| Bug fix? | no
| New feature? | yes
| Deprecations? | no
| Issues | -
| License | MIT
`getSubscribedEvents()` has three positional shapes and none of them says what the slots mean. `['onFoo', 10]` reads as a pair of unrelated values until you go and check, and the nested variant compounds it.
This accepts a named form wherever a positional listener is accepted:
```php
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => ['method' => 'onRequest', 'priority' => 10],
KernelEvents::RESPONSE => [
['method' => 'onEarlyResponse', 'priority' => 100],
['method' => 'onLateResponse', 'priority' => -100],
],
];
}
```
`priority` is optional and defaults to 0, the same as the positional form. Both spellings can be mixed inside one list, since each entry is resolved on its own. The three existing shapes keep working untouched.
### Notes for review
The named form has to be checked **before** the positional one: reading `$params[0]` on a named array warns, so the order of the branches is load bearing rather than stylistic. `removeSubscriber()` needed the same treatment, for the same reason.
`EventDispatcher` is the only thing in the tree that parses the return value of `getSubscribedEvents()`, so it is the only place to change. The container path inherits it for free, because `ExtractingEventDispatcher` extends `EventDispatcher` and only overrides `addListener()`.
### Relation to #66012
Independent, and deliberately so. #66012 adds `before`/`after` ordering constraints and leaves subscribers able to be targeted by a constraint but unable to declare one, because `getSubscribedEvents()` has no slot to put one in. This adds the slot, without adding the constraints.
Either can merge first. Whichever lands second gets rebased, and wiring `before`/`after` into the named form is then a small follow-up rather than something bolted onto either PR.
Commits
-------
9ec5e5b [EventDispatcher] Allow getSubscribedEvents() to name its keys
…eners with before/after constraints Priorities cannot express "run between these two" when the two services are declared by bundles you do not control and share a priority. This adds "before" and "after" constraints, resolved at compile time into an order. Constraints are applied on top of the existing order: the priority sort seeds the result, then items move only as far as the constraints require. A target that is not installed is ignored, so an optional dependency does not become fatal, and a cycle throws naming the services it runs through. Event listeners are covered too. Their runtime order is (priority DESC, insertion ASC), so the pass reorders the addListener() calls and raises a priority only when a constraint puts a listener ahead of a higher-priority one. A collection with no constraint keeps byte-identical calls.
nicolas-grekas
force-pushed
the
tagged-before-after
branch
from
September 11, 2026 15:18
595aec6 to
d6dbfbf
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Priorities cannot express "run between these two" when the two services share a priority and are declared by bundles you do not control. That is the case @aboks opened #64580 with, and inventing a number does not solve it.
This adds
beforeandafterconstraints, resolved at compile time:Event listeners are covered too, which is where the pain is most common:
How it orders
Constraints are applied on top of the existing order rather than replacing it. The priority sort seeds the result, then items move only as far as the constraints require, depth-first in seed order.
A before BandB after Adescribe the same edge and give the same result.A target is matched by service id first, then by class, so
SomeClass::classworks for autoconfigured services without introducing a second referent.A target that is not in the collection is ignored, so a constraint pointing at an optional bundle does not become fatal when that bundle is absent. A cycle throws at compile time, naming the services it runs through.
Event listeners
The runtime order for one event is (priority DESC, insertion ASC), so
RegisterListenersPassreorders theaddListener()calls and raises a priority only when a constraint puts a listener ahead of a higher-priority one. The listener that declared the constraint moves; the one it points at keeps its priority.Two consequences worth knowing:
getSubscribedEvents()has no slot for them.A collection with no constraints produces byte-identical
addListener()calls, which I checked against 8.2 on a container mixing priorities, two dispatchers, multi-tag listeners and a subscriber registering twice on one event.Targeting a single listener
Naming a service targets every registration it has on that event, which is too coarse when a service listens twice.
LocaleListenersubscribes tokernel.requestwithsetDefaultLocaleat 100 andonKernelRequestat 16, so a constraint has to be able to name one of them:Both halves accept a service id or a class, so
'locale::onKernelRequest'works too. If the service part is present on that event but the method is not one of its listeners, it throws, since the constraint would otherwise silently do nothing:A target whose service part is absent stays ignored, so an uninstalled bundle is still not fatal. This is listener-only; tagged services have no method dimension.
On the algorithm
#64580 floated C3 linearization. It does not fit: C3 preserves the relative order of every pair inside each input list, so feeding it the priority order as the tie-break turns every incidental adjacency into a hard constraint, and
c3merge([C, A], [A, B, C])then fails on the simplest possible input. Its distinguishing property over a topological sort is monotonicity across an inheritance hierarchy, and a tag produces exactly one linearization, so there is nothing to be monotonic with respect to.A depth-first topological sort in seed order is used instead. Compared with Kahn's algorithm seeded the same way, both are valid topological sorts with the same displacement, but Kahn moves the wrong element: for
J before BoverA..Jit demotesBto last, where the depth-first order promotesJ. Demoting a third-party listener nobody constrained is not something a metric catches.