Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b6bf9de

Browse files
feature #48992 [HttpKernel] Introduce pinnable value resolvers with #[ValueResolver] and #[AsPinnedValueResolver] (MatTheCat)
This PR was merged into the 6.3 branch. Discussion ---------- [HttpKernel] Introduce pinnable value resolvers with `#[ValueResolver]` and `#[AsPinnedValueResolver]` | Q | A | ------------- | --- | Branch? | 6.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | Fix #48927 | License | MIT | Doc PR | symfony/symfony-docs#17763 Introducing a new `ValueResolver` attribute, which allows to - “pin” a value resolver to an argument, meaning only said resolver will be called - prevent a resolver to be called for an argument Every existing resolver-related attribute (`MapEntity`, `CurrentUser`…) now extends `ValueResolver`. Each `controller.argument_value_resolver` tag is added a `name` attribute, which is the resolver’s FQCN. This is the first argument `ValueResolver` expects. A new `AsPinnedValueResolver` attribute is added for autoconfiguration, adding the `controller.pinned_value_resolver` tag. Such resolvers can only be “pinned”, meaning they won’t ever be called for an argument missing the `ValueResolver` attribute. Commits ------- 245485c [HttpKernel] Introduce pinnable value resolvers with `#[ValueResolver]` and `#[AsPinnedValueResolver]`
2 parents 0c6012f + 245485c commit b6bf9de

File tree

15 files changed

+293
-63
lines changed

15 files changed

+293
-63
lines changed

src/Symfony/Bridge/Doctrine/Attribute/MapEntity.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@
1111

1212
namespace Symfony\Bridge\Doctrine\Attribute;
1313

14+
use Symfony\Bridge\Doctrine\ArgumentResolver\EntityValueResolver;
15+
use Symfony\Component\HttpKernel\Attribute\ValueResolver;
16+
1417
/**
1518
* Indicates that a controller argument should receive an Entity.
1619
*/
1720
#[\Attribute(\Attribute::TARGET_PARAMETER)]
18-
class MapEntity
21+
class MapEntity extends ValueResolver
1922
{
2023
public function __construct(
2124
public ?string $class = null,
@@ -26,8 +29,10 @@ public function __construct(
2629
public ?bool $stripNull = null,
2730
public array|string|null $id = null,
2831
public ?bool $evictCache = null,
29-
public bool $disabled = false,
32+
bool $disabled = false,
33+
string $resolver = EntityValueResolver::class,
3034
) {
35+
parent::__construct($resolver, $disabled);
3136
}
3237

3338
public function withDefaults(self $defaults, ?string $class): static

src/Symfony/Bridge/Doctrine/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"symfony/config": "^5.4|^6.0",
3131
"symfony/dependency-injection": "^6.2",
3232
"symfony/form": "^5.4.21|^6.2.7",
33-
"symfony/http-kernel": "^6.2",
33+
"symfony/http-kernel": "^6.3",
3434
"symfony/messenger": "^5.4|^6.0",
3535
"symfony/doctrine-messenger": "^5.4|^6.0",
3636
"symfony/property-access": "^5.4|^6.0",

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
use Symfony\Component\HttpClient\UriTemplateHttpClient;
8484
use Symfony\Component\HttpFoundation\Request;
8585
use Symfony\Component\HttpKernel\Attribute\AsController;
86+
use Symfony\Component\HttpKernel\Attribute\AsPinnedValueResolver;
8687
use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface;
8788
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
8889
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\BackedEnumValueResolver;
@@ -691,6 +692,10 @@ public function load(array $configs, ContainerBuilder $container)
691692
$definition->addTag('messenger.message_handler', $tagAttributes);
692693
});
693694

695+
$container->registerAttributeForAutoconfiguration(AsPinnedValueResolver::class, static function (ChildDefinition $definition, AsPinnedValueResolver $attribute): void {
696+
$definition->addTag('controller.pinned_value_resolver', $attribute->name ? ['name' => $attribute->name] : []);
697+
});
698+
694699
if (!$container->getParameter('kernel.debug')) {
695700
// remove tagged iterator argument for resource checkers
696701
$container->getDefinition('config_cache_factory')->setArguments([]);

src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,40 +46,41 @@
4646
->args([
4747
service('argument_metadata_factory'),
4848
abstract_arg('argument value resolvers'),
49+
abstract_arg('pinned value resolvers'),
4950
])
5051

5152
->set('argument_resolver.backed_enum_resolver', BackedEnumValueResolver::class)
52-
->tag('controller.argument_value_resolver', ['priority' => 100])
53+
->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => BackedEnumValueResolver::class])
5354

5455
->set('argument_resolver.uid', UidValueResolver::class)
55-
->tag('controller.argument_value_resolver', ['priority' => 100])
56+
->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => UidValueResolver::class])
5657

5758
->set('argument_resolver.datetime', DateTimeValueResolver::class)
5859
->args([
5960
service('clock')->nullOnInvalid(),
6061
])
61-
->tag('controller.argument_value_resolver', ['priority' => 100])
62+
->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => DateTimeValueResolver::class])
6263

6364
->set('argument_resolver.request_attribute', RequestAttributeValueResolver::class)
64-
->tag('controller.argument_value_resolver', ['priority' => 100])
65+
->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => RequestAttributeValueResolver::class])
6566

6667
->set('argument_resolver.request', RequestValueResolver::class)
67-
->tag('controller.argument_value_resolver', ['priority' => 50])
68+
->tag('controller.argument_value_resolver', ['priority' => 50, 'name' => RequestValueResolver::class])
6869

6970
->set('argument_resolver.session', SessionValueResolver::class)
70-
->tag('controller.argument_value_resolver', ['priority' => 50])
71+
->tag('controller.argument_value_resolver', ['priority' => 50, 'name' => SessionValueResolver::class])
7172

7273
->set('argument_resolver.service', ServiceValueResolver::class)
7374
->args([
7475
abstract_arg('service locator, set in RegisterControllerArgumentLocatorsPass'),
7576
])
76-
->tag('controller.argument_value_resolver', ['priority' => -50])
77+
->tag('controller.argument_value_resolver', ['priority' => -50, 'name' => ServiceValueResolver::class])
7778

7879
->set('argument_resolver.default', DefaultValueResolver::class)
79-
->tag('controller.argument_value_resolver', ['priority' => -100])
80+
->tag('controller.argument_value_resolver', ['priority' => -100, 'name' => DefaultValueResolver::class])
8081

8182
->set('argument_resolver.variadic', VariadicValueResolver::class)
82-
->tag('controller.argument_value_resolver', ['priority' => -150])
83+
->tag('controller.argument_value_resolver', ['priority' => -150, 'name' => VariadicValueResolver::class])
8384

8485
->set('response_listener', ResponseListener::class)
8586
->args([

src/Symfony/Bundle/SecurityBundle/Resources/config/security.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@
100100
->args([
101101
service('security.token_storage'),
102102
])
103-
->tag('controller.argument_value_resolver', ['priority' => 120])
103+
->tag('controller.argument_value_resolver', ['priority' => 120, 'name' => UserValueResolver::class])
104104

105105
// Authentication related services
106106
->set('security.authentication.trust_resolver', AuthenticationTrustResolver::class)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\Attribute;
13+
14+
/**
15+
* Service tag to autoconfigure pinned value resolvers.
16+
*/
17+
#[\Attribute(\Attribute::TARGET_CLASS)]
18+
class AsPinnedValueResolver
19+
{
20+
public function __construct(
21+
public readonly ?string $name = null,
22+
) {
23+
}
24+
}

src/Symfony/Component/HttpKernel/Attribute/MapDateTime.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,19 @@
1111

1212
namespace Symfony\Component\HttpKernel\Attribute;
1313

14+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DateTimeValueResolver;
15+
1416
/**
1517
* Controller parameter tag to configure DateTime arguments.
1618
*/
1719
#[\Attribute(\Attribute::TARGET_PARAMETER)]
18-
class MapDateTime
20+
class MapDateTime extends ValueResolver
1921
{
2022
public function __construct(
21-
public readonly ?string $format = null
23+
public readonly ?string $format = null,
24+
bool $disabled = false,
25+
string $resolver = DateTimeValueResolver::class,
2226
) {
27+
parent::__construct($resolver, $disabled);
2328
}
2429
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\Attribute;
13+
14+
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
15+
16+
#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)]
17+
class ValueResolver
18+
{
19+
/**
20+
* @param class-string<ValueResolverInterface>|string $name
21+
*/
22+
public function __construct(
23+
public string $name,
24+
public bool $disabled = false,
25+
) {
26+
}
27+
}

src/Symfony/Component/HttpKernel/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ CHANGELOG
1010
* Use an instance of `Psr\Clock\ClockInterface` to generate the current date time in `DateTimeValueResolver`
1111
* Add `#[WithLogLevel]` for defining log levels for exceptions
1212
* Add `skip_response_headers` to the `HttpCache` options
13+
* Introduce pinnable value resolvers with `#[ValueResolver]` and `#[AsPinnedValueResolver]`
1314

1415
6.2
1516
---

src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
namespace Symfony\Component\HttpKernel\Controller;
1313

14+
use Psr\Container\ContainerInterface;
1415
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpKernel\Attribute\ValueResolver;
1517
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver;
1618
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver;
1719
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver;
@@ -20,6 +22,8 @@
2022
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\VariadicValueResolver;
2123
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory;
2224
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactoryInterface;
25+
use Symfony\Component\HttpKernel\Exception\ResolverNotFoundException;
26+
use Symfony\Contracts\Service\ServiceProviderInterface;
2327

2428
/**
2529
* Responsible for resolving the arguments passed to an action.
@@ -30,25 +34,54 @@ final class ArgumentResolver implements ArgumentResolverInterface
3034
{
3135
private ArgumentMetadataFactoryInterface $argumentMetadataFactory;
3236
private iterable $argumentValueResolvers;
37+
private ?ContainerInterface $namedResolvers;
3338

3439
/**
3540
* @param iterable<mixed, ArgumentValueResolverInterface|ValueResolverInterface> $argumentValueResolvers
3641
*/
37-
public function __construct(ArgumentMetadataFactoryInterface $argumentMetadataFactory = null, iterable $argumentValueResolvers = [])
42+
public function __construct(ArgumentMetadataFactoryInterface $argumentMetadataFactory = null, iterable $argumentValueResolvers = [], ContainerInterface $namedResolvers = null)
3843
{
3944
$this->argumentMetadataFactory = $argumentMetadataFactory ?? new ArgumentMetadataFactory();
4045
$this->argumentValueResolvers = $argumentValueResolvers ?: self::getDefaultArgumentValueResolvers();
46+
$this->namedResolvers = $namedResolvers;
4147
}
4248

4349
public function getArguments(Request $request, callable $controller, \ReflectionFunctionAbstract $reflector = null): array
4450
{
4551
$arguments = [];
4652

4753
foreach ($this->argumentMetadataFactory->createArgumentMetadata($controller, $reflector) as $metadata) {
48-
foreach ($this->argumentValueResolvers as $resolver) {
54+
$argumentValueResolvers = $this->argumentValueResolvers;
55+
$disabledResolvers = [];
56+
57+
if ($this->namedResolvers && $attributes = $metadata->getAttributesOfType(ValueResolver::class, $metadata::IS_INSTANCEOF)) {
58+
$resolverName = null;
59+
foreach ($attributes as $attribute) {
60+
if ($attribute->disabled) {
61+
$disabledResolvers[$attribute->name] = true;
62+
} elseif ($resolverName) {
63+
throw new \LogicException(sprintf('You can only pin one resolver per argument, but argument "$%s" of "%s()" has more.', $metadata->getName(), $this->getPrettyName($controller)));
64+
} else {
65+
$resolverName = $attribute->name;
66+
}
67+
}
68+
69+
if ($resolverName) {
70+
if (!$this->namedResolvers->has($resolverName)) {
71+
throw new ResolverNotFoundException($resolverName, $this->namedResolvers instanceof ServiceProviderInterface ? array_keys($this->namedResolvers->getProvidedServices()) : []);
72+
}
73+
74+
$argumentValueResolvers = [$this->namedResolvers->get($resolverName)];
75+
}
76+
}
77+
78+
foreach ($argumentValueResolvers as $name => $resolver) {
4979
if ((!$resolver instanceof ValueResolverInterface || $resolver instanceof TraceableValueResolver) && !$resolver->supports($request, $metadata)) {
5080
continue;
5181
}
82+
if (isset($disabledResolvers[\is_int($name) ? $resolver::class : $name])) {
83+
continue;
84+
}
5285

5386
$count = 0;
5487
foreach ($resolver->resolve($request, $metadata) as $argument) {
@@ -70,15 +103,7 @@ public function getArguments(Request $request, callable $controller, \Reflection
70103
}
71104
}
72105

73-
$representative = $controller;
74-
75-
if (\is_array($representative)) {
76-
$representative = sprintf('%s::%s()', $representative[0]::class, $representative[1]);
77-
} elseif (\is_object($representative)) {
78-
$representative = get_debug_type($representative);
79-
}
80-
81-
throw new \RuntimeException(sprintf('Controller "%s" requires that you provide a value for the "$%s" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or because there is a non optional argument after this one.', $representative, $metadata->getName()));
106+
throw new \RuntimeException(sprintf('Controller "%s" requires that you provide a value for the "$%s" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or because there is a non optional argument after this one.', $this->getPrettyName($controller), $metadata->getName()));
82107
}
83108

84109
return $arguments;
@@ -97,4 +122,17 @@ public static function getDefaultArgumentValueResolvers(): iterable
97122
new VariadicValueResolver(),
98123
];
99124
}
125+
126+
private function getPrettyName($controller): string
127+
{
128+
if (\is_array($controller)) {
129+
return $controller[0]::class.'::'.$controller[1];
130+
}
131+
132+
if (\is_object($controller)) {
133+
return get_debug_type($controller);
134+
}
135+
136+
return $controller;
137+
}
100138
}

src/Symfony/Component/HttpKernel/DependencyInjection/ControllerArgumentValueResolverPass.php

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
namespace Symfony\Component\HttpKernel\DependencyInjection;
1313

1414
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
15+
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
16+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
1517
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1618
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
1719
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -37,10 +39,24 @@ public function process(ContainerBuilder $container)
3739
return;
3840
}
3941

40-
$resolvers = $this->findAndSortTaggedServices('controller.argument_value_resolver', $container);
42+
$definitions = $container->getDefinitions();
43+
$namedResolvers = $this->findAndSortTaggedServices(new TaggedIteratorArgument('controller.pinned_value_resolver', 'name', needsIndexes: true), $container);
44+
$resolvers = $this->findAndSortTaggedServices(new TaggedIteratorArgument('controller.argument_value_resolver', 'name', needsIndexes: true), $container);
45+
46+
foreach ($resolvers as $name => $resolverReference) {
47+
$id = (string) $resolverReference;
48+
49+
if ($definitions[$id]->hasTag('controller.pinned_value_resolver')) {
50+
unset($resolvers[$name]);
51+
} else {
52+
$namedResolvers[$name] ??= clone $resolverReference;
53+
}
54+
}
55+
56+
$resolvers = array_values($resolvers);
4157

4258
if ($container->getParameter('kernel.debug') && class_exists(Stopwatch::class) && $container->has('debug.stopwatch')) {
43-
foreach ($resolvers as $resolverReference) {
59+
foreach ($resolvers + $namedResolvers as $resolverReference) {
4460
$id = (string) $resolverReference;
4561
$container->register("debug.$id", TraceableValueResolver::class)
4662
->setDecoratedService($id)
@@ -51,6 +67,7 @@ public function process(ContainerBuilder $container)
5167
$container
5268
->getDefinition('argument_resolver')
5369
->replaceArgument(1, new IteratorArgument($resolvers))
70+
->setArgument(2, new ServiceLocatorArgument($namedResolvers))
5471
;
5572
}
5673
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\Exception;
13+
14+
class ResolverNotFoundException extends \RuntimeException
15+
{
16+
/**
17+
* @param string[] $alternatives
18+
*/
19+
public function __construct(string $name, array $alternatives = [])
20+
{
21+
$msg = sprintf('You have requested a non-existent resolver "%s".', $name);
22+
if ($alternatives) {
23+
if (1 === \count($alternatives)) {
24+
$msg .= ' Did you mean this: "';
25+
} else {
26+
$msg .= ' Did you mean one of these: "';
27+
}
28+
$msg .= implode('", "', $alternatives).'"?';
29+
}
30+
31+
parent::__construct($msg);
32+
}
33+
}

0 commit comments

Comments
 (0)