diff --git a/src/DependencyInjection/WorkerResetCompilerPass.php b/src/DependencyInjection/WorkerResetCompilerPass.php new file mode 100644 index 00000000..ef0ff36c --- /dev/null +++ b/src/DependencyInjection/WorkerResetCompilerPass.php @@ -0,0 +1,83 @@ += 8.0.8). + * + * This pass creates a separate ServicesResetter for the worker that excludes the + * debug.event_dispatcher, so the global services_resetter remains unchanged for + * HTTP request resets. + */ +final class WorkerResetCompilerPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if (!$container->hasDefinition(ResetServicesListener::class)) { + return; + } + + if (!$container->hasDefinition('debug.event_dispatcher')) { + return; + } + + $services = []; + /** @var array> $methods */ + $methods = []; + + foreach ($container->findTaggedServiceIds('kernel.reset', true) as $id => $tags) { + if ($id === 'debug.event_dispatcher') { + continue; + } + + $services[$id] = new Reference($id, ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE); + + foreach ($tags as $attributes) { + /** @var array{method?: string, on_invalid?: string} $attributes */ + if (!isset($attributes['method'])) { + continue; + } + + $methods[$id] ??= []; + + $method = $attributes['method']; + + if (($attributes['on_invalid'] ?? null) === 'ignore') { + $method = '?' . $method; + } + + $methods[$id][] = $method; + } + } + + if ($services === []) { + return; + } + + $container->register('patchlevel.worker.services_resetter', ServicesResetter::class) + ->setArguments([ + new IteratorArgument($services), + $methods, + ]); + + $container->getDefinition(ResetServicesListener::class) + ->setArgument(0, new Reference('patchlevel.worker.services_resetter')); + } +} diff --git a/src/PatchlevelEventSourcingBundle.php b/src/PatchlevelEventSourcingBundle.php index 78a641e5..27f0a8b4 100644 --- a/src/PatchlevelEventSourcingBundle.php +++ b/src/PatchlevelEventSourcingBundle.php @@ -12,6 +12,7 @@ use Patchlevel\EventSourcingBundle\DependencyInjection\RepositoryCompilerPass; use Patchlevel\EventSourcingBundle\DependencyInjection\SubscriberGuardCompilePass; use Patchlevel\EventSourcingBundle\DependencyInjection\TranslatorCompilerPass; +use Patchlevel\EventSourcingBundle\DependencyInjection\WorkerResetCompilerPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -27,5 +28,6 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new TranslatorCompilerPass()); $container->addCompilerPass(new DoctrineCleanupCompilerPass()); $container->addCompilerPass(new HydratorCompilerPass()); + $container->addCompilerPass(new WorkerResetCompilerPass()); } } diff --git a/tests/Unit/DependencyInjection/WorkerResetCompilerPassTest.php b/tests/Unit/DependencyInjection/WorkerResetCompilerPassTest.php new file mode 100644 index 00000000..d04634f9 --- /dev/null +++ b/tests/Unit/DependencyInjection/WorkerResetCompilerPassTest.php @@ -0,0 +1,171 @@ +register(ResetServicesListener::class) + ->setArguments([new Reference('services_resetter')]); + + $container->register('debug.event_dispatcher') + ->addTag('kernel.reset', ['method' => 'reset']); + + $container->register('some.other.service') + ->addTag('kernel.reset', ['method' => 'reset']); + + $pass = new WorkerResetCompilerPass(); + $pass->process($container); + + self::assertTrue($container->hasDefinition('patchlevel.worker.services_resetter')); + + $definition = $container->getDefinition('patchlevel.worker.services_resetter'); + self::assertSame(ServicesResetter::class, $definition->getClass()); + + /** @var array> $methods */ + $methods = $definition->getArgument(1); + self::assertArrayNotHasKey('debug.event_dispatcher', $methods); + self::assertArrayHasKey('some.other.service', $methods); + self::assertSame(['reset'], $methods['some.other.service']); + + $listenerDef = $container->getDefinition(ResetServicesListener::class); + self::assertEquals( + new Reference('patchlevel.worker.services_resetter'), + $listenerDef->getArgument(0), + ); + } + + public function testSkipsWhenNoDebugDispatcher(): void + { + $container = new ContainerBuilder(); + + $container->register(ResetServicesListener::class) + ->setArguments([new Reference('services_resetter')]); + + $container->register('some.other.service') + ->addTag('kernel.reset', ['method' => 'reset']); + + $pass = new WorkerResetCompilerPass(); + $pass->process($container); + + self::assertFalse($container->hasDefinition('patchlevel.worker.services_resetter')); + + $listenerDef = $container->getDefinition(ResetServicesListener::class); + self::assertEquals( + new Reference('services_resetter'), + $listenerDef->getArgument(0), + ); + } + + public function testSkipsWhenNoResetServicesListener(): void + { + $container = new ContainerBuilder(); + + $container->register('debug.event_dispatcher') + ->addTag('kernel.reset', ['method' => 'reset']); + + $container->register('some.other.service') + ->addTag('kernel.reset', ['method' => 'reset']); + + $pass = new WorkerResetCompilerPass(); + $pass->process($container); + + // The pass must not create the worker resetter when the listener is absent. + self::assertFalse($container->hasDefinition('patchlevel.worker.services_resetter')); + } + + public function testSkipsWhenNoResettableServicesRemain(): void + { + $container = new ContainerBuilder(); + + $container->register(ResetServicesListener::class) + ->setArguments([new Reference('services_resetter')]); + + $container->register('debug.event_dispatcher') + ->addTag('kernel.reset', ['method' => 'reset']); + + $pass = new WorkerResetCompilerPass(); + $pass->process($container); + + self::assertFalse($container->hasDefinition('patchlevel.worker.services_resetter')); + } + + public function testHandlesOnInvalidIgnoreAttribute(): void + { + $container = new ContainerBuilder(); + + $container->register(ResetServicesListener::class) + ->setArguments([new Reference('services_resetter')]); + + $container->register('debug.event_dispatcher') + ->addTag('kernel.reset', ['method' => 'reset']); + + $container->register('some.service') + ->addTag('kernel.reset', ['method' => 'reset', 'on_invalid' => 'ignore']); + + $pass = new WorkerResetCompilerPass(); + $pass->process($container); + + self::assertTrue($container->hasDefinition('patchlevel.worker.services_resetter')); + + /** @var array> $methods */ + $methods = $container->getDefinition('patchlevel.worker.services_resetter')->getArgument(1); + self::assertSame(['?reset'], $methods['some.service']); + } + + public function testSkipsTagsWithoutMethodAttribute(): void + { + $container = new ContainerBuilder(); + + $container->register(ResetServicesListener::class) + ->setArguments([new Reference('services_resetter')]); + + $container->register('debug.event_dispatcher') + ->addTag('kernel.reset', ['method' => 'reset']); + + $container->register('some.service') + ->addTag('kernel.reset', []) + ->addTag('kernel.reset', ['method' => 'reset']); + + $pass = new WorkerResetCompilerPass(); + $pass->process($container); + + /** @var array> $methods */ + $methods = $container->getDefinition('patchlevel.worker.services_resetter')->getArgument(1); + self::assertSame(['reset'], $methods['some.service']); + } + + public function testPreservesMultipleResetMethodsOnSameService(): void + { + $container = new ContainerBuilder(); + + $container->register(ResetServicesListener::class) + ->setArguments([new Reference('services_resetter')]); + + $container->register('debug.event_dispatcher') + ->addTag('kernel.reset', ['method' => 'reset']); + + $container->register('some.service') + ->addTag('kernel.reset', ['method' => 'resetA']) + ->addTag('kernel.reset', ['method' => 'resetB']); + + $pass = new WorkerResetCompilerPass(); + $pass->process($container); + + /** @var array> $methods */ + $methods = $container->getDefinition('patchlevel.worker.services_resetter')->getArgument(1); + self::assertSame(['resetA', 'resetB'], $methods['some.service']); + } +} diff --git a/tests/Unit/PatchlevelEventSourcingBundleTest.php b/tests/Unit/PatchlevelEventSourcingBundleTest.php index 2dcd00ec..beddfc51 100644 --- a/tests/Unit/PatchlevelEventSourcingBundleTest.php +++ b/tests/Unit/PatchlevelEventSourcingBundleTest.php @@ -1543,6 +1543,36 @@ public function testFullBuild(): void self::assertInstanceOf(ResetServicesListener::class, $container->get(ResetServicesListener::class)); } + public function testWorkerResetExcludesDebugDispatcher(): void + { + $container = new ContainerBuilder(); + + $container->register('debug.event_dispatcher', \stdClass::class) + ->addTag('kernel.reset', ['method' => 'reset']) + ->setPublic(true); + + $container->register('some.resettable.service', \stdClass::class) + ->addTag('kernel.reset', ['method' => 'reset']) + ->setPublic(true); + + $this->compileContainer( + $container, + [ + 'patchlevel_event_sourcing' => [ + 'connection' => ['service' => 'doctrine.dbal.eventstore_connection'], + ], + ], + ); + + self::assertTrue($container->has('patchlevel.worker.services_resetter')); + + $listenerDef = $container->getDefinition(ResetServicesListener::class); + self::assertEquals( + new Reference('patchlevel.worker.services_resetter'), + $listenerDef->getArgument(0), + ); + } + public function testNamedRepository(): void { $container = new ContainerBuilder();