Skip to content

Commit 1c10de0

Browse files
acranostrolucky
authored andcommitted
Fix erroneous order of events for multi events per listener
fixes doctrine#1884
1 parent 6efa4f8 commit 1c10de0

2 files changed

Lines changed: 99 additions & 43 deletions

File tree

src/DependencyInjection/Compiler/EntityListenerPass.php

Lines changed: 50 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
use Doctrine\Bundle\DoctrineBundle\Mapping\EntityListenerServiceResolver;
77
use Symfony\Component\DependencyInjection\ChildDefinition;
88
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
9-
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
109
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
1110
use Symfony\Component\DependencyInjection\ContainerBuilder;
1211
use Symfony\Component\DependencyInjection\Definition;
@@ -17,6 +16,7 @@
1716
use function method_exists;
1817
use function sprintf;
1918
use function substr;
19+
use function usort;
2020

2121
/**
2222
* Class for Symfony bundles to register entity listeners
@@ -25,67 +25,74 @@
2525
*/
2626
class EntityListenerPass implements CompilerPassInterface
2727
{
28-
use PriorityTaggedServiceTrait;
29-
3028
/** @return void */
3129
public function process(ContainerBuilder $container)
3230
{
33-
$resolvers = $this->findAndSortTaggedServices('doctrine.orm.entity_listener', $container);
34-
3531
$lazyServiceReferencesByResolver = [];
3632

37-
foreach ($resolvers as $reference) {
38-
$id = $reference->__toString();
39-
foreach ($container->getDefinition($id)->getTag('doctrine.orm.entity_listener') as $attributes) {
40-
$name = $attributes['entity_manager'] ?? $container->getParameter('doctrine.default_entity_manager');
41-
$entityManager = sprintf('doctrine.orm.%s_entity_manager', $name);
33+
$serviceTags = [];
34+
foreach ($container->findTaggedServiceIds('doctrine.orm.entity_listener', true) as $id => $tags) {
35+
foreach ($tags as $attributes) {
36+
$serviceTags[] = [
37+
'serviceId' => $id,
38+
'attributes' => $attributes,
39+
];
40+
}
41+
}
4242

43-
if (! $container->hasDefinition($entityManager)) {
44-
continue;
45-
}
43+
usort($serviceTags, static fn (array $a, array $b) => ($b['attributes']['priority'] ?? 0) <=> ($a['attributes']['priority'] ?? 0));
4644

47-
$resolverId = sprintf('doctrine.orm.%s_entity_listener_resolver', $name);
45+
foreach ($serviceTags as $tag) {
46+
$id = $tag['serviceId'];
47+
$attributes = $tag['attributes'];
48+
$name = $attributes['entity_manager'] ?? $container->getParameter('doctrine.default_entity_manager');
49+
$entityManager = sprintf('doctrine.orm.%s_entity_manager', $name);
4850

49-
if (! $container->has($resolverId)) {
50-
continue;
51-
}
51+
if (! $container->hasDefinition($entityManager)) {
52+
continue;
53+
}
5254

53-
$resolver = $container->findDefinition($resolverId);
54-
$resolver->setPublic(true);
55+
$resolverId = sprintf('doctrine.orm.%s_entity_listener_resolver', $name);
5556

56-
if (isset($attributes['entity'])) {
57-
$this->attachToListener($container, $name, $this->getConcreteDefinitionClass($container->findDefinition($id), $container, $id), $attributes);
58-
}
57+
if (! $container->has($resolverId)) {
58+
continue;
59+
}
5960

60-
$resolverClass = $this->getResolverClass($resolver, $container, $resolverId);
61-
$resolverSupportsLazyListeners = is_a($resolverClass, EntityListenerServiceResolver::class, true);
61+
$resolver = $container->findDefinition($resolverId);
62+
$resolver->setPublic(true);
6263

63-
$lazyByAttribute = isset($attributes['lazy']) && $attributes['lazy'];
64-
if ($lazyByAttribute && ! $resolverSupportsLazyListeners) {
65-
throw new InvalidArgumentException(sprintf(
66-
'Lazy-loaded entity listeners can only be resolved by a resolver implementing %s.',
67-
EntityListenerServiceResolver::class,
68-
));
69-
}
64+
if (isset($attributes['entity'])) {
65+
$this->attachToListener($container, $name, $this->getConcreteDefinitionClass($container->findDefinition($id), $container, $id), $attributes);
66+
}
7067

71-
if (! isset($attributes['lazy']) && $resolverSupportsLazyListeners || $lazyByAttribute) {
72-
$listener = $container->findDefinition($id);
68+
$resolverClass = $this->getResolverClass($resolver, $container, $resolverId);
69+
$resolverSupportsLazyListeners = is_a($resolverClass, EntityListenerServiceResolver::class, true);
7370

74-
$resolver->addMethodCall('registerService', [$this->getConcreteDefinitionClass($listener, $container, $id), $id]);
71+
$lazyByAttribute = isset($attributes['lazy']) && $attributes['lazy'];
72+
if ($lazyByAttribute && ! $resolverSupportsLazyListeners) {
73+
throw new InvalidArgumentException(sprintf(
74+
'Lazy-loaded entity listeners can only be resolved by a resolver implementing %s.',
75+
EntityListenerServiceResolver::class,
76+
));
77+
}
78+
79+
if (! isset($attributes['lazy']) && $resolverSupportsLazyListeners || $lazyByAttribute) {
80+
$listener = $container->findDefinition($id);
7581

76-
// if the resolver uses the default class we will use a service locator for all listeners
77-
if ($resolverClass === ContainerEntityListenerResolver::class) {
78-
if (! isset($lazyServiceReferencesByResolver[$resolverId])) {
79-
$lazyServiceReferencesByResolver[$resolverId] = [];
80-
}
82+
$resolver->addMethodCall('registerService', [$this->getConcreteDefinitionClass($listener, $container, $id), $id]);
8183

82-
$lazyServiceReferencesByResolver[$resolverId][$id] = new Reference($id);
83-
} else {
84-
$listener->setPublic(true);
84+
// if the resolver uses the default class we will use a service locator for all listeners
85+
if ($resolverClass === ContainerEntityListenerResolver::class) {
86+
if (! isset($lazyServiceReferencesByResolver[$resolverId])) {
87+
$lazyServiceReferencesByResolver[$resolverId] = [];
8588
}
89+
90+
$lazyServiceReferencesByResolver[$resolverId][$id] = new Reference($id);
8691
} else {
87-
$resolver->addMethodCall('register', [new Reference($id)]);
92+
$listener->setPublic(true);
8893
}
94+
} else {
95+
$resolver->addMethodCall('register', [new Reference($id)]);
8996
}
9097
}
9198

tests/DependencyInjection/Compiler/EntityListenerPassTest.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,43 @@ public static function provideEvents(): iterable
7373
yield 'With event and custom method' => [Events::postLoad, 'postLoadHandler', 'postLoadHandler'];
7474
yield 'With event and no matching method' => [Events::postLoad, null, '__invoke'];
7575
}
76+
77+
public function testMultipleAttributesOnSameClassKeepTheCorrectOrder(): void
78+
{
79+
$container = new ContainerBuilder();
80+
$container->addCompilerPass(new EntityListenerPass());
81+
82+
$container->setParameter('doctrine.default_entity_manager', 'default');
83+
$container->register('doctrine.orm.default_entity_manager', EntityManager::class);
84+
$container->register('doctrine.orm.default_entity_listener_resolver', ContainerEntityListenerResolver::class);
85+
$container->register('doctrine.orm.default_listeners.attach_entity_listeners', AttachEntityListenersListener::class)
86+
->setPublic(true);
87+
88+
$container->register(TestListener::class)
89+
->addTag('doctrine.orm.entity_listener', ['entity' => stdClass::class, 'event' => Events::prePersist])
90+
->addTag('doctrine.orm.entity_listener', ['entity' => stdClass::class, 'event' => Events::postPersist]);
91+
$container->register(TestListener2::class)
92+
->addTag(
93+
'doctrine.orm.entity_listener',
94+
['entity' => stdClass::class, 'event' => Events::prePersist, 'priority' => 1],
95+
)
96+
->addTag(
97+
'doctrine.orm.entity_listener',
98+
['entity' => stdClass::class, 'event' => Events::postPersist, 'priority' => -1],
99+
);
100+
101+
$container->compile();
102+
103+
$this->assertSame(
104+
[
105+
['addEntityListener', ['stdClass', TestListener2::class, 'prePersist']],
106+
['addEntityListener', ['stdClass', TestListener::class, 'prePersist']],
107+
['addEntityListener', ['stdClass', TestListener::class, 'postPersist']],
108+
['addEntityListener', ['stdClass', TestListener2::class, 'postPersist']],
109+
],
110+
$container->getDefinition('doctrine.orm.default_listeners.attach_entity_listeners')->getMethodCalls(),
111+
);
112+
}
76113
}
77114

78115
class TestListener
@@ -93,3 +130,15 @@ public function __invoke(): void
93130
{
94131
}
95132
}
133+
134+
135+
class TestListener2
136+
{
137+
public function prePersist(): void
138+
{
139+
}
140+
141+
public function postPersist(): void
142+
{
143+
}
144+
}

0 commit comments

Comments
 (0)