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 9cbc853

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

File tree

7 files changed

+201
-0
lines changed
Filter options

7 files changed

+201
-0
lines changed

‎src/Symfony/Component/Routing/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Routing/CHANGELOG.md
+1Lines 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
---
+50Lines 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+
}
+20Lines 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+
}
+20Lines 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+
}
+20Lines 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+
}
+20Lines 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+
}
+70Lines 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+
$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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.