8000 feature #45803 [Routing] Add EnumRequirement to help generate route r… · symfony/symfony@9cbc853 · GitHub
[go: up one dir, main page]

Skip to content

Commit 9cbc853

Browse files
committed
feature #45803 [Routing] Add EnumRequirement to help generate route requirements from a \BackedEnum (fancyweb)
This PR was merged into the 6.1 branch. Discussion ---------- [Routing] Add EnumRequirement to help generate route requirements from a \BackedEnum | Q | A | ------------- | --- | Branch? | 6.1 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | - | License | MIT | Doc PR | - Ref #44831 I'd like to limit a route parameter allowed values to the backed values of an enum to use it in conjunction with the new `\BackedEnum` argument resolver (ie fail from the start). Also, sometimes, I'd like to limit it only to a subset of the backed values. I couldn't find a way to do that because enums can't implement `__toString()` and accessing `->value` is not considered a constant operation. We can leverage the fact that route requirements can be a `\Stringable`. Before (no enum): ```php #[Route(path: '/foo/{bar}', requirements: ['bar' => FooEnum::AAA.'|'.FooEnum::BBB])] ``` Allow all enum cases: ```php #[Route(path: '/foo/{bar}', requirements: ['bar' => new EnumRequirement(Foo::class)])] ``` Allow a subset: ```php #[Route(path: '/foo/{bar}', requirements: ['bar' => new EnumRequirement(Foo::class, Foo::Aaa, Foo::Bbb)])] ``` Probably not the best solution but I hope we can find something for that use case for 6.1 😄 cc @ogizanagi Commits ------- ce87606 [Routing] Add EnumRequirement to help generate route requirements from a \BackedEnum
2 parents 98dc2ec + ce87606 commit 9cbc853

File tree

7 files changed

+201
-0
lines changed

7 files changed

+201
-0
lines changed

src/Symfony/Component/Routing/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ CHANGELOG
88
* Allow using UTF-8 parameter names
99
* Support the `attribute` type (alias of `annotation`) in annotation loaders
1010
* Already encoded slashes are not decoded nor double-encoded anymore when generating URLs (query parameters)
11+
* Add `EnumRequirement` to help generate route requirements from a `\BackedEnum`
1112

1213
5.3
1314
---
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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\Routing\Requirement;
13+
14+
use Symfony\Component\Routing\Exception\InvalidArgumentException;
15+
16+
final class EnumRequirement implements \Stringable
17+
{
18+
/**
19+
* @var string[]
20+
*/
21+
private readonly array $values;
22+
23+
/**
24+
* @template T of \BackedEnum
25+
* @param class-string<T> $enum
26+
* @param T ...$cases
27+
*/
28+
public function __construct(string $enum, \BackedEnum ...$cases)
29+
{
30+
if (!\is_subclass_of($enum, \BackedEnum::class, true)) {
31+
throw new InvalidArgumentException(sprintf('"%s" is not a \BackedEnum class.', $enum));
32+
}
33+
34+
foreach ($cases as $case) {
35+
if (!$case instanceof $enum) {
36+
throw new InvalidArgumentException(sprintf('"%s::%s" is not a case of "%s".', \get_class($case), $case->name, $enum));
37+
}
38+
}
39+
40+
$this->values = array_unique(array_map(
41+
static fn (\BackedEnum $e): string => $e->value,
42+
$cases ?: $enum::cases(),
43+
));
44+
}
45+
46+
public function __toString(): string
47+
{
48+
return implode('|', array_map(preg_quote(...), $this->values));
49+
}
50+
}
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\Routing\Tests\Fixtures\Enum;
13+
14+
enum TestIntBackedEnum: int
15+
{
16+
case Hearts = 10;
17+
case Diamonds = 20;
18+
case Clubs = 30;
19+
case Spades = 40;
20+
}
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\Routing\Tests\Fixtures\Enum;
13+
14+
enum TestStringBackedEnum: string
15+
{
16+
case Hearts = 'hearts';
17+
case Diamonds = 'diamonds';
18+
case Clubs = 'clubs';
19+
case Spades = 'spades';
20+
}
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\Routing\Tests\Fixtures\Enum;
13+
14+
enum TestStringBackedEnum2: string
15+
{
16+
case Hearts = 'hearts';
17+
case Diamonds = 'diamonds';
18+
case Clubs = 'clubs';
19+
case Spades = 'spa|des';
20+
}
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\Routing\Tests\Fixtures\Enum;
13+
14+
enum TestUnitEnum
15+
{
16+
case Hearts;
17+
case Diamonds;
18+
case Clubs;
19+
case Spades;
20+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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\Routing\Tests\Requirement;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Routing\Exception\InvalidArgumentException;
16+
use Symfony\Component\Routing\Requirement\EnumRequirement;
17+
use Symfony\Component\Routing\Route;
18+
use Symfony\Component\Routing\Tests\Fixtures\Enum\TestIntBackedEnum;
19+
use Symfony\Component\Routing\Tests\Fixtures\Enum\TestStringBackedEnum;
20+
use Symfony\Component\Routing\Tests\Fixtures\Enum\TestStringBackedEnum2;
21+
use Symfony\Component\Routing\Tests\Fixtures\Enum\TestUnitEnum;
22+
23+
class EnumRequirementTest extends TestCase
24+
{
25+
public function testNotABackedEnum()
26+
{
27+
$this->expectException(InvalidArgumentException::class);
28+
$this->expectExceptionMessage('"Symfony\Component\Routing\Tests\Fixtures\Enum\TestUnitEnum" is not a \BackedEnum class.');
29+
30+
new EnumRequirement(TestUnitEnum::class);
31+
}
32+
33+
public function testCaseFromAnotherEnum()
34+
{
35+
$this->expectException(InvalidArgumentException::class);
36+
$this->expectExceptionMessage('"Symfony\Component\Routing\Tests\Fixtures\Enum\TestStringBackedEnum2::Spades" is not a case of "Symfony\Component\Routing\Tests\Fixtures\Enum\TestStringBackedEnum".');
37+
38+
new EnumRequirement(TestStringBackedEnum::class, TestStringBackedEnum::Diamonds, TestStringBackedEnum2::Spades);
39+
}
40+
41+
/**
42+
* @dataProvider provideToString
43+
*/
44+
public function testToString(string $expected, string $enum, \BackedEnum ...$cases)
45+
{
46< BD36 span class="diff-text-marker">+
$this->assertSame($expected, (string) new EnumRequirement($enum, ...$cases));
47+
}
48+
49+
public function provideToString()
50+
{
51+
return [
52+
['hearts|diamonds|clubs|spades', TestStringBackedEnum::class],
53+
['10|20|30|40', TestIntBackedEnum::class],
54+
['diamonds|spades', TestStringBackedEnum::class, TestStringBackedEnum::Diamonds, TestStringBackedEnum::Spades],
55+
['hearts|diamonds|clubs|spa\|des', TestStringBackedEnum2::class],
56+
];
57+
}
58+
59+
public function testInRoute()
60+
{
61+
$this->assertSame([
62+
'bar' => 'hearts|diamonds|clubs|spades',
63+
], (new Route(
64+
path: '/foo/{bar}',
65+
requirements: [
66+
'bar' => new EnumRequirement(TestStringBackedEnum::class),
67+
],
68+
))->getRequirements());
69+
}
70+
}

0 commit comments

Comments
 (0)
0