phoenix icon indicating copy to clipboard operation
phoenix copied to clipboard

[EntityListener] with multiple listeners per class only the first priority is used

Open acran opened this issue 6 months ago • 2 comments

Bug Report

Q A
Version 2.14.0

Summary

Using the AsEntityListener attribute to listen to entity events one can set a priority for each event listener. The documentation suggests that then all listeners for an entity event are sorted by priority and strictly executed in that order.

But when having multiple event listeners in a single class with differing priorities this actually produces unexpected results. Because only the priority of the first defined listener is actually used and also applied to the other listeners.

This is due to EntityListenerPass::process() using PriorityTaggedServiceTrait::findAndSortTaggedServices() which effectively sorts all tagged services by the priority of the first attribute.

Current behavior

The defined entity listeners are not correctly executed in order of their individual priorities.

Expected behavior

Each defined entity listener should be executed according to its specific priority.

How to reproduce

Given these two classes with entity listeners

#[AsEntityListener(entity: User::class, event: Events::prePersist, priority: 0)]
#[AsEntityListener(entity: User::class, event: Events::postPersist, priority: 0)]
final class FirstListener {
	public function prePersist(User $user, PrePersistEventArgs $event) {
		dump('this is ' . __METHOD__);
	}

	public function postPersist(User $user, PostPersistEventArgs $event) {
		dump('this is ' . __METHOD__);
	}
}
#[AsEntityListener(entity: User::class, event: Events::prePersist, priority: 1)]
#[AsEntityListener(entity: User::class, event: Events::postPersist, priority: -1)]
final class SecondListener {
	public function prePersist(User $user, PrePersistEventArgs $event) {
		dump('this is ' . __METHOD__);
	}

	public function postPersist(User $user, PostPersistEventArgs $event) {
		dump('this is ' . __METHOD__);
	}
}

The expected output order of the dump() statements would be

"this is App\EventListener\SecondListener::prePersist" "this is App\EventListener\FirstListener::prePersist" "this is App\EventListener\FirstListener::postPersist" "this is App\EventListener\SecondListener::postPersist"

but actually is

"this is App\EventListener\SecondListener::prePersist" "this is App\EventListener\FirstListener::prePersist" "this is App\EventListener\SecondListener::postPersist" "this is App\EventListener\FirstListener::postPersist"

Switching just the order of the attributes on the second class

#[AsEntityListener(entity: User::class, event: Events::postPersist, priority: -1)]
#[AsEntityListener(entity: User::class, event: Events::prePersist, priority: 1)]
final class SecondListener {
	public function prePersist(User $user, PrePersistEventArgs $event) {
		dump('this is ' . __METHOD__);
	}

	public function postPersist(User $user, PostPersistEventArgs $event) {
		dump('this is ' . __METHOD__);
	}
}

the output becomes

"this is App\EventListener\FirstListener::prePersist" "this is App\EventListener\SecondListener::prePersist" "this is App\EventListener\FirstListener::postPersist" "this is App\EventListener\SecondListener::postPersist"

acran avatar Jun 05 '25 14:06 acran

Fix at https://github.com/doctrine/DoctrineBundle/pull/1885, can you check if fix solves your problem?

ostrolucky avatar Jun 05 '25 21:06 ostrolucky

Wow, that's a fast reaction, thank you!

can you check if fix solves your problem?

Not really unfortunately. Though it now correctly sorts the listeners within a single service, it does now not sort the service themselves at all. So the output of above example now becomes

"this is App\EventListener\FirstListener::prePersist" "this is App\EventListener\SecondListener::prePersist" "this is App\EventListener\FirstListener::postPersist" "this is App\EventListener\SecondListener::postPersist"

regardless of the used priorities.

acran avatar Jun 06 '25 10:06 acran

Wow, that's a fast reaction, thank you!

can you check if fix solves your problem?

Not really unfortunately. Though it now correctly sorts the listeners within a single service, it does now not sort the service themselves at all. So the output of above example now becomes

"this is App\EventListener\FirstListener::prePersist" "this is App\EventListener\SecondListener::prePersist" "this is App\EventListener\FirstListener::postPersist" "this is App\EventListener\SecondListener::postPersist"

regardless of the used priorities.

Hello, I can confirm this behaviour as well too, it is not completely fixed yet (but thanks for the work done)

Geolim4 avatar Jul 21 '25 12:07 Geolim4