Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit b6bf9de

Browse filesBrowse 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
Copy full SHA for b6bf9de

File tree

15 files changed

+293
-63
lines changed
Filter options

15 files changed

+293
-63
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Bridge/Doctrine/Attribute/MapEntity.php
+7-2Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bridge/Doctrine/composer.json
+1-1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+5Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php
+10-9Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Resources/config/security.php
+1-1Lines 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)
+24Lines 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 <fabien@symfony.com>
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/Attribute/MapDateTime.php
+7-2Lines 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
}
+27Lines 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 <fabien@symfony.com>
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/CHANGELOG.md
+1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php
+49-11Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/DependencyInjection/ControllerArgumentValueResolverPass.php
+19-2Lines 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
}
+33Lines 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 <fabien@symfony.com>
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.