Skip to content

Commit d74a72c

Browse files
committed
[HttpKernel] Add #[ValueResolver] for specifying a controller argument resolver
1 parent 1f7bc10 commit d74a72c

File tree

5 files changed

+156
-9
lines changed

5 files changed

+156
-9
lines changed

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

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver;
1515
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
16+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\AttributeValueResolver;
1617
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\BackedEnumValueResolver;
1718
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DateTimeValueResolver;
1819
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver;
@@ -48,38 +49,44 @@
4849
abstract_arg('argument value resolvers'),
4950
])
5051

52+
->set('argument_resolver.attribute', AttributeValueResolver::class)
53+
->args([
54+
tagged_locator('controller.argument_value_resolver', 'name'),
55+
])
56+
->tag('controller.argument_value_resolver', ['priority' => 150])
57+
5158
->set('argument_resolver.backed_enum_resolver', BackedEnumValueResolver::class)
52-
->tag('controller.argument_value_resolver', ['priority' => 100])
59+
->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => 'backed_enum'])
5360

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

5764
->set('argument_resolver.datetime', DateTimeValueResolver::class)
5865
->args([
5966
service('clock')->nullOnInvalid(),
6067
])
61-
->tag('controller.argument_value_resolver', ['priority' => 100])
68+
->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => 'datetime'])
6269

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

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

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

7279
->set('argument_resolver.service', ServiceValueResolver::class)
7380
->args([
7481
abstract_arg('service locator, set in RegisterControllerArgumentLocatorsPass'),
7582
])
76-
->tag('controller.argument_value_resolver', ['priority' => -50])
83+
->tag('controller.argument_value_resolver', ['priority' => -50, 'name' => 'service'])
7784

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

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

8491
->set('response_listener', ResponseListener::class)
8592
->args([
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
15+
class ValueResolver
16+
{
17+
public function __construct(
18+
public readonly string $name,
19+
) {
20+
}
21+
}

src/Symfony/Component/HttpKernel/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ CHANGELOG
99
* Add `#[WithHttpStatus]` for defining status codes for exceptions
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
12+
* Add `#[ValueResolver]` for specifying a controller argument resolver
1213

1314
6.2
1415
---
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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\Controller\ArgumentResolver;
13+
14+
use Psr\Container\ContainerInterface;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpKernel\Attribute\ValueResolver;
17+
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
18+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
19+
20+
/**
21+
* Delegates value resolving of arguments bearing the {@see ValueResolver} attribute to the specified resolver.
22+
*
23+
* @author Mathieu Lechat <mathieu.lechat@les-tilleuls.com>
24+
*/
25+
final class AttributeValueResolver implements ValueResolverInterface
26+
{
27+
public function __construct(private readonly ContainerInterface $resolvers)
28+
{
29+
}
30+
31+
public function resolve(Request $request, ArgumentMetadata $argument): iterable
32+
{
33+
$attributes = $argument->getAttributesOfType(ValueResolver::class);
34+
35+
if (!$attributes) {
36+
return [];
37+
}
38+
39+
$resolverName = $attributes[0]->name;
40+
$resolver = $this->resolvers->get($resolverName);
41+
if (!$resolver instanceof ValueResolverInterface) {
42+
throw new \UnexpectedValueException(sprintf('"%s" resolver must implement %s, but got %s.', $resolverName, ValueResolverInterface::class, get_debug_type($resolver)));
43+
}
44+
45+
return $resolver->resolve($request, $argument);
46+
}
47+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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\Tests\Controller\ArgumentResolver;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
16+
use Symfony\Component\DependencyInjection\ServiceLocator;
17+
use Symfony\Component\HttpFoundation\Request;
18+
use Symfony\Component\HttpKernel\Attribute\ValueResolver;
19+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\AttributeValueResolver;
20+
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
21+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
22+
23+
class AttributeValueResolverTest extends TestCase
24+
{
25+
public function testResolve()
26+
{
27+
$resolvedValue = ['bar'];
28+
$targetedResolver = $this->createStub(ValueResolverInterface::class);
29+
$targetedResolver->method('resolve')->willReturn($resolvedValue);
30+
$resolver = new AttributeValueResolver(new ServiceLocator(['foo' => static fn () => $targetedResolver]));
31+
32+
$request = Request::create('/');
33+
$argument = new ArgumentMetadata('dummy', null, false, false, null, false, [new ValueResolver('foo')]);
34+
35+
$this->assertEquals($resolvedValue, $resolver->resolve($request, $argument));
36+
}
37+
38+
public function testMissingAttribute()
39+
{
40+
$resolver = new AttributeValueResolver(new ServiceLocator([]));
41+
42+
$request = Request::create('/');
43+
$argument = new ArgumentMetadata('dummy', null, false, false, null);
44+
45+
$this->assertEquals([], $resolver->resolve($request, $argument));
46+
}
47+
48+
public function testMissingResolver()
49+
{
50+
$resolver = new AttributeValueResolver(new ServiceLocator([]));
51+
52+
$request = Request::create('/');
53+
$argument = new ArgumentMetadata('dummy', null, false, false, null, false, [new ValueResolver('foo')]);
54+
55+
$this->expectException(ServiceNotFoundException::class);
56+
57+
$resolver->resolve($request, $argument);
58+
}
59+
60+
public function testInvalidResolver()
61+
{
62+
$resolver = new AttributeValueResolver(new ServiceLocator(['foo' => static fn () => 'bar']));
63+
64+
$request = Request::create('/');
65+
$argument = new ArgumentMetadata('dummy', null, false, false, null, false, [new ValueResolver('foo')]);
66+
67+
$this->expectException(\UnexpectedValueException::class);
68+
69+
$resolver->resolve($request, $argument);
70+
}
71+
}

0 commit comments

Comments
 (0)