Skip to content

Commit 5f05553

Browse files
committed
Replace deprecated defaultIndexMethod usage with a compiler pass
1 parent 4d98ba8 commit 5f05553

4 files changed

Lines changed: 234 additions & 3 deletions

File tree

config/services.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
tagged_iterator('purgatory.subscription_resolver'),
7373
tagged_iterator('purgatory.route_metadata_provider'),
7474
service('doctrine'),
75-
tagged_locator('purgatory.target_resolver', defaultIndexMethod: 'for'),
75+
tagged_locator('purgatory.target_resolver', indexAttribute: 'for'),
7676
service('sofascore.purgatory.expression_language')->nullOnInvalid(),
7777
])
7878

@@ -142,7 +142,7 @@
142142
->args([
143143
service('sofascore.purgatory.configuration_loader'),
144144
service('sofascore.purgatory.expression_language')->nullOnInvalid(),
145-
tagged_locator('purgatory.route_param_value_resolver', defaultIndexMethod: 'for'),
145+
tagged_locator('purgatory.route_param_value_resolver', indexAttribute: 'for'),
146146
])
147147

148148
->set('sofascore.purgatory.route_provider.created_entity', CreatedEntityRouteProvider::class)
@@ -202,7 +202,7 @@
202202
->set('sofascore.purgatory.route_param_value_resolver.compound', CompoundValuesResolver::class)
203203
->tag('purgatory.route_param_value_resolver')
204204
->args([
205-
tagged_locator('purgatory.route_param_value_resolver', defaultIndexMethod: 'for'),
205+
tagged_locator('purgatory.route_param_value_resolver', indexAttribute: 'for'),
206206
])
207207

208208
->set('sofascore.purgatory.route_param_value_resolver.enum', EnumValuesResolver::class)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass;
6+
7+
use Sofascore\PurgatoryBundle\Attribute\Target\TargetInterface;
8+
use Sofascore\PurgatoryBundle\Cache\TargetResolver\TargetResolverInterface;
9+
use Sofascore\PurgatoryBundle\Exception\RuntimeException;
10+
use Sofascore\PurgatoryBundle\RouteParamValueResolver\ValuesResolverInterface;
11+
use Symfony\Component\DependencyInjection\ChildDefinition;
12+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
13+
use Symfony\Component\DependencyInjection\ContainerBuilder;
14+
15+
/**
16+
* Sets the "for" tag attribute used as the service locator index from the tagged
17+
* class's static "for()" method, replacing the "defaultIndexMethod" reflection
18+
* fallback which is deprecated since Symfony 8.1.
19+
*/
20+
final class ResolveTaggedLocatorIndexPass implements CompilerPassInterface
21+
{
22+
private const TAG_TO_INTERFACE = [
23+
'purgatory.target_resolver' => TargetResolverInterface::class,
24+
'purgatory.route_param_value_resolver' => ValuesResolverInterface::class,
25+
];
26+
27+
public function process(ContainerBuilder $container): void
28+
{
29+
foreach (self::TAG_TO_INTERFACE as $tagName => $interface) {
30+
/** @var list<array<string, mixed>> $tags */
31+
foreach ($container->findTaggedServiceIds($tagName, true) as $id => $tags) {
32+
if (!array_any($tags, static fn (array $attributes): bool => !isset($attributes['for']))) {
33+
continue;
34+
}
35+
36+
$definition = $container->getDefinition($id);
37+
$class = $definition->getClass();
38+
39+
$parent = $definition;
40+
while (null === $class && $parent instanceof ChildDefinition) {
41+
$parent = $container->findDefinition($parent->getParent());
42+
$class = $parent->getClass();
43+
}
44+
45+
/** @var ?class-string<TargetResolverInterface<TargetInterface>|ValuesResolverInterface<array<mixed>>> $class */
46+
$class = $container->getParameterBag()->resolveValue($class);
47+
48+
if (!\is_string($class)) {
49+
throw new RuntimeException(\sprintf('The class of the service "%s" tagged with "%s" could not be determined.', $id, $tagName));
50+
}
51+
52+
if (!is_a($class, $interface, true)) {
53+
throw new RuntimeException(\sprintf('The class "%s" of the service "%s" tagged with "%s" must implement "%s".', $class, $id, $tagName, $interface));
54+
}
55+
56+
$for = $class::for();
57+
58+
$definition->clearTag($tagName);
59+
foreach ($tags as $attributes) {
60+
$definition->addTag($tagName, $attributes + ['for' => $for]);
61+
}
62+
}
63+
}
64+
}
65+
}

src/PurgatoryBundle.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass\RegisterExpressionLanguageProvidersPass;
99
use Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass\RegisterPurgerPass;
1010
use Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass\RegisterRouteParamServicesPass;
11+
use Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass\ResolveTaggedLocatorIndexPass;
12+
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
1113
use Symfony\Component\DependencyInjection\ContainerBuilder;
1214
use Symfony\Component\HttpKernel\Bundle\Bundle;
1315

@@ -19,6 +21,9 @@ public function build(ContainerBuilder $container): void
1921
$container->addCompilerPass(new RegisterExpressionLanguageProvidersPass());
2022
$container->addCompilerPass(new RegisterPurgerPass());
2123
$container->addCompilerPass(new RegisterRouteParamServicesPass());
24+
// Runs after all before-optimization passes so that late-added tags are seen,
25+
// but before ServiceLocatorTagPass which consumes the "for" attribute.
26+
$container->addCompilerPass(new ResolveTaggedLocatorIndexPass(), PassConfig::TYPE_OPTIMIZE, 1);
2227
}
2328

2429
public function getPath(): string
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sofascore\PurgatoryBundle\Tests\DependencyInjection\CompilerPass;
6+
7+
use PHPUnit\Framework\Attributes\CoversClass;
8+
use PHPUnit\Framework\TestCase;
9+
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\RawValues;
10+
use Sofascore\PurgatoryBundle\Attribute\Target\ForProperties;
11+
use Sofascore\PurgatoryBundle\Cache\TargetResolver\ForPropertiesResolver;
12+
use Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass\ResolveTaggedLocatorIndexPass;
13+
use Sofascore\PurgatoryBundle\Exception\RuntimeException;
14+
use Sofascore\PurgatoryBundle\RouteParamValueResolver\RawValuesResolver;
15+
use Symfony\Component\DependencyInjection\ChildDefinition;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
18+
#[CoversClass(ResolveTaggedLocatorIndexPass::class)]
19+
final class ResolveTaggedLocatorIndexPassTest extends TestCase
20+
{
21+
public function testForAttributeIsResolvedFromTheTaggedClass(): void
22+
{
23+
$container = new ContainerBuilder();
24+
$container->register(id: 'foo', class: ForPropertiesResolver::class)
25+
->addTag(name: 'purgatory.target_resolver');
26+
$container->register(id: 'bar', class: RawValuesResolver::class)
27+
->addTag(name: 'purgatory.route_param_value_resolver', attributes: ['priority' => 10]);
28+
29+
(new ResolveTaggedLocatorIndexPass())->process($container);
30+
31+
self::assertSame(
32+
[['for' => ForProperties::class]],
33+
$container->getDefinition('foo')->getTag('purgatory.target_resolver'),
34+
);
35+
self::assertSame(
36+
[['priority' => 10, 'for' => RawValues::type()]],
37+
$container->getDefinition('bar')->getTag('purgatory.route_param_value_resolver'),
38+
);
39+
}
40+
41+
public function testExistingKeyIsNotOverwritten(): void
42+
{
43+
$container = new ContainerBuilder();
44+
$container->register(id: 'foo', class: ForPropertiesResolver::class)
45+
->addTag(name: 'purgatory.target_resolver', attributes: ['for' => \stdClass::class]);
46+
47+
(new ResolveTaggedLocatorIndexPass())->process($container);
48+
49+
self::assertSame(
50+
[['for' => \stdClass::class]],
51+
$container->getDefinition('foo')->getTag('purgatory.target_resolver'),
52+
);
53+
}
54+
55+
public function testClassIsNotValidatedWhenAllTagsHaveAnExplicitKey(): void
56+
{
57+
// Both definitions would fail validation ("foo" does not implement the expected
58+
// interface, "bar" has no class), so this test only passes if services whose
59+
// tags all have an explicit "for" attribute are skipped before validation.
60+
$container = new ContainerBuilder();
61+
$container->register(id: 'foo', class: \stdClass::class)
62+
->addTag(name: 'purgatory.target_resolver', attributes: ['for' => \stdClass::class]);
63+
$container->register(id: 'bar')
64+
->addTag(name: 'purgatory.route_param_value_resolver', attributes: ['for' => \stdClass::class]);
65+
66+
(new ResolveTaggedLocatorIndexPass())->process($container);
67+
68+
self::assertSame(
69+
[['for' => \stdClass::class]],
70+
$container->getDefinition('foo')->getTag('purgatory.target_resolver'),
71+
);
72+
self::assertSame(
73+
[['for' => \stdClass::class]],
74+
$container->getDefinition('bar')->getTag('purgatory.route_param_value_resolver'),
75+
);
76+
}
77+
78+
public function testOnlyTagsWithoutAnExplicitKeyAreStamped(): void
79+
{
80+
$container = new ContainerBuilder();
81+
$container->register(id: 'foo', class: ForPropertiesResolver::class)
82+
->addTag(name: 'purgatory.target_resolver', attributes: ['for' => \stdClass::class])
83+
->addTag(name: 'purgatory.target_resolver');
84+
85+
(new ResolveTaggedLocatorIndexPass())->process($container);
86+
87+
self::assertSame(
88+
[['for' => \stdClass::class], ['for' => ForProperties::class]],
89+
$container->getDefinition('foo')->getTag('purgatory.target_resolver'),
90+
);
91+
}
92+
93+
public function testClassIsResolvedFromParentDefinition(): void
94+
{
95+
$container = new ContainerBuilder();
96+
$container->register(id: 'foo', class: ForPropertiesResolver::class);
97+
$container->setDefinition(
98+
id: 'bar',
99+
definition: (new ChildDefinition('foo'))->addTag(name: 'purgatory.target_resolver'),
100+
);
101+
102+
(new ResolveTaggedLocatorIndexPass())->process($container);
103+
104+
self::assertSame(
105+
[['for' => ForProperties::class]],
106+
$container->getDefinition('bar')->getTag('purgatory.target_resolver'),
107+
);
108+
}
109+
110+
public function testClassIsResolvedFromParameter(): void
111+
{
112+
$container = new ContainerBuilder();
113+
$container->setParameter('foo.class', ForPropertiesResolver::class);
114+
$container->register(id: 'foo', class: '%foo.class%')
115+
->addTag(name: 'purgatory.target_resolver');
116+
117+
(new ResolveTaggedLocatorIndexPass())->process($container);
118+
119+
self::assertSame(
120+
[['for' => ForProperties::class]],
121+
$container->getDefinition('foo')->getTag('purgatory.target_resolver'),
122+
);
123+
}
124+
125+
public function testExceptionIsThrownWhenClassCannotBeDetermined(): void
126+
{
127+
$container = new ContainerBuilder();
128+
$container->register(id: 'foo')
129+
->addTag(name: 'purgatory.target_resolver');
130+
131+
$this->expectException(RuntimeException::class);
132+
$this->expectExceptionMessage('The class of the service "foo" tagged with "purgatory.target_resolver" could not be determined.');
133+
134+
(new ResolveTaggedLocatorIndexPass())->process($container);
135+
}
136+
137+
public function testExceptionIsThrownWhenClassResolvesToANonStringValue(): void
138+
{
139+
$container = new ContainerBuilder();
140+
$container->setParameter('foo.class', ['not', 'a', 'class']);
141+
$container->register(id: 'foo', class: '%foo.class%')
142+
->addTag(name: 'purgatory.target_resolver');
143+
144+
$this->expectException(RuntimeException::class);
145+
$this->expectExceptionMessage('The class of the service "foo" tagged with "purgatory.target_resolver" could not be determined.');
146+
147+
(new ResolveTaggedLocatorIndexPass())->process($container);
148+
}
149+
150+
public function testExceptionIsThrownWhenClassDoesNotImplementExpectedInterface(): void
151+
{
152+
$container = new ContainerBuilder();
153+
$container->register(id: 'foo', class: \stdClass::class)
154+
->addTag(name: 'purgatory.route_param_value_resolver');
155+
156+
$this->expectException(RuntimeException::class);
157+
$this->expectExceptionMessage('The class "stdClass" of the service "foo" tagged with "purgatory.route_param_value_resolver" must implement "Sofascore\PurgatoryBundle\RouteParamValueResolver\ValuesResolverInterface".');
158+
159+
(new ResolveTaggedLocatorIndexPass())->process($container);
160+
}
161+
}

0 commit comments

Comments
 (0)