From 5f055538444eadeb94de4da004c324201ece7d23 Mon Sep 17 00:00:00 2001 From: HypeMC Date: Fri, 21 Aug 2026 14:50:13 +0200 Subject: [PATCH] Replace deprecated `defaultIndexMethod` usage with a compiler pass --- config/services.php | 6 +- .../ResolveTaggedLocatorIndexPass.php | 65 +++++++ src/PurgatoryBundle.php | 5 + .../ResolveTaggedLocatorIndexPassTest.php | 161 ++++++++++++++++++ 4 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 src/DependencyInjection/CompilerPass/ResolveTaggedLocatorIndexPass.php create mode 100644 tests/DependencyInjection/CompilerPass/ResolveTaggedLocatorIndexPassTest.php diff --git a/config/services.php b/config/services.php index 1f451147..3e988a34 100644 --- a/config/services.php +++ b/config/services.php @@ -72,7 +72,7 @@ tagged_iterator('purgatory.subscription_resolver'), tagged_iterator('purgatory.route_metadata_provider'), service('doctrine'), - tagged_locator('purgatory.target_resolver', defaultIndexMethod: 'for'), + tagged_locator('purgatory.target_resolver', indexAttribute: 'for'), service('sofascore.purgatory.expression_language')->nullOnInvalid(), ]) @@ -142,7 +142,7 @@ ->args([ service('sofascore.purgatory.configuration_loader'), service('sofascore.purgatory.expression_language')->nullOnInvalid(), - tagged_locator('purgatory.route_param_value_resolver', defaultIndexMethod: 'for'), + tagged_locator('purgatory.route_param_value_resolver', indexAttribute: 'for'), ]) ->set('sofascore.purgatory.route_provider.created_entity', CreatedEntityRouteProvider::class) @@ -202,7 +202,7 @@ ->set('sofascore.purgatory.route_param_value_resolver.compound', CompoundValuesResolver::class) ->tag('purgatory.route_param_value_resolver') ->args([ - tagged_locator('purgatory.route_param_value_resolver', defaultIndexMethod: 'for'), + tagged_locator('purgatory.route_param_value_resolver', indexAttribute: 'for'), ]) ->set('sofascore.purgatory.route_param_value_resolver.enum', EnumValuesResolver::class) diff --git a/src/DependencyInjection/CompilerPass/ResolveTaggedLocatorIndexPass.php b/src/DependencyInjection/CompilerPass/ResolveTaggedLocatorIndexPass.php new file mode 100644 index 00000000..55c21972 --- /dev/null +++ b/src/DependencyInjection/CompilerPass/ResolveTaggedLocatorIndexPass.php @@ -0,0 +1,65 @@ + TargetResolverInterface::class, + 'purgatory.route_param_value_resolver' => ValuesResolverInterface::class, + ]; + + public function process(ContainerBuilder $container): void + { + foreach (self::TAG_TO_INTERFACE as $tagName => $interface) { + /** @var list> $tags */ + foreach ($container->findTaggedServiceIds($tagName, true) as $id => $tags) { + if (!array_any($tags, static fn (array $attributes): bool => !isset($attributes['for']))) { + continue; + } + + $definition = $container->getDefinition($id); + $class = $definition->getClass(); + + $parent = $definition; + while (null === $class && $parent instanceof ChildDefinition) { + $parent = $container->findDefinition($parent->getParent()); + $class = $parent->getClass(); + } + + /** @var ?class-string|ValuesResolverInterface>> $class */ + $class = $container->getParameterBag()->resolveValue($class); + + if (!\is_string($class)) { + throw new RuntimeException(\sprintf('The class of the service "%s" tagged with "%s" could not be determined.', $id, $tagName)); + } + + if (!is_a($class, $interface, true)) { + throw new RuntimeException(\sprintf('The class "%s" of the service "%s" tagged with "%s" must implement "%s".', $class, $id, $tagName, $interface)); + } + + $for = $class::for(); + + $definition->clearTag($tagName); + foreach ($tags as $attributes) { + $definition->addTag($tagName, $attributes + ['for' => $for]); + } + } + } + } +} diff --git a/src/PurgatoryBundle.php b/src/PurgatoryBundle.php index f69357b4..b69cbeec 100644 --- a/src/PurgatoryBundle.php +++ b/src/PurgatoryBundle.php @@ -8,6 +8,8 @@ use Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass\RegisterExpressionLanguageProvidersPass; use Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass\RegisterPurgerPass; use Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass\RegisterRouteParamServicesPass; +use Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass\ResolveTaggedLocatorIndexPass; +use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -19,6 +21,9 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new RegisterExpressionLanguageProvidersPass()); $container->addCompilerPass(new RegisterPurgerPass()); $container->addCompilerPass(new RegisterRouteParamServicesPass()); + // Runs after all before-optimization passes so that late-added tags are seen, + // but before ServiceLocatorTagPass which consumes the "for" attribute. + $container->addCompilerPass(new ResolveTaggedLocatorIndexPass(), PassConfig::TYPE_OPTIMIZE, 1); } public function getPath(): string diff --git a/tests/DependencyInjection/CompilerPass/ResolveTaggedLocatorIndexPassTest.php b/tests/DependencyInjection/CompilerPass/ResolveTaggedLocatorIndexPassTest.php new file mode 100644 index 00000000..accef1c6 --- /dev/null +++ b/tests/DependencyInjection/CompilerPass/ResolveTaggedLocatorIndexPassTest.php @@ -0,0 +1,161 @@ +register(id: 'foo', class: ForPropertiesResolver::class) + ->addTag(name: 'purgatory.target_resolver'); + $container->register(id: 'bar', class: RawValuesResolver::class) + ->addTag(name: 'purgatory.route_param_value_resolver', attributes: ['priority' => 10]); + + (new ResolveTaggedLocatorIndexPass())->process($container); + + self::assertSame( + [['for' => ForProperties::class]], + $container->getDefinition('foo')->getTag('purgatory.target_resolver'), + ); + self::assertSame( + [['priority' => 10, 'for' => RawValues::type()]], + $container->getDefinition('bar')->getTag('purgatory.route_param_value_resolver'), + ); + } + + public function testExistingKeyIsNotOverwritten(): void + { + $container = new ContainerBuilder(); + $container->register(id: 'foo', class: ForPropertiesResolver::class) + ->addTag(name: 'purgatory.target_resolver', attributes: ['for' => \stdClass::class]); + + (new ResolveTaggedLocatorIndexPass())->process($container); + + self::assertSame( + [['for' => \stdClass::class]], + $container->getDefinition('foo')->getTag('purgatory.target_resolver'), + ); + } + + public function testClassIsNotValidatedWhenAllTagsHaveAnExplicitKey(): void + { + // Both definitions would fail validation ("foo" does not implement the expected + // interface, "bar" has no class), so this test only passes if services whose + // tags all have an explicit "for" attribute are skipped before validation. + $container = new ContainerBuilder(); + $container->register(id: 'foo', class: \stdClass::class) + ->addTag(name: 'purgatory.target_resolver', attributes: ['for' => \stdClass::class]); + $container->register(id: 'bar') + ->addTag(name: 'purgatory.route_param_value_resolver', attributes: ['for' => \stdClass::class]); + + (new ResolveTaggedLocatorIndexPass())->process($container); + + self::assertSame( + [['for' => \stdClass::class]], + $container->getDefinition('foo')->getTag('purgatory.target_resolver'), + ); + self::assertSame( + [['for' => \stdClass::class]], + $container->getDefinition('bar')->getTag('purgatory.route_param_value_resolver'), + ); + } + + public function testOnlyTagsWithoutAnExplicitKeyAreStamped(): void + { + $container = new ContainerBuilder(); + $container->register(id: 'foo', class: ForPropertiesResolver::class) + ->addTag(name: 'purgatory.target_resolver', attributes: ['for' => \stdClass::class]) + ->addTag(name: 'purgatory.target_resolver'); + + (new ResolveTaggedLocatorIndexPass())->process($container); + + self::assertSame( + [['for' => \stdClass::class], ['for' => ForProperties::class]], + $container->getDefinition('foo')->getTag('purgatory.target_resolver'), + ); + } + + public function testClassIsResolvedFromParentDefinition(): void + { + $container = new ContainerBuilder(); + $container->register(id: 'foo', class: ForPropertiesResolver::class); + $container->setDefinition( + id: 'bar', + definition: (new ChildDefinition('foo'))->addTag(name: 'purgatory.target_resolver'), + ); + + (new ResolveTaggedLocatorIndexPass())->process($container); + + self::assertSame( + [['for' => ForProperties::class]], + $container->getDefinition('bar')->getTag('purgatory.target_resolver'), + ); + } + + public function testClassIsResolvedFromParameter(): void + { + $container = new ContainerBuilder(); + $container->setParameter('foo.class', ForPropertiesResolver::class); + $container->register(id: 'foo', class: '%foo.class%') + ->addTag(name: 'purgatory.target_resolver'); + + (new ResolveTaggedLocatorIndexPass())->process($container); + + self::assertSame( + [['for' => ForProperties::class]], + $container->getDefinition('foo')->getTag('purgatory.target_resolver'), + ); + } + + public function testExceptionIsThrownWhenClassCannotBeDetermined(): void + { + $container = new ContainerBuilder(); + $container->register(id: 'foo') + ->addTag(name: 'purgatory.target_resolver'); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('The class of the service "foo" tagged with "purgatory.target_resolver" could not be determined.'); + + (new ResolveTaggedLocatorIndexPass())->process($container); + } + + public function testExceptionIsThrownWhenClassResolvesToANonStringValue(): void + { + $container = new ContainerBuilder(); + $container->setParameter('foo.class', ['not', 'a', 'class']); + $container->register(id: 'foo', class: '%foo.class%') + ->addTag(name: 'purgatory.target_resolver'); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('The class of the service "foo" tagged with "purgatory.target_resolver" could not be determined.'); + + (new ResolveTaggedLocatorIndexPass())->process($container); + } + + public function testExceptionIsThrownWhenClassDoesNotImplementExpectedInterface(): void + { + $container = new ContainerBuilder(); + $container->register(id: 'foo', class: \stdClass::class) + ->addTag(name: 'purgatory.route_param_value_resolver'); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('The class "stdClass" of the service "foo" tagged with "purgatory.route_param_value_resolver" must implement "Sofascore\PurgatoryBundle\RouteParamValueResolver\ValuesResolverInterface".'); + + (new ResolveTaggedLocatorIndexPass())->process($container); + } +}