From c2a9d9de85c472d63adde405ffb4630a7b490217 Mon Sep 17 00:00:00 2001 From: Maxime Steinhausser Date: Fri, 7 Oct 2022 18:56:27 +0200 Subject: [PATCH] [CI] Update edges --- .editorconfig | 3 + .github/workflows/ci.yml | 22 +- Makefile | 7 + src/Bridge/Symfony/Bundle/config/services.php | 14 +- .../BackedEnumValueResolver.php | 33 +- .../QueryBodyBackedEnumValueResolver.php | 152 +++++--- .../config/{config.yml => config.yaml} | 2 +- .../Symfony/config/config_6.2.yaml | 2 + .../config/{mongodb.yml => mongodb.yaml} | 0 .../Symfony/config/{mysql.yml => mysql.yaml} | 12 +- .../config/{routing.yml => routing.yaml} | 0 .../Integration/Symfony/src/Kernel.php | 10 +- .../BackedEnumValueResolverTest.php | 33 +- ...cyQueryBodyBackedEnumValueResolverTest.php | 324 ++++++++++++++++++ .../QueryBodyBackedEnumValueResolverTest.php | 38 +- 15 files changed, 573 insertions(+), 79 deletions(-) rename tests/Fixtures/Integration/Symfony/config/{config.yml => config.yaml} (95%) create mode 100644 tests/Fixtures/Integration/Symfony/config/config_6.2.yaml rename tests/Fixtures/Integration/Symfony/config/{mongodb.yml => mongodb.yaml} (100%) rename tests/Fixtures/Integration/Symfony/config/{mysql.yml => mysql.yaml} (58%) rename tests/Fixtures/Integration/Symfony/config/{routing.yml => routing.yaml} (100%) create mode 100644 tests/Unit/Bridge/Symfony/HttpKernel/Controller/ArgumentResolver/LegacyQueryBodyBackedEnumValueResolverTest.php diff --git a/.editorconfig b/.editorconfig index 4d33ce29..fd2b6d6e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -19,3 +19,6 @@ indent_size = 2 [*.md] trim_trailing_whitespace = false max_line_length = 120 + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb85b7cd..9b62ccd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,15 +74,31 @@ jobs: os: 'ubuntu-latest' php: '8.1' symfony: '6.0.*@dev' - mongodb: true - mysql: true allow-unstable: true + + - name: 'Test Symfony 6.1 [Linux, PHP 8.1]' + os: 'ubuntu-latest' + php: '8.1' + symfony: '6.1.*@dev' + allow-unstable: true + mysql: true + mongodb: true # Bleeding edge (unreleased dev versions where failures are allowed) - name: 'Test next Symfony [Linux, PHP 8.1] (allowed failure)' os: 'ubuntu-latest' php: '8.1' - symfony: '6.1.*@dev' + symfony: '6.2.*@dev' + composer-flags: '--ignore-platform-req php' + allow-unstable: true + allow-failure: true + mysql: true + mongodb: true + + - name: 'Test next Symfony [Linux, PHP 8.2] (allowed failure)' + os: 'ubuntu-latest' + php: '8.2' + symfony: '6.2.*@dev' composer-flags: '--ignore-platform-req php' allow-unstable: true allow-failure: true diff --git a/Makefile b/Makefile index 43a12095..3ddc1a9d 100644 --- a/Makefile +++ b/Makefile @@ -46,6 +46,13 @@ install.61: symfony composer config minimum-stability dev symfony composer update +## Install - Install Symfony 6.2 deps +install.62: setup +install.62: export SYMFONY_REQUIRE = 6.2.*@dev +install.62: + symfony composer config minimum-stability dev + symfony composer update + ## Install - Add Doctrine ODM deps deps.odm.add: symfony composer require --no-update --no-interaction --dev "doctrine/mongodb-odm:^2.3" "doctrine/mongodb-odm-bundle:^4.4.1" diff --git a/src/Bridge/Symfony/Bundle/config/services.php b/src/Bridge/Symfony/Bundle/config/services.php index f52524fb..f817fb59 100644 --- a/src/Bridge/Symfony/Bundle/config/services.php +++ b/src/Bridge/Symfony/Bundle/config/services.php @@ -14,13 +14,17 @@ use Elao\Enum\Bridge\Symfony\HttpKernel\Controller\ArgumentResolver\BackedEnumValueResolver; use Elao\Enum\Bridge\Symfony\HttpKernel\Controller\ArgumentResolver\QueryBodyBackedEnumValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\BackedEnumValueResolver as SymfonyBackedEnumValueResolver; return static function (ContainerConfigurator $container) { - $container->services() - ->set(BackedEnumValueResolver::class)->tag('controller.argument_value_resolver', [ - 'priority' => 105, // Prior RequestAttributeValueResolver - ]) - ; + if (!class_exists(SymfonyBackedEnumValueResolver::class)) { + $container->services() + ->set(BackedEnumValueResolver::class)->tag('controller.argument_value_resolver', [ + 'priority' => 105, // Prior RequestAttributeValueResolver + ]) + ; + } + $container->services() ->set(QueryBodyBackedEnumValueResolver::class)->tag('controller.argument_value_resolver', [ 'priority' => 110, // Prior BackedEnumValueResolver diff --git a/src/Bridge/Symfony/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php b/src/Bridge/Symfony/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php index 1c901570..202f18c5 100644 --- a/src/Bridge/Symfony/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php +++ b/src/Bridge/Symfony/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php @@ -13,15 +13,30 @@ namespace Elao\Enum\Bridge\Symfony\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\BackedEnumValueResolver as SymfonyBackedEnumValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +// Suggest using Symfony 6.1+ resolver +if (class_exists(SymfonyBackedEnumValueResolver::class)) { + trigger_deprecation( + 'elao/enum', + '2.1', + 'The "%s" class is deprecated with "symfony/http-kernel" >= 6.1, use "%s" instead.', + BackedEnumValueResolver::class, + SymfonyBackedEnumValueResolver::class + ); +} + +// Legacy (<6.1) resolver /** * Attempt to resolve backed enum cases from request attributes, for a route path parameter, * leading to a 404 Not Found if the attribute value isn't a valid backing value for the enum type. * * @author Maxime Steinhausser + * + * @final */ class BackedEnumValueResolver implements ArgumentValueResolverInterface { @@ -53,7 +68,14 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable } if (!\is_int($value) && !\is_string($value)) { - throw new \LogicException(sprintf('Could not resolve the "%s $%s" controller argument: expecting an int or string, got %s.', $argument->getType(), $argument->getName(), get_debug_type($value))); + throw new \LogicException( + sprintf( + 'Could not resolve the "%s $%s" controller argument: expecting an int or string, got "%s".', + $argument->getType(), + $argument->getName(), + get_debug_type($value) + ) + ); } /** @var class-string<\BackedEnum> $enumType */ @@ -62,7 +84,14 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable try { yield $enumType::from($value); } catch (\ValueError $error) { - throw new NotFoundHttpException(sprintf('Could not resolve the "%s $%s" controller argument: %s', $argument->getType(), $argument->getName(), $error->getMessage()), $error); + throw new NotFoundHttpException( + sprintf( + 'Could not resolve the "%s $%s" controller argument: %s', + $argument->getType(), + $argument->getName(), + $error->getMessage() + ), $error + ); } } } diff --git a/src/Bridge/Symfony/HttpKernel/Controller/ArgumentResolver/QueryBodyBackedEnumValueResolver.php b/src/Bridge/Symfony/HttpKernel/Controller/ArgumentResolver/QueryBodyBackedEnumValueResolver.php index 207bfe8a..ec5d790b 100644 --- a/src/Bridge/Symfony/HttpKernel/Controller/ArgumentResolver/QueryBodyBackedEnumValueResolver.php +++ b/src/Bridge/Symfony/HttpKernel/Controller/ArgumentResolver/QueryBodyBackedEnumValueResolver.php @@ -17,76 +17,138 @@ use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; -class QueryBodyBackedEnumValueResolver implements ArgumentValueResolverInterface +/** + * @internal + */ +function resolveValues(Request $request, ArgumentMetadata $argument): array { - public function supports(Request $request, ArgumentMetadata $argument): bool - { - if (!is_a($argument->getType(), \BackedEnum::class, true)) { - return false; - } + $from = $argument->getAttributes(BackedEnumFromQuery::class, ArgumentMetadata::IS_INSTANCEOF)[0] + ?? $argument->getAttributes(BackedEnumFromBody::class, ArgumentMetadata::IS_INSTANCEOF)[0] + ?? null; - $resolvedValues = $this->resolveValues($request, $argument); + if (null === $from) { + return []; + } - if ([] === $resolvedValues) { - // do not support if no value was resolved at all. - // letting the \Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver be used - // or \Symfony\Component\HttpKernel\Controller\ArgumentResolver fail with a meaningful error. - return false; - } + $key = $argument->getName(); - if (!$argument->isNullable() && \in_array(null, $resolvedValues, true)) { - // do not support if the argument isn't nullable but a null value was found, - // letting the \Symfony\Component\HttpKernel\Controller\ArgumentResolver fail with a meaningful error - return false; - } + $bag = match (true) { + $from instanceof BackedEnumFromQuery => $request->query, + $from instanceof BackedEnumFromBody => $request->request, + }; - return true; + if (!$bag->has($key)) { + return []; } - private function resolveValues(Request $request, ArgumentMetadata $argument): array - { - $from = $argument->getAttributes(BackedEnumFromQuery::class, ArgumentMetadata::IS_INSTANCEOF)[0] - ?? $argument->getAttributes(BackedEnumFromBody::class, ArgumentMetadata::IS_INSTANCEOF)[0] - ?? null; + $values = $argument->isVariadic() ? $bag->all($key) : $bag->get($key); - if (null === $from) { - return []; + if (!$argument->isVariadic()) { + $values = [$values]; + } + + foreach ($values as &$value) { + // Consider empty string from query/body as null + if ($value === '') { + $value = null; } + } - $key = $argument->getName(); + return $values; +} - $bag = match (true) { - $from instanceof BackedEnumFromQuery => $request->query, - $from instanceof BackedEnumFromBody => $request->request, - }; +// Legacy (<6.2) resolver +if (!interface_exists(ValueResolverInterface::class)) { + /** + * @final + */ + class QueryBodyBackedEnumValueResolver implements ArgumentValueResolverInterface + { + public function supports(Request $request, ArgumentMetadata $argument): bool + { + if (!is_a($argument->getType(), \BackedEnum::class, true)) { + return false; + } - if (!$bag->has($key)) { - return []; - } + $resolvedValues = resolveValues($request, $argument); - $values = $argument->isVariadic() ? $bag->all($key) : $bag->get($key); + if ([] === $resolvedValues) { + // do not support if no value was resolved at all. + // letting the \Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver be used + // or \Symfony\Component\HttpKernel\Controller\ArgumentResolver fail with a meaningful error. + return false; + } - if (!$argument->isVariadic()) { - $values = [$values]; + if (!$argument->isNullable() && \in_array(null, $resolvedValues, true)) { + // do not support if the argument isn't nullable but a null value was found, + // letting the \Symfony\Component\HttpKernel\Controller\ArgumentResolver fail with a meaningful error + return false; + } + + return true; } - foreach ($values as &$value) { - // Consider empty string from query/body as null - if ($value === '') { - $value = null; + public function resolve(Request $request, ArgumentMetadata $argument): iterable + { + $values = resolveValues($request, $argument); + + foreach ($values as $value) { + if ($value === null) { + yield null; + + continue; + } + + /** @var class-string<\BackedEnum> $enumType */ + $enumType = $argument->getType(); + + try { + yield $enumType::from($value); + } catch (\ValueError|\TypeError $error) { + throw new BadRequestException(sprintf( + 'Could not resolve the "%s $%s" controller argument: %s', + $argument->getType(), + $argument->getName(), + $error->getMessage(), + )); + } } } - - return $values; } + return; +} + +/** + * @final + */ +class QueryBodyBackedEnumValueResolver implements ValueResolverInterface +{ public function resolve(Request $request, ArgumentMetadata $argument): iterable { - $values = $this->resolveValues($request, $argument); + if (!is_a($argument->getType(), \BackedEnum::class, true)) { + return []; + } + + $resolvedValues = resolveValues($request, $argument); + + if ([] === $resolvedValues) { + // do not support if no value was resolved at all. + // letting the \Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver be used + // or \Symfony\Component\HttpKernel\Controller\ArgumentResolver fail with a meaningful error. + return []; + } + + if (!$argument->isNullable() && \in_array(null, $resolvedValues, true)) { + // do not support if the argument isn't nullable but a null value was found, + // letting the \Symfony\Component\HttpKernel\Controller\ArgumentResolver fail with a meaningful error + return []; + } - foreach ($values as $value) { + foreach ($resolvedValues as $value) { if ($value === null) { yield null; diff --git a/tests/Fixtures/Integration/Symfony/config/config.yml b/tests/Fixtures/Integration/Symfony/config/config.yaml similarity index 95% rename from tests/Fixtures/Integration/Symfony/config/config.yml rename to tests/Fixtures/Integration/Symfony/config/config.yaml index a38888ed..6245c575 100644 --- a/tests/Fixtures/Integration/Symfony/config/config.yml +++ b/tests/Fixtures/Integration/Symfony/config/config.yaml @@ -2,7 +2,7 @@ framework: secret: 'elao' form: true router: - resource: '%kernel.project_dir%/config/routing.yml' + resource: '%kernel.project_dir%/config/routing.yaml' strict_requirements: '%kernel.debug%' utf8: true session: diff --git a/tests/Fixtures/Integration/Symfony/config/config_6.2.yaml b/tests/Fixtures/Integration/Symfony/config/config_6.2.yaml new file mode 100644 index 00000000..4d9f958d --- /dev/null +++ b/tests/Fixtures/Integration/Symfony/config/config_6.2.yaml @@ -0,0 +1,2 @@ +framework: + catch_all_throwables: true diff --git a/tests/Fixtures/Integration/Symfony/config/mongodb.yml b/tests/Fixtures/Integration/Symfony/config/mongodb.yaml similarity index 100% rename from tests/Fixtures/Integration/Symfony/config/mongodb.yml rename to tests/Fixtures/Integration/Symfony/config/mongodb.yaml diff --git a/tests/Fixtures/Integration/Symfony/config/mysql.yml b/tests/Fixtures/Integration/Symfony/config/mysql.yaml similarity index 58% rename from tests/Fixtures/Integration/Symfony/config/mysql.yml rename to tests/Fixtures/Integration/Symfony/config/mysql.yaml index c45ee244..f422f691 100644 --- a/tests/Fixtures/Integration/Symfony/config/mysql.yml +++ b/tests/Fixtures/Integration/Symfony/config/mysql.yaml @@ -9,9 +9,9 @@ doctrine: alias: AppMySQL elao_enum: - doctrine: - types: - suit_sql_enum: - class: App\Enum\Suit - type: enum - default: !php/const App\Enum\Suit::Spades + doctrine: + types: + suit_sql_enum: + class: App\Enum\Suit + type: enum + default: !php/const App\Enum\Suit::Spades diff --git a/tests/Fixtures/Integration/Symfony/config/routing.yml b/tests/Fixtures/Integration/Symfony/config/routing.yaml similarity index 100% rename from tests/Fixtures/Integration/Symfony/config/routing.yml rename to tests/Fixtures/Integration/Symfony/config/routing.yaml diff --git a/tests/Fixtures/Integration/Symfony/src/Kernel.php b/tests/Fixtures/Integration/Symfony/src/Kernel.php index 13bd296e..1de6a3e7 100644 --- a/tests/Fixtures/Integration/Symfony/src/Kernel.php +++ b/tests/Fixtures/Integration/Symfony/src/Kernel.php @@ -36,14 +36,18 @@ class_exists(DoctrineMongoDBBundle::class) ? new DoctrineMongoDBBundle() : null, public function registerContainerConfiguration(LoaderInterface $loader) { - $loader->load($this->getProjectDir() . '/config/config.yml'); + $loader->load($this->getProjectDir() . '/config/config.yaml'); if (str_starts_with($_ENV['DOCTRINE_DBAL_URL'], 'pdo-mysql:')) { - $loader->load($this->getProjectDir() . '/config/mysql.yml'); + $loader->load($this->getProjectDir() . '/config/mysql.yaml'); + } + + if (self::VERSION_ID >= 60200) { + $loader->load($this->getProjectDir() . '/config/config_6.2.yaml'); } if (class_exists(DoctrineMongoDBBundle::class)) { - $loader->load($this->getProjectDir() . '/config/mongodb.yml'); + $loader->load($this->getProjectDir() . '/config/mongodb.yaml'); } } diff --git a/tests/Unit/Bridge/Symfony/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolverTest.php b/tests/Unit/Bridge/Symfony/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolverTest.php index 8e630f60..26068f7f 100644 --- a/tests/Unit/Bridge/Symfony/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolverTest.php +++ b/tests/Unit/Bridge/Symfony/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolverTest.php @@ -16,6 +16,7 @@ use Elao\Enum\Tests\Fixtures\Enum\Suit; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\BackedEnumValueResolver as SymfonyBackedEnumValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -26,6 +27,10 @@ class BackedEnumValueResolverTest extends TestCase */ public function testSupports(Request $request, ArgumentMetadata $metadata, bool $expectedSupport) { + if (class_exists(SymfonyBackedEnumValueResolver::class)) { + $this->markTestSkipped('This test is only relevant for Symfony <6.1. Use Symfony\'s resolver instead.'); + } + $resolver = new BackedEnumValueResolver(); self::assertSame($expectedSupport, $resolver->supports($request, $metadata)); @@ -73,11 +78,15 @@ public function provideTestSupportsData(): iterable */ public function testResolve(Request $request, ArgumentMetadata $metadata, $expected) { + if (class_exists(SymfonyBackedEnumValueResolver::class)) { + $this->markTestSkipped('This test is only relevant for Symfony <6.1. Use Symfony\'s resolver instead.'); + } + $resolver = new BackedEnumValueResolver(); /** @var \Generator $results */ $results = $resolver->resolve($request, $metadata); - self::assertSame($expected, iterator_to_array($results)); + self::assertSame($expected, \is_array($results) ? $results : iterator_to_array($results)); } public function provideTestResolveData(): iterable @@ -100,6 +109,10 @@ public function provideTestResolveData(): iterable public function testResolveThrowsNotFoundOnInvalidValue() { + if (class_exists(SymfonyBackedEnumValueResolver::class)) { + $this->markTestSkipped('This test is only relevant for Symfony <6.1. Use Symfony\'s resolver instead.'); + } + $resolver = new BackedEnumValueResolver(); $request = self::createRequest(['suit' => 'foo']); $metadata = self::createArgumentMetadata('suit', Suit::class); @@ -107,23 +120,31 @@ public function testResolveThrowsNotFoundOnInvalidValue() $this->expectException(NotFoundHttpException::class); $this->expectExceptionMessage('Could not resolve the "Elao\Enum\Tests\Fixtures\Enum\Suit $suit" controller argument: "foo" is not a valid backing value for enum'); - /** @var \Generator $results */ + /** @var \Generator|array $results */ $results = $resolver->resolve($request, $metadata); - iterator_to_array($results); + if (!\is_array($results)) { + iterator_to_array($results); + } } public function testResolveThrowsOnUnexpectedType() { + if (class_exists(SymfonyBackedEnumValueResolver::class)) { + $this->markTestSkipped('This test is only relevant for Symfony <6.1. Use Symfony\'s resolver instead.'); + } + $resolver = new BackedEnumValueResolver(); $request = self::createRequest(['suit' => false]); $metadata = self::createArgumentMetadata('suit', Suit::class); $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Could not resolve the "Elao\Enum\Tests\Fixtures\Enum\Suit $suit" controller argument: expecting an int or string, got bool.'); + $this->expectExceptionMessage('Could not resolve the "Elao\Enum\Tests\Fixtures\Enum\Suit $suit" controller argument: expecting an int or string, got "bool".'); - /** @var \Generator $results */ + /** @var \Generator|array $results */ $results = $resolver->resolve($request, $metadata); - iterator_to_array($results); + if (!\is_array($results)) { + iterator_to_array($results); + } } private static function createRequest(array $attributes = []): Request diff --git a/tests/Unit/Bridge/Symfony/HttpKernel/Controller/ArgumentResolver/LegacyQueryBodyBackedEnumValueResolverTest.php b/tests/Unit/Bridge/Symfony/HttpKernel/Controller/ArgumentResolver/LegacyQueryBodyBackedEnumValueResolverTest.php new file mode 100644 index 00000000..8fd26c77 --- /dev/null +++ b/tests/Unit/Bridge/Symfony/HttpKernel/Controller/ArgumentResolver/LegacyQueryBodyBackedEnumValueResolverTest.php @@ -0,0 +1,324 @@ + + */ + +namespace Elao\Enum\Tests\Unit\Bridge\Symfony\HttpKernel\Controller\ArgumentResolver; + +use Composer\InstalledVersions; +use Composer\Semver\VersionParser; +use Elao\Enum\Bridge\Symfony\HttpKernel\Controller\ArgumentResolver\Attributes\BackedEnumFromBody; +use Elao\Enum\Bridge\Symfony\HttpKernel\Controller\ArgumentResolver\Attributes\BackedEnumFromQuery; +use Elao\Enum\Bridge\Symfony\HttpKernel\Controller\ArgumentResolver\QueryBodyBackedEnumValueResolver; +use Elao\Enum\Tests\Fixtures\Enum\Suit; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Exception\BadRequestException; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; + +class LegacyQueryBodyBackedEnumValueResolverTest extends TestCase +{ + /** + * @dataProvider provides testSupports data + */ + public function testSupports(Request $request, ArgumentMetadata $metadata, bool $expectedSupport): void + { + if (interface_exists(ValueResolverInterface::class)) { + $this->markTestSkipped('Symfony >=6.2'); + } + + $resolver = new QueryBodyBackedEnumValueResolver(); + + self::assertSame($expectedSupport, $resolver->supports($request, $metadata)); + } + + public function provides testSupports data(): iterable + { + yield 'no PHP 8.1 attribute' => [ + self::getRequest(['suit' => 'H']), + self::getArgumentMetadata( + 'suit', + \stdClass::class, + attributes: [] + ), + false, + ]; + + yield 'unsupported type' => [ + self::getRequest(['suit' => 'H']), + self::getArgumentMetadata( + 'suit', + \stdClass::class, + attributes: [new BackedEnumFromQuery()] + ), + false, + ]; + + yield 'from query' => [ + self::getRequest(query: ['suit' => 'H']), + self::getArgumentMetadata( + 'suit', + Suit::class, + attributes: [new BackedEnumFromQuery()], + ), + true, + ]; + + yield 'missing from query' => [ + self::getRequest(query: []), + self::getArgumentMetadata( + 'suit', + Suit::class, + attributes: [new BackedEnumFromQuery()], + ), + false, + ]; + + yield 'from body' => [ + self::getRequest(body: ['suit' => 'H']), + self::getArgumentMetadata( + 'suit', + Suit::class, + attributes: [new BackedEnumFromBody()], + ), + true, + ]; + + yield 'missing from body' => [ + self::getRequest(body: []), + self::getArgumentMetadata( + 'suit', + Suit::class, + attributes: [new BackedEnumFromBody()], + ), + false, + ]; + + yield 'non-nullable with null found (casted from empty string)' => [ + self::getRequest(query: ['suit' => '']), + self::getArgumentMetadata( + 'suit', + Suit::class, + attributes: [new BackedEnumFromQuery()], + ), + false, + ]; + + yield 'nullable with null found (casted from empty string)' => [ + self::getRequest(query: ['suit' => '']), + self::getArgumentMetadata( + 'suit', + Suit::class, + nullable: true, + attributes: [new BackedEnumFromQuery()], + ), + true, + ]; + + yield 'non-nullable with no value found' => [ + self::getRequest(), + self::getArgumentMetadata( + 'suit', + Suit::class, + attributes: [new BackedEnumFromQuery()], + ), + false, + ]; + + yield 'supports variadics' => [ + self::getRequest(query: ['suits' => ['H', 'S']]), + self::getArgumentMetadata( + 'suits', + Suit::class, + variadic: true, + attributes: [new BackedEnumFromQuery()], + ), + true, + ]; + } + + /** + * @dataProvider provides testResolve data + */ + public function testResolve(Request $request, ArgumentMetadata $metadata, $expected): void + { + if (interface_exists(ValueResolverInterface::class)) { + $this->markTestSkipped('Symfony >=6.2'); + } + + $resolver = new QueryBodyBackedEnumValueResolver(); + + if (!$resolver->supports($request, $metadata)) { + throw new \LogicException(sprintf( + 'Invalid test case %s, since the supports method returned false', + $this->getName(true), + )); + } + + /** @var \Generator $results */ + $results = $resolver->resolve($request, $metadata); + + self::assertSame($expected, iterator_to_array($results)); + } + + public function provides testResolve data(): iterable + { + yield 'from query' => [ + self::getRequest(query: ['suit' => 'H']), + self::getArgumentMetadata( + 'suit', + Suit::class, + attributes: [new BackedEnumFromQuery()], + ), + [Suit::Hearts], + ]; + + yield 'from body' => [ + self::getRequest(body: ['suit' => 'H']), + self::getArgumentMetadata( + 'suit', + Suit::class, + attributes: [new BackedEnumFromBody()], + ), + [Suit::Hearts], + ]; + + yield 'nullable with null found (casted from empty string)' => [ + self::getRequest(query: ['suit' => '']), + self::getArgumentMetadata( + 'suit', + Suit::class, + nullable: true, + attributes: [new BackedEnumFromQuery()], + ), + [null], + ]; + + yield 'with variadics' => [ + self::getRequest(query: ['suit' => ['H', 'S']]), + self::getArgumentMetadata( + 'suit', + Suit::class, + variadic: true, + attributes: [new BackedEnumFromQuery()], + ), + [Suit::Hearts, Suit::Spades], + ]; + + yield 'nullable, with variadics' => [ + self::getRequest(query: ['suit' => ['', '']]), + self::getArgumentMetadata( + 'suit', + Suit::class, + nullable: true, + variadic: true, + attributes: [new BackedEnumFromQuery()], + ), + [null, null], + ]; + } + + public function testResolveThrowsOnInvalidValue(): void + { + if (interface_exists(ValueResolverInterface::class)) { + $this->markTestSkipped('Symfony >=6.2'); + } + + $resolver = new QueryBodyBackedEnumValueResolver(); + $request = self::getRequest(query: ['suit' => 'foo']); + $metadata = self::getArgumentMetadata('suit', Suit::class, attributes: [new BackedEnumFromQuery()]); + + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('Could not resolve the "Elao\Enum\Tests\Fixtures\Enum\Suit $suit" controller argument: "foo" is not a valid backing value for enum "Elao\Enum\Tests\Fixtures\Enum\Suit"'); + + /** @var \Generator $results */ + $results = $resolver->resolve($request, $metadata); + iterator_to_array($results); + } + + public function testResolveThrowsUnexpectedType(): void + { + if (interface_exists(ValueResolverInterface::class)) { + $this->markTestSkipped('Symfony >=6.2'); + } + + $resolver = new QueryBodyBackedEnumValueResolver(); + $request = self::getRequest(query: ['suit' => true]); + $metadata = self::getArgumentMetadata('suit', Suit::class, attributes: [new BackedEnumFromQuery()]); + + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('Could not resolve the "Elao\Enum\Tests\Fixtures\Enum\Suit $suit" controller argument: Elao\Enum\Tests\Fixtures\Enum\Suit::from(): Argument #1 ($value) must be of type string, bool given'); + + /** @var \Generator $results */ + $results = $resolver->resolve($request, $metadata); + iterator_to_array($results); + } + + public function testResolveThrowsNonVariadicsArrayValue(): void + { + if (interface_exists(ValueResolverInterface::class)) { + $this->markTestSkipped('Symfony >=6.2'); + } + + if (!InstalledVersions::satisfies(new VersionParser(), 'symfony/http-kernel', '^6.0')) { + self::markTestSkipped(); + } + + $resolver = new QueryBodyBackedEnumValueResolver(); + $request = self::getRequest(query: ['suit' => ['H', 'S']]); + $metadata = self::getArgumentMetadata('suit', Suit::class, attributes: [new BackedEnumFromQuery()]); + + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('Input value "suit" contains a non-scalar value.'); + + /** @var \Generator $results */ + $results = $resolver->resolve($request, $metadata); + iterator_to_array($results); + } + + public function testResolveThrowsVariadicsScalarValue(): void + { + if (interface_exists(ValueResolverInterface::class)) { + $this->markTestSkipped('Symfony >=6.2'); + } + + $resolver = new QueryBodyBackedEnumValueResolver(); + $request = self::getRequest(query: ['suit' => 'H']); + $metadata = self::getArgumentMetadata('suit', Suit::class, variadic: true, attributes: [new BackedEnumFromQuery()]); + + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('Unexpected value for parameter "suit": expecting "array", got "string".'); + + /** @var \Generator $results */ + $results = $resolver->resolve($request, $metadata); + iterator_to_array($results); + } + + private static function getRequest(array $query = [], array $body = []): Request + { + $request = new Request(); + + $request->query->replace($query); + $request->request->replace($body); + + return $request; + } + + private static function getArgumentMetadata( + string $name, + string $type, + bool $nullable = false, + bool $variadic = false, + array $attributes = [] + ): ArgumentMetadata { + return new ArgumentMetadata($name, $type, $variadic, false, null, $nullable, $attributes); + } +} diff --git a/tests/Unit/Bridge/Symfony/HttpKernel/Controller/ArgumentResolver/QueryBodyBackedEnumValueResolverTest.php b/tests/Unit/Bridge/Symfony/HttpKernel/Controller/ArgumentResolver/QueryBodyBackedEnumValueResolverTest.php index 606f1149..0597330f 100644 --- a/tests/Unit/Bridge/Symfony/HttpKernel/Controller/ArgumentResolver/QueryBodyBackedEnumValueResolverTest.php +++ b/tests/Unit/Bridge/Symfony/HttpKernel/Controller/ArgumentResolver/QueryBodyBackedEnumValueResolverTest.php @@ -21,6 +21,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; class QueryBodyBackedEnumValueResolverTest extends TestCase @@ -30,9 +31,17 @@ class QueryBodyBackedEnumValueResolverTest extends TestCase */ public function testSupports(Request $request, ArgumentMetadata $metadata, bool $expectedSupport): void { + if (!interface_exists(ValueResolverInterface::class)) { + $this->markTestSkipped('Symfony <6.2'); + } + $resolver = new QueryBodyBackedEnumValueResolver(); - self::assertSame($expectedSupport, $resolver->supports($request, $metadata)); + /** @var \Generator|array $results */ + $results = $resolver->resolve($request, $metadata); + $results = \is_array($results) ? $results : iterator_to_array($results); + + $expectedSupport ? self::assertNotEmpty($results) : self::assertSame([], $results); } public function provides testSupports data(): iterable @@ -145,15 +154,12 @@ public function provides testSupports data(): iterable */ public function testResolve(Request $request, ArgumentMetadata $metadata, $expected): void { - $resolver = new QueryBodyBackedEnumValueResolver(); - - if (!$resolver->supports($request, $metadata)) { - throw new \LogicException(sprintf( - 'Invalid test case %s, since the supports method returned false', - $this->getName(true), - )); + if (!interface_exists(ValueResolverInterface::class)) { + $this->markTestSkipped('Symfony <6.2'); } + $resolver = new QueryBodyBackedEnumValueResolver(); + /** @var \Generator $results */ $results = $resolver->resolve($request, $metadata); @@ -219,6 +225,10 @@ public function provides testResolve data(): iterable public function testResolveThrowsOnInvalidValue(): void { + if (!interface_exists(ValueResolverInterface::class)) { + $this->markTestSkipped('Symfony <6.2'); + } + $resolver = new QueryBodyBackedEnumValueResolver(); $request = self::getRequest(query: ['suit' => 'foo']); $metadata = self::getArgumentMetadata('suit', Suit::class, attributes: [new BackedEnumFromQuery()]); @@ -233,6 +243,10 @@ public function testResolveThrowsOnInvalidValue(): void public function testResolveThrowsUnexpectedType(): void { + if (!interface_exists(ValueResolverInterface::class)) { + $this->markTestSkipped('Symfony <6.2'); + } + $resolver = new QueryBodyBackedEnumValueResolver(); $request = self::getRequest(query: ['suit' => true]); $metadata = self::getArgumentMetadata('suit', Suit::class, attributes: [new BackedEnumFromQuery()]); @@ -247,6 +261,10 @@ public function testResolveThrowsUnexpectedType(): void public function testResolveThrowsNonVariadicsArrayValue(): void { + if (!interface_exists(ValueResolverInterface::class)) { + $this->markTestSkipped('Symfony <6.2'); + } + if (!InstalledVersions::satisfies(new VersionParser(), 'symfony/http-kernel', '^6.0')) { self::markTestSkipped(); } @@ -265,6 +283,10 @@ public function testResolveThrowsNonVariadicsArrayValue(): void public function testResolveThrowsVariadicsScalarValue(): void { + if (!interface_exists(ValueResolverInterface::class)) { + $this->markTestSkipped('Symfony <6.2'); + } + $resolver = new QueryBodyBackedEnumValueResolver(); $request = self::getRequest(query: ['suit' => 'H']); $metadata = self::getArgumentMetadata('suit', Suit::class, variadic: true, attributes: [new BackedEnumFromQuery()]);