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

Skip to content

[DependencyInjection][EventDispatcher] Order tagged services and listeners with before/after constraints - #66012

Open
nicolas-grekas wants to merge 1 commit into
symfony:8.2from
nicolas-grekas:tagged-before-after
Open

[DependencyInjection][EventDispatcher] Order tagged services and listeners with before/after constraints#66012
nicolas-grekas wants to merge 1 commit into
symfony:8.2from
nicolas-grekas:tagged-before-after

Conversation

@nicolas-grekas

@nicolas-grekas nicolas-grekas commented Sep 11, 2026

Copy link
Copy Markdown
Member
Q A
Branch? 8.2
Bug fix? no
New feature? yes
Deprecations? no
Issues Fix #64580
License MIT

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 before and after constraints, resolved at compile time:

#[AsTaggedItem(after: BundleB\Handler::class, before: BundleC\Handler::class)]
class MyHandler
{
}
services:
    App\Handler\TsvHandler:
        tags:
            - name: app.handler
              before: App\Handler\CsvHandler

Event listeners are covered too, which is where the pain is most common:

#[AsEventListener(event: 'kernel.request', before: ProfilerListener::class)]
class MyListener
{
}

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 B and B after A describe the same edge and give the same result.

A target is matched by service id first, then by class, so SomeClass::class works 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 RegisterListenersPass reorders the addListener() 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:

  • a raised listener also moves ahead of listeners added at runtime between the two priorities;
  • subscribers can be targeted but cannot declare constraints, since 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. LocaleListener subscribes to kernel.request with setDefaultLocale at 100 and onKernelRequest at 16, so a constraint has to be able to name one of them:

#[AsEventListener(event: 'kernel.request', before: LocaleListener::class.'::onKernelRequest')]

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:

Invalid "before" constraint on listener "a": "sub" does not listen to event "event" with method "onLat".

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 B over A..J it demotes B to last, where the depth-first order promotes J. Demoting a third-party listener nobody constrained is not something a metric catches.

@carsonbot carsonbot added this to the 8.2 milestone Sep 11, 2026
@nicolas-grekas
nicolas-grekas force-pushed the tagged-before-after branch 2 times, most recently from c15687b to d8fb14b Compare September 11, 2026 13:31
@nicolas-grekas
nicolas-grekas force-pushed the tagged-before-after branch 2 times, most recently from 2c1b10a to 595aec6 Compare September 11, 2026 14:08
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[DependencyInjection] Support partial ordering of tagged services via before/after attributes in tags

2 participants