From ce876065ff1aea0471a1c2d5cd16ba47803d23d3 Mon Sep 17 00:00:00 2001 From: Thomas Calvet Date: Mon, 21 Mar 2022 18:05:13 +0100 Subject: [PATCH] [Routing] Add EnumRequirement to help generate route requirements from a \BackedEnum --- src/Symfony/Component/Routing/CHANGELOG.md | 1 + .../Routing/Requirement/EnumRequirement.php | 50 +++++++++++++ .../Tests/Fixtures/Enum/TestIntBackedEnum.php | 20 ++++++ .../Fixtures/Enum/TestStringBackedEnum.php | 20 ++++++ .../Fixtures/Enum/TestStringBackedEnum2.php | 20 ++++++ .../Tests/Fixtures/Enum/TestUnitEnum.php | 20 ++++++ .../Tests/Requirement/EnumRequirementTest.php | 70 +++++++++++++++++++ 7 files changed, 201 insertions(+) create mode 100644 src/Symfony/Component/Routing/Requirement/EnumRequirement.php create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/Enum/TestIntBackedEnum.php create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/Enum/TestStringBackedEnum.php create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/Enum/TestStringBackedEnum2.php create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/Enum/TestUnitEnum.php create mode 100644 src/Symfony/Component/Routing/Tests/Requirement/EnumRequirementTest.php diff --git a/src/Symfony/Component/Routing/CHANGELOG.md b/src/Symfony/Component/Routing/CHANGELOG.md index 66f4999ca2648..dee13d3a0980f 100644 --- a/src/Symfony/Component/Routing/CHANGELOG.md +++ b/src/Symfony/Component/Routing/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * Allow using UTF-8 parameter names * Support the `attribute` type (alias of `annotation`) in annotation loaders * Already encoded slashes are not decoded nor double-encoded anymore when generating URLs (query parameters) + * Add `EnumRequirement` to help generate route requirements from a `\BackedEnum` 5.3 --- diff --git a/src/Symfony/Component/Routing/Requirement/EnumRequirement.php b/src/Symfony/Component/Routing/Requirement/EnumRequirement.php new file mode 100644 index 0000000000000..c7a094d2c4213 --- /dev/null +++ b/src/Symfony/Component/Routing/Requirement/EnumRequirement.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Requirement; + +use Symfony\Component\Routing\Exception\InvalidArgumentException; + +final class EnumRequirement implements \Stringable +{ + /** + * @var string[] + */ + private readonly array $values; + + /** + * @template T of \BackedEnum + * @param class-string $enum + * @param T ...$cases + */ + public function __construct(string $enum, \BackedEnum ...$cases) + { + if (!\is_subclass_of($enum, \BackedEnum::class, true)) { + throw new InvalidArgumentException(sprintf('"%s" is not a \BackedEnum class.', $enum)); + } + + foreach ($cases as $case) { + if (!$case instanceof $enum) { + throw new InvalidArgumentException(sprintf('"%s::%s" is not a case of "%s".', \get_class($case), $case->name, $enum)); + } + } + + $this->values = array_unique(array_map( + static fn (\BackedEnum $e): string => $e->value, + $cases ?: $enum::cases(), + )); + } + + public function __toString(): string + { + return implode('|', array_map(preg_quote(...), $this->values)); + } +} diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/Enum/TestIntBackedEnum.php b/src/Symfony/Component/Routing/Tests/Fixtures/Enum/TestIntBackedEnum.php new file mode 100644 index 0000000000000..17327a8011f09 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/Enum/TestIntBackedEnum.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Tests\Fixtures\Enum; + +enum TestIntBackedEnum: int +{ + case Hearts = 10; + case Diamonds = 20; + case Clubs = 30; + case Spades = 40; +} diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/Enum/TestStringBackedEnum.php b/src/Symfony/Component/Routing/Tests/Fixtures/Enum/TestStringBackedEnum.php new file mode 100644 index 0000000000000..bbecf63042143 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/Enum/TestStringBackedEnum.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Tests\Fixtures\Enum; + +enum TestStringBackedEnum: string +{ + case Hearts = 'hearts'; + case Diamonds = 'diamonds'; + case Clubs = 'clubs'; + case Spades = 'spades'; +} diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/Enum/TestStringBackedEnum2.php b/src/Symfony/Component/Routing/Tests/Fixtures/Enum/TestStringBackedEnum2.php new file mode 100644 index 0000000000000..01b98437eba3e --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/Enum/TestStringBackedEnum2.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Tests\Fixtures\Enum; + +enum TestStringBackedEnum2: string +{ + case Hearts = 'hearts'; + case Diamonds = 'diamonds'; + case Clubs = 'clubs'; + case Spades = 'spa|des'; +} diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/Enum/TestUnitEnum.php b/src/Symfony/Component/Routing/Tests/Fixtures/Enum/TestUnitEnum.php new file mode 100644 index 0000000000000..c2d42313753b8 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/Enum/TestUnitEnum.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Tests\Fixtures\Enum; + +enum TestUnitEnum +{ + case Hearts; + case Diamonds; + case Clubs; + case Spades; +} diff --git a/src/Symfony/Component/Routing/Tests/Requirement/EnumRequirementTest.php b/src/Symfony/Component/Routing/Tests/Requirement/EnumRequirementTest.php new file mode 100644 index 0000000000000..18484c03d142a --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Requirement/EnumRequirementTest.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Tests\Requirement; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Routing\Exception\InvalidArgumentException; +use Symfony\Component\Routing\Requirement\EnumRequirement; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\Tests\Fixtures\Enum\TestIntBackedEnum; +use Symfony\Component\Routing\Tests\Fixtures\Enum\TestStringBackedEnum; +use Symfony\Component\Routing\Tests\Fixtures\Enum\TestStringBackedEnum2; +use Symfony\Component\Routing\Tests\Fixtures\Enum\TestUnitEnum; + +class EnumRequirementTest extends TestCase +{ + public function testNotABackedEnum() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('"Symfony\Component\Routing\Tests\Fixtures\Enum\TestUnitEnum" is not a \BackedEnum class.'); + + new EnumRequirement(TestUnitEnum::class); + } + + public function testCaseFromAnotherEnum() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('"Symfony\Component\Routing\Tests\Fixtures\Enum\TestStringBackedEnum2::Spades" is not a case of "Symfony\Component\Routing\Tests\Fixtures\Enum\TestStringBackedEnum".'); + + new EnumRequirement(TestStringBackedEnum::class, TestStringBackedEnum::Diamonds, TestStringBackedEnum2::Spades); + } + + /** + * @dataProvider provideToString + */ + public function testToString(string $expected, string $enum, \BackedEnum ...$cases) + { + $this->assertSame($expected, (string) new EnumRequirement($enum, ...$cases)); + } + + public function provideToString() + { + return [ + ['hearts|diamonds|clubs|spades', TestStringBackedEnum::class], + ['10|20|30|40', TestIntBackedEnum::class], + ['diamonds|spades', TestStringBackedEnum::class, TestStringBackedEnum::Diamonds, TestStringBackedEnum::Spades], + ['hearts|diamonds|clubs|spa\|des', TestStringBackedEnum2::class], + ]; + } + + public function testInRoute() + { + $this->assertSame([ + 'bar' => 'hearts|diamonds|clubs|spades', + ], (new Route( + path: '/foo/{bar}', + requirements: [ + 'bar' => new EnumRequirement(TestStringBackedEnum::class), + ], + ))->getRequirements()); + } +}