8000 [HttpKernel] Add a controller argument resolver for backed enums · symfony/symfony@ee790c4 · GitHub
[go: up one dir, main page]

Skip to content

Commit ee790c4

committed
[HttpKernel] Add a controller argument resolver for backed enums
1 parent 424de23 commit ee790c4

File tree

6 files changed

+261
-0
lines changed

6 files changed

+261
-0
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
use Symfony\Component\HttpKernel\Attribute\AsController;
8080
use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface;
8181
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
82+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\BackedEnumValueResolver;
8283
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
8384
use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface;
8485
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
@@ -238,6 +239,11 @@ public function load(array $configs, ContainerBuilder $container)
238239
$loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config'));
239240

240241
$loader->load('web.php');
242+
243+
if (\PHP_VERSION_ID < 80100 || !class_exists(BackedEnumValueResolver::class)) {
244+
$container->removeDefinition('argument_resolver.backed_enum_resolver');
245+
}
246+
241247
$loader->load('services.php');
242248
$loader->load('fragment_renderer.php');
243249
$loader->load('error_renderer.php');

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

Lines changed: 6 additions & 0 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\BackedEnumValueResolver;
1617
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver;
1718
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver;
1819
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver;
@@ -45,6 +46,11 @@
4546
abstract_arg('argument value resolvers'),
4647
])
4748

49+
->set('argument_resolver.backed_enum_resolver', BackedEnumValueResolver::class)
50+
->tag('controller.argument_value_resolver', [
51+
'priority' => 105, // prior to the RequestAttributeValueResolver
52+
])
53+
4854
->set('argument_resolver.request_attribute', RequestAttributeValueResolver::class)
4955
->tag('controller.argument_value_resolver', ['priority' => 100])
5056

src/Symfony/Component/HttpKernel/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
6.1
5+
---
6+
7+
* Add `BackedEnumValueResolver` to resolve backed enum cases from request attributes in controller arguments
8+
49
6.0
510
---
611

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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 Symfony\Component\HttpFoundation\Exception\BadRequestException;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
17+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
18+
19+
/**
20+
* Attempt to resolve backed enum cases from request attributes.
21+
*
22+
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
23+
*/
24+
class BackedEnumValueResolver implements ArgumentValueResolverInterface
25+
{
26+
public function supports(Request $request, ArgumentMetadata $argument): bool
27+
{
28+
if (!is_subclass_of($argument->getType(), \BackedEnum::class, true)) {
29+
return false;
30+
}
31+
32+
// do not support if no value can be resolved at all
33+
// letting the \Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver be used
34+
// or \Symfony\Component\HttpKernel\Controller\ArgumentResolver fail with a meaningful error.
35+
return $request->attributes->has($argument->getName());
36+
}
37+
38+
public function resolve(Request $request, ArgumentMetadata $argument): iterable
39+
{
40+
/** @var interface-string<\BackedEnum> $enumType */
41+
$enumType = $argument->getType();
42+
$values = $argument->isVariadic() ? $request->attributes->all($argument->getName()) : [$request->attributes->get($argument->getName())];
43+
44+
foreach ($values as $value) {
45+
if (null === $value) {
46+
yield null;
47+
48+
continue;
49+
}
50+
51+
try {
52+
yield $enumType::from($value);
53+
54+
continue;
55+
} catch (\ValueError|\TypeError $error) {
56+
throw new BadRequestException(sprintf('Could not resolve the "%s" controller argument: %s', $argument->getName(), $error->getMessage()), $error->getCode(), $error);
57+
}
58+
}
59+
}
60+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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\HttpFoundation\Exception\BadRequestException;
16+
use Symfony\Component\HttpFoundation\Request;
17+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\BackedEnumValueResolver;
18+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
19+
use Symfony\Component\HttpKernel\Tests\Fixtures\Suit;
20+
21+
/**
22+
* @requires PHP 8.1
23+
*/
24+
class BackedEnumValueResolverTest extends TestCase
25+
{
26+
/**
27+
* @dataProvider provideTestSupportsData
28+
*/
29+
public function testSupports(Request $request, ArgumentMetadata $metadata, bool $expectedSupport)
30+
{
31+
$resolver = new BackedEnumValueResolver();
32+
33+
self::assertSame($expectedSupport, $resolver->supports($request, $metadata));
34+
}
35+
36+
public function provideTestSupportsData(): iterable
37+
{
38+
yield 'unsupported type' => [
39+
self::createRequest(['suit' => 'H']),
40+
self::createArgumentMetadata('suit', \stdClass::class),
41+
false,
42+
];
43+
44+
yield 'supports from attributes' => [
45+
self::createRequest(['suit' => 'H']),
46+
self::createArgumentMetadata('suit', Suit::class),
47+
true,
48+
];
49+
50+
yield 'with null attribute value' => [
51+
self::createRequest(['suit' => null]),
52+
self::createArgumentMetadata('suit', Suit::class),
53+
true,
54+
];
55+
56+
yield 'without matching attribute' => [
57+
self::createRequest(),
58+
self::createArgumentMetadata('suit', Suit::class),
59+
false,
60+
];
61+
62+
yield 'supports variadics' => [
63+
self::createRequest(['suit' => ['H', 'S']]),
64+
self::createArgumentMetadata(
65+
'suit',
66+
Suit::class,
67+
variadic: true,
68+
),
69+
true,
70+
];
71+
}
72+
73+
/**
74+
* @dataProvider provideTestResolveData
75+
*/
76+
public function testResolve(Request $request, ArgumentMetadata $metadata, $expected)
77+
{
78+
$resolver = new BackedEnumValueResolver();
79+
/** @var \Generator $results */
80+
$results = $resolver->resolve($request, $metadata);
81+
82+
self::assertSame($expected, iterator_to_array($results));
83+
}
84+
85+
public function provideTestResolveData(): iterable
86+
{
87+
yield 'resolves from attributes' => [
88+
self::createRequest(['suit' => 'H']),
89+
self::createArgumentMetadata('suit', Suit::class),
90+
[Suit::Hearts],
91+
];
92+
93+
yield 'with null attribute value' => [
94+
self::createRequest(['suit' => null]),
95+
self::createArgumentMetadata(
96+
'suit',
97+
Suit::class,
98+
),
99+
[null],
100+
];
101+
102+
yield 'with variadics' => [
103+
self::createRequest(['suits' => ['H', null, 'S']]),
104+
self::createArgumentMetadata(
105+
'suits',
106+
Suit::class,
107+
variadic: true
108+
),
109+
[Suit::Hearts, null, Suit::Spades],
110+
];
111+
}
112+
113+
public function testResolveThrowsOnInvalidValue()
114+
{
115+
$resolver = new BackedEnumValueResolver();
116+
$request = self::createRequest(['suit' => 'foo']);
117+
$metadata = self::createArgumentMetadata('suit', Suit::class);
118+
119+
$this->expectException(BadRequestException::class);
120+
$this->expectExceptionMessage('Could not resolve the "suit" controller argument: "foo" is not a valid backing value for enum "Symfony\Component\HttpKernel\Tests\Fixtures\Suit"');
121+
122+
/** @var \Generator $results */
123+
$results = $resolver->resolve($request, $metadata);
124+
iterator_to_array($results);
125+
}
126+
127+
public function testResolveThrowsOnNonVariadicArgumentWithMultipleValues()
128+
{
129+
$resolver = new BackedEnumValueResolver();
130+
$request = self::createRequest(['suit' => ['H', 'S']]);
131+
$metadata = self::createArgumentMetadata('suit', Suit::class);
132+
133+
$this->expectException(BadRequestException::class);
134+
$this->expectExceptionMessage('Could not resolve the "suit" controller argument: Symfony\Component\HttpKernel\Tests\Fixtures\Suit::from(): Argument #1 ($value) must be of type string, array given');
135+
136+
/** @var \Generator $results */
137+
$results = $resolver->resolve($request, $metadata);
138+
iterator_to_array($results);
139+
}
140+
141+
public function testResolveThrowsOnVariadicArgumentWithNonArrayValue()
142+
{
143+
$resolver = new BackedEnumValueResolver();
144+
$request = self::createRequest(['suits' => 'H']);
145+
$metadata = self::createArgumentMetadata('suits', Suit::class, variadic: true);
146+
147+
$this->expectException(BadRequestException::class);
148+
$this->expectExceptionMessage('Unexpected value for parameter "suits": expecting "array", got "string".');
149+
150+
/** @var \Generator $results */
151+
$results = $resolver->resolve($request, $metadata);
152+
iterator_to_array($results);
153+
}
154+
155+
private static function createRequest(array $attributes = []): Request
156+
{
157+
return new Request([], [], $attributes);
158+
}
159+
160+
private static function createArgumentMetadata(string $name, string $type, bool $variadic = false): ArgumentMetadata
161+
{
162+
return new ArgumentMetadata($name, $type, $variadic, false, null);
163+
}
164+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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\Fixtures;
13+
14+
enum Suit: string
15+
{
16+
case Hearts = 'H';
17+
case Diamonds = 'D';
18+
case Clubs = 'C';
19+
case Spades = 'S';
20+
}

0 commit comments

Comments
 (0)
0