Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
])

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

namespace Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass;

use Sofascore\PurgatoryBundle\Attribute\Target\TargetInterface;
use Sofascore\PurgatoryBundle\Cache\TargetResolver\TargetResolverInterface;
use Sofascore\PurgatoryBundle\Exception\RuntimeException;
use Sofascore\PurgatoryBundle\RouteParamValueResolver\ValuesResolverInterface;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
* Sets the "for" tag attribute used as the service locator index from the tagged
* class's static "for()" method, replacing the "defaultIndexMethod" reflection
* fallback which is deprecated since Symfony 8.1.
*/
final class ResolveTaggedLocatorIndexPass implements CompilerPassInterface
{
private const TAG_TO_INTERFACE = [
'purgatory.target_resolver' => 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<array<string, mixed>> $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<TargetResolverInterface<TargetInterface>|ValuesResolverInterface<array<mixed>>> $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]);
}
}
}
}
}
5 changes: 5 additions & 0 deletions src/PurgatoryBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<?php

declare(strict_types=1);

namespace Sofascore\PurgatoryBundle\Tests\DependencyInjection\CompilerPass;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\RawValues;
use Sofascore\PurgatoryBundle\Attribute\Target\ForProperties;
use Sofascore\PurgatoryBundle\Cache\TargetResolver\ForPropertiesResolver;
use Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass\ResolveTaggedLocatorIndexPass;
use Sofascore\PurgatoryBundle\Exception\RuntimeException;
use Sofascore\PurgatoryBundle\RouteParamValueResolver\RawValuesResolver;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;

#[CoversClass(ResolveTaggedLocatorIndexPass::class)]
final class ResolveTaggedLocatorIndexPassTest extends TestCase
{
public function testForAttributeIsResolvedFromTheTaggedClass(): void
{
$container = new ContainerBuilder();
$container->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);
}
}