From 7fa3df5c6a034bd300f067d923ab32a772036b2b Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 7 Jul 2023 16:21:49 +0200 Subject: [PATCH 001/120] Add SymfonyDescriber --- RouteDescriber/SymfonyDescriber.php | 169 ++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 RouteDescriber/SymfonyDescriber.php diff --git a/RouteDescriber/SymfonyDescriber.php b/RouteDescriber/SymfonyDescriber.php new file mode 100644 index 000000000..a41e964f6 --- /dev/null +++ b/RouteDescriber/SymfonyDescriber.php @@ -0,0 +1,169 @@ +getMethodParameter($reflectionMethod, [MapRequestPayload::class, MapQueryParameter::class]); + + foreach ($this->getOperations($api, $route) as $operation) { + foreach ($parameters as $parameter) { + $parameterName = $parameter->getName(); + + if ($attribute = $this->getAttribute($parameter, MapRequestPayload::class)) { + /** @var OA\RequestBody $requestBody */ + $requestBody = Util::getChild($operation, OA\RequestBody::class); + + if (! is_array($attribute->acceptFormat)) { + $contentSchema = $this->getContentSchemaForType($requestBody, $attribute->acceptFormat ?? 'json'); + $contentSchema->ref = new Model(type: $parameter->getType()->getName()); + + $schema = Util::getProperty($contentSchema, $parameterName); + + $this->describeCommonSchemaFromParameter($schema, $parameter); + } else { + foreach ($attribute->acceptFormat as $format) { + $contentSchema = $this->getContentSchemaForType($requestBody, $format); + $contentSchema->ref = new Model(type: $parameter->getType()->getName()); + + $schema = Util::getProperty($contentSchema, $parameterName); + + $this->describeCommonSchemaFromParameter($schema, $parameter); + } + } + } + + if ($attribute = $this->getAttribute($parameter, MapQueryParameter::class)) { + $operationParameter = Util::getOperationParameter($operation, $parameterName, 'query'); + $operationParameter->name = $attribute->name ?? $parameterName; + $operationParameter->allowEmptyValue = $parameter->allowsNull(); + + $operationParameter->required = ! $parameter->isDefaultValueAvailable() && ! $parameter->allowsNull(); + + /** @var OA\Schema $schema */ + $schema = Util::getChild($operationParameter, OA\Schema::class); + + if ($attribute->filter === FILTER_VALIDATE_REGEXP) { + $schema->pattern = $attribute->options['regexp']; + } + + $this->describeCommonSchemaFromParameter($schema, $parameter); + } + } + } + } + + /** + * @param class-string[] $attributes + * + * @return ReflectionParameter[] + */ + private function getMethodParameter(ReflectionMethod $reflectionMethod, array $attributes): array + { + $parameters = []; + + foreach ($reflectionMethod->getParameters() as $parameter) { + foreach ($parameter->getAttributes() as $attribute) { + if (in_array($attribute->getName(), $attributes, true)) { + $parameters[] = $parameter; + } + } + } + + return $parameters; + } + + private function describeCommonSchemaFromParameter(OA\Schema $schema, ReflectionParameter $parameter): void + { + if ($parameter->isDefaultValueAvailable()) { + $schema->default = $parameter->getDefaultValue(); + } + + if (Generator::UNDEFINED === $schema->type) { + if ($parameter->getType()->isBuiltin()) { + $schema->type = $parameter->getType()->getName(); + } + } + } + + /** + * @param class-string $attribute + * + * @return T|null + * + * @template T of object + */ + private function getAttribute(ReflectionParameter $parameter, string $attribute): ?object + { + if ($attribute = $parameter->getAttributes($attribute, ReflectionAttribute::IS_INSTANCEOF)) { + return $attribute[0]->newInstance(); + } + + return null; + } + + private function getContentSchemaForType(OA\RequestBody $requestBody, string $type): OA\Schema + { + $requestBody->content = Generator::UNDEFINED !== $requestBody->content ? $requestBody->content : []; + switch ($type) { + case 'json': + $contentType = 'application/json'; + + break; + case 'xml': + $contentType = 'application/xml'; + + break; + default: + throw new InvalidArgumentException('Unsupported media type'); + } + + if (! isset($requestBody->content[$contentType])) { + $weakContext = Util::createWeakContext($requestBody->_context); + $requestBody->content[$contentType] = new OA\MediaType( + [ + 'mediaType' => $contentType, + '_context' => $weakContext, + ] + ); + + /** @var OA\Schema $schema */ + $schema = Util::getChild( + $requestBody->content[$contentType], + OA\Schema::class + ); + $schema->type = 'object'; + } + + return Util::getChild( + $requestBody->content[$contentType], + OA\Schema::class + ); + } +} From d5a69ee9ca0cc33971ce1c41d95258bd2df957d4 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 7 Jul 2023 16:31:49 +0200 Subject: [PATCH 002/120] Add SymfonyDescriber dependency injection --- Resources/config/symfony.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 Resources/config/symfony.xml diff --git a/Resources/config/symfony.xml b/Resources/config/symfony.xml new file mode 100644 index 000000000..ab5bee360 --- /dev/null +++ b/Resources/config/symfony.xml @@ -0,0 +1,12 @@ + + + + + + + + + + From 013ea83a845118b4f7c7a73d08753c6554b80d28 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 7 Jul 2023 19:49:54 +0200 Subject: [PATCH 003/120] Fix codestyle --- RouteDescriber/SymfonyDescriber.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RouteDescriber/SymfonyDescriber.php b/RouteDescriber/SymfonyDescriber.php index a41e964f6..9f65a5884 100644 --- a/RouteDescriber/SymfonyDescriber.php +++ b/RouteDescriber/SymfonyDescriber.php @@ -40,7 +40,7 @@ public function describe(OA\OpenApi $api, Route $route, ReflectionMethod $reflec /** @var OA\RequestBody $requestBody */ $requestBody = Util::getChild($operation, OA\RequestBody::class); - if (! is_array($attribute->acceptFormat)) { + if (!is_array($attribute->acceptFormat)) { $contentSchema = $this->getContentSchemaForType($requestBody, $attribute->acceptFormat ?? 'json'); $contentSchema->ref = new Model(type: $parameter->getType()->getName()); @@ -64,12 +64,12 @@ public function describe(OA\OpenApi $api, Route $route, ReflectionMethod $reflec $operationParameter->name = $attribute->name ?? $parameterName; $operationParameter->allowEmptyValue = $parameter->allowsNull(); - $operationParameter->required = ! $parameter->isDefaultValueAvailable() && ! $parameter->allowsNull(); + $operationParameter->required = !$parameter->isDefaultValueAvailable() && ! $parameter->allowsNull(); /** @var OA\Schema $schema */ $schema = Util::getChild($operationParameter, OA\Schema::class); - if ($attribute->filter === FILTER_VALIDATE_REGEXP) { + if (FILTER_VALIDATE_REGEXP === $attribute->filter) { $schema->pattern = $attribute->options['regexp']; } @@ -144,7 +144,7 @@ private function getContentSchemaForType(OA\RequestBody $requestBody, string $ty throw new InvalidArgumentException('Unsupported media type'); } - if (! isset($requestBody->content[$contentType])) { + if (!isset($requestBody->content[$contentType])) { $weakContext = Util::createWeakContext($requestBody->_context); $requestBody->content[$contentType] = new OA\MediaType( [ From 467476a4f26e84e972f0afa7100f0afb14171204 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 7 Jul 2023 19:53:38 +0200 Subject: [PATCH 004/120] fixup! Fix codestyle --- RouteDescriber/SymfonyDescriber.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RouteDescriber/SymfonyDescriber.php b/RouteDescriber/SymfonyDescriber.php index 9f65a5884..e4f891375 100644 --- a/RouteDescriber/SymfonyDescriber.php +++ b/RouteDescriber/SymfonyDescriber.php @@ -64,7 +64,7 @@ public function describe(OA\OpenApi $api, Route $route, ReflectionMethod $reflec $operationParameter->name = $attribute->name ?? $parameterName; $operationParameter->allowEmptyValue = $parameter->allowsNull(); - $operationParameter->required = !$parameter->isDefaultValueAvailable() && ! $parameter->allowsNull(); + $operationParameter->required = !$parameter->isDefaultValueAvailable() && !$parameter->allowsNull(); /** @var OA\Schema $schema */ $schema = Util::getChild($operationParameter, OA\Schema::class); From 48258910a2b4a01e7f4092a6f97b9d72825f41c4 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 7 Jul 2023 20:19:15 +0200 Subject: [PATCH 005/120] Add php 8 checks --- RouteDescriber/SymfonyDescriber.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/RouteDescriber/SymfonyDescriber.php b/RouteDescriber/SymfonyDescriber.php index e4f891375..10601ca66 100644 --- a/RouteDescriber/SymfonyDescriber.php +++ b/RouteDescriber/SymfonyDescriber.php @@ -9,7 +9,6 @@ use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; use OpenApi\Generator; -use ReflectionAttribute; use ReflectionMethod; use ReflectionParameter; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; @@ -86,6 +85,10 @@ public function describe(OA\OpenApi $api, Route $route, ReflectionMethod $reflec */ private function getMethodParameter(ReflectionMethod $reflectionMethod, array $attributes): array { + if (PHP_VERSION_ID < 80100) { + return []; + } + $parameters = []; foreach ($reflectionMethod->getParameters() as $parameter) { @@ -121,7 +124,11 @@ private function describeCommonSchemaFromParameter(OA\Schema $schema, Reflection */ private function getAttribute(ReflectionParameter $parameter, string $attribute): ?object { - if ($attribute = $parameter->getAttributes($attribute, ReflectionAttribute::IS_INSTANCEOF)) { + if (PHP_VERSION_ID < 80100) { + return null; + } + + if ($attribute = $parameter->getAttributes($attribute, \ReflectionAttribute::IS_INSTANCEOF)) { return $attribute[0]->newInstance(); } From a9ba360a97a736baf3a50498651287be7df3cd1d Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 7 Jul 2023 20:19:47 +0200 Subject: [PATCH 006/120] Add symfony.xml loading --- DependencyInjection/NelmioApiDocExtension.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index cf6ac501e..284960b66 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -30,6 +30,9 @@ use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; +use Symfony\Component\HttpKernel\Attribute\MapQueryString; +use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Routing\RouteCollection; @@ -159,6 +162,15 @@ public function load(array $configs, ContainerBuilder $container): void ->setArgument(1, $config['media_types']); } + if ( + PHP_VERSION_ID > 80100 + && class_exists(MapRequestPayload::class) + && class_exists(MapQueryParameter::class) + && class_exists(MapQueryString::class) + ) { + $loader->load('symfony.xml'); + } + $bundles = $container->getParameter('kernel.bundles'); if (!isset($bundles['TwigBundle']) || !class_exists('Symfony\Component\Asset\Packages')) { $container->removeDefinition('nelmio_api_doc.controller.swagger_ui'); From b23fd1b02d2fd66e4c4ebc1b4f7986418a44edb9 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 7 Jul 2023 21:17:46 +0200 Subject: [PATCH 007/120] Temp: increase max self deprecations --- phpunit.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 695969160..0ddb09e34 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -14,7 +14,7 @@ - + From dbeb6be4cfb2915f427f068bb4b1cedbc0cfc50a Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 7 Jul 2023 21:31:39 +0200 Subject: [PATCH 008/120] Add Exception throw when invalid php version is used --- RouteDescriber/SymfonyDescriber.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/RouteDescriber/SymfonyDescriber.php b/RouteDescriber/SymfonyDescriber.php index 10601ca66..713e99262 100644 --- a/RouteDescriber/SymfonyDescriber.php +++ b/RouteDescriber/SymfonyDescriber.php @@ -11,6 +11,7 @@ use OpenApi\Generator; use ReflectionMethod; use ReflectionParameter; +use RuntimeException; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\Routing\Route; @@ -21,12 +22,14 @@ final class SymfonyDescriber implements RouteDescriberInterface { + private const PHP_VERSION_ERROR = self::class . ' can only be used in PHP 8 or above.'; + use RouteDescriberTrait; public function describe(OA\OpenApi $api, Route $route, ReflectionMethod $reflectionMethod): void { if (PHP_VERSION_ID < 80100) { - return; + throw new RuntimeException(self::PHP_VERSION_ERROR); } $parameters = $this->getMethodParameter($reflectionMethod, [MapRequestPayload::class, MapQueryParameter::class]); @@ -86,7 +89,7 @@ public function describe(OA\OpenApi $api, Route $route, ReflectionMethod $reflec private function getMethodParameter(ReflectionMethod $reflectionMethod, array $attributes): array { if (PHP_VERSION_ID < 80100) { - return []; + throw new RuntimeException(self::PHP_VERSION_ERROR); } $parameters = []; @@ -125,7 +128,7 @@ private function describeCommonSchemaFromParameter(OA\Schema $schema, Reflection private function getAttribute(ReflectionParameter $parameter, string $attribute): ?object { if (PHP_VERSION_ID < 80100) { - return null; + throw new RuntimeException(self::PHP_VERSION_ERROR); } if ($attribute = $parameter->getAttributes($attribute, \ReflectionAttribute::IS_INSTANCEOF)) { From 6db82c3c3f8eeb64d9a83a33436d175d757153c2 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 7 Jul 2023 21:33:27 +0200 Subject: [PATCH 009/120] Fix codestyle --- RouteDescriber/SymfonyDescriber.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RouteDescriber/SymfonyDescriber.php b/RouteDescriber/SymfonyDescriber.php index 713e99262..908f82622 100644 --- a/RouteDescriber/SymfonyDescriber.php +++ b/RouteDescriber/SymfonyDescriber.php @@ -22,7 +22,7 @@ final class SymfonyDescriber implements RouteDescriberInterface { - private const PHP_VERSION_ERROR = self::class . ' can only be used in PHP 8 or above.'; + private const PHP_VERSION_ERROR = self::class.' can only be used in PHP 8 or above.'; use RouteDescriberTrait; From cf366c845761e9fbd0179f52f9a7961f5674d580 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 7 Jul 2023 22:30:36 +0200 Subject: [PATCH 010/120] Add describeRequestBody method --- RouteDescriber/SymfonyDescriber.php | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/RouteDescriber/SymfonyDescriber.php b/RouteDescriber/SymfonyDescriber.php index 908f82622..56b47b753 100644 --- a/RouteDescriber/SymfonyDescriber.php +++ b/RouteDescriber/SymfonyDescriber.php @@ -43,20 +43,10 @@ public function describe(OA\OpenApi $api, Route $route, ReflectionMethod $reflec $requestBody = Util::getChild($operation, OA\RequestBody::class); if (!is_array($attribute->acceptFormat)) { - $contentSchema = $this->getContentSchemaForType($requestBody, $attribute->acceptFormat ?? 'json'); - $contentSchema->ref = new Model(type: $parameter->getType()->getName()); - - $schema = Util::getProperty($contentSchema, $parameterName); - - $this->describeCommonSchemaFromParameter($schema, $parameter); + $this->describeRequestBody($requestBody, $parameter, $attribute->acceptFormat ?? 'json'); } else { foreach ($attribute->acceptFormat as $format) { - $contentSchema = $this->getContentSchemaForType($requestBody, $format); - $contentSchema->ref = new Model(type: $parameter->getType()->getName()); - - $schema = Util::getProperty($contentSchema, $parameterName); - - $this->describeCommonSchemaFromParameter($schema, $parameter); + $this->describeRequestBody($requestBody, $parameter, $format); } } } @@ -138,6 +128,17 @@ private function getAttribute(ReflectionParameter $parameter, string $attribute) return null; } + private function describeRequestBody(OA\RequestBody $requestBody, ReflectionParameter $parameter, string $format): void + { + $contentSchema = $this->getContentSchemaForType($requestBody, $format); + $contentSchema->ref = new Model(type: $parameter->getType()->getName()); + $contentSchema->type = 'object'; + + $schema = Util::getProperty($contentSchema, $parameter->getName()); + + $this->describeCommonSchemaFromParameter($schema, $parameter); + } + private function getContentSchemaForType(OA\RequestBody $requestBody, string $type): OA\Schema { $requestBody->content = Generator::UNDEFINED !== $requestBody->content ? $requestBody->content : []; From 399c05b1a2ce6ed7d220a7d03974b9b7baf5658d Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 7 Jul 2023 23:24:19 +0200 Subject: [PATCH 011/120] Use elseif --- RouteDescriber/SymfonyDescriber.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/RouteDescriber/SymfonyDescriber.php b/RouteDescriber/SymfonyDescriber.php index 56b47b753..5d216bd49 100644 --- a/RouteDescriber/SymfonyDescriber.php +++ b/RouteDescriber/SymfonyDescriber.php @@ -49,9 +49,7 @@ public function describe(OA\OpenApi $api, Route $route, ReflectionMethod $reflec $this->describeRequestBody($requestBody, $parameter, $format); } } - } - - if ($attribute = $this->getAttribute($parameter, MapQueryParameter::class)) { + } elseif ($attribute = $this->getAttribute($parameter, MapQueryParameter::class)) { $operationParameter = Util::getOperationParameter($operation, $parameterName, 'query'); $operationParameter->name = $attribute->name ?? $parameterName; $operationParameter->allowEmptyValue = $parameter->allowsNull(); From 81abaa853964b9cc8989181d1e6e92ce194e5b77 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 7 Jul 2023 23:24:50 +0200 Subject: [PATCH 012/120] Only check for php version once --- RouteDescriber/SymfonyDescriber.php | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/RouteDescriber/SymfonyDescriber.php b/RouteDescriber/SymfonyDescriber.php index 5d216bd49..3703735e4 100644 --- a/RouteDescriber/SymfonyDescriber.php +++ b/RouteDescriber/SymfonyDescriber.php @@ -22,14 +22,12 @@ final class SymfonyDescriber implements RouteDescriberInterface { - private const PHP_VERSION_ERROR = self::class.' can only be used in PHP 8 or above.'; - use RouteDescriberTrait; public function describe(OA\OpenApi $api, Route $route, ReflectionMethod $reflectionMethod): void { if (PHP_VERSION_ID < 80100) { - throw new RuntimeException(self::PHP_VERSION_ERROR); + throw new RuntimeException(self::class.' can only be used in PHP 8 or above.'); } $parameters = $this->getMethodParameter($reflectionMethod, [MapRequestPayload::class, MapQueryParameter::class]); @@ -76,10 +74,6 @@ public function describe(OA\OpenApi $api, Route $route, ReflectionMethod $reflec */ private function getMethodParameter(ReflectionMethod $reflectionMethod, array $attributes): array { - if (PHP_VERSION_ID < 80100) { - throw new RuntimeException(self::PHP_VERSION_ERROR); - } - $parameters = []; foreach ($reflectionMethod->getParameters() as $parameter) { @@ -115,10 +109,6 @@ private function describeCommonSchemaFromParameter(OA\Schema $schema, Reflection */ private function getAttribute(ReflectionParameter $parameter, string $attribute): ?object { - if (PHP_VERSION_ID < 80100) { - throw new RuntimeException(self::PHP_VERSION_ERROR); - } - if ($attribute = $parameter->getAttributes($attribute, \ReflectionAttribute::IS_INSTANCEOF)) { return $attribute[0]->newInstance(); } From a2775af0ebb7efa8abe81ec171ba40e164f7a6d0 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 7 Jul 2023 23:25:09 +0200 Subject: [PATCH 013/120] Add SymfonyDescriberTest for MapRequestPayload --- Tests/RouteDescriber/SymfonyDescriberTest.php | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 Tests/RouteDescriber/SymfonyDescriberTest.php diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php new file mode 100644 index 000000000..28a29845a --- /dev/null +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -0,0 +1,100 @@ +symfonyDescriber = new SymfonyDescriber(); + } + + /** + * @dataProvider provideMapRequestPayloadTestData + * + * @requires PHP >= 8 + * @param string|string[] $expectedMediaTypes + */ + public function testMapRequestPayloadParamRegistersRequestBody( + MapRequestPayload $mapRequestPayload, + array $expectedMediaTypes + ): void { + $classType = stdClass::class; + + $reflectionNamedType = $this->createStub(ReflectionNamedType::class); + $reflectionNamedType->method('getName')->willReturn($classType); + + $api = new OpenApi([]); + + $controllerMethodMock = $this->createStub(\ReflectionMethod::class); + + $reflectionAttributeMock = $this->createStub(ReflectionAttribute::class); + $reflectionAttributeMock->method('getName')->willReturn(MapRequestPayload::class); + $reflectionAttributeMock->method('newInstance')->willReturn($mapRequestPayload); + + $reflectionParameterStub= $this->createStub(ReflectionParameter::class); + $reflectionParameterStub->method('getType')->willReturn($reflectionNamedType); + $reflectionParameterStub->method('getAttributes')->willReturn([$reflectionAttributeMock]); + + $controllerMethodMock->method('getParameters')->willReturn([$reflectionParameterStub]); + + $this->symfonyDescriber->describe( + $api, + new Route('/'), + $controllerMethodMock + ); + + foreach ($expectedMediaTypes as $expectedMediaType) { + $requestBodyContent = $api->paths[0]->get->requestBody->content[$expectedMediaType]; + + self::assertSame($expectedMediaType, $requestBodyContent->mediaType); + self::assertSame('object', $requestBodyContent->schema->type); + self::assertSame($classType, $requestBodyContent->schema->ref->type); + } + } + + public function provideMapRequestPayloadTestData(): Generator + { + yield 'it sets default mediaType to json' => [ + new MapRequestPayload(), + ['application/json'], + ]; + + yield 'it sets the mediaType to json' => [ + new MapRequestPayload('json'), + ['application/json'], + ]; + + yield 'it sets the mediaType to xml' => [ + new MapRequestPayload('xml'), + ['application/xml'], + ]; + + yield 'it sets multiple mediaTypes' => [ + new MapRequestPayload(['json', 'xml']), + ['application/json', 'application/xml'], + ]; + } +} From 6fc37377a4fb9ecc2c478056d31912d9fd7e55bb Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 7 Jul 2023 23:27:20 +0200 Subject: [PATCH 014/120] Fix annotation --- Tests/RouteDescriber/SymfonyDescriberTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index 28a29845a..99fd720f3 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -35,7 +35,8 @@ protected function setUp(): void * @dataProvider provideMapRequestPayloadTestData * * @requires PHP >= 8 - * @param string|string[] $expectedMediaTypes + * + * @param string[] $expectedMediaTypes */ public function testMapRequestPayloadParamRegistersRequestBody( MapRequestPayload $mapRequestPayload, From f3e2638d13cddf1a22779cc92335071e60472a99 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 7 Jul 2023 23:33:52 +0200 Subject: [PATCH 015/120] Skip test if attributes don't exist --- Tests/RouteDescriber/SymfonyDescriberTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index 99fd720f3..fab1afd79 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -19,6 +19,8 @@ use ReflectionNamedType; use ReflectionParameter; use stdClass; +use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; +use Symfony\Component\HttpKernel\Attribute\MapQueryString; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\Routing\Route; @@ -28,6 +30,14 @@ class SymfonyDescriberTest extends TestCase protected function setUp(): void { + if ( + !class_exists(MapRequestPayload::class) + && !class_exists(MapQueryParameter::class) + && !class_exists(MapQueryString::class) + ) { + $this->markTestSkipped('Symfony 6.3 attributes not found'); + } + $this->symfonyDescriber = new SymfonyDescriber(); } From 95dfba768bb88444c12dcb5536b047a210658b16 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 7 Jul 2023 23:34:48 +0200 Subject: [PATCH 016/120] Skip test based on php version --- Tests/RouteDescriber/SymfonyDescriberTest.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index fab1afd79..533378c15 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -30,6 +30,10 @@ class SymfonyDescriberTest extends TestCase protected function setUp(): void { + if (\PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Attributes require PHP 8'); + } + if ( !class_exists(MapRequestPayload::class) && !class_exists(MapQueryParameter::class) @@ -44,8 +48,6 @@ protected function setUp(): void /** * @dataProvider provideMapRequestPayloadTestData * - * @requires PHP >= 8 - * * @param string[] $expectedMediaTypes */ public function testMapRequestPayloadParamRegistersRequestBody( From 7690d6220dee517f4137f8585631562adbe4e6f7 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 7 Jul 2023 23:39:11 +0200 Subject: [PATCH 017/120] Move $mapRequestPayload type to annotation --- Tests/RouteDescriber/SymfonyDescriberTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index 533378c15..2a993f620 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -48,10 +48,11 @@ protected function setUp(): void /** * @dataProvider provideMapRequestPayloadTestData * + * @param MapRequestPayload $mapRequestPayload * @param string[] $expectedMediaTypes */ public function testMapRequestPayloadParamRegistersRequestBody( - MapRequestPayload $mapRequestPayload, + $mapRequestPayload, array $expectedMediaTypes ): void { $classType = stdClass::class; From e1c4201c21af5946907a9ce28eeb84d497f2e6c9 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 7 Jul 2023 23:39:49 +0200 Subject: [PATCH 018/120] Fix annotation style --- Tests/RouteDescriber/SymfonyDescriberTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index 2a993f620..9f67bf3e3 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -49,7 +49,7 @@ protected function setUp(): void * @dataProvider provideMapRequestPayloadTestData * * @param MapRequestPayload $mapRequestPayload - * @param string[] $expectedMediaTypes + * @param string[] $expectedMediaTypes */ public function testMapRequestPayloadParamRegistersRequestBody( $mapRequestPayload, From 32b59dddec81cf87f47fe63510e7270f6cd57036 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 7 Jul 2023 23:47:12 +0200 Subject: [PATCH 019/120] Fix SymfonyDescriberTest for older symfony versions --- Tests/RouteDescriber/SymfonyDescriberTest.php | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index 9f67bf3e3..fb5fa0e01 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -31,7 +31,7 @@ class SymfonyDescriberTest extends TestCase protected function setUp(): void { if (\PHP_VERSION_ID < 80100) { - $this->markTestSkipped('Attributes require PHP 8'); + self::markTestSkipped('Attributes require PHP 8'); } if ( @@ -39,20 +39,24 @@ protected function setUp(): void && !class_exists(MapQueryParameter::class) && !class_exists(MapQueryString::class) ) { - $this->markTestSkipped('Symfony 6.3 attributes not found'); + self::markTestSkipped('Symfony 6.3 attributes not found'); } $this->symfonyDescriber = new SymfonyDescriber(); } + public function testMapRequestPayload(): void + { + foreach (self::provideMapRequestPayloadTestData() as $testData) { + $this->testMapRequestPayloadParamRegistersRequestBody(...$testData); + } + } + /** - * @dataProvider provideMapRequestPayloadTestData - * - * @param MapRequestPayload $mapRequestPayload - * @param string[] $expectedMediaTypes + * @param string[] $expectedMediaTypes */ - public function testMapRequestPayloadParamRegistersRequestBody( - $mapRequestPayload, + private function testMapRequestPayloadParamRegistersRequestBody( + MapRequestPayload $mapRequestPayload, array $expectedMediaTypes ): void { $classType = stdClass::class; @@ -89,7 +93,7 @@ public function testMapRequestPayloadParamRegistersRequestBody( } } - public function provideMapRequestPayloadTestData(): Generator + public static function provideMapRequestPayloadTestData(): Generator { yield 'it sets default mediaType to json' => [ new MapRequestPayload(), From 5356227a721c9c6e43cb6cec039fcb7df40a6af5 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 7 Jul 2023 23:56:23 +0200 Subject: [PATCH 020/120] Remove version check --- RouteDescriber/SymfonyDescriber.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/RouteDescriber/SymfonyDescriber.php b/RouteDescriber/SymfonyDescriber.php index 3703735e4..684501e1f 100644 --- a/RouteDescriber/SymfonyDescriber.php +++ b/RouteDescriber/SymfonyDescriber.php @@ -11,14 +11,12 @@ use OpenApi\Generator; use ReflectionMethod; use ReflectionParameter; -use RuntimeException; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\Routing\Route; use function in_array; use function is_array; use const FILTER_VALIDATE_REGEXP; -use const PHP_VERSION_ID; final class SymfonyDescriber implements RouteDescriberInterface { @@ -26,10 +24,6 @@ final class SymfonyDescriber implements RouteDescriberInterface public function describe(OA\OpenApi $api, Route $route, ReflectionMethod $reflectionMethod): void { - if (PHP_VERSION_ID < 80100) { - throw new RuntimeException(self::class.' can only be used in PHP 8 or above.'); - } - $parameters = $this->getMethodParameter($reflectionMethod, [MapRequestPayload::class, MapQueryParameter::class]); foreach ($this->getOperations($api, $route) as $operation) { From c05b1a2e585cf51396024995fda88edc9ddbce40 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 14 Jul 2023 15:11:51 +0200 Subject: [PATCH 021/120] Remove usage of in_array to check for attribute --- RouteDescriber/SymfonyDescriber.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/RouteDescriber/SymfonyDescriber.php b/RouteDescriber/SymfonyDescriber.php index 684501e1f..9ee0fbaaf 100644 --- a/RouteDescriber/SymfonyDescriber.php +++ b/RouteDescriber/SymfonyDescriber.php @@ -14,7 +14,6 @@ use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\Routing\Route; -use function in_array; use function is_array; use const FILTER_VALIDATE_REGEXP; @@ -71,8 +70,8 @@ private function getMethodParameter(ReflectionMethod $reflectionMethod, array $a $parameters = []; foreach ($reflectionMethod->getParameters() as $parameter) { - foreach ($parameter->getAttributes() as $attribute) { - if (in_array($attribute->getName(), $attributes, true)) { + foreach ($attributes as $attribute) { + if ($parameter->getAttributes($attribute, \ReflectionAttribute::IS_INSTANCEOF)) { $parameters[] = $parameter; } } From f01d5f99e88990beb7e69dde536eba3cbd32fd6a Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 14 Jul 2023 15:12:17 +0200 Subject: [PATCH 022/120] Change elseif to separate if statement --- RouteDescriber/SymfonyDescriber.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/RouteDescriber/SymfonyDescriber.php b/RouteDescriber/SymfonyDescriber.php index 9ee0fbaaf..7cd6e60c5 100644 --- a/RouteDescriber/SymfonyDescriber.php +++ b/RouteDescriber/SymfonyDescriber.php @@ -27,8 +27,6 @@ public function describe(OA\OpenApi $api, Route $route, ReflectionMethod $reflec foreach ($this->getOperations($api, $route) as $operation) { foreach ($parameters as $parameter) { - $parameterName = $parameter->getName(); - if ($attribute = $this->getAttribute($parameter, MapRequestPayload::class)) { /** @var OA\RequestBody $requestBody */ $requestBody = Util::getChild($operation, OA\RequestBody::class); @@ -40,9 +38,11 @@ public function describe(OA\OpenApi $api, Route $route, ReflectionMethod $reflec $this->describeRequestBody($requestBody, $parameter, $format); } } - } elseif ($attribute = $this->getAttribute($parameter, MapQueryParameter::class)) { - $operationParameter = Util::getOperationParameter($operation, $parameterName, 'query'); - $operationParameter->name = $attribute->name ?? $parameterName; + } + + if ($attribute = $this->getAttribute($parameter, MapQueryParameter::class)) { + $operationParameter = Util::getOperationParameter($operation, $parameter->getName(), 'query'); + $operationParameter->name = $attribute->name ?? $parameter->getName(); $operationParameter->allowEmptyValue = $parameter->allowsNull(); $operationParameter->required = !$parameter->isDefaultValueAvailable() && !$parameter->allowsNull(); From de387271ae0fa12436eb52d48ab3d7f8e9e137f7 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 14 Jul 2023 15:13:08 +0200 Subject: [PATCH 023/120] Fix testMapRequestPayloadParamRegistersRequestBody for split up if statement --- Tests/RouteDescriber/SymfonyDescriberTest.php | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index fb5fa0e01..393aa5a7d 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -68,13 +68,23 @@ private function testMapRequestPayloadParamRegistersRequestBody( $controllerMethodMock = $this->createStub(\ReflectionMethod::class); - $reflectionAttributeMock = $this->createStub(ReflectionAttribute::class); - $reflectionAttributeMock->method('getName')->willReturn(MapRequestPayload::class); - $reflectionAttributeMock->method('newInstance')->willReturn($mapRequestPayload); + $reflectionAttributeStub = $this->createStub(ReflectionAttribute::class); + $reflectionAttributeStub->method('getName')->willReturn(MapRequestPayload::class); + $reflectionAttributeStub->method('newInstance')->willReturn($mapRequestPayload); - $reflectionParameterStub= $this->createStub(ReflectionParameter::class); + $reflectionParameterStub = $this->createMock(ReflectionParameter::class); $reflectionParameterStub->method('getType')->willReturn($reflectionNamedType); - $reflectionParameterStub->method('getAttributes')->willReturn([$reflectionAttributeMock]); + $reflectionParameterStub + ->expects(self::atLeastOnce()) + ->method('getAttributes') + ->willReturnCallback(static function (mixed $argument) use ($reflectionAttributeStub) { + if ($argument === MapRequestPayload::class) { + return [$reflectionAttributeStub]; + } + + return []; + }) + ; $controllerMethodMock->method('getParameters')->willReturn([$reflectionParameterStub]); From 322ef4a53010f38795b46bc8a6e3b5b4fb4affa6 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 14 Jul 2023 15:13:20 +0200 Subject: [PATCH 024/120] Add testMapQueryParameter --- Tests/RouteDescriber/SymfonyDescriberTest.php | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index 393aa5a7d..6d6dfb923 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -125,4 +125,127 @@ public static function provideMapRequestPayloadTestData(): Generator ['application/json', 'application/xml'], ]; } + + public function testMapQueryParameter(): void + { + foreach (self::provideMapQueryParameterTestData() as $testData) { + $this->testMapQueryParameterParamRegistersParameter(...$testData); + } + } + + /** + * @param array $mapQueryParameterDataCollection + */ + private function testMapQueryParameterParamRegistersParameter(array $mapQueryParameterDataCollection): void { + $api = new OpenApi([]); + + $reflectionParameters = []; + foreach ($mapQueryParameterDataCollection as $mapQueryParameterData) { + $reflectionNamedType = $this->createStub(ReflectionNamedType::class); + $reflectionNamedType->method('isBuiltin')->willReturn(true); + $reflectionNamedType->method('getName')->willReturn($mapQueryParameterData['type']); + + $reflectionAttributeStub = $this->createStub(ReflectionAttribute::class); + $reflectionAttributeStub->method('getName')->willReturn($mapQueryParameterData['name']); + $reflectionAttributeStub->method('newInstance')->willReturn($mapQueryParameterData['instance']); + + $reflectionParameterStub= $this->createStub(ReflectionParameter::class); + $reflectionParameterStub->method('getName')->willReturn($mapQueryParameterData['name']); + $reflectionParameterStub->method('getType')->willReturn($reflectionNamedType); + $reflectionParameterStub + ->expects(self::atLeastOnce()) + ->method('getAttributes') + ->willReturnCallback(static function (mixed $argument) use ($reflectionAttributeStub) { + if ($argument === MapQueryParameter::class) { + return [$reflectionAttributeStub]; + } + + return []; + }) + ; + + if (isset($mapQueryParameterData['defaultValue'])) { + $reflectionParameterStub->method('isDefaultValueAvailable')->willReturn(true); + $reflectionParameterStub->method('getDefaultValue')->willReturn($mapQueryParameterData['defaultValue']); + } else { + $reflectionParameterStub->method('isDefaultValueAvailable')->willReturn(false); + $reflectionParameterStub->method('getDefaultValue')->willThrowException(new \ReflectionException()); + } + + + $reflectionParameters[] = $reflectionParameterStub; + } + + $controllerMethodMock = $this->createStub(\ReflectionMethod::class); + $controllerMethodMock->method('getParameters')->willReturn($reflectionParameters); + + $this->symfonyDescriber->describe( + $api, + new Route('/'), + $controllerMethodMock + ); + + foreach ($mapQueryParameterDataCollection as $key => $mapQueryParameterData) { + $parameter = $api->paths[0]->get->parameters[$key]; + + self::assertSame($mapQueryParameterData['instance']->name ?? $mapQueryParameterData['name'], $parameter->name); + self::assertSame('query', $parameter->in); + self::assertSame(!$reflectionParameters[$key]->isDefaultValueAvailable() && !$reflectionParameters[$key]->allowsNull(), $parameter->required); + + $schema = $parameter->schema; + self::assertSame($mapQueryParameterData['type'], $schema->type); + if (isset($mapQueryParameterData['defaultValue'])) { + self::assertSame($mapQueryParameterData['defaultValue'], $schema->default); + } + } + } + + public static function provideMapQueryParameterTestData(): Generator + { + yield 'it sets a single query parameter' => [ + [ + [ + 'instance' => new MapQueryParameter(), + 'type' => 'int', + 'name' => 'parameter1', + ] + ], + ]; + + yield 'it sets two query parameters' => [ + [ + [ + 'instance' => new MapQueryParameter(), + 'type' => 'int', + 'name' => 'parameter1', + ], + [ + 'instance' => new MapQueryParameter(), + 'type' => 'int', + 'name' => 'parameter2', + ] + ], + ]; + + yield 'it sets a single query parameter with default value' => [ + [ + [ + 'instance' => new MapQueryParameter(), + 'type' => 'string', + 'name' => 'parameterDefault', + 'defaultValue' => 'Some default value', + ] + ], + ]; + + yield 'it uses MapQueryParameter manually defined name' => [ + [ + [ + 'instance' => new MapQueryParameter('name'), + 'type' => 'string', + 'name' => 'parameter', + ] + ], + ]; + } } From f2221bd4796663473a3fb2cfe1869d746bfc0c97 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 14 Jul 2023 15:15:12 +0200 Subject: [PATCH 025/120] Fix codestyle --- Tests/RouteDescriber/SymfonyDescriberTest.php | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index 6d6dfb923..eaee252a0 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -77,8 +77,8 @@ private function testMapRequestPayloadParamRegistersRequestBody( $reflectionParameterStub ->expects(self::atLeastOnce()) ->method('getAttributes') - ->willReturnCallback(static function (mixed $argument) use ($reflectionAttributeStub) { - if ($argument === MapRequestPayload::class) { + ->willReturnCallback(static function (string $argument) use ($reflectionAttributeStub) { + if (MapRequestPayload::class === $argument) { return [$reflectionAttributeStub]; } @@ -136,7 +136,8 @@ public function testMapQueryParameter(): void /** * @param array $mapQueryParameterDataCollection */ - private function testMapQueryParameterParamRegistersParameter(array $mapQueryParameterDataCollection): void { + private function testMapQueryParameterParamRegistersParameter(array $mapQueryParameterDataCollection): void + { $api = new OpenApi([]); $reflectionParameters = []; @@ -155,8 +156,8 @@ private function testMapQueryParameterParamRegistersParameter(array $mapQueryPar $reflectionParameterStub ->expects(self::atLeastOnce()) ->method('getAttributes') - ->willReturnCallback(static function (mixed $argument) use ($reflectionAttributeStub) { - if ($argument === MapQueryParameter::class) { + ->willReturnCallback(static function (string $argument) use ($reflectionAttributeStub) { + if (MapQueryParameter::class === $argument) { return [$reflectionAttributeStub]; } @@ -208,7 +209,7 @@ public static function provideMapQueryParameterTestData(): Generator 'instance' => new MapQueryParameter(), 'type' => 'int', 'name' => 'parameter1', - ] + ], ], ]; @@ -223,7 +224,7 @@ public static function provideMapQueryParameterTestData(): Generator 'instance' => new MapQueryParameter(), 'type' => 'int', 'name' => 'parameter2', - ] + ], ], ]; @@ -234,7 +235,7 @@ public static function provideMapQueryParameterTestData(): Generator 'type' => 'string', 'name' => 'parameterDefault', 'defaultValue' => 'Some default value', - ] + ], ], ]; @@ -244,7 +245,7 @@ public static function provideMapQueryParameterTestData(): Generator 'instance' => new MapQueryParameter('name'), 'type' => 'string', 'name' => 'parameter', - ] + ], ], ]; } From 00bf81036e30a3c6300d755e2c736caf3d80c3af Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 14 Jul 2023 15:15:43 +0200 Subject: [PATCH 026/120] Remove newline --- Tests/RouteDescriber/SymfonyDescriberTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index eaee252a0..3e0b6f13c 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -173,7 +173,6 @@ private function testMapQueryParameterParamRegistersParameter(array $mapQueryPar $reflectionParameterStub->method('getDefaultValue')->willThrowException(new \ReflectionException()); } - $reflectionParameters[] = $reflectionParameterStub; } From f987613a3dee45fa5cabca5054d1a79e572b78e3 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 14 Jul 2023 15:44:30 +0200 Subject: [PATCH 027/120] Expand docs for symfony controller mapping --- Resources/doc/index.rst | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Resources/doc/index.rst b/Resources/doc/index.rst index 3b21567c3..3a17df99e 100644 --- a/Resources/doc/index.rst +++ b/Resources/doc/index.rst @@ -7,7 +7,7 @@ OpenAPI (Swagger) format and provides a sandbox to interactively experiment with What's supported? ----------------- -This bundle supports *Symfony* route requirements, PHP annotations, `Swagger-Php`_ annotations, +This bundle supports *Symfony* route requirements, *Symfony* request mapping (`Symfony MapQueryParameter`_, `Symfony MapRequestPayload`_), PHP annotations, `Swagger-Php`_ annotations, `FOSRestBundle`_ annotations and applications using `Api-Platform`_. .. _`Swagger-Php`: https://github.com/zircote/swagger-php @@ -239,6 +239,17 @@ The normal PHPDoc block on the controller method is used for the summary and des However, unlike in those examples, when using this bundle you don't need to specify paths and you can easily document models as well as some other properties described below as they can be automatically be documented using the Symfony integration. +.. tip:: + + **NelmioApiDocBundle** understand **symfony's** `Symfony MapQueryParameter`_ & `Symfony MapRequestPayload`_. + Using these attributes inside your controller allows this bundle to automatically create the necessary documentation, + these will automatically be mapped to their respective `OA\Parameter` & `OA\RequestBody`. + +.. versionadded:: 6.3 + + The :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapRequestPayload` and :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapQueryParameter` attributes + were introduced in Symfony 6.3. + Use Models ---------- @@ -584,3 +595,5 @@ If you need more complex features, take a look at: .. _`JMS serializer`: https://jmsyst.com/libs/serializer .. _`Symfony form`: https://symfony.com/doc/current/forms.html .. _`Symfony serializer`: https://symfony.com/doc/current/components/serializer.html +.. _`Symfony MapRequestPayload`: https://symfony.com/doc/current/controller.html#mapping-request-payload +.. _`Symfony MapQueryParameter`: https://symfony.com/doc/current/controller.html#mapping-query-parameters-individually From 1acd5dce7ec2cfab0ae401d773a4f7b32f67df35 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 14 Jul 2023 15:48:37 +0200 Subject: [PATCH 028/120] Add backticks --- Resources/doc/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/doc/index.rst b/Resources/doc/index.rst index 3a17df99e..d6c7208d8 100644 --- a/Resources/doc/index.rst +++ b/Resources/doc/index.rst @@ -243,7 +243,7 @@ The normal PHPDoc block on the controller method is used for the summary and des **NelmioApiDocBundle** understand **symfony's** `Symfony MapQueryParameter`_ & `Symfony MapRequestPayload`_. Using these attributes inside your controller allows this bundle to automatically create the necessary documentation, - these will automatically be mapped to their respective `OA\Parameter` & `OA\RequestBody`. + these will automatically be mapped to their respective ``OA\Parameter`` & ``OA\RequestBody`` attribute. .. versionadded:: 6.3 From 0b2c6278453b32c8f2110d438bd56272297062b1 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 14 Jul 2023 15:50:13 +0200 Subject: [PATCH 029/120] Clarify docs --- Resources/doc/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/doc/index.rst b/Resources/doc/index.rst index d6c7208d8..3dde6d218 100644 --- a/Resources/doc/index.rst +++ b/Resources/doc/index.rst @@ -243,7 +243,7 @@ The normal PHPDoc block on the controller method is used for the summary and des **NelmioApiDocBundle** understand **symfony's** `Symfony MapQueryParameter`_ & `Symfony MapRequestPayload`_. Using these attributes inside your controller allows this bundle to automatically create the necessary documentation, - these will automatically be mapped to their respective ``OA\Parameter`` & ``OA\RequestBody`` attribute. + these will automatically be mapped to their respective ``OA\Parameter`` & ``OA\RequestBody`` annotation/attribute. .. versionadded:: 6.3 From 123c611a3d47e0300ad0f7b9e8af273f77eab136 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 14 Jul 2023 15:52:40 +0200 Subject: [PATCH 030/120] Upgrade major_version to 6 --- phpunit.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0ddb09e34..92651ca2f 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -19,7 +19,7 @@ - Tests + Tests/RouteDescriber From 15ce29bc839f02689af339fd464df7ddd80312a0 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 14 Jul 2023 15:55:01 +0200 Subject: [PATCH 031/120] Upgrade versionadded_directive_min_version to 6.0 --- .doctor-rst.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.doctor-rst.yaml b/.doctor-rst.yaml index b22ff8c10..ae39fbe61 100644 --- a/.doctor-rst.yaml +++ b/.doctor-rst.yaml @@ -53,10 +53,10 @@ rules: # master versionadded_directive_major_version: - major_version: 5 + major_version: 6 versionadded_directive_min_version: - min_version: '5.0' + min_version: '6.0' deprecated_directive_major_version: major_version: 5 @@ -71,4 +71,4 @@ whitelist: lines: - '.. code-block:: twig' - '// bin/console' - - '.. code-block:: php' \ No newline at end of file + - '.. code-block:: php' From c75b5396007bb46a24f43c30aaf44805d13f0ba8 Mon Sep 17 00:00:00 2001 From: Djordy Koert Date: Fri, 14 Jul 2023 15:57:27 +0200 Subject: [PATCH 032/120] Revert max allowed self deprecations --- phpunit.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 92651ca2f..ca94e54da 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -14,7 +14,7 @@ - + From 476cf225cbb55c3326f9dd223ebce55c60de0807 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 14 Jul 2023 16:00:23 +0200 Subject: [PATCH 033/120] Revert phpunit.xml.dist changes --- phpunit.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 92651ca2f..0ddb09e34 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -19,7 +19,7 @@ - Tests/RouteDescriber + Tests From 44a43128b9a2eb2845d328687ee7cc82329bc5f4 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 14 Jul 2023 16:21:05 +0200 Subject: [PATCH 034/120] Revert "Revert max allowed self deprecations" This reverts commit c75b5396007bb46a24f43c30aaf44805d13f0ba8. --- phpunit.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 695969160..0ddb09e34 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -14,7 +14,7 @@ - + From b537b5b5e48d3599aa85ec8b7c5de5ba57c43dc3 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 14 Jul 2023 19:47:25 +0200 Subject: [PATCH 035/120] Remove not working generator bypass and replace with iterable --- Tests/RouteDescriber/SymfonyDescriberTest.php | 26 +++++-------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index 3e0b6f13c..1327e7400 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -11,7 +11,6 @@ namespace Nelmio\ApiDocBundle\Tests\RouteDescriber; -use Generator; use Nelmio\ApiDocBundle\RouteDescriber\SymfonyDescriber; use OpenApi\Annotations\OpenApi; use PHPUnit\Framework\TestCase; @@ -45,17 +44,10 @@ protected function setUp(): void $this->symfonyDescriber = new SymfonyDescriber(); } - public function testMapRequestPayload(): void - { - foreach (self::provideMapRequestPayloadTestData() as $testData) { - $this->testMapRequestPayloadParamRegistersRequestBody(...$testData); - } - } - /** - * @param string[] $expectedMediaTypes + * @dataProvider provideMapRequestPayloadTestData */ - private function testMapRequestPayloadParamRegistersRequestBody( + public function testMapRequestPayloadParamRegistersRequestBody( MapRequestPayload $mapRequestPayload, array $expectedMediaTypes ): void { @@ -103,7 +95,7 @@ private function testMapRequestPayloadParamRegistersRequestBody( } } - public static function provideMapRequestPayloadTestData(): Generator + public static function provideMapRequestPayloadTestData(): iterable { yield 'it sets default mediaType to json' => [ new MapRequestPayload(), @@ -126,17 +118,11 @@ public static function provideMapRequestPayloadTestData(): Generator ]; } - public function testMapQueryParameter(): void - { - foreach (self::provideMapQueryParameterTestData() as $testData) { - $this->testMapQueryParameterParamRegistersParameter(...$testData); - } - } - /** + * @dataProvider provideMapQueryParameterTestData * @param array $mapQueryParameterDataCollection */ - private function testMapQueryParameterParamRegistersParameter(array $mapQueryParameterDataCollection): void + public function testMapQueryParameterParamRegistersParameter(array $mapQueryParameterDataCollection): void { $api = new OpenApi([]); @@ -200,7 +186,7 @@ private function testMapQueryParameterParamRegistersParameter(array $mapQueryPar } } - public static function provideMapQueryParameterTestData(): Generator + public static function provideMapQueryParameterTestData(): iterable { yield 'it sets a single query parameter' => [ [ From 82e97055fb6f35a477f29915081dcfc2ca0797fe Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 14 Jul 2023 20:11:54 +0200 Subject: [PATCH 036/120] Update testMapQueryParameter to work with 'controller' classes --- Tests/RouteDescriber/SymfonyDescriberTest.php | 147 +++++++----------- 1 file changed, 52 insertions(+), 95 deletions(-) diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index 1327e7400..897bfa5a0 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -120,118 +120,75 @@ public static function provideMapRequestPayloadTestData(): iterable /** * @dataProvider provideMapQueryParameterTestData - * @param array $mapQueryParameterDataCollection */ - public function testMapQueryParameterParamRegistersParameter(array $mapQueryParameterDataCollection): void + public function testMapQueryParameter(object $controllerClass): void { $api = new OpenApi([]); - $reflectionParameters = []; - foreach ($mapQueryParameterDataCollection as $mapQueryParameterData) { - $reflectionNamedType = $this->createStub(ReflectionNamedType::class); - $reflectionNamedType->method('isBuiltin')->willReturn(true); - $reflectionNamedType->method('getName')->willReturn($mapQueryParameterData['type']); - - $reflectionAttributeStub = $this->createStub(ReflectionAttribute::class); - $reflectionAttributeStub->method('getName')->willReturn($mapQueryParameterData['name']); - $reflectionAttributeStub->method('newInstance')->willReturn($mapQueryParameterData['instance']); - - $reflectionParameterStub= $this->createStub(ReflectionParameter::class); - $reflectionParameterStub->method('getName')->willReturn($mapQueryParameterData['name']); - $reflectionParameterStub->method('getType')->willReturn($reflectionNamedType); - $reflectionParameterStub - ->expects(self::atLeastOnce()) - ->method('getAttributes') - ->willReturnCallback(static function (string $argument) use ($reflectionAttributeStub) { - if (MapQueryParameter::class === $argument) { - return [$reflectionAttributeStub]; - } - - return []; - }) - ; - - if (isset($mapQueryParameterData['defaultValue'])) { - $reflectionParameterStub->method('isDefaultValueAvailable')->willReturn(true); - $reflectionParameterStub->method('getDefaultValue')->willReturn($mapQueryParameterData['defaultValue']); - } else { - $reflectionParameterStub->method('isDefaultValueAvailable')->willReturn(false); - $reflectionParameterStub->method('getDefaultValue')->willThrowException(new \ReflectionException()); - } - - $reflectionParameters[] = $reflectionParameterStub; - } - - $controllerMethodMock = $this->createStub(\ReflectionMethod::class); - $controllerMethodMock->method('getParameters')->willReturn($reflectionParameters); + $controllerReflectionMethod = new \ReflectionMethod($controllerClass, 'route'); $this->symfonyDescriber->describe( $api, new Route('/'), - $controllerMethodMock + $controllerReflectionMethod ); - foreach ($mapQueryParameterDataCollection as $key => $mapQueryParameterData) { - $parameter = $api->paths[0]->get->parameters[$key]; + foreach ($controllerReflectionMethod->getParameters() as $key => $parameter) { + /** @var MapQueryParameter $mapQueryParameter */ + $mapQueryParameter = $parameter->getAttributes(MapQueryParameter::class, ReflectionAttribute::IS_INSTANCEOF)[0]->newInstance(); - self::assertSame($mapQueryParameterData['instance']->name ?? $mapQueryParameterData['name'], $parameter->name); - self::assertSame('query', $parameter->in); - self::assertSame(!$reflectionParameters[$key]->isDefaultValueAvailable() && !$reflectionParameters[$key]->allowsNull(), $parameter->required); + $documentationParameter = $api->paths[0]->get->parameters[$key]; + self::assertSame($mapQueryParameter->name ?? $parameter->getName(), $documentationParameter->name); + self::assertSame('query', $documentationParameter->in); + self::assertSame(!$parameter->isDefaultValueAvailable() && !$parameter->allowsNull(), $documentationParameter->required); + + $schema = $documentationParameter->schema; + self::assertSame($parameter->getType()->getName(), $schema->type); + if ($parameter->isDefaultValueAvailable()) { + self::assertSame($parameter->getDefaultValue(), $schema->default); + } - $schema = $parameter->schema; - self::assertSame($mapQueryParameterData['type'], $schema->type); - if (isset($mapQueryParameterData['defaultValue'])) { - self::assertSame($mapQueryParameterData['defaultValue'], $schema->default); + if ($mapQueryParameter->filter === FILTER_VALIDATE_REGEXP) { + self::assertSame($mapQueryParameter->options['regexp'], $schema->pattern); } } } public static function provideMapQueryParameterTestData(): iterable { - yield 'it sets a single query parameter' => [ - [ - [ - 'instance' => new MapQueryParameter(), - 'type' => 'int', - 'name' => 'parameter1', - ], - ], - ]; - - yield 'it sets two query parameters' => [ - [ - [ - 'instance' => new MapQueryParameter(), - 'type' => 'int', - 'name' => 'parameter1', - ], - [ - 'instance' => new MapQueryParameter(), - 'type' => 'int', - 'name' => 'parameter2', - ], - ], - ]; - - yield 'it sets a single query parameter with default value' => [ - [ - [ - 'instance' => new MapQueryParameter(), - 'type' => 'string', - 'name' => 'parameterDefault', - 'defaultValue' => 'Some default value', - ], - ], - ]; - - yield 'it uses MapQueryParameter manually defined name' => [ - [ - [ - 'instance' => new MapQueryParameter('name'), - 'type' => 'string', - 'name' => 'parameter', - ], - ], - ]; + yield 'it documents query parameters' => [new class() { + public function route( + #[MapQueryParameter] int $parameter1, + #[MapQueryParameter] int $parameter2 + ) { } + }]; + + yield 'it documents query parameters with default values' => [new class() { + public function route( + #[MapQueryParameter] int $parameter1 = 123, + #[MapQueryParameter] int $parameter2 = 456 + ) { } + }]; + + yield 'it documents query parameters with nullable types' => [new class() { + public function route( + #[MapQueryParameter] ?int $parameter1, + #[MapQueryParameter] ?int $parameter2 + ) { } + }]; + + yield 'it uses MapQueryParameter name argument as name' => [new class() { + public function route( + #[MapQueryParameter('someOtherParameter1Name')] int $parameter1, + #[MapQueryParameter('someOtherParameter2Name')] int $parameter2 + ) { } + }]; + + yield 'it uses documents regex pattern' => [new class() { + public function route( + #[MapQueryParameter(filter: FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^\d+$/'])] int $parameter1, + #[MapQueryParameter(filter: FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^\d+$/'])] int $parameter2 + ) { } + }]; } } From 8528dff54a3e430e9353acb56ceb46c8e1911265 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 14 Jul 2023 20:17:55 +0200 Subject: [PATCH 037/120] Update testMapRequestPayload to work with 'controller' classes --- Tests/RouteDescriber/SymfonyDescriberTest.php | 68 ++++++++----------- 1 file changed, 28 insertions(+), 40 deletions(-) diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index 897bfa5a0..ffb057b16 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -15,8 +15,6 @@ use OpenApi\Annotations\OpenApi; use PHPUnit\Framework\TestCase; use ReflectionAttribute; -use ReflectionNamedType; -use ReflectionParameter; use stdClass; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Attribute\MapQueryString; @@ -46,44 +44,18 @@ protected function setUp(): void /** * @dataProvider provideMapRequestPayloadTestData + * + * @param string[] $expectedMediaTypes */ - public function testMapRequestPayloadParamRegistersRequestBody( - MapRequestPayload $mapRequestPayload, - array $expectedMediaTypes - ): void { - $classType = stdClass::class; - - $reflectionNamedType = $this->createStub(ReflectionNamedType::class); - $reflectionNamedType->method('getName')->willReturn($classType); - + public function testMapRequestPayload(object $controllerClass, array $expectedMediaTypes): void { $api = new OpenApi([]); - $controllerMethodMock = $this->createStub(\ReflectionMethod::class); - - $reflectionAttributeStub = $this->createStub(ReflectionAttribute::class); - $reflectionAttributeStub->method('getName')->willReturn(MapRequestPayload::class); - $reflectionAttributeStub->method('newInstance')->willReturn($mapRequestPayload); - - $reflectionParameterStub = $this->createMock(ReflectionParameter::class); - $reflectionParameterStub->method('getType')->willReturn($reflectionNamedType); - $reflectionParameterStub - ->expects(self::atLeastOnce()) - ->method('getAttributes') - ->willReturnCallback(static function (string $argument) use ($reflectionAttributeStub) { - if (MapRequestPayload::class === $argument) { - return [$reflectionAttributeStub]; - } - - return []; - }) - ; - - $controllerMethodMock->method('getParameters')->willReturn([$reflectionParameterStub]); + $controllerReflectionMethod = new \ReflectionMethod($controllerClass, 'route'); $this->symfonyDescriber->describe( $api, new Route('/'), - $controllerMethodMock + $controllerReflectionMethod ); foreach ($expectedMediaTypes as $expectedMediaType) { @@ -91,29 +63,45 @@ public function testMapRequestPayloadParamRegistersRequestBody( self::assertSame($expectedMediaType, $requestBodyContent->mediaType); self::assertSame('object', $requestBodyContent->schema->type); - self::assertSame($classType, $requestBodyContent->schema->ref->type); + self::assertSame(stdClass::class, $requestBodyContent->schema->ref->type); } } public static function provideMapRequestPayloadTestData(): iterable { yield 'it sets default mediaType to json' => [ - new MapRequestPayload(), + new class() { + public function route( + #[MapRequestPayload] stdClass $payload + ) { } + }, ['application/json'], ]; - yield 'it sets the mediaType to json' => [ - new MapRequestPayload('json'), + yield 'it sets mediaType to json' => [ + new class() { + public function route( + #[MapRequestPayload('json')] stdClass $payload + ) { } + }, ['application/json'], ]; - yield 'it sets the mediaType to xml' => [ - new MapRequestPayload('xml'), + yield 'it sets mediaType to xml' => [ + new class() { + public function route( + #[MapRequestPayload('xml')] stdClass $payload + ) { } + }, ['application/xml'], ]; yield 'it sets multiple mediaTypes' => [ - new MapRequestPayload(['json', 'xml']), + new class() { + public function route( + #[MapRequestPayload(['json', 'xml'])] stdClass $payload + ) { } + }, ['application/json', 'application/xml'], ]; } From bf233a89d7d04e86fe998e228a7f3153f324ee9c Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 14 Jul 2023 20:19:12 +0200 Subject: [PATCH 038/120] Remove check for MapQueryString existence --- DependencyInjection/NelmioApiDocExtension.php | 2 -- Tests/RouteDescriber/SymfonyDescriberTest.php | 2 -- 2 files changed, 4 deletions(-) diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index 284960b66..8f36aa1e0 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -31,7 +31,6 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; -use Symfony\Component\HttpKernel\Attribute\MapQueryString; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Routing\RouteCollection; @@ -166,7 +165,6 @@ public function load(array $configs, ContainerBuilder $container): void PHP_VERSION_ID > 80100 && class_exists(MapRequestPayload::class) && class_exists(MapQueryParameter::class) - && class_exists(MapQueryString::class) ) { $loader->load('symfony.xml'); } diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index ffb057b16..a152e7ef6 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -17,7 +17,6 @@ use ReflectionAttribute; use stdClass; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; -use Symfony\Component\HttpKernel\Attribute\MapQueryString; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\Routing\Route; @@ -34,7 +33,6 @@ protected function setUp(): void if ( !class_exists(MapRequestPayload::class) && !class_exists(MapQueryParameter::class) - && !class_exists(MapQueryString::class) ) { self::markTestSkipped('Symfony 6.3 attributes not found'); } From 6cc79e3c4647a83ba2ae0d07314a6fd163c0e1e2 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 14 Jul 2023 20:20:36 +0200 Subject: [PATCH 039/120] Fix codestyle --- Tests/RouteDescriber/SymfonyDescriberTest.php | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index a152e7ef6..6d790f895 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -45,7 +45,8 @@ protected function setUp(): void * * @param string[] $expectedMediaTypes */ - public function testMapRequestPayload(object $controllerClass, array $expectedMediaTypes): void { + public function testMapRequestPayload(object $controllerClass, array $expectedMediaTypes): void + { $api = new OpenApi([]); $controllerReflectionMethod = new \ReflectionMethod($controllerClass, 'route'); @@ -71,7 +72,8 @@ public static function provideMapRequestPayloadTestData(): iterable new class() { public function route( #[MapRequestPayload] stdClass $payload - ) { } + ) { + } }, ['application/json'], ]; @@ -80,7 +82,8 @@ public function route( new class() { public function route( #[MapRequestPayload('json')] stdClass $payload - ) { } + ) { + } }, ['application/json'], ]; @@ -89,7 +92,8 @@ public function route( new class() { public function route( #[MapRequestPayload('xml')] stdClass $payload - ) { } + ) { + } }, ['application/xml'], ]; @@ -98,7 +102,8 @@ public function route( new class() { public function route( #[MapRequestPayload(['json', 'xml'])] stdClass $payload - ) { } + ) { + } }, ['application/json', 'application/xml'], ]; @@ -146,35 +151,40 @@ public static function provideMapQueryParameterTestData(): iterable public function route( #[MapQueryParameter] int $parameter1, #[MapQueryParameter] int $parameter2 - ) { } + ) { + } }]; yield 'it documents query parameters with default values' => [new class() { public function route( #[MapQueryParameter] int $parameter1 = 123, #[MapQueryParameter] int $parameter2 = 456 - ) { } + ) { + } }]; yield 'it documents query parameters with nullable types' => [new class() { public function route( #[MapQueryParameter] ?int $parameter1, #[MapQueryParameter] ?int $parameter2 - ) { } + ) { + } }]; yield 'it uses MapQueryParameter name argument as name' => [new class() { public function route( #[MapQueryParameter('someOtherParameter1Name')] int $parameter1, #[MapQueryParameter('someOtherParameter2Name')] int $parameter2 - ) { } + ) { + } }]; yield 'it uses documents regex pattern' => [new class() { public function route( #[MapQueryParameter(filter: FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^\d+$/'])] int $parameter1, #[MapQueryParameter(filter: FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^\d+$/'])] int $parameter2 - ) { } + ) { + } }]; } } From d9328179602ff3c0d4deae5e889d51d5070b4d21 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 14 Jul 2023 20:21:26 +0200 Subject: [PATCH 040/120] Swap comparison order --- Tests/RouteDescriber/SymfonyDescriberTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index 6d790f895..afe6ab3bd 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -139,7 +139,7 @@ public function testMapQueryParameter(object $controllerClass): void self::assertSame($parameter->getDefaultValue(), $schema->default); } - if ($mapQueryParameter->filter === FILTER_VALIDATE_REGEXP) { + if (FILTER_VALIDATE_REGEXP === $mapQueryParameter->filter) { self::assertSame($mapQueryParameter->options['regexp'], $schema->pattern); } } From 9370281664fac20a120fa84e1f2b31ae0127e1d2 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 11 Aug 2023 16:08:18 +0200 Subject: [PATCH 041/120] initial MapQueryString setup --- DependencyInjection/NelmioApiDocExtension.php | 2 + RouteDescriber/SymfonyDescriber.php | 38 ++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index 8f36aa1e0..284960b66 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -31,6 +31,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; +use Symfony\Component\HttpKernel\Attribute\MapQueryString; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Routing\RouteCollection; @@ -165,6 +166,7 @@ public function load(array $configs, ContainerBuilder $container): void PHP_VERSION_ID > 80100 && class_exists(MapRequestPayload::class) && class_exists(MapQueryParameter::class) + && class_exists(MapQueryString::class) ) { $loader->load('symfony.xml'); } diff --git a/RouteDescriber/SymfonyDescriber.php b/RouteDescriber/SymfonyDescriber.php index 7cd6e60c5..7f8091ac7 100644 --- a/RouteDescriber/SymfonyDescriber.php +++ b/RouteDescriber/SymfonyDescriber.php @@ -6,24 +6,30 @@ use InvalidArgumentException; use Nelmio\ApiDocBundle\Annotation\Model; +use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; +use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; +use Nelmio\ApiDocBundle\Model\ModelRegistry; use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; use OpenApi\Generator; use ReflectionMethod; use ReflectionParameter; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; +use Symfony\Component\HttpKernel\Attribute\MapQueryString; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; +use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Routing\Route; use function is_array; use const FILTER_VALIDATE_REGEXP; -final class SymfonyDescriber implements RouteDescriberInterface +final class SymfonyDescriber implements RouteDescriberInterface, ModelRegistryAwareInterface { use RouteDescriberTrait; + use ModelRegistryAwareTrait; public function describe(OA\OpenApi $api, Route $route, ReflectionMethod $reflectionMethod): void { - $parameters = $this->getMethodParameter($reflectionMethod, [MapRequestPayload::class, MapQueryParameter::class]); + $parameters = $this->getMethodParameter($reflectionMethod, [MapRequestPayload::class, MapQueryParameter::class, MapQueryString::class]); foreach ($this->getOperations($api, $route) as $operation) { foreach ($parameters as $parameter) { @@ -56,6 +62,34 @@ public function describe(OA\OpenApi $api, Route $route, ReflectionMethod $reflec $this->describeCommonSchemaFromParameter($schema, $parameter); } + + if ($this->getAttribute($parameter, MapQueryString::class)) { + $model = new \Nelmio\ApiDocBundle\Model\Model(new Type(Type::BUILTIN_TYPE_OBJECT, $parameter->allowsNull(), $parameter->getType()->getName())); + $modelRef = $this->modelRegistry->register($model); + $this->modelRegistry->registerSchemas(); + + $nativeModelName = str_replace(OA\Components::SCHEMA_REF, '', $modelRef); + + $schemaModel = Util::getSchema($api, $nativeModelName); + if (Generator::UNDEFINED === $schemaModel->properties) { + continue; + } + + $isModelOptional = $parameter->isDefaultValueAvailable() || $parameter->allowsNull(); + + foreach ($schemaModel->properties as $property) { + $operationParameter = Util::getOperationParameter($operation, $property->property, 'query'); + $operationParameter->name = $property->property; + + $isQueryOptional = (Generator::UNDEFINED !== $property->nullable &&$property->nullable) + || Generator::UNDEFINED !== $property->default + || $isModelOptional; + + $operationParameter->allowEmptyValue = $isQueryOptional; + $operationParameter->required = !$isQueryOptional; + $operationParameter->example = $property->default; + } + } } } } From 807779e0dde3c2bfefefbeb0db11bcc0f72a600e Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 11 Aug 2023 16:43:11 +0200 Subject: [PATCH 042/120] Move annotation describe methods to their own SymfonyAnnotationDescriber classes --- .../SymfonyAnnotationDescriber.php | 15 ++ .../SymfonyAnnotationHelper.php | 41 +++++ .../SymfonyMapQueryParameterDescriber.php | 47 ++++++ .../SymfonyMapQueryStringDescriber.php | 59 +++++++ .../SymfonyMapRequestPayloadDescriber.php | 95 +++++++++++ RouteDescriber/SymfonyDescriber.php | 156 ++---------------- .../SymfonyDescriberMapQueryStringClass.php | 29 ++++ .../SymfonyMapQueryParameterDescriberTest.php | 98 +++++++++++ 8 files changed, 399 insertions(+), 141 deletions(-) create mode 100644 RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationDescriber.php create mode 100644 RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationHelper.php create mode 100644 RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php create mode 100644 RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php create mode 100644 RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php create mode 100644 Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php create mode 100644 Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationDescriber.php new file mode 100644 index 000000000..03fbf257b --- /dev/null +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationDescriber.php @@ -0,0 +1,15 @@ + $attribute + * + * @return T|null + * + * @template T of object + */ + public static function getAttribute(ReflectionParameter $parameter, string $attribute): ?object + { + if ($attribute = $parameter->getAttributes($attribute, \ReflectionAttribute::IS_INSTANCEOF)) { + return $attribute[0]->newInstance(); + } + + return null; + } + + public static function describeCommonSchemaFromParameter(OA\Schema $schema, ReflectionParameter $parameter): void + { + if ($parameter->isDefaultValueAvailable()) { + $schema->default = $parameter->getDefaultValue(); + } + + if (Generator::UNDEFINED === $schema->type) { + if ($parameter->getType()->isBuiltin()) { + $schema->type = $parameter->getType()->getName(); + } + } + } +} diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php new file mode 100644 index 000000000..f8b35387a --- /dev/null +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php @@ -0,0 +1,47 @@ +hasType(); + } + + public function describe(OA\OpenApi $api, Operation $operation, ReflectionParameter $parameter): void + { + $attribute = SymfonyAnnotationHelper::getAttribute($parameter, MapQueryParameter::class); + + $operationParameter = Util::getOperationParameter($operation, $parameter->getName(), 'query'); + $operationParameter->name = $attribute->name ?? $parameter->getName(); + $operationParameter->allowEmptyValue = $parameter->allowsNull(); + + $operationParameter->required = !$parameter->isDefaultValueAvailable() && !$parameter->allowsNull(); + + /** @var OA\Schema $schema */ + $schema = Util::getChild($operationParameter, OA\Schema::class); + + if (FILTER_VALIDATE_REGEXP === $attribute->filter) { + $schema->pattern = $attribute->options['regexp']; + } + + SymfonyAnnotationHelper::describeCommonSchemaFromParameter($schema, $parameter); + } +} diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php new file mode 100644 index 000000000..a7da935e0 --- /dev/null +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php @@ -0,0 +1,59 @@ +hasType(); + } + + public function describe(OA\OpenApi $api, Operation $operation, ReflectionParameter $parameter): void + { + $model = new Model(new Type(Type::BUILTIN_TYPE_OBJECT, $parameter->allowsNull(), $parameter->getType()->getName())); + $modelRef = $this->modelRegistry->register($model); + $this->modelRegistry->registerSchemas(); + + $nativeModelName = str_replace(OA\Components::SCHEMA_REF, '', $modelRef); + + $schemaModel = Util::getSchema($api, $nativeModelName); + if (Generator::UNDEFINED === $schemaModel->properties) { + return; + } + + $isModelOptional = $parameter->isDefaultValueAvailable() || $parameter->allowsNull(); + + foreach ($schemaModel->properties as $property) { + $operationParameter = Util::getOperationParameter($operation, $property->property, 'query'); + $operationParameter->name = $property->property; + + $isQueryOptional = (Generator::UNDEFINED !== $property->nullable &&$property->nullable) + || Generator::UNDEFINED !== $property->default + || $isModelOptional; + + $operationParameter->allowEmptyValue = $isQueryOptional; + $operationParameter->required = !$isQueryOptional; + $operationParameter->example = $property->default; + } + } +} diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php new file mode 100644 index 000000000..5d94edf9f --- /dev/null +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php @@ -0,0 +1,95 @@ +hasType(); + } + + public function describe(OA\OpenApi $api, Operation $operation, ReflectionParameter $parameter): void + { + $attribute = SymfonyAnnotationHelper::getAttribute($parameter, MapRequestPayload::class); + + /** @var OA\RequestBody $requestBody */ + $requestBody = Util::getChild($operation, OA\RequestBody::class); + + if (!is_array($attribute->acceptFormat)) { + $this->describeRequestBody($requestBody, $parameter, $attribute->acceptFormat ?? 'json'); + } else { + foreach ($attribute->acceptFormat as $format) { + $this->describeRequestBody($requestBody, $parameter, $format); + } + } + } + + private function describeRequestBody(OA\RequestBody $requestBody, ReflectionParameter $parameter, string $format): void + { + $contentSchema = $this->getContentSchemaForType($requestBody, $format); + $contentSchema->ref = new \Nelmio\ApiDocBundle\Annotation\Model(type: $parameter->getType()->getName()); + $contentSchema->type = 'object'; + + $schema = Util::getProperty($contentSchema, $parameter->getName()); + + SymfonyAnnotationHelper::describeCommonSchemaFromParameter($schema, $parameter); + } + + private function getContentSchemaForType(OA\RequestBody $requestBody, string $type): OA\Schema + { + $requestBody->content = Generator::UNDEFINED !== $requestBody->content ? $requestBody->content : []; + switch ($type) { + case 'json': + $contentType = 'application/json'; + + break; + case 'xml': + $contentType = 'application/xml'; + + break; + default: + throw new InvalidArgumentException('Unsupported media type'); + } + + if (!isset($requestBody->content[$contentType])) { + $weakContext = Util::createWeakContext($requestBody->_context); + $requestBody->content[$contentType] = new OA\MediaType( + [ + 'mediaType' => $contentType, + '_context' => $weakContext, + ] + ); + + /** @var OA\Schema $schema */ + $schema = Util::getChild( + $requestBody->content[$contentType], + OA\Schema::class + ); + $schema->type = 'object'; + } + + return Util::getChild( + $requestBody->content[$contentType], + OA\Schema::class + ); + } +} diff --git a/RouteDescriber/SymfonyDescriber.php b/RouteDescriber/SymfonyDescriber.php index 7f8091ac7..6ca1c0472 100644 --- a/RouteDescriber/SymfonyDescriber.php +++ b/RouteDescriber/SymfonyDescriber.php @@ -10,6 +10,7 @@ use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; use Nelmio\ApiDocBundle\Model\ModelRegistry; use Nelmio\ApiDocBundle\OpenApiPhp\Util; +use Nelmio\ApiDocBundle\RouteDescriber\SymfonyAnnotationDescriber\SymfonyAnnotationDescriber; use OpenApi\Annotations as OA; use OpenApi\Generator; use ReflectionMethod; @@ -27,169 +28,42 @@ final class SymfonyDescriber implements RouteDescriberInterface, ModelRegistryAw use RouteDescriberTrait; use ModelRegistryAwareTrait; + /** + * @param SymfonyAnnotationDescriber[] $annotationDescribers + */ + public function __construct( + private iterable $annotationDescribers = [], + ) { + } + public function describe(OA\OpenApi $api, Route $route, ReflectionMethod $reflectionMethod): void { - $parameters = $this->getMethodParameter($reflectionMethod, [MapRequestPayload::class, MapQueryParameter::class, MapQueryString::class]); + $parameters = $this->getMethodParameter($reflectionMethod); foreach ($this->getOperations($api, $route) as $operation) { foreach ($parameters as $parameter) { - if ($attribute = $this->getAttribute($parameter, MapRequestPayload::class)) { - /** @var OA\RequestBody $requestBody */ - $requestBody = Util::getChild($operation, OA\RequestBody::class); - - if (!is_array($attribute->acceptFormat)) { - $this->describeRequestBody($requestBody, $parameter, $attribute->acceptFormat ?? 'json'); - } else { - foreach ($attribute->acceptFormat as $format) { - $this->describeRequestBody($requestBody, $parameter, $format); - } - } - } - - if ($attribute = $this->getAttribute($parameter, MapQueryParameter::class)) { - $operationParameter = Util::getOperationParameter($operation, $parameter->getName(), 'query'); - $operationParameter->name = $attribute->name ?? $parameter->getName(); - $operationParameter->allowEmptyValue = $parameter->allowsNull(); - - $operationParameter->required = !$parameter->isDefaultValueAvailable() && !$parameter->allowsNull(); - - /** @var OA\Schema $schema */ - $schema = Util::getChild($operationParameter, OA\Schema::class); - - if (FILTER_VALIDATE_REGEXP === $attribute->filter) { - $schema->pattern = $attribute->options['regexp']; - } - - $this->describeCommonSchemaFromParameter($schema, $parameter); - } - - if ($this->getAttribute($parameter, MapQueryString::class)) { - $model = new \Nelmio\ApiDocBundle\Model\Model(new Type(Type::BUILTIN_TYPE_OBJECT, $parameter->allowsNull(), $parameter->getType()->getName())); - $modelRef = $this->modelRegistry->register($model); - $this->modelRegistry->registerSchemas(); - - $nativeModelName = str_replace(OA\Components::SCHEMA_REF, '', $modelRef); - - $schemaModel = Util::getSchema($api, $nativeModelName); - if (Generator::UNDEFINED === $schemaModel->properties) { + foreach ($this->annotationDescribers as $annotationDescriber) { + if (! $annotationDescriber->supports($parameter)) { continue; } - $isModelOptional = $parameter->isDefaultValueAvailable() || $parameter->allowsNull(); - - foreach ($schemaModel->properties as $property) { - $operationParameter = Util::getOperationParameter($operation, $property->property, 'query'); - $operationParameter->name = $property->property; - - $isQueryOptional = (Generator::UNDEFINED !== $property->nullable &&$property->nullable) - || Generator::UNDEFINED !== $property->default - || $isModelOptional; - - $operationParameter->allowEmptyValue = $isQueryOptional; - $operationParameter->required = !$isQueryOptional; - $operationParameter->example = $property->default; - } + $annotationDescriber->describe($api, $operation, $parameter); } } } } /** - * @param class-string[] $attributes - * * @return ReflectionParameter[] */ - private function getMethodParameter(ReflectionMethod $reflectionMethod, array $attributes): array + private function getMethodParameter(ReflectionMethod $reflectionMethod,): array { $parameters = []; foreach ($reflectionMethod->getParameters() as $parameter) { - foreach ($attributes as $attribute) { - if ($parameter->getAttributes($attribute, \ReflectionAttribute::IS_INSTANCEOF)) { - $parameters[] = $parameter; - } - } + $parameters[] = $parameter; } return $parameters; } - - private function describeCommonSchemaFromParameter(OA\Schema $schema, ReflectionParameter $parameter): void - { - if ($parameter->isDefaultValueAvailable()) { - $schema->default = $parameter->getDefaultValue(); - } - - if (Generator::UNDEFINED === $schema->type) { - if ($parameter->getType()->isBuiltin()) { - $schema->type = $parameter->getType()->getName(); - } - } - } - - /** - * @param class-string $attribute - * - * @return T|null - * - * @template T of object - */ - private function getAttribute(ReflectionParameter $parameter, string $attribute): ?object - { - if ($attribute = $parameter->getAttributes($attribute, \ReflectionAttribute::IS_INSTANCEOF)) { - return $attribute[0]->newInstance(); - } - - return null; - } - - private function describeRequestBody(OA\RequestBody $requestBody, ReflectionParameter $parameter, string $format): void - { - $contentSchema = $this->getContentSchemaForType($requestBody, $format); - $contentSchema->ref = new Model(type: $parameter->getType()->getName()); - $contentSchema->type = 'object'; - - $schema = Util::getProperty($contentSchema, $parameter->getName()); - - $this->describeCommonSchemaFromParameter($schema, $parameter); - } - - private function getContentSchemaForType(OA\RequestBody $requestBody, string $type): OA\Schema - { - $requestBody->content = Generator::UNDEFINED !== $requestBody->content ? $requestBody->content : []; - switch ($type) { - case 'json': - $contentType = 'application/json'; - - break; - case 'xml': - $contentType = 'application/xml'; - - break; - default: - throw new InvalidArgumentException('Unsupported media type'); - } - - if (!isset($requestBody->content[$contentType])) { - $weakContext = Util::createWeakContext($requestBody->_context); - $requestBody->content[$contentType] = new OA\MediaType( - [ - 'mediaType' => $contentType, - '_context' => $weakContext, - ] - ); - - /** @var OA\Schema $schema */ - $schema = Util::getChild( - $requestBody->content[$contentType], - OA\Schema::class - ); - $schema->type = 'object'; - } - - return Util::getChild( - $requestBody->content[$contentType], - OA\Schema::class - ); - } } diff --git a/Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php b/Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php new file mode 100644 index 000000000..d564c4c31 --- /dev/null +++ b/Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php @@ -0,0 +1,29 @@ +title = 'SelfDescribingTitle'; + $schema->description = $model->getType()->getClassName(); + $schema->type = 'object'; + + $schema->properties = [ + new Property([ + 'property' => 'id', + 'type' => Type::BUILTIN_TYPE_INT, + 'nullable' => false, + ]), + ]; + } +} diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php new file mode 100644 index 000000000..722d30cd0 --- /dev/null +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php @@ -0,0 +1,98 @@ +symfonyMapQueryParameterDescriber = new SymfonyMapQueryParameterDescriber(); + } + + /** + * @dataProvider provideMapQueryParameterTestData + */ + public function testMapQueryParameter(callable $function): void + { + $api = new OpenApi([]); + + $parameter = new ReflectionParameter($function, 'parameter1'); + + $this->symfonyMapQueryParameterDescriber->describe( + $api, + new Operation([]), + $parameter + ); + + /** @var MapQueryParameter $mapQueryParameter */ + $mapQueryParameter = $parameter->getAttributes(MapQueryParameter::class, ReflectionAttribute::IS_INSTANCEOF)[0]->newInstance(); + + $documentationParameter = $api->paths[0]->get->parameters['parameter1']; + self::assertSame($mapQueryParameter->name ?? $parameter->getName(), $documentationParameter->name); + self::assertSame('query', $documentationParameter->in); + self::assertSame(!$parameter->isDefaultValueAvailable() && !$parameter->allowsNull(), $documentationParameter->required); + + $schema = $documentationParameter->schema; + self::assertSame($parameter->getType()->getName(), $schema->type); + if ($parameter->isDefaultValueAvailable()) { + self::assertSame($parameter->getDefaultValue(), $schema->default); + } + + if (FILTER_VALIDATE_REGEXP === $mapQueryParameter->filter) { + self::assertSame($mapQueryParameter->options['regexp'], $schema->pattern); + } + } + + public static function provideMapQueryParameterTestData(): iterable + { + yield 'it documents query parameters' => [ + function ( + #[MapQueryParameter] int $parameter1, + ) { + } + ]; + + yield 'it documents query parameters with default values' => [ + function ( + #[MapQueryParameter] int $parameter1 = 123, + ) { + } + ]; + + yield 'it documents query parameters with nullable types' => [ + function ( + #[MapQueryParameter] ?int $parameter1, + ) { + } + ]; + + yield 'it uses MapQueryParameter name argument as name' => [ + function ( + #[MapQueryParameter('someOtherParameter1Name')] int $parameter1, + ) { + } + ]; + + + yield 'it uses documents regex pattern' => [ + function ( + #[MapQueryParameter(filter: FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^\d+$/'])] int $parameter1, + ) { + } + ]; + } +} From e319f8808711c292b65509eb0b92e921fd95581c Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 11 Aug 2023 16:46:22 +0200 Subject: [PATCH 043/120] Cleanup --- RouteDescriber/SymfonyDescriber.php | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/RouteDescriber/SymfonyDescriber.php b/RouteDescriber/SymfonyDescriber.php index 6ca1c0472..f31e26957 100644 --- a/RouteDescriber/SymfonyDescriber.php +++ b/RouteDescriber/SymfonyDescriber.php @@ -4,29 +4,15 @@ namespace Nelmio\ApiDocBundle\RouteDescriber; -use InvalidArgumentException; -use Nelmio\ApiDocBundle\Annotation\Model; -use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; -use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; -use Nelmio\ApiDocBundle\Model\ModelRegistry; -use Nelmio\ApiDocBundle\OpenApiPhp\Util; use Nelmio\ApiDocBundle\RouteDescriber\SymfonyAnnotationDescriber\SymfonyAnnotationDescriber; use OpenApi\Annotations as OA; -use OpenApi\Generator; use ReflectionMethod; use ReflectionParameter; -use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; -use Symfony\Component\HttpKernel\Attribute\MapQueryString; -use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; -use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Routing\Route; -use function is_array; -use const FILTER_VALIDATE_REGEXP; -final class SymfonyDescriber implements RouteDescriberInterface, ModelRegistryAwareInterface +final class SymfonyDescriber implements RouteDescriberInterface { use RouteDescriberTrait; - use ModelRegistryAwareTrait; /** * @param SymfonyAnnotationDescriber[] $annotationDescribers From 19834b57a4efa1ed169c2380f0ba806cdd8a96ae Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 11 Aug 2023 23:42:47 +0200 Subject: [PATCH 044/120] Add annotation describer services --- Resources/config/symfony.xml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Resources/config/symfony.xml b/Resources/config/symfony.xml index ab5bee360..e243a439c 100644 --- a/Resources/config/symfony.xml +++ b/Resources/config/symfony.xml @@ -4,9 +4,19 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + + + + + + + + + + + - From 16ad28a02ff524f0bff3df23bc58ff0bc25a0399 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 00:19:49 +0200 Subject: [PATCH 045/120] Move to own test files --- .../SymfonyMapQueryParameterDescriberTest.php | 25 ++- .../SymfonyMapRequestPayloadDescriberTest.php | 94 +++++++++++ Tests/RouteDescriber/SymfonyDescriberTest.php | 158 +----------------- 3 files changed, 119 insertions(+), 158 deletions(-) create mode 100644 Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php index 722d30cd0..563740693 100644 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php @@ -9,10 +9,9 @@ use OpenApi\Annotations\OpenApi; use PHPUnit\Framework\TestCase; use ReflectionAttribute; -use ReflectionMethod; use ReflectionParameter; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; -use Symfony\Component\Routing\Route; +use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; class SymfonyMapQueryParameterDescriberTest extends TestCase { @@ -20,6 +19,17 @@ class SymfonyMapQueryParameterDescriberTest extends TestCase protected function setUp(): void { + if (\PHP_VERSION_ID < 80100) { + self::markTestSkipped('Attributes require PHP 8'); + } + + if ( + !class_exists(MapRequestPayload::class) + && !class_exists(MapQueryParameter::class) + ) { + self::markTestSkipped('Symfony 6.3 attributes not found'); + } + $this->symfonyMapQueryParameterDescriber = new SymfonyMapQueryParameterDescriber(); } @@ -28,20 +38,18 @@ protected function setUp(): void */ public function testMapQueryParameter(callable $function): void { - $api = new OpenApi([]); - $parameter = new ReflectionParameter($function, 'parameter1'); $this->symfonyMapQueryParameterDescriber->describe( - $api, - new Operation([]), + new OpenApi([]), + $operation = new Operation([]), $parameter ); /** @var MapQueryParameter $mapQueryParameter */ $mapQueryParameter = $parameter->getAttributes(MapQueryParameter::class, ReflectionAttribute::IS_INSTANCEOF)[0]->newInstance(); - $documentationParameter = $api->paths[0]->get->parameters['parameter1']; + $documentationParameter = $operation->parameters[0]; self::assertSame($mapQueryParameter->name ?? $parameter->getName(), $documentationParameter->name); self::assertSame('query', $documentationParameter->in); self::assertSame(!$parameter->isDefaultValueAvailable() && !$parameter->allowsNull(), $documentationParameter->required); @@ -87,8 +95,7 @@ function ( } ]; - - yield 'it uses documents regex pattern' => [ + yield 'it documents regex pattern' => [ function ( #[MapQueryParameter(filter: FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^\d+$/'])] int $parameter1, ) { diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php new file mode 100644 index 000000000..e1cfd02ab --- /dev/null +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php @@ -0,0 +1,94 @@ +symfonyMapRequestPayloadDescriber = new SymfonyMapRequestPayloadDescriber(); + } + + /** + * @dataProvider provideMapRequestPayloadTestData + * + * @param string[] $expectedMediaTypes + */ + public function testMapRequestPayload(callable $function, array $expectedMediaTypes): void + { + $parameter = new ReflectionParameter($function, 'payload'); + + $this->symfonyMapRequestPayloadDescriber->describe( + new OpenApi([]), + $operation = new Operation([]), + $parameter + ); + + foreach ($expectedMediaTypes as $expectedMediaType) { + $requestBodyContent = $operation->requestBody->content[$expectedMediaType]; + + self::assertSame($expectedMediaType, $requestBodyContent->mediaType); + self::assertSame('object', $requestBodyContent->schema->type); + self::assertSame(stdClass::class, $requestBodyContent->schema->ref->type); + } + } + + public static function provideMapRequestPayloadTestData(): iterable + { + yield 'it sets default mediaType to json' => [ + function( + #[MapRequestPayload] stdClass $payload + ) { + }, + ['application/json'], + ]; + + yield 'it sets mediaType to json' => [ + function( + #[MapRequestPayload('json')] stdClass $payload + ) { + }, + ['application/json'], + ]; + + yield 'it sets mediaType to xml' => [ + function ( + #[MapRequestPayload('xml')] stdClass $payload + ) { + }, + ['application/xml'], + ]; + + yield 'it sets multiple mediaTypes' => [ + function( + #[MapRequestPayload(['json', 'xml'])] stdClass $payload + ) { + }, + ['application/json', 'application/xml'], + ]; + } +} diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index afe6ab3bd..ad8768500 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -11,14 +11,14 @@ namespace Nelmio\ApiDocBundle\Tests\RouteDescriber; +use Nelmio\ApiDocBundle\Model\ModelRegistry; +use Nelmio\ApiDocBundle\ModelDescriber\SelfDescribingModelDescriber; use Nelmio\ApiDocBundle\RouteDescriber\SymfonyDescriber; use OpenApi\Annotations\OpenApi; +use OpenApi\Context; use PHPUnit\Framework\TestCase; -use ReflectionAttribute; -use stdClass; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; -use Symfony\Component\Routing\Route; class SymfonyDescriberTest extends TestCase { @@ -37,154 +37,14 @@ protected function setUp(): void self::markTestSkipped('Symfony 6.3 attributes not found'); } - $this->symfonyDescriber = new SymfonyDescriber(); - } - - /** - * @dataProvider provideMapRequestPayloadTestData - * - * @param string[] $expectedMediaTypes - */ - public function testMapRequestPayload(object $controllerClass, array $expectedMediaTypes): void - { - $api = new OpenApi([]); - - $controllerReflectionMethod = new \ReflectionMethod($controllerClass, 'route'); - - $this->symfonyDescriber->describe( - $api, - new Route('/'), - $controllerReflectionMethod - ); - - foreach ($expectedMediaTypes as $expectedMediaType) { - $requestBodyContent = $api->paths[0]->get->requestBody->content[$expectedMediaType]; - - self::assertSame($expectedMediaType, $requestBodyContent->mediaType); - self::assertSame('object', $requestBodyContent->schema->type); - self::assertSame(stdClass::class, $requestBodyContent->schema->ref->type); - } - } - - public static function provideMapRequestPayloadTestData(): iterable - { - yield 'it sets default mediaType to json' => [ - new class() { - public function route( - #[MapRequestPayload] stdClass $payload - ) { - } - }, - ['application/json'], - ]; - - yield 'it sets mediaType to json' => [ - new class() { - public function route( - #[MapRequestPayload('json')] stdClass $payload - ) { - } - }, - ['application/json'], - ]; - - yield 'it sets mediaType to xml' => [ - new class() { - public function route( - #[MapRequestPayload('xml')] stdClass $payload - ) { - } - }, - ['application/xml'], - ]; - - yield 'it sets multiple mediaTypes' => [ - new class() { - public function route( - #[MapRequestPayload(['json', 'xml'])] stdClass $payload - ) { - } - }, - ['application/json', 'application/xml'], - ]; - } - - /** - * @dataProvider provideMapQueryParameterTestData - */ - public function testMapQueryParameter(object $controllerClass): void - { - $api = new OpenApi([]); - - $controllerReflectionMethod = new \ReflectionMethod($controllerClass, 'route'); - - $this->symfonyDescriber->describe( - $api, - new Route('/'), - $controllerReflectionMethod + $registry = new ModelRegistry( + [new SelfDescribingModelDescriber()], + new OpenApi(['_context' => new Context()]), + [] ); - foreach ($controllerReflectionMethod->getParameters() as $key => $parameter) { - /** @var MapQueryParameter $mapQueryParameter */ - $mapQueryParameter = $parameter->getAttributes(MapQueryParameter::class, ReflectionAttribute::IS_INSTANCEOF)[0]->newInstance(); - - $documentationParameter = $api->paths[0]->get->parameters[$key]; - self::assertSame($mapQueryParameter->name ?? $parameter->getName(), $documentationParameter->name); - self::assertSame('query', $documentationParameter->in); - self::assertSame(!$parameter->isDefaultValueAvailable() && !$parameter->allowsNull(), $documentationParameter->required); - - $schema = $documentationParameter->schema; - self::assertSame($parameter->getType()->getName(), $schema->type); - if ($parameter->isDefaultValueAvailable()) { - self::assertSame($parameter->getDefaultValue(), $schema->default); - } - - if (FILTER_VALIDATE_REGEXP === $mapQueryParameter->filter) { - self::assertSame($mapQueryParameter->options['regexp'], $schema->pattern); - } - } - } - - public static function provideMapQueryParameterTestData(): iterable - { - yield 'it documents query parameters' => [new class() { - public function route( - #[MapQueryParameter] int $parameter1, - #[MapQueryParameter] int $parameter2 - ) { - } - }]; - - yield 'it documents query parameters with default values' => [new class() { - public function route( - #[MapQueryParameter] int $parameter1 = 123, - #[MapQueryParameter] int $parameter2 = 456 - ) { - } - }]; - - yield 'it documents query parameters with nullable types' => [new class() { - public function route( - #[MapQueryParameter] ?int $parameter1, - #[MapQueryParameter] ?int $parameter2 - ) { - } - }]; - - yield 'it uses MapQueryParameter name argument as name' => [new class() { - public function route( - #[MapQueryParameter('someOtherParameter1Name')] int $parameter1, - #[MapQueryParameter('someOtherParameter2Name')] int $parameter2 - ) { - } - }]; + $this->symfonyDescriber = new SymfonyDescriber(); - yield 'it uses documents regex pattern' => [new class() { - public function route( - #[MapQueryParameter(filter: FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^\d+$/'])] int $parameter1, - #[MapQueryParameter(filter: FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^\d+$/'])] int $parameter2 - ) { - } - }]; + $this->symfonyDescriber->setModelRegistry($registry); } } From 2a524780b680d1083277e8fb30454a78129418cd Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 00:20:13 +0200 Subject: [PATCH 046/120] Cleanup --- .../SymfonyMapQueryParameterDescriber.php | 9 ++------- .../SymfonyMapQueryStringDescriber.php | 3 +-- .../SymfonyMapRequestPayloadDescriber.php | 9 ++------- 3 files changed, 5 insertions(+), 16 deletions(-) diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php index f8b35387a..bac3036fc 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php @@ -4,18 +4,13 @@ namespace Nelmio\ApiDocBundle\RouteDescriber\SymfonyAnnotationDescriber; -use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; -use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; use Nelmio\ApiDocBundle\OpenApiPhp\Util; -use OpenApi\Annotations\Operation; use ReflectionParameter; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use OpenApi\Annotations as OA; -final class SymfonyMapQueryParameterDescriber implements SymfonyAnnotationDescriber, ModelRegistryAwareInterface +final class SymfonyMapQueryParameterDescriber implements SymfonyAnnotationDescriber { - use ModelRegistryAwareTrait; - public function supports(ReflectionParameter $parameter): bool { if (!SymfonyAnnotationHelper::getAttribute($parameter, MapQueryParameter::class)) { @@ -25,7 +20,7 @@ public function supports(ReflectionParameter $parameter): bool return $parameter->hasType(); } - public function describe(OA\OpenApi $api, Operation $operation, ReflectionParameter $parameter): void + public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionParameter $parameter): void { $attribute = SymfonyAnnotationHelper::getAttribute($parameter, MapQueryParameter::class); diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php index a7da935e0..398408b95 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php @@ -8,7 +8,6 @@ use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; use Nelmio\ApiDocBundle\Model\Model; use Nelmio\ApiDocBundle\OpenApiPhp\Util; -use OpenApi\Annotations\Operation; use OpenApi\Generator; use ReflectionParameter; use Symfony\Component\HttpKernel\Attribute\MapQueryString; @@ -28,7 +27,7 @@ public function supports(ReflectionParameter $parameter): bool return $parameter->hasType(); } - public function describe(OA\OpenApi $api, Operation $operation, ReflectionParameter $parameter): void + public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionParameter $parameter): void { $model = new Model(new Type(Type::BUILTIN_TYPE_OBJECT, $parameter->allowsNull(), $parameter->getType()->getName())); $modelRef = $this->modelRegistry->register($model); diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php index 5d94edf9f..54fd2345a 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php @@ -5,19 +5,14 @@ namespace Nelmio\ApiDocBundle\RouteDescriber\SymfonyAnnotationDescriber; use InvalidArgumentException; -use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; -use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; use Nelmio\ApiDocBundle\OpenApiPhp\Util; -use OpenApi\Annotations\Operation; use OpenApi\Generator; use ReflectionParameter; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use OpenApi\Annotations as OA; -final class SymfonyMapRequestPayloadDescriber implements SymfonyAnnotationDescriber, ModelRegistryAwareInterface +final class SymfonyMapRequestPayloadDescriber implements SymfonyAnnotationDescriber { - use ModelRegistryAwareTrait; - public function supports(ReflectionParameter $parameter): bool { if (!SymfonyAnnotationHelper::getAttribute($parameter, MapRequestPayload::class)) { @@ -27,7 +22,7 @@ public function supports(ReflectionParameter $parameter): bool return $parameter->hasType(); } - public function describe(OA\OpenApi $api, Operation $operation, ReflectionParameter $parameter): void + public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionParameter $parameter): void { $attribute = SymfonyAnnotationHelper::getAttribute($parameter, MapRequestPayload::class); From 9d89fdcd1f82a3ae8fe6e8a4f5ea592dfe871604 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 00:20:41 +0200 Subject: [PATCH 047/120] Call setModelRegistry on annotation describers --- .../SymfonyAnnotationDescriber.php | 3 +-- RouteDescriber/SymfonyDescriber.php | 9 ++++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationDescriber.php index 03fbf257b..c2bee071c 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationDescriber.php @@ -4,12 +4,11 @@ namespace Nelmio\ApiDocBundle\RouteDescriber\SymfonyAnnotationDescriber; -use OpenApi\Annotations\Operation; use ReflectionParameter; use OpenApi\Annotations as OA; interface SymfonyAnnotationDescriber { public function supports(ReflectionParameter $parameter): bool; - public function describe(OA\OpenApi $api, Operation $operation, ReflectionParameter $parameter): void; + public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionParameter $parameter): void; } diff --git a/RouteDescriber/SymfonyDescriber.php b/RouteDescriber/SymfonyDescriber.php index f31e26957..c6f0ef137 100644 --- a/RouteDescriber/SymfonyDescriber.php +++ b/RouteDescriber/SymfonyDescriber.php @@ -4,15 +4,18 @@ namespace Nelmio\ApiDocBundle\RouteDescriber; +use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; +use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; use Nelmio\ApiDocBundle\RouteDescriber\SymfonyAnnotationDescriber\SymfonyAnnotationDescriber; use OpenApi\Annotations as OA; use ReflectionMethod; use ReflectionParameter; use Symfony\Component\Routing\Route; -final class SymfonyDescriber implements RouteDescriberInterface +final class SymfonyDescriber implements RouteDescriberInterface, ModelRegistryAwareInterface { use RouteDescriberTrait; + use ModelRegistryAwareTrait; /** * @param SymfonyAnnotationDescriber[] $annotationDescribers @@ -29,6 +32,10 @@ public function describe(OA\OpenApi $api, Route $route, ReflectionMethod $reflec foreach ($this->getOperations($api, $route) as $operation) { foreach ($parameters as $parameter) { foreach ($this->annotationDescribers as $annotationDescriber) { + if ($annotationDescriber instanceof ModelRegistryAwareInterface) { + $annotationDescriber->setModelRegistry($this->modelRegistry); + } + if (! $annotationDescriber->supports($parameter)) { continue; } From 3c53bb1cdab389e38f599b3ed0b881dd29cb482d Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 00:44:36 +0200 Subject: [PATCH 048/120] Use accessible values for tests --- .../SymfonyDescriberMapQueryStringClass.php | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php b/Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php index d564c4c31..d3692ede5 100644 --- a/Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php +++ b/Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php @@ -10,19 +10,33 @@ use OpenApi\Annotations\Schema; use Symfony\Component\PropertyInfo\Type; -class SymfonyDescriberMapQueryStringClass implements SelfDescribingModelInterface +final class SymfonyDescriberMapQueryStringClass implements SelfDescribingModelInterface { + public const SCHEMA = 'SymfonyDescriberMapQueryStringClass'; + public const TITLE = 'SelfDescribingTitle'; + public const TYPE = 'object'; + public static function describe(Schema $schema, Model $model): void { - $schema->title = 'SelfDescribingTitle'; + $schema->schema = self::SCHEMA; + $schema->title = self::TITLE; $schema->description = $model->getType()->getClassName(); - $schema->type = 'object'; + $schema->type = self::TYPE; + + $schema->properties = self::getProperties(); + } - $schema->properties = [ + /** + * @return Property[] + */ + public static function getProperties(): array + { + return [ new Property([ 'property' => 'id', 'type' => Type::BUILTIN_TYPE_INT, 'nullable' => false, + 'default' => 123, ]), ]; } From b61f10564dac1e6a450e05e2005399ef4939a89d Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 00:44:52 +0200 Subject: [PATCH 049/120] Add SymfonyMapQueryStringDescriberTest --- .../SymfonyMapQueryStringDescriberTest.php | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php new file mode 100644 index 000000000..6f7a90161 --- /dev/null +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php @@ -0,0 +1,92 @@ +openApi = new OpenApi([]); + + $this->symfonyMapQueryStringDescriber = new SymfonyMapQueryStringDescriber(); + + $registry = new ModelRegistry([new SelfDescribingModelDescriber()], $this->openApi, []); + + $this->symfonyMapQueryStringDescriber->setModelRegistry($registry); + } + + /** + * @dataProvider provideMapQueryStringTestData + */ + public function testMapQueryString(callable $function): void + { + $parameter = new ReflectionParameter($function, 'parameter1'); + + $this->symfonyMapQueryStringDescriber->describe( + $this->openApi, + $operation = new Operation([]), + $parameter + ); + + // Test it registers the model + $modelSchema = $this->openApi->components->schemas[0]; + $expectedModelProperties = SymfonyDescriberMapQueryStringClass::getProperties(); + + self::assertSame(SymfonyDescriberMapQueryStringClass::SCHEMA, $modelSchema->schema); + self::assertSame(SymfonyDescriberMapQueryStringClass::TITLE, $modelSchema->title); + self::assertSame(SymfonyDescriberMapQueryStringClass::TYPE, $modelSchema->type); + self::assertEquals($expectedModelProperties, $modelSchema->properties); + + foreach ($expectedModelProperties as $key => $expectedModelProperty) { + $queryParameter = $operation->parameters[$key]; + + self::assertSame('query', $queryParameter->in); + self::assertSame($expectedModelProperty->property, $queryParameter->name); + $isQueryOptional = (Generator::UNDEFINED !== $expectedModelProperty->nullable && $expectedModelProperty->nullable) + || Generator::UNDEFINED !== $expectedModelProperty->default; + + self::assertSame($isQueryOptional, $queryParameter->allowEmptyValue); + self::assertSame(!$isQueryOptional, $queryParameter->required); + } + + } + + public static function provideMapQueryStringTestData(): iterable + { + yield 'it documents query string parameters' => [ + function ( + #[MapQueryString] SymfonyDescriberMapQueryStringClass $parameter1, + ) { + } + ]; + } +} From 317292442bf7a870ce6df98c59e9e333a20cd7f2 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 00:46:53 +0200 Subject: [PATCH 050/120] Only check availability of needed attribute --- .../SymfonyMapQueryParameterDescriberTest.php | 8 ++------ .../SymfonyMapQueryStringDescriberTest.php | 9 ++------- .../SymfonyMapRequestPayloadDescriberTest.php | 8 ++------ 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php index 563740693..cbdecd57b 100644 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php @@ -11,7 +11,6 @@ use ReflectionAttribute; use ReflectionParameter; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; -use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; class SymfonyMapQueryParameterDescriberTest extends TestCase { @@ -23,11 +22,8 @@ protected function setUp(): void self::markTestSkipped('Attributes require PHP 8'); } - if ( - !class_exists(MapRequestPayload::class) - && !class_exists(MapQueryParameter::class) - ) { - self::markTestSkipped('Symfony 6.3 attributes not found'); + if (!class_exists(MapQueryParameter::class)) { + self::markTestSkipped('Symfony 6.3 MapRequestPayload attribute not found'); } $this->symfonyMapQueryParameterDescriber = new SymfonyMapQueryParameterDescriber(); diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php index 6f7a90161..928807e7d 100644 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php @@ -13,9 +13,7 @@ use OpenApi\Generator; use PHPUnit\Framework\TestCase; use ReflectionParameter; -use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Attribute\MapQueryString; -use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; class SymfonyMapQueryStringDescriberTest extends TestCase { @@ -28,11 +26,8 @@ protected function setUp(): void self::markTestSkipped('Attributes require PHP 8'); } - if ( - !class_exists(MapRequestPayload::class) - && !class_exists(MapQueryParameter::class) - ) { - self::markTestSkipped('Symfony 6.3 attributes not found'); + if (!class_exists(MapQueryString::class)) { + self::markTestSkipped('Symfony 6.3 MapQueryString attribute not found'); } $this->openApi = new OpenApi([]); diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php index e1cfd02ab..052c28bf0 100644 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php @@ -10,7 +10,6 @@ use PHPUnit\Framework\TestCase; use ReflectionParameter; use stdClass; -use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; class SymfonyMapRequestPayloadDescriberTest extends TestCase @@ -23,11 +22,8 @@ protected function setUp(): void self::markTestSkipped('Attributes require PHP 8'); } - if ( - !class_exists(MapRequestPayload::class) - && !class_exists(MapQueryParameter::class) - ) { - self::markTestSkipped('Symfony 6.3 attributes not found'); + if (!class_exists(MapRequestPayload::class)) { + self::markTestSkipped('Symfony 6.3 MapRequestPayload attribute not found'); } $this->symfonyMapRequestPayloadDescriber = new SymfonyMapRequestPayloadDescriber(); From 34d6bb8a976af7323fc3e9638ad4d7fe4c458c07 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 00:47:13 +0200 Subject: [PATCH 051/120] Fix message --- .../SymfonyMapQueryParameterDescriberTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php index cbdecd57b..0d007e3e8 100644 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php @@ -23,7 +23,7 @@ protected function setUp(): void } if (!class_exists(MapQueryParameter::class)) { - self::markTestSkipped('Symfony 6.3 MapRequestPayload attribute not found'); + self::markTestSkipped('Symfony 6.3 MapQueryParameter attribute not found'); } $this->symfonyMapQueryParameterDescriber = new SymfonyMapQueryParameterDescriber(); From 0a6489a326fa6ce983bc60a6fc064e61caf0dc11 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 01:04:33 +0200 Subject: [PATCH 052/120] Fix styleci --- .../SymfonyAnnotationDescriber.php | 2 +- .../SymfonyAnnotationHelper.php | 2 +- .../SymfonyMapQueryParameterDescriber.php | 2 +- .../SymfonyMapQueryStringDescriber.php | 6 +++--- .../SymfonyMapRequestPayloadDescriber.php | 2 +- RouteDescriber/SymfonyDescriber.php | 4 ++-- .../SymfonyMapQueryParameterDescriberTest.php | 13 +++++++------ .../SymfonyMapQueryStringDescriberTest.php | 5 +++-- .../SymfonyMapRequestPayloadDescriberTest.php | 9 +++++---- Tests/RouteDescriber/SymfonyDescriberTest.php | 3 ++- 10 files changed, 26 insertions(+), 22 deletions(-) diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationDescriber.php index c2bee071c..4247b8b6b 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationDescriber.php @@ -4,8 +4,8 @@ namespace Nelmio\ApiDocBundle\RouteDescriber\SymfonyAnnotationDescriber; -use ReflectionParameter; use OpenApi\Annotations as OA; +use ReflectionParameter; interface SymfonyAnnotationDescriber { diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationHelper.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationHelper.php index a92e9a425..0a378e00d 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationHelper.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationHelper.php @@ -4,9 +4,9 @@ namespace Nelmio\ApiDocBundle\RouteDescriber\SymfonyAnnotationDescriber; +use OpenApi\Annotations as OA; use OpenApi\Generator; use ReflectionParameter; -use OpenApi\Annotations as OA; final class SymfonyAnnotationHelper { diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php index bac3036fc..a3700f784 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php @@ -5,9 +5,9 @@ namespace Nelmio\ApiDocBundle\RouteDescriber\SymfonyAnnotationDescriber; use Nelmio\ApiDocBundle\OpenApiPhp\Util; +use OpenApi\Annotations as OA; use ReflectionParameter; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; -use OpenApi\Annotations as OA; final class SymfonyMapQueryParameterDescriber implements SymfonyAnnotationDescriber { diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php index 398408b95..a17731d2e 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php @@ -8,11 +8,11 @@ use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; use Nelmio\ApiDocBundle\Model\Model; use Nelmio\ApiDocBundle\OpenApiPhp\Util; +use OpenApi\Annotations as OA; use OpenApi\Generator; use ReflectionParameter; use Symfony\Component\HttpKernel\Attribute\MapQueryString; use Symfony\Component\PropertyInfo\Type; -use OpenApi\Annotations as OA; final class SymfonyMapQueryStringDescriber implements SymfonyAnnotationDescriber, ModelRegistryAwareInterface { @@ -36,7 +36,7 @@ public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionPar $nativeModelName = str_replace(OA\Components::SCHEMA_REF, '', $modelRef); $schemaModel = Util::getSchema($api, $nativeModelName); - if (Generator::UNDEFINED === $schemaModel->properties) { + if (Generator::UNDEFINED === $schemaModel->properties) { return; } @@ -46,7 +46,7 @@ public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionPar $operationParameter = Util::getOperationParameter($operation, $property->property, 'query'); $operationParameter->name = $property->property; - $isQueryOptional = (Generator::UNDEFINED !== $property->nullable &&$property->nullable) + $isQueryOptional = (Generator::UNDEFINED !== $property->nullable && $property->nullable) || Generator::UNDEFINED !== $property->default || $isModelOptional; diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php index 54fd2345a..2eb03bea3 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php @@ -6,10 +6,10 @@ use InvalidArgumentException; use Nelmio\ApiDocBundle\OpenApiPhp\Util; +use OpenApi\Annotations as OA; use OpenApi\Generator; use ReflectionParameter; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; -use OpenApi\Annotations as OA; final class SymfonyMapRequestPayloadDescriber implements SymfonyAnnotationDescriber { diff --git a/RouteDescriber/SymfonyDescriber.php b/RouteDescriber/SymfonyDescriber.php index c6f0ef137..55ccd6e82 100644 --- a/RouteDescriber/SymfonyDescriber.php +++ b/RouteDescriber/SymfonyDescriber.php @@ -36,7 +36,7 @@ public function describe(OA\OpenApi $api, Route $route, ReflectionMethod $reflec $annotationDescriber->setModelRegistry($this->modelRegistry); } - if (! $annotationDescriber->supports($parameter)) { + if (!$annotationDescriber->supports($parameter)) { continue; } @@ -49,7 +49,7 @@ public function describe(OA\OpenApi $api, Route $route, ReflectionMethod $reflec /** * @return ReflectionParameter[] */ - private function getMethodParameter(ReflectionMethod $reflectionMethod,): array + private function getMethodParameter(ReflectionMethod $reflectionMethod): array { $parameters = []; diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php index 0d007e3e8..bd96eb7c3 100644 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php @@ -11,6 +11,7 @@ use ReflectionAttribute; use ReflectionParameter; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; +use const PHP_VERSION_ID; class SymfonyMapQueryParameterDescriberTest extends TestCase { @@ -18,7 +19,7 @@ class SymfonyMapQueryParameterDescriberTest extends TestCase protected function setUp(): void { - if (\PHP_VERSION_ID < 80100) { + if (PHP_VERSION_ID < 80100) { self::markTestSkipped('Attributes require PHP 8'); } @@ -67,35 +68,35 @@ public static function provideMapQueryParameterTestData(): iterable function ( #[MapQueryParameter] int $parameter1, ) { - } + }, ]; yield 'it documents query parameters with default values' => [ function ( #[MapQueryParameter] int $parameter1 = 123, ) { - } + }, ]; yield 'it documents query parameters with nullable types' => [ function ( #[MapQueryParameter] ?int $parameter1, ) { - } + }, ]; yield 'it uses MapQueryParameter name argument as name' => [ function ( #[MapQueryParameter('someOtherParameter1Name')] int $parameter1, ) { - } + }, ]; yield 'it documents regex pattern' => [ function ( #[MapQueryParameter(filter: FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^\d+$/'])] int $parameter1, ) { - } + }, ]; } } diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php index 928807e7d..1facc669a 100644 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use ReflectionParameter; use Symfony\Component\HttpKernel\Attribute\MapQueryString; +use const PHP_VERSION_ID; class SymfonyMapQueryStringDescriberTest extends TestCase { @@ -22,7 +23,7 @@ class SymfonyMapQueryStringDescriberTest extends TestCase protected function setUp(): void { - if (\PHP_VERSION_ID < 80100) { + if (PHP_VERSION_ID < 80100) { self::markTestSkipped('Attributes require PHP 8'); } @@ -81,7 +82,7 @@ public static function provideMapQueryStringTestData(): iterable function ( #[MapQueryString] SymfonyDescriberMapQueryStringClass $parameter1, ) { - } + }, ]; } } diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php index 052c28bf0..9ab6ba872 100644 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php @@ -11,6 +11,7 @@ use ReflectionParameter; use stdClass; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; +use const PHP_VERSION_ID; class SymfonyMapRequestPayloadDescriberTest extends TestCase { @@ -18,7 +19,7 @@ class SymfonyMapRequestPayloadDescriberTest extends TestCase protected function setUp(): void { - if (\PHP_VERSION_ID < 80100) { + if (PHP_VERSION_ID < 80100) { self::markTestSkipped('Attributes require PHP 8'); } @@ -56,7 +57,7 @@ public function testMapRequestPayload(callable $function, array $expectedMediaTy public static function provideMapRequestPayloadTestData(): iterable { yield 'it sets default mediaType to json' => [ - function( + function ( #[MapRequestPayload] stdClass $payload ) { }, @@ -64,7 +65,7 @@ function( ]; yield 'it sets mediaType to json' => [ - function( + function ( #[MapRequestPayload('json')] stdClass $payload ) { }, @@ -80,7 +81,7 @@ function ( ]; yield 'it sets multiple mediaTypes' => [ - function( + function ( #[MapRequestPayload(['json', 'xml'])] stdClass $payload ) { }, diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index ad8768500..f7dba2b48 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -19,6 +19,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; +use const PHP_VERSION_ID; class SymfonyDescriberTest extends TestCase { @@ -26,7 +27,7 @@ class SymfonyDescriberTest extends TestCase protected function setUp(): void { - if (\PHP_VERSION_ID < 80100) { + if (PHP_VERSION_ID < 80100) { self::markTestSkipped('Attributes require PHP 8'); } From 1e18c3c0d3662cc8b71c474993a760e77d199911 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 01:04:33 +0200 Subject: [PATCH 053/120] Fix styleci --- .../SymfonyAnnotationDescriber/SymfonyAnnotationDescriber.php | 1 + .../SymfonyMapQueryStringDescriberTest.php | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationDescriber.php index 4247b8b6b..6d73938aa 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationDescriber.php @@ -10,5 +10,6 @@ interface SymfonyAnnotationDescriber { public function supports(ReflectionParameter $parameter): bool; + public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionParameter $parameter): void; } diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php index 1facc669a..1e9db6b93 100644 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php @@ -73,7 +73,6 @@ public function testMapQueryString(callable $function): void self::assertSame($isQueryOptional, $queryParameter->allowEmptyValue); self::assertSame(!$isQueryOptional, $queryParameter->required); } - } public static function provideMapQueryStringTestData(): iterable From 65d479ee2754810d2b51a7ec37562fa78645070e Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 02:39:05 +0200 Subject: [PATCH 054/120] Expand SymfonyMapQueryStringDescriber to copy property data to query parameter data --- .../SymfonyMapQueryStringDescriber.php | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php index a17731d2e..9906339ed 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php @@ -10,6 +10,7 @@ use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; use OpenApi\Generator; +use ReflectionClass; use ReflectionParameter; use Symfony\Component\HttpKernel\Attribute\MapQueryString; use Symfony\Component\PropertyInfo\Type; @@ -43,16 +44,63 @@ public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionPar $isModelOptional = $parameter->isDefaultValueAvailable() || $parameter->allowsNull(); foreach ($schemaModel->properties as $property) { + $constructorParameter = $this->getConstructorReflectionParameterForProperty($parameter, $property); + $operationParameter = Util::getOperationParameter($operation, $property->property, 'query'); - $operationParameter->name = $property->property; + $this->copyPropertyValuesToQuery($operationParameter, $property); $isQueryOptional = (Generator::UNDEFINED !== $property->nullable && $property->nullable) - || Generator::UNDEFINED !== $property->default + || $constructorParameter?->isDefaultValueAvailable() || $isModelOptional; - $operationParameter->allowEmptyValue = $isQueryOptional; - $operationParameter->required = !$isQueryOptional; - $operationParameter->example = $property->default; + if (Generator::UNDEFINED === $operationParameter->allowEmptyValue) { + $operationParameter->allowEmptyValue = $isQueryOptional; + } + + if (Generator::UNDEFINED === $operationParameter->required) { + $operationParameter->required = !$isQueryOptional; + } + + if (Generator::UNDEFINED === $operationParameter->example && $constructorParameter?->isDefaultValueAvailable()) { + $operationParameter->example = $constructorParameter->getDefaultValue(); + } } } + private function getConstructorReflectionParameterForProperty(ReflectionParameter $parameter, OA\Property $property): ?ReflectionParameter + { + $reflectionClass = new ReflectionClass($parameter->getType()->getName()); + + $parameters = $reflectionClass->getConstructor()->getParameters(); + + foreach ($parameters as $parameter) { + if ($property->property === $parameter->getName()) { + return $parameter; + } + } + + return null; + } + + private function copyPropertyValuesToQuery(OA\Parameter $parameter, OA\Property $property): void + { + $parameter->schema = Util::getChild($parameter, OA\Schema::class); + $parameter->schema->title = $property->title; + $parameter->schema->type = $property->type; + $parameter->schema->items = $property->items; + $parameter->schema->example = $property->example; + $parameter->schema->nullable = $property->nullable; + $parameter->schema->enum = $property->enum; + $parameter->schema->default = $property->default; + $parameter->schema->minimum = $property->minimum; + $parameter->schema->exclusiveMinimum = $property->exclusiveMinimum; + $parameter->schema->maximum = $property->maximum; + $parameter->schema->exclusiveMaximum = $property->exclusiveMaximum; + $parameter->schema->required = $property->required; + $parameter->schema->deprecated = $property->deprecated; + + $parameter->name = $property->property; + $parameter->description = $parameter->schema->description; + $parameter->required = $parameter->schema->required; + $parameter->deprecated = $parameter->schema->deprecated; + } } From e231847fb33115802f2add760810113abeafd0bb Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 02:40:39 +0200 Subject: [PATCH 055/120] Fix style --- .../SymfonyMapQueryStringDescriber.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php index 9906339ed..3e5d7d831 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php @@ -72,7 +72,7 @@ private function getConstructorReflectionParameterForProperty(ReflectionParamete $parameters = $reflectionClass->getConstructor()->getParameters(); - foreach ($parameters as $parameter) { + foreach ($parameters as $parameter) { if ($property->property === $parameter->getName()) { return $parameter; } @@ -81,7 +81,7 @@ private function getConstructorReflectionParameterForProperty(ReflectionParamete return null; } - private function copyPropertyValuesToQuery(OA\Parameter $parameter, OA\Property $property): void + private function copyPropertyValuesToQuery(OA\Parameter $parameter, OA\Property $property): void { $parameter->schema = Util::getChild($parameter, OA\Schema::class); $parameter->schema->title = $property->title; From 7a335e3c495b77cb427a82df8f9a8473c6e71ebc Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 02:41:17 +0200 Subject: [PATCH 056/120] Add missing newline --- .../SymfonyMapQueryStringDescriber.php | 1 + 1 file changed, 1 insertion(+) diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php index 3e5d7d831..84b58af42 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php @@ -66,6 +66,7 @@ public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionPar } } } + private function getConstructorReflectionParameterForProperty(ReflectionParameter $parameter, OA\Property $property): ?ReflectionParameter { $reflectionClass = new ReflectionClass($parameter->getType()->getName()); From 5576e7a58095138e8d9ca6dc571bd329df436a97 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 02:43:52 +0200 Subject: [PATCH 057/120] Fix test php 7.2 compatability --- .../SymfonyMapQueryParameterDescriberTest.php | 5 ++++- .../SymfonyMapQueryStringDescriberTest.php | 10 ++++++++-- .../SymfonyMapRequestPayloadDescriberTest.php | 5 ++++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php index bd96eb7c3..341a1ae0d 100644 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php @@ -15,7 +15,10 @@ class SymfonyMapQueryParameterDescriberTest extends TestCase { - private SymfonyMapQueryParameterDescriber $symfonyMapQueryParameterDescriber; + /** + * @var SymfonyMapQueryParameterDescriber $symfonyMapQueryParameterDescriber + */ + private $symfonyMapQueryParameterDescriber; protected function setUp(): void { diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php index 1e9db6b93..aafb24780 100644 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php @@ -18,8 +18,14 @@ class SymfonyMapQueryStringDescriberTest extends TestCase { - private OpenApi $openApi; - private SymfonyMapQueryStringDescriber $symfonyMapQueryStringDescriber; + /** + * @var OpenApi $openApi + */ + private $openApi; + /** + * @var SymfonyMapQueryStringDescriber $symfonyMapQueryStringDescriber + */ + private $symfonyMapQueryStringDescriber; protected function setUp(): void { diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php index 9ab6ba872..74009d0c5 100644 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php @@ -15,7 +15,10 @@ class SymfonyMapRequestPayloadDescriberTest extends TestCase { - private SymfonyMapRequestPayloadDescriber $symfonyMapRequestPayloadDescriber; + /** + * @var SymfonyMapRequestPayloadDescriber $symfonyMapRequestPayloadDescriber + */ + private $symfonyMapRequestPayloadDescriber; protected function setUp(): void { From 1fe99853e24f72b93ac32e3d87899b696b831459 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 02:44:41 +0200 Subject: [PATCH 058/120] Remove annotation var name --- .../SymfonyMapQueryParameterDescriberTest.php | 2 +- .../SymfonyMapQueryStringDescriberTest.php | 4 ++-- .../SymfonyMapRequestPayloadDescriberTest.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php index 341a1ae0d..a67c0c877 100644 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php @@ -16,7 +16,7 @@ class SymfonyMapQueryParameterDescriberTest extends TestCase { /** - * @var SymfonyMapQueryParameterDescriber $symfonyMapQueryParameterDescriber + * @var SymfonyMapQueryParameterDescriber */ private $symfonyMapQueryParameterDescriber; diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php index aafb24780..a0ef42f00 100644 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php @@ -19,11 +19,11 @@ class SymfonyMapQueryStringDescriberTest extends TestCase { /** - * @var OpenApi $openApi + * @var OpenApi */ private $openApi; /** - * @var SymfonyMapQueryStringDescriber $symfonyMapQueryStringDescriber + * @var SymfonyMapQueryStringDescriber */ private $symfonyMapQueryStringDescriber; diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php index 74009d0c5..14bebba81 100644 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php @@ -16,7 +16,7 @@ class SymfonyMapRequestPayloadDescriberTest extends TestCase { /** - * @var SymfonyMapRequestPayloadDescriber $symfonyMapRequestPayloadDescriber + * @var SymfonyMapRequestPayloadDescriber */ private $symfonyMapRequestPayloadDescriber; From bdcb5a2e3f31b9641a71b2de75d76e90499ae688 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 03:20:17 +0200 Subject: [PATCH 059/120] Fix missing values --- .../SymfonyMapQueryStringDescriber.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php index 84b58af42..378370aae 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php @@ -71,9 +71,11 @@ private function getConstructorReflectionParameterForProperty(ReflectionParamete { $reflectionClass = new ReflectionClass($parameter->getType()->getName()); - $parameters = $reflectionClass->getConstructor()->getParameters(); + if (!$contructor = $reflectionClass->getConstructor()) { + return null; + } - foreach ($parameters as $parameter) { + foreach ($contructor->getParameters() as $parameter) { if ($property->property === $parameter->getName()) { return $parameter; } @@ -86,6 +88,7 @@ private function copyPropertyValuesToQuery(OA\Parameter $parameter, OA\Property { $parameter->schema = Util::getChild($parameter, OA\Schema::class); $parameter->schema->title = $property->title; + $parameter->schema->description = $property->description; $parameter->schema->type = $property->type; $parameter->schema->items = $property->items; $parameter->schema->example = $property->example; @@ -103,5 +106,6 @@ private function copyPropertyValuesToQuery(OA\Parameter $parameter, OA\Property $parameter->description = $parameter->schema->description; $parameter->required = $parameter->schema->required; $parameter->deprecated = $parameter->schema->deprecated; + $parameter->example = $parameter->schema->example; } } From 7544dc56c8480f6d7c90e7629c1a2f667956297a Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 03:20:53 +0200 Subject: [PATCH 060/120] Fix missing values --- .../Fixtures/SymfonyDescriberMapQueryStringClass.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php b/Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php index d3692ede5..5482e91cd 100644 --- a/Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php +++ b/Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php @@ -8,7 +8,6 @@ use Nelmio\ApiDocBundle\ModelDescriber\SelfDescribingModelInterface; use OpenApi\Annotations\Property; use OpenApi\Annotations\Schema; -use Symfony\Component\PropertyInfo\Type; final class SymfonyDescriberMapQueryStringClass implements SelfDescribingModelInterface { @@ -34,7 +33,7 @@ public static function getProperties(): array return [ new Property([ 'property' => 'id', - 'type' => Type::BUILTIN_TYPE_INT, + 'type' => 'int', 'nullable' => false, 'default' => 123, ]), From 87de11c88cf68e1a677c4fc2632df886c98549a8 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 03:21:17 +0200 Subject: [PATCH 061/120] Add DTO testing class --- Tests/RouteDescriber/Fixtures/DTO.php | 69 +++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 Tests/RouteDescriber/Fixtures/DTO.php diff --git a/Tests/RouteDescriber/Fixtures/DTO.php b/Tests/RouteDescriber/Fixtures/DTO.php new file mode 100644 index 000000000..a37be818e --- /dev/null +++ b/Tests/RouteDescriber/Fixtures/DTO.php @@ -0,0 +1,69 @@ +properties = self::getProperties(); + } + + /** + * @return Property[] + */ + public static function getProperties(): array + { + return [ + new Property([ + 'property' => 'id', + 'type' => 'int', + ]), + new Property([ + 'property' => 'name', + 'type' => 'string', + ]), + new Property([ + 'property' => 'nullableName', + 'type' => 'string', + 'nullable' => true, + ]), + new Property([ + 'property' => 'nameWithExample', + 'type' => 'string', + 'example' => self::EXAMPLE_NAME, + ]), + new Property([ + 'property' => 'nameWithDescription', + 'type' => 'string', + 'description' => self::DESCRIPTION, + ]), + ]; + } +} From 28309cf863824629576665c6d137617b51cb1774 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 03:21:26 +0200 Subject: [PATCH 062/120] Expand SymfonyMapQueryStringDescriberTest --- .../SymfonyMapQueryStringDescriberTest.php | 80 +++++++++++++++++-- 1 file changed, 73 insertions(+), 7 deletions(-) diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php index a0ef42f00..f596fc080 100644 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php @@ -8,9 +8,9 @@ use Nelmio\ApiDocBundle\Model\ModelRegistry; use Nelmio\ApiDocBundle\ModelDescriber\SelfDescribingModelDescriber; use Nelmio\ApiDocBundle\RouteDescriber\SymfonyAnnotationDescriber\SymfonyMapQueryStringDescriber; +use Nelmio\ApiDocBundle\Tests\RouteDescriber\Fixtures\DTO; use Nelmio\ApiDocBundle\Tests\RouteDescriber\Fixtures\SymfonyDescriberMapQueryStringClass; use OpenApi\Annotations\OpenApi; -use OpenApi\Generator; use PHPUnit\Framework\TestCase; use ReflectionParameter; use Symfony\Component\HttpKernel\Attribute\MapQueryString; @@ -48,8 +48,10 @@ protected function setUp(): void /** * @dataProvider provideMapQueryStringTestData + * + * @param array{optional: bool} $expectations */ - public function testMapQueryString(callable $function): void + public function testMapQueryString(callable $function, array $expectations): void { $parameter = new ReflectionParameter($function, 'parameter1'); @@ -73,11 +75,8 @@ public function testMapQueryString(callable $function): void self::assertSame('query', $queryParameter->in); self::assertSame($expectedModelProperty->property, $queryParameter->name); - $isQueryOptional = (Generator::UNDEFINED !== $expectedModelProperty->nullable && $expectedModelProperty->nullable) - || Generator::UNDEFINED !== $expectedModelProperty->default; - - self::assertSame($isQueryOptional, $queryParameter->allowEmptyValue); - self::assertSame(!$isQueryOptional, $queryParameter->required); + self::assertSame($expectations['optional'], $queryParameter->allowEmptyValue); + self::assertSame(!$expectations['optional'], $queryParameter->required); } } @@ -88,6 +87,73 @@ function ( #[MapQueryString] SymfonyDescriberMapQueryStringClass $parameter1, ) { }, + [ + 'optional' => false, + ], + ]; + + yield 'it documents a nullable type as optional' => [ + function ( + #[MapQueryString] ?SymfonyDescriberMapQueryStringClass $parameter1, + ) { + }, + [ + 'optional' => true, + ], + ]; + + yield 'it documents a default value as optional' => [ + function ( + #[MapQueryString] ?SymfonyDescriberMapQueryStringClass $parameter1, + ) { + }, + [ + 'optional' => true, + ], ]; } + + public function testItDescribesProperties(): void + { + $function = function ( + #[MapQueryString] DTO $DTO, + ) { + }; + + $parameter = new ReflectionParameter($function, 'DTO'); + + $this->symfonyMapQueryStringDescriber->describe( + $this->openApi, + $operation = new Operation([]), + $parameter + ); + + // Test it registers the model + $modelSchema = $this->openApi->components->schemas[0]; + $expectedModelProperties = DTO::getProperties(); + + self::assertEquals($expectedModelProperties, $modelSchema->properties); + + self::assertSame('id', $operation->parameters[0]->name); + self::assertSame('int', $operation->parameters[0]->schema->type); + + self::assertSame('name', $operation->parameters[1]->name); + + self::assertSame('nullableName', $operation->parameters[2]->name); + self::assertSame('string', $operation->parameters[2]->schema->type); + self::assertSame(false, $operation->parameters[2]->required); + self::assertSame(true, $operation->parameters[2]->schema->nullable); + + self::assertSame('nameWithExample', $operation->parameters[3]->name); + self::assertSame('string', $operation->parameters[3]->schema->type); + self::assertSame(true, $operation->parameters[3]->required); + self::assertSame(DTO::EXAMPLE_NAME, $operation->parameters[3]->schema->example); + self::assertSame(DTO::EXAMPLE_NAME, $operation->parameters[3]->example); + + self::assertSame('nameWithDescription', $operation->parameters[4]->name); + self::assertSame('string', $operation->parameters[4]->schema->type); + self::assertSame(true, $operation->parameters[4]->required); + self::assertSame(DTO::DESCRIPTION, $operation->parameters[4]->schema->description); + self::assertSame(DTO::DESCRIPTION, $operation->parameters[4]->description); + } } From 8000dc19c4324b91675dc0e4ba373f5c5ef393c2 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 03:37:36 +0200 Subject: [PATCH 063/120] Add SymfonyDescriberTest tests --- Tests/RouteDescriber/SymfonyDescriberTest.php | 103 +++++++++++++++--- 1 file changed, 89 insertions(+), 14 deletions(-) diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index f7dba2b48..10638baea 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -12,40 +12,115 @@ namespace Nelmio\ApiDocBundle\Tests\RouteDescriber; use Nelmio\ApiDocBundle\Model\ModelRegistry; -use Nelmio\ApiDocBundle\ModelDescriber\SelfDescribingModelDescriber; +use Nelmio\ApiDocBundle\OpenApiPhp\Util; +use Nelmio\ApiDocBundle\RouteDescriber\SymfonyAnnotationDescriber\SymfonyAnnotationDescriber; use Nelmio\ApiDocBundle\RouteDescriber\SymfonyDescriber; use OpenApi\Annotations\OpenApi; +use OpenApi\Annotations\Operation; use OpenApi\Context; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; -use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; +use ReflectionMethod; +use ReflectionParameter; +use Symfony\Component\Routing\Route; use const PHP_VERSION_ID; class SymfonyDescriberTest extends TestCase { + /** + * @var MockObject&SymfonyAnnotationDescriber + */ + private $symfonyAnnotationDescriberMock; + + /** + * @var SymfonyDescriber + */ private $symfonyDescriber; + /** + * @var ModelRegistry + */ + private $modelRegistry; + /** + * @var OpenApi + */ + private $openApi; + protected function setUp(): void { if (PHP_VERSION_ID < 80100) { self::markTestSkipped('Attributes require PHP 8'); } - if ( - !class_exists(MapRequestPayload::class) - && !class_exists(MapQueryParameter::class) - ) { - self::markTestSkipped('Symfony 6.3 attributes not found'); - } + $this->symfonyAnnotationDescriberMock = $this->createMock(SymfonyAnnotationDescriber::class); - $registry = new ModelRegistry( - [new SelfDescribingModelDescriber()], - new OpenApi(['_context' => new Context()]), + $this->modelRegistry = new ModelRegistry( + [], + $this->openApi = new OpenApi([]), [] ); - $this->symfonyDescriber = new SymfonyDescriber(); + $this->symfonyDescriber = new SymfonyDescriber( + [$this->symfonyAnnotationDescriberMock] + ); + + $this->symfonyDescriber->setModelRegistry($this->modelRegistry); + } + + public function testDescribe(): void + { + $reflectionParameter = $this->createStub(ReflectionParameter::class); + + $reflectionMethodStub = $this->createStub(ReflectionMethod::class); + $reflectionMethodStub->method('getParameters')->willReturn([$reflectionParameter]); + + $this->symfonyAnnotationDescriberMock + ->expects(self::exactly(count(Util::OPERATIONS))) + ->method('supports') + ->with($reflectionParameter) + ->willReturn(true) + ; + + $this->symfonyAnnotationDescriberMock + ->expects(self::exactly(count(Util::OPERATIONS))) + ->method('describe') + ->with( + $this->openApi, + self::isInstanceOf(Operation::class), + $reflectionParameter, + ) + ; + + $this->symfonyDescriber->describe( + $this->openApi, + new Route('/'), + $reflectionMethodStub, + ); + } + + public function testDescribeSkipsUnsupportedDescribers(): void + { + $reflectionParameter = $this->createStub(ReflectionParameter::class); + + $reflectionMethodStub = $this->createStub(ReflectionMethod::class); + $reflectionMethodStub->method('getParameters')->willReturn([$reflectionParameter]); + + $this->symfonyAnnotationDescriberMock + ->expects(self::exactly(count(Util::OPERATIONS))) + ->method('supports') + ->with($reflectionParameter) + ->willReturn(false) + ; - $this->symfonyDescriber->setModelRegistry($registry); + $this->symfonyAnnotationDescriberMock + ->expects(self::never()) + ->method('describe') + ; + + $this->symfonyDescriber->describe( + $this->openApi, + new Route('/'), + $reflectionMethodStub, + ); } } From 21bc561e83522f25fcb60028b94010bae6bcb811 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 03:38:11 +0200 Subject: [PATCH 064/120] Remove unused import --- Tests/RouteDescriber/SymfonyDescriberTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index 10638baea..851a1f03a 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -17,7 +17,6 @@ use Nelmio\ApiDocBundle\RouteDescriber\SymfonyDescriber; use OpenApi\Annotations\OpenApi; use OpenApi\Annotations\Operation; -use OpenApi\Context; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use ReflectionMethod; From 924cf153c59bec42118ae56a9982e6451e5e9174 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 03:41:03 +0200 Subject: [PATCH 065/120] Remove trailing commas --- Tests/RouteDescriber/SymfonyDescriberTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index 851a1f03a..fab4f508b 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -86,14 +86,14 @@ public function testDescribe(): void ->with( $this->openApi, self::isInstanceOf(Operation::class), - $reflectionParameter, + $reflectionParameter ) ; $this->symfonyDescriber->describe( $this->openApi, new Route('/'), - $reflectionMethodStub, + $reflectionMethodStub ); } @@ -119,7 +119,7 @@ public function testDescribeSkipsUnsupportedDescribers(): void $this->symfonyDescriber->describe( $this->openApi, new Route('/'), - $reflectionMethodStub, + $reflectionMethodStub ); } } From fa0c07cb175155b840e9c33e4f24b2c9148046f2 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 14:18:39 +0200 Subject: [PATCH 066/120] Copy ref --- .../SymfonyMapQueryStringDescriber.php | 1 + 1 file changed, 1 insertion(+) diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php index 378370aae..01fa95057 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php @@ -87,6 +87,7 @@ private function getConstructorReflectionParameterForProperty(ReflectionParamete private function copyPropertyValuesToQuery(OA\Parameter $parameter, OA\Property $property): void { $parameter->schema = Util::getChild($parameter, OA\Schema::class); + $parameter->schema->ref = $property->ref; $parameter->schema->title = $property->title; $parameter->schema->description = $property->description; $parameter->schema->type = $property->type; From 97485e82d994096becf01136c2b6c2b82e6bb4a1 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 14:18:57 +0200 Subject: [PATCH 067/120] Remove setting allowEmptyValue --- .../SymfonyMapQueryStringDescriber.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php index 01fa95057..970215e66 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php @@ -53,10 +53,6 @@ public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionPar || $constructorParameter?->isDefaultValueAvailable() || $isModelOptional; - if (Generator::UNDEFINED === $operationParameter->allowEmptyValue) { - $operationParameter->allowEmptyValue = $isQueryOptional; - } - if (Generator::UNDEFINED === $operationParameter->required) { $operationParameter->required = !$isQueryOptional; } From d37f119ebf952c2bbd4d956de454b4ff1733fe0d Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 14:27:21 +0200 Subject: [PATCH 068/120] Remove empty value test --- .../SymfonyMapQueryStringDescriberTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php index f596fc080..9cbcb7b22 100644 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php @@ -75,7 +75,6 @@ public function testMapQueryString(callable $function, array $expectations): voi self::assertSame('query', $queryParameter->in); self::assertSame($expectedModelProperty->property, $queryParameter->name); - self::assertSame($expectations['optional'], $queryParameter->allowEmptyValue); self::assertSame(!$expectations['optional'], $queryParameter->required); } } From b8de40f7a0b6ebb177503e51c9457f8bfc9fbcea Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 14:42:51 +0200 Subject: [PATCH 069/120] Update documentation --- Resources/doc/index.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Resources/doc/index.rst b/Resources/doc/index.rst index 3dde6d218..41d501ef3 100644 --- a/Resources/doc/index.rst +++ b/Resources/doc/index.rst @@ -7,7 +7,7 @@ OpenAPI (Swagger) format and provides a sandbox to interactively experiment with What's supported? ----------------- -This bundle supports *Symfony* route requirements, *Symfony* request mapping (`Symfony MapQueryParameter`_, `Symfony MapRequestPayload`_), PHP annotations, `Swagger-Php`_ annotations, +This bundle supports *Symfony* route requirements, *Symfony* request mapping (`Symfony MapQueryString`_, `Symfony MapQueryParameter`_, `Symfony MapRequestPayload`_), PHP annotations, `Swagger-Php`_ annotations, `FOSRestBundle`_ annotations and applications using `Api-Platform`_. .. _`Swagger-Php`: https://github.com/zircote/swagger-php @@ -241,7 +241,7 @@ The normal PHPDoc block on the controller method is used for the summary and des .. tip:: - **NelmioApiDocBundle** understand **symfony's** `Symfony MapQueryParameter`_ & `Symfony MapRequestPayload`_. + **NelmioApiDocBundle** understand **symfony's** `Symfony MapQueryString`_ , `Symfony MapQueryParameter`_ & `Symfony MapRequestPayload`_. Using these attributes inside your controller allows this bundle to automatically create the necessary documentation, these will automatically be mapped to their respective ``OA\Parameter`` & ``OA\RequestBody`` annotation/attribute. @@ -595,5 +595,6 @@ If you need more complex features, take a look at: .. _`JMS serializer`: https://jmsyst.com/libs/serializer .. _`Symfony form`: https://symfony.com/doc/current/forms.html .. _`Symfony serializer`: https://symfony.com/doc/current/components/serializer.html -.. _`Symfony MapRequestPayload`: https://symfony.com/doc/current/controller.html#mapping-request-payload +.. _`Symfony MapQueryString`: https://symfony.com/doc/current/controller.html#mapping-the-whole-query-string .. _`Symfony MapQueryParameter`: https://symfony.com/doc/current/controller.html#mapping-query-parameters-individually +.. _`Symfony MapRequestPayload`: https://symfony.com/doc/current/controller.html#mapping-request-payload From d5865ccc17449c895f9a59ec476cd279cb01666d Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 15:35:23 +0200 Subject: [PATCH 070/120] Merge documentation instead of overwriting --- .../SymfonyMapQueryStringDescriber.php | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php index 970215e66..f9bfabac4 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php @@ -46,20 +46,22 @@ public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionPar foreach ($schemaModel->properties as $property) { $constructorParameter = $this->getConstructorReflectionParameterForProperty($parameter, $property); - $operationParameter = Util::getOperationParameter($operation, $property->property, 'query'); - $this->copyPropertyValuesToQuery($operationParameter, $property); + $newParameter = $this->createParameterFromProperty($property); $isQueryOptional = (Generator::UNDEFINED !== $property->nullable && $property->nullable) || $constructorParameter?->isDefaultValueAvailable() || $isModelOptional; - if (Generator::UNDEFINED === $operationParameter->required) { - $operationParameter->required = !$isQueryOptional; + if (Generator::UNDEFINED === $newParameter->required) { + $newParameter->required = !$isQueryOptional; } - if (Generator::UNDEFINED === $operationParameter->example && $constructorParameter?->isDefaultValueAvailable()) { - $operationParameter->example = $constructorParameter->getDefaultValue(); + if (Generator::UNDEFINED === $newParameter->example && $constructorParameter?->isDefaultValueAvailable()) { + $newParameter->example = $constructorParameter->getDefaultValue(); } + + $operationParameter = Util::getOperationParameter($operation, $property->property, 'query'); + $operationParameter->mergeProperties($newParameter); } } @@ -80,8 +82,9 @@ private function getConstructorReflectionParameterForProperty(ReflectionParamete return null; } - private function copyPropertyValuesToQuery(OA\Parameter $parameter, OA\Property $property): void + private function createParameterFromProperty(OA\Property $property): OA\Parameter { + $parameter = new OA\Parameter([]); $parameter->schema = Util::getChild($parameter, OA\Schema::class); $parameter->schema->ref = $property->ref; $parameter->schema->title = $property->title; @@ -104,5 +107,7 @@ private function copyPropertyValuesToQuery(OA\Parameter $parameter, OA\Property $parameter->required = $parameter->schema->required; $parameter->deprecated = $parameter->schema->deprecated; $parameter->example = $parameter->schema->example; + + return $parameter; } } From 4de6671b62d13689b5b80f677569ce326f4a5b61 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 16:14:04 +0200 Subject: [PATCH 071/120] Expand symfony controller mapping attribute documentation --- Resources/doc/index.rst | 17 +-- Resources/doc/symfony_attributes.rst | 159 +++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 12 deletions(-) create mode 100644 Resources/doc/symfony_attributes.rst diff --git a/Resources/doc/index.rst b/Resources/doc/index.rst index 41d501ef3..bd9800115 100644 --- a/Resources/doc/index.rst +++ b/Resources/doc/index.rst @@ -7,7 +7,7 @@ OpenAPI (Swagger) format and provides a sandbox to interactively experiment with What's supported? ----------------- -This bundle supports *Symfony* route requirements, *Symfony* request mapping (`Symfony MapQueryString`_, `Symfony MapQueryParameter`_, `Symfony MapRequestPayload`_), PHP annotations, `Swagger-Php`_ annotations, +This bundle supports *Symfony* route requirements, *Symfony* request mapping (:doc:`symfony_attributes`), PHP annotations, `Swagger-Php`_ annotations, `FOSRestBundle`_ annotations and applications using `Api-Platform`_. .. _`Swagger-Php`: https://github.com/zircote/swagger-php @@ -241,14 +241,9 @@ The normal PHPDoc block on the controller method is used for the summary and des .. tip:: - **NelmioApiDocBundle** understand **symfony's** `Symfony MapQueryString`_ , `Symfony MapQueryParameter`_ & `Symfony MapRequestPayload`_. - Using these attributes inside your controller allows this bundle to automatically create the necessary documentation, - these will automatically be mapped to their respective ``OA\Parameter`` & ``OA\RequestBody`` annotation/attribute. - -.. versionadded:: 6.3 - - The :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapRequestPayload` and :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapQueryParameter` attributes - were introduced in Symfony 6.3. + **NelmioApiDocBundle** understands **symfony's** controller attributes. + Using these attributes inside your controller allows this bundle to automatically create the necessary documentation. + More information can be found here: :doc:`symfony_attributes`. Use Models ---------- @@ -587,6 +582,7 @@ If you need more complex features, take a look at: commands faq security + symfony_attributes .. _`SwaggerPHP examples`: https://github.com/zircote/swagger-php/tree/master/Examples .. _`Symfony PropertyInfo component`: https://symfony.com/doc/current/components/property_info.html @@ -595,6 +591,3 @@ If you need more complex features, take a look at: .. _`JMS serializer`: https://jmsyst.com/libs/serializer .. _`Symfony form`: https://symfony.com/doc/current/forms.html .. _`Symfony serializer`: https://symfony.com/doc/current/components/serializer.html -.. _`Symfony MapQueryString`: https://symfony.com/doc/current/controller.html#mapping-the-whole-query-string -.. _`Symfony MapQueryParameter`: https://symfony.com/doc/current/controller.html#mapping-query-parameters-individually -.. _`Symfony MapRequestPayload`: https://symfony.com/doc/current/controller.html#mapping-request-payload diff --git a/Resources/doc/symfony_attributes.rst b/Resources/doc/symfony_attributes.rst new file mode 100644 index 000000000..5207a9649 --- /dev/null +++ b/Resources/doc/symfony_attributes.rst @@ -0,0 +1,159 @@ +Symfony attributes +================================ + +NelmioApiDocBundle has the ability to automatically create documentation from **symfony** controller attributes. + +MapQueryString +------------------------------- + +Using the `Symfony MapQueryString`_ attribute allows NelmioApiDocBundle to automatically generate your query parameter documentation for your endpoint from your object. + +.. versionadded:: 6.3 + + The :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapQueryString` attribute was introduced in Symfony 6.3. + +Modify generated documentation +~~~~~~~ + +Modifying the generated documentation can easily by done in two ways, by:: +- Customizing the documentation of an object's property (``#[OA\Property]`` attribute) +- Customizing the documentation of a query parameter (``#[OA\Parameter]`` attribute) + +Customizing the documentation of a specific query parameter can be done by adding the ``#[OA\Parameter]`` attribute to your controller method. +Make sure that the ``in`` property is set to ``'query'`` and that the ``name`` property is set to the object's property name which you want to customize. + + .. code-block:: php-attributes + + #[OA\Parameter( + name: 'id', + description: 'Some additional parameter description', + in: 'query', + )] + +MapQueryParameter +------------------------------- + +Using the `Symfony MapQueryParameter`_ attribute allows NelmioApiDocBundle to automatically generate your query parameter documentation for your endpoint. + +.. versionadded:: 6.3 + + The :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapQueryParameter` attribute was introduced in Symfony 6.3. + + +Modify generated documentation +~~~~~~~ + +Customizing the documentation of the query parameter can be done by adding the ``#[OA\Parameter]`` attribute to your controller method. +Make sure that the ``in`` property is set to ``'query'`` and that the ``name`` property is set to the name of the controller method parameter. + + .. code-block:: php-attributes + + #[OA\Parameter( + name: 'id', + description: 'Some additional parameter description', + in: 'query', + )] + +MapRequestPayload +------------------------------- + +Using the `Symfony MapRequestPayload`_ attribute allows NelmioApiDocBundle to automatically generate your request body documentation for your endpoint. + +.. versionadded:: 6.3 + + The :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapRequestPayload` attribute was introduced in Symfony 6.3. + + +Modify generated documentation +~~~~~~~ + +Customizing the documentation of the request body can be done by adding the ``#[OA\RequestBody]`` attribute to your controller method. + + .. code-block:: php-attributes + + #[OA\RequestBody( + groups: ["create"], + ) + +Complete example +---------------------- + .. code-block:: php-attributes + class UserQuery + { + public int $userId; + } + + .. code-block:: php-attributes + + use Symfony\Component\Serializer\Annotation\Groups; + use Symfony\Component\Validator\Constraints as Assert; + + class UserDto + { + #[Groups(["default", "create", "update"])] + #[Assert\NotBlank(groups: ["default", "create"])] + public string $username; + } + + .. code-block:: php-attributes + + namespace AppBundle\Controller; + + use AppBundle\UserQuery; + use AppBundle\UserDTO; + use Nelmio\ApiDocBundle\Annotation\Model; + use Nelmio\ApiDocBundle\Annotation\Security; + use OpenApi\Attributes as OA; + use Symfony\Component\Routing\Annotation\Route; + + class UserController + { + /** + * Find user with MapQueryString. + */ + #[Route('/api/users', methods: ['GET'])] + #[OA\Parameter( + name: 'userId', + description: 'Id of the user to find', + in: 'query', + )] + public function findUser(#[MapQueryString] UserQuery $userQuery) + { + // ... + } + + /** + * Find user with MapQueryParameter. + */ + #[Route('/api/users/v2', methods: ['GET'])] + #[OA\Parameter( + name: 'userId', + description: 'Id of the user to find', + in: 'query', + )] + public function findUserV2(#[MapQueryParameter] int $userId) + { + // ... + } + + /** + * Create a new user. + */ + #[Route('/api/users', methods: ['POST'])] + #[OA\RequestBody( + groups: ['create'], + )] + public function createUser(#[MapRequestPayload] UserDTO $user) + { + // ... + } + } + +Disclaimer +---------------------- + +Make sure to use at least php 8 (annotations) to make use of this functionality + +.. _`Symfony MapQueryString`: https://symfony.com/doc/current/controller.html#mapping-the-whole-query-string +.. _`Symfony MapQueryParameter`: https://symfony.com/doc/current/controller.html#mapping-query-parameters-individually +.. _`Symfony MapRequestPayload`: https://symfony.com/doc/current/controller.html#mapping-request-payload From 112d67df5b1a2f22117463268c8cc50180fc75ac Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 16:18:28 +0200 Subject: [PATCH 072/120] Fix RST --- Resources/doc/symfony_attributes.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Resources/doc/symfony_attributes.rst b/Resources/doc/symfony_attributes.rst index 5207a9649..06624a67f 100644 --- a/Resources/doc/symfony_attributes.rst +++ b/Resources/doc/symfony_attributes.rst @@ -15,9 +15,9 @@ Using the `Symfony MapQueryString`_ attribute allows NelmioApiDocBundle to autom Modify generated documentation ~~~~~~~ -Modifying the generated documentation can easily by done in two ways, by:: -- Customizing the documentation of an object's property (``#[OA\Property]`` attribute) -- Customizing the documentation of a query parameter (``#[OA\Parameter]`` attribute) +Modifying the generated documentation can easily by done in two ways, by: +* Customizing the documentation of an object's property (``#[OA\Property]`` attribute) +* Customizing the documentation of a query parameter (``#[OA\Parameter]`` attribute) Customizing the documentation of a specific query parameter can be done by adding the ``#[OA\Parameter]`` attribute to your controller method. Make sure that the ``in`` property is set to ``'query'`` and that the ``name`` property is set to the object's property name which you want to customize. @@ -78,6 +78,7 @@ Customizing the documentation of the request body can be done by adding the ``#[ Complete example ---------------------- .. code-block:: php-attributes + class UserQuery { public int $userId; @@ -99,10 +100,8 @@ Complete example namespace AppBundle\Controller; - use AppBundle\UserQuery; use AppBundle\UserDTO; - use Nelmio\ApiDocBundle\Annotation\Model; - use Nelmio\ApiDocBundle\Annotation\Security; + use AppBundle\UserQuery; use OpenApi\Attributes as OA; use Symfony\Component\Routing\Annotation\Route; From d0db0ed4e222bebff8659e39cab9c6cc8e83b566 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 12 Aug 2023 16:19:35 +0200 Subject: [PATCH 073/120] Fix RST (missing blank line) --- Resources/doc/symfony_attributes.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/Resources/doc/symfony_attributes.rst b/Resources/doc/symfony_attributes.rst index 06624a67f..f30e7100a 100644 --- a/Resources/doc/symfony_attributes.rst +++ b/Resources/doc/symfony_attributes.rst @@ -77,6 +77,7 @@ Customizing the documentation of the request body can be done by adding the ``#[ Complete example ---------------------- + .. code-block:: php-attributes class UserQuery From 0a4a1a6e04102510135acfc42a5162e8b473add3 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sun, 13 Aug 2023 15:47:59 +0200 Subject: [PATCH 074/120] Revert max self deprecations --- phpunit.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0ddb09e34..695969160 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -14,7 +14,7 @@ - + From 8f62e194bc7909da19729df5158db561b14ab11f Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sun, 13 Aug 2023 16:47:34 +0200 Subject: [PATCH 075/120] Use modelDescriber to describe model instead of registering all models --- Resources/config/symfony.xml | 4 +++- .../SymfonyMapQueryStringDescriber.php | 24 ++++++++++++++++++- .../SymfonyMapQueryStringDescriberTest.php | 4 ++-- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/Resources/config/symfony.xml b/Resources/config/symfony.xml index e243a439c..0a9c6fe5e 100644 --- a/Resources/config/symfony.xml +++ b/Resources/config/symfony.xml @@ -8,7 +8,9 @@ - + + + diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php index f9bfabac4..a30868367 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php @@ -7,6 +7,7 @@ use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; use Nelmio\ApiDocBundle\Model\Model; +use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface; use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; use OpenApi\Generator; @@ -19,6 +20,14 @@ final class SymfonyMapQueryStringDescriber implements SymfonyAnnotationDescriber { use ModelRegistryAwareTrait; + /** + * @param ModelDescriberInterface[] $modelDescribers + */ + public function __construct( + private iterable $modelDescribers, + ) { + } + public function supports(ReflectionParameter $parameter): bool { if (!SymfonyAnnotationHelper::getAttribute($parameter, MapQueryString::class)) { @@ -32,11 +41,24 @@ public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionPar { $model = new Model(new Type(Type::BUILTIN_TYPE_OBJECT, $parameter->allowsNull(), $parameter->getType()->getName())); $modelRef = $this->modelRegistry->register($model); - $this->modelRegistry->registerSchemas(); $nativeModelName = str_replace(OA\Components::SCHEMA_REF, '', $modelRef); $schemaModel = Util::getSchema($api, $nativeModelName); + + foreach ($this->modelDescribers as $modelDescriber) { + if ($modelDescriber instanceof ModelRegistryAwareInterface) { + $modelDescriber->setModelRegistry($this->modelRegistry); + } + + if ($modelDescriber->supports($model)) { + $modelDescriber->describe($model, $schemaModel); + + break; + } + } + + // There are no properties to map to query parameters if (Generator::UNDEFINED === $schemaModel->properties) { return; } diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php index 9cbcb7b22..79d9c8ee1 100644 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php @@ -39,9 +39,9 @@ protected function setUp(): void $this->openApi = new OpenApi([]); - $this->symfonyMapQueryStringDescriber = new SymfonyMapQueryStringDescriber(); + $this->symfonyMapQueryStringDescriber = new SymfonyMapQueryStringDescriber([new SelfDescribingModelDescriber()]); - $registry = new ModelRegistry([new SelfDescribingModelDescriber()], $this->openApi, []); + $registry = new ModelRegistry([], $this->openApi, []); $this->symfonyMapQueryStringDescriber->setModelRegistry($registry); } From 81de1e46f1c108001e5f8463f7e9b9abd60d03bb Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sun, 13 Aug 2023 17:05:49 +0200 Subject: [PATCH 076/120] Create weak context --- .../SymfonyMapQueryStringDescriber.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php index a30868367..80da0b9ca 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php @@ -106,7 +106,7 @@ private function getConstructorReflectionParameterForProperty(ReflectionParamete private function createParameterFromProperty(OA\Property $property): OA\Parameter { - $parameter = new OA\Parameter([]); + $parameter = new OA\Parameter(['_context' => Util::createWeakContext($property->_context)]); $parameter->schema = Util::getChild($parameter, OA\Schema::class); $parameter->schema->ref = $property->ref; $parameter->schema->title = $property->title; From ea02d6ee0a8e17464b8efcf2aa556a1aa097c38f Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sun, 13 Aug 2023 17:54:59 +0200 Subject: [PATCH 077/120] Get schema from property instead of manually setting every property --- .../SymfonyMapQueryStringDescriber.php | 44 +++++-------------- 1 file changed, 12 insertions(+), 32 deletions(-) diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php index 80da0b9ca..ea7661f6a 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php @@ -68,22 +68,20 @@ public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionPar foreach ($schemaModel->properties as $property) { $constructorParameter = $this->getConstructorReflectionParameterForProperty($parameter, $property); - $newParameter = $this->createParameterFromProperty($property); + $operationParameter = Util::getOperationParameter($operation, $property->property, 'query'); + $this->addParameterValuesFromProperty($operationParameter, $property); $isQueryOptional = (Generator::UNDEFINED !== $property->nullable && $property->nullable) || $constructorParameter?->isDefaultValueAvailable() || $isModelOptional; - if (Generator::UNDEFINED === $newParameter->required) { - $newParameter->required = !$isQueryOptional; + if (Generator::UNDEFINED === $operationParameter->required) { + $operationParameter->required = !$isQueryOptional; } - if (Generator::UNDEFINED === $newParameter->example && $constructorParameter?->isDefaultValueAvailable()) { - $newParameter->example = $constructorParameter->getDefaultValue(); + if (Generator::UNDEFINED === $operationParameter->example && $constructorParameter?->isDefaultValueAvailable()) { + $operationParameter->example = $constructorParameter->getDefaultValue(); } - - $operationParameter = Util::getOperationParameter($operation, $property->property, 'query'); - $operationParameter->mergeProperties($newParameter); } } @@ -104,32 +102,14 @@ private function getConstructorReflectionParameterForProperty(ReflectionParamete return null; } - private function createParameterFromProperty(OA\Property $property): OA\Parameter + private function addParameterValuesFromProperty(OA\Parameter $parameter, OA\Property $property): void { - $parameter = new OA\Parameter(['_context' => Util::createWeakContext($property->_context)]); - $parameter->schema = Util::getChild($parameter, OA\Schema::class); - $parameter->schema->ref = $property->ref; - $parameter->schema->title = $property->title; - $parameter->schema->description = $property->description; - $parameter->schema->type = $property->type; - $parameter->schema->items = $property->items; - $parameter->schema->example = $property->example; - $parameter->schema->nullable = $property->nullable; - $parameter->schema->enum = $property->enum; - $parameter->schema->default = $property->default; - $parameter->schema->minimum = $property->minimum; - $parameter->schema->exclusiveMinimum = $property->exclusiveMinimum; - $parameter->schema->maximum = $property->maximum; - $parameter->schema->exclusiveMaximum = $property->exclusiveMaximum; - $parameter->schema->required = $property->required; - $parameter->schema->deprecated = $property->deprecated; + $parameter->schema = $property; $parameter->name = $property->property; - $parameter->description = $parameter->schema->description; - $parameter->required = $parameter->schema->required; - $parameter->deprecated = $parameter->schema->deprecated; - $parameter->example = $parameter->schema->example; - - return $parameter; + $parameter->description = $property->description; + $parameter->required = $property->required; + $parameter->deprecated = $property->deprecated; + $parameter->example = $property->example; } } From 0f1a43a7035a4db725fcd6dd373a0b90dc06e027 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sun, 13 Aug 2023 17:57:50 +0200 Subject: [PATCH 078/120] Add newline at end of file --- Tests/Functional/Resources/routes.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Functional/Resources/routes.yaml b/Tests/Functional/Resources/routes.yaml index 9b52786a0..4a60c6a2f 100644 --- a/Tests/Functional/Resources/routes.yaml +++ b/Tests/Functional/Resources/routes.yaml @@ -42,4 +42,4 @@ doc_json: doc_yaml: path: /{area}/docs.yaml - controller: nelmio_api_doc.controller.swagger_yaml \ No newline at end of file + controller: nelmio_api_doc.controller.swagger_yaml From 31c06c1de39fbe44c97f596494a47ddea05fed7b Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sun, 13 Aug 2023 18:12:55 +0200 Subject: [PATCH 079/120] Fix style --- .../SymfonyMapQueryStringDescriber.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php index ea7661f6a..2e241426b 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php @@ -102,7 +102,7 @@ private function getConstructorReflectionParameterForProperty(ReflectionParamete return null; } - private function addParameterValuesFromProperty(OA\Parameter $parameter, OA\Property $property): void + private function addParameterValuesFromProperty(OA\Parameter $parameter, OA\Property $property): void { $parameter->schema = $property; From a346209deaa8d82d2455e0dff27c8c231524aa6b Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sun, 13 Aug 2023 19:13:23 +0200 Subject: [PATCH 080/120] Prevent overwriting non-default values --- .../SymfonyMapQueryStringDescriber.php | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php index 2e241426b..c3bae6580 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php @@ -75,12 +75,10 @@ public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionPar || $constructorParameter?->isDefaultValueAvailable() || $isModelOptional; - if (Generator::UNDEFINED === $operationParameter->required) { - $operationParameter->required = !$isQueryOptional; - } + $this->overwriteParameterValue($operationParameter, 'required', !$isQueryOptional); - if (Generator::UNDEFINED === $operationParameter->example && $constructorParameter?->isDefaultValueAvailable()) { - $operationParameter->example = $constructorParameter->getDefaultValue(); + if ($constructorParameter?->isDefaultValueAvailable()) { + $this->overwriteParameterValue($operationParameter, 'example', $constructorParameter->getDefaultValue()); } } } @@ -104,12 +102,20 @@ private function getConstructorReflectionParameterForProperty(ReflectionParamete private function addParameterValuesFromProperty(OA\Parameter $parameter, OA\Property $property): void { - $parameter->schema = $property; + $this->overwriteParameterValue($parameter, 'schema', $property); + $this->overwriteParameterValue($parameter, 'name', $property->property); + $this->overwriteParameterValue($parameter, 'description', $property->description); + $this->overwriteParameterValue($parameter, 'required', $property->required); + $this->overwriteParameterValue($parameter, 'deprecated', $property->deprecated); + $this->overwriteParameterValue($parameter, 'example', $property->example); + } + + private function overwriteParameterValue(OA\Parameter $parameter, string $property, $value): void + { + if (!Generator::isDefault($parameter->{$property})) { + return; + } - $parameter->name = $property->property; - $parameter->description = $property->description; - $parameter->required = $property->required; - $parameter->deprecated = $property->deprecated; - $parameter->example = $property->example; + $parameter->{$property} = $value; } } From 703a5b23f5733ffd271f296a84fd2f3a2b5dc9f8 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sun, 13 Aug 2023 19:14:18 +0200 Subject: [PATCH 081/120] Add functional test for MapQueryString --- .../Functional/Controller/ApiController81.php | 43 +++++++ .../Entity/SymfonyMapQueryString.php | 16 +++ Tests/Functional/SymfonyFunctionalTest.php | 120 ++++++++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 Tests/Functional/Entity/SymfonyMapQueryString.php create mode 100644 Tests/Functional/SymfonyFunctionalTest.php diff --git a/Tests/Functional/Controller/ApiController81.php b/Tests/Functional/Controller/ApiController81.php index b12f5cd06..9b1c275b5 100644 --- a/Tests/Functional/Controller/ApiController81.php +++ b/Tests/Functional/Controller/ApiController81.php @@ -16,7 +16,9 @@ use Nelmio\ApiDocBundle\Annotation\Security; use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article; use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article81; +use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyMapQueryString; use OpenApi\Attributes as OA; +use Symfony\Component\HttpKernel\Attribute\MapQueryString; use Symfony\Component\Routing\Annotation\Route; class ApiController81 extends ApiController80 @@ -72,4 +74,45 @@ public function inlinePathParameters( public function enum() { } + + #[Route('/article_map_query_string')] + #[OA\Response(response: '200', description: '')] + public function fetchArticleFromMapQueryString( + #[MapQueryString] SymfonyMapQueryString $article81Query + ) { + } + + #[Route('/article_map_query_string_nullable')] + #[OA\Response(response: '200', description: '')] + public function fetchArticleFromMapQueryStringNullable( + #[MapQueryString] ?SymfonyMapQueryString $article81Query + ) { + } + + #[Route('/article_map_query_string_overwrite_parameters')] + #[OA\Parameter( + name: 'id', + in: 'query', + description: 'Query parameter id description' + )] + #[OA\Parameter( + name: 'name', + in: 'query', + description: 'Query parameter name description' + )] + #[OA\Parameter( + name: 'nullableName', + in: 'query', + description: 'Query parameter nullableName description' + )] + #[OA\Parameter( + name: 'article81Enum', + in: 'query', + description: 'Query parameter article81Enum description' + )] + #[OA\Response(response: '200', description: '')] + public function fetchArticleFromMapQueryStringOverwriteParameters( + #[MapQueryString] SymfonyMapQueryString $article81Query + ) { + } } diff --git a/Tests/Functional/Entity/SymfonyMapQueryString.php b/Tests/Functional/Entity/SymfonyMapQueryString.php new file mode 100644 index 000000000..09cc93f0c --- /dev/null +++ b/Tests/Functional/Entity/SymfonyMapQueryString.php @@ -0,0 +1,16 @@ + 'api.example.com']); + } + + public function testMapQueryStringModelGetsCreated(): void + { + if (!class_exists(MapQueryString::class)) { + self::markTestSkipped('Symfony 6.3 MapQueryString attribute not found'); + } + + $expected = [ + 'schema' => 'SymfonyMapQueryString', + 'properties' => [ + 'id' => [ + 'type' => 'integer', + ], + 'name' => [ + 'type' => 'string', + ], + 'nullableName' => [ + 'type' => 'string', + 'nullable' => true, + ], + 'article81Enum' => [ + '$ref' => '#/components/schemas/Article81', + ], + ], + 'type' => 'object', + ]; + + $this->assertSame($expected, json_decode($this->getModel('SymfonyMapQueryString')->toJson(), true)); + } + + public function testMapQueryString(): void + { + if (!class_exists(MapQueryString::class)) { + self::markTestSkipped('Symfony 6.3 MapQueryString attribute not found'); + } + + $operation = $this->getOperation('/api/article_map_query_string', 'get'); + + $in = 'query'; + + $parameter = $this->getParameter($operation, 'id', $in); + $this->assertTrue($parameter->required); + + $parameter = $this->getParameter($operation, 'name', $in); + $this->assertTrue($parameter->required); + + $parameter = $this->getParameter($operation, 'nullableName', $in); + $this->assertFalse($parameter->required); + + $parameter = $this->getParameter($operation, 'article81Enum', $in); + + $property = $this->getProperty($this->getModel('SymfonyMapQueryString'), 'article81Enum'); + $this->assertTrue($parameter->required); + $this->assertSame($property, $parameter->schema); + } + + public function testMapQueryStringParametersAreOptional(): void + { + if (!class_exists(MapQueryString::class)) { + self::markTestSkipped('Symfony 6.3 MapQueryString attribute not found'); + } + + $operation = $this->getOperation('/api/article_map_query_string_nullable', 'get'); + + $in = 'query'; + + $parameter = $this->getParameter($operation, 'id', $in); + $this->assertFalse($parameter->required); + + $parameter = $this->getParameter($operation, 'name', $in); + $this->assertFalse($parameter->required); + + $parameter = $this->getParameter($operation, 'nullableName', $in); + $this->assertFalse($parameter->required); + + $parameter = $this->getParameter($operation, 'article81Enum', $in); + $this->assertFalse($parameter->required); + } + + public function testMapQueryStringParametersOverwriteParameters(): void + { + if (!class_exists(MapQueryString::class)) { + self::markTestSkipped('Symfony 6.3 MapQueryString attribute not found'); + } + + $operation = $this->getOperation('/api/article_map_query_string_overwrite_parameters', 'get'); + + foreach (['id', 'name', 'nullableName', 'article81Enum'] as $name) { + $parameter = $this->getParameter($operation, $name, 'query'); + $this->assertSame($parameter->description, sprintf('Query parameter %s description', $name)); + } + } +} From a97579ba15c0a6d4a7ef74d39db79b1a5230f2cd Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sun, 13 Aug 2023 19:47:49 +0200 Subject: [PATCH 082/120] Use modifyAnnotationValue helper method instead of overwriting --- .../SymfonyAnnotationHelper.php | 17 +++++++++---- .../SymfonyMapQueryParameterDescriber.php | 9 +++---- .../SymfonyMapQueryStringDescriber.php | 25 ++++++------------- .../SymfonyMapRequestPayloadDescriber.php | 10 ++++---- 4 files changed, 29 insertions(+), 32 deletions(-) diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationHelper.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationHelper.php index 0a378e00d..7444ba662 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationHelper.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationHelper.php @@ -29,13 +29,20 @@ public static function getAttribute(ReflectionParameter $parameter, string $attr public static function describeCommonSchemaFromParameter(OA\Schema $schema, ReflectionParameter $parameter): void { if ($parameter->isDefaultValueAvailable()) { - $schema->default = $parameter->getDefaultValue(); + self::modifyAnnotationValue($schema, 'default', $parameter->getDefaultValue()); } - if (Generator::UNDEFINED === $schema->type) { - if ($parameter->getType()->isBuiltin()) { - $schema->type = $parameter->getType()->getName(); - } + if ($parameter->getType()->isBuiltin()) { + self::modifyAnnotationValue($schema, 'type', $parameter->getType()->getName()); } } + + public static function modifyAnnotationValue(OA\AbstractAnnotation $parameter, string $property, $value): void + { + if (!Generator::isDefault($parameter->{$property})) { + return; + } + + $parameter->{$property} = $value; + } } diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php index a3700f784..ba8a200c1 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php @@ -25,16 +25,15 @@ public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionPar $attribute = SymfonyAnnotationHelper::getAttribute($parameter, MapQueryParameter::class); $operationParameter = Util::getOperationParameter($operation, $parameter->getName(), 'query'); - $operationParameter->name = $attribute->name ?? $parameter->getName(); - $operationParameter->allowEmptyValue = $parameter->allowsNull(); - - $operationParameter->required = !$parameter->isDefaultValueAvailable() && !$parameter->allowsNull(); + SymfonyAnnotationHelper::modifyAnnotationValue($operationParameter, 'name', $attribute->name ?? $parameter->getName()); + SymfonyAnnotationHelper::modifyAnnotationValue($operationParameter, 'allowEmptyValue', $parameter->allowsNull()); + SymfonyAnnotationHelper::modifyAnnotationValue($operationParameter, 'required', !$parameter->isDefaultValueAvailable() && !$parameter->allowsNull()); /** @var OA\Schema $schema */ $schema = Util::getChild($operationParameter, OA\Schema::class); if (FILTER_VALIDATE_REGEXP === $attribute->filter) { - $schema->pattern = $attribute->options['regexp']; + SymfonyAnnotationHelper::modifyAnnotationValue($schema, 'pattern', $attribute->options['regexp']); } SymfonyAnnotationHelper::describeCommonSchemaFromParameter($schema, $parameter); diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php index c3bae6580..f8cf9c9e8 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php @@ -75,10 +75,10 @@ public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionPar || $constructorParameter?->isDefaultValueAvailable() || $isModelOptional; - $this->overwriteParameterValue($operationParameter, 'required', !$isQueryOptional); + SymfonyAnnotationHelper::modifyAnnotationValue($operationParameter, 'required', !$isQueryOptional); if ($constructorParameter?->isDefaultValueAvailable()) { - $this->overwriteParameterValue($operationParameter, 'example', $constructorParameter->getDefaultValue()); + SymfonyAnnotationHelper::modifyAnnotationValue($operationParameter, 'example', $constructorParameter->getDefaultValue()); } } } @@ -102,20 +102,11 @@ private function getConstructorReflectionParameterForProperty(ReflectionParamete private function addParameterValuesFromProperty(OA\Parameter $parameter, OA\Property $property): void { - $this->overwriteParameterValue($parameter, 'schema', $property); - $this->overwriteParameterValue($parameter, 'name', $property->property); - $this->overwriteParameterValue($parameter, 'description', $property->description); - $this->overwriteParameterValue($parameter, 'required', $property->required); - $this->overwriteParameterValue($parameter, 'deprecated', $property->deprecated); - $this->overwriteParameterValue($parameter, 'example', $property->example); - } - - private function overwriteParameterValue(OA\Parameter $parameter, string $property, $value): void - { - if (!Generator::isDefault($parameter->{$property})) { - return; - } - - $parameter->{$property} = $value; + SymfonyAnnotationHelper::modifyAnnotationValue($parameter, 'schema', $property); + SymfonyAnnotationHelper::modifyAnnotationValue($parameter, 'name', $property->property); + SymfonyAnnotationHelper::modifyAnnotationValue($parameter, 'description', $property->description); + SymfonyAnnotationHelper::modifyAnnotationValue($parameter, 'required', $property->required); + SymfonyAnnotationHelper::modifyAnnotationValue($parameter, 'deprecated', $property->deprecated); + SymfonyAnnotationHelper::modifyAnnotationValue($parameter, 'example', $property->example); } } diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php index 2eb03bea3..e31160b59 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php @@ -5,9 +5,9 @@ namespace Nelmio\ApiDocBundle\RouteDescriber\SymfonyAnnotationDescriber; use InvalidArgumentException; +use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; -use OpenApi\Generator; use ReflectionParameter; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; @@ -41,8 +41,8 @@ public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionPar private function describeRequestBody(OA\RequestBody $requestBody, ReflectionParameter $parameter, string $format): void { $contentSchema = $this->getContentSchemaForType($requestBody, $format); - $contentSchema->ref = new \Nelmio\ApiDocBundle\Annotation\Model(type: $parameter->getType()->getName()); - $contentSchema->type = 'object'; + SymfonyAnnotationHelper::modifyAnnotationValue($contentSchema, 'ref', new Model(type: $parameter->getType()->getName())); + SymfonyAnnotationHelper::modifyAnnotationValue($contentSchema, 'type', 'object'); $schema = Util::getProperty($contentSchema, $parameter->getName()); @@ -51,7 +51,7 @@ private function describeRequestBody(OA\RequestBody $requestBody, ReflectionPara private function getContentSchemaForType(OA\RequestBody $requestBody, string $type): OA\Schema { - $requestBody->content = Generator::UNDEFINED !== $requestBody->content ? $requestBody->content : []; + SymfonyAnnotationHelper::modifyAnnotationValue($requestBody, 'content', []); switch ($type) { case 'json': $contentType = 'application/json'; @@ -79,7 +79,7 @@ private function getContentSchemaForType(OA\RequestBody $requestBody, string $ty $requestBody->content[$contentType], OA\Schema::class ); - $schema->type = 'object'; + SymfonyAnnotationHelper::modifyAnnotationValue($schema, 'type', 'object'); } return Util::getChild( From 868f559e36e6f37b31960834ef289194f0f7457d Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sun, 13 Aug 2023 19:55:02 +0200 Subject: [PATCH 083/120] Fix incorrect name is used for query --- .../SymfonyMapQueryParameterDescriber.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php index ba8a200c1..abd310764 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php @@ -24,8 +24,8 @@ public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionPar { $attribute = SymfonyAnnotationHelper::getAttribute($parameter, MapQueryParameter::class); - $operationParameter = Util::getOperationParameter($operation, $parameter->getName(), 'query'); - SymfonyAnnotationHelper::modifyAnnotationValue($operationParameter, 'name', $attribute->name ?? $parameter->getName()); + $operationParameter = Util::getOperationParameter($operation, $attribute->name ?? $parameter->getName(), 'query'); + SymfonyAnnotationHelper::modifyAnnotationValue($operationParameter, 'allowEmptyValue', $parameter->allowsNull()); SymfonyAnnotationHelper::modifyAnnotationValue($operationParameter, 'required', !$parameter->isDefaultValueAvailable() && !$parameter->allowsNull()); From d4ca40ac0cbd7ead304891b4e51db2cbb5bdf798 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sun, 13 Aug 2023 20:02:54 +0200 Subject: [PATCH 084/120] Transform int to integer --- .../SymfonyAnnotationHelper.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationHelper.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationHelper.php index 7444ba662..d1ad59c81 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationHelper.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationHelper.php @@ -33,7 +33,13 @@ public static function describeCommonSchemaFromParameter(OA\Schema $schema, Refl } if ($parameter->getType()->isBuiltin()) { - self::modifyAnnotationValue($schema, 'type', $parameter->getType()->getName()); + $type = $parameter->getType()->getName(); + + if (in_array($type, ['int', 'integer'], true)) { + $type = 'integer'; + } + + self::modifyAnnotationValue($schema, 'type', $type); } } From ec856d7d8f92f332aca082b31bac90bd89f01a14 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sun, 13 Aug 2023 20:03:50 +0200 Subject: [PATCH 085/120] Remove allowEmptyValue --- .../SymfonyMapQueryParameterDescriber.php | 1 - 1 file changed, 1 deletion(-) diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php index abd310764..c4d3ab55c 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php @@ -26,7 +26,6 @@ public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionPar $operationParameter = Util::getOperationParameter($operation, $attribute->name ?? $parameter->getName(), 'query'); - SymfonyAnnotationHelper::modifyAnnotationValue($operationParameter, 'allowEmptyValue', $parameter->allowsNull()); SymfonyAnnotationHelper::modifyAnnotationValue($operationParameter, 'required', !$parameter->isDefaultValueAvailable() && !$parameter->allowsNull()); /** @var OA\Schema $schema */ From f543584e0ede3d7edf6aee3d634bbfe932c3747a Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sun, 13 Aug 2023 20:27:26 +0200 Subject: [PATCH 086/120] Fix type comparison --- .../SymfonyMapQueryParameterDescriberTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php index a67c0c877..b711023f7 100644 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php @@ -55,7 +55,7 @@ public function testMapQueryParameter(callable $function): void self::assertSame(!$parameter->isDefaultValueAvailable() && !$parameter->allowsNull(), $documentationParameter->required); $schema = $documentationParameter->schema; - self::assertSame($parameter->getType()->getName(), $schema->type); + self::assertSame('integer', $schema->type); if ($parameter->isDefaultValueAvailable()) { self::assertSame($parameter->getDefaultValue(), $schema->default); } From 552070e7fb2e17905efffb2bd049e0e162d025dc Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sun, 13 Aug 2023 20:35:05 +0200 Subject: [PATCH 087/120] Fix enum not being used in test --- Tests/Functional/Entity/SymfonyMapQueryString.php | 2 +- Tests/Functional/SymfonyFunctionalTest.php | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/Functional/Entity/SymfonyMapQueryString.php b/Tests/Functional/Entity/SymfonyMapQueryString.php index 09cc93f0c..99d682a27 100644 --- a/Tests/Functional/Entity/SymfonyMapQueryString.php +++ b/Tests/Functional/Entity/SymfonyMapQueryString.php @@ -10,7 +10,7 @@ public function __construct( public int $id, public string $name, public ?string $nullableName, - public Article81 $article81Enum, + public ArticleType81 $articleType81, ) { } } diff --git a/Tests/Functional/SymfonyFunctionalTest.php b/Tests/Functional/SymfonyFunctionalTest.php index 68cdb7847..83be70dba 100644 --- a/Tests/Functional/SymfonyFunctionalTest.php +++ b/Tests/Functional/SymfonyFunctionalTest.php @@ -45,8 +45,8 @@ public function testMapQueryStringModelGetsCreated(): void 'type' => 'string', 'nullable' => true, ], - 'article81Enum' => [ - '$ref' => '#/components/schemas/Article81', + 'articleType81' => [ + '$ref' => '#/components/schemas/ArticleType81', ], ], 'type' => 'object', @@ -74,11 +74,11 @@ public function testMapQueryString(): void $parameter = $this->getParameter($operation, 'nullableName', $in); $this->assertFalse($parameter->required); - $parameter = $this->getParameter($operation, 'article81Enum', $in); + $parameter = $this->getParameter($operation, 'articleType81', $in); - $property = $this->getProperty($this->getModel('SymfonyMapQueryString'), 'article81Enum'); + $property = $this->getProperty($this->getModel('SymfonyMapQueryString'), 'articleType81'); $this->assertTrue($parameter->required); - $this->assertSame($property, $parameter->schema); + $this->assertEquals($property, $parameter->schema); } public function testMapQueryStringParametersAreOptional(): void @@ -100,7 +100,7 @@ public function testMapQueryStringParametersAreOptional(): void $parameter = $this->getParameter($operation, 'nullableName', $in); $this->assertFalse($parameter->required); - $parameter = $this->getParameter($operation, 'article81Enum', $in); + $parameter = $this->getParameter($operation, 'articleType81', $in); $this->assertFalse($parameter->required); } @@ -112,7 +112,7 @@ public function testMapQueryStringParametersOverwriteParameters(): void $operation = $this->getOperation('/api/article_map_query_string_overwrite_parameters', 'get'); - foreach (['id', 'name', 'nullableName', 'article81Enum'] as $name) { + foreach (['id', 'name', 'nullableName', 'articleType81'] as $name) { $parameter = $this->getParameter($operation, $name, 'query'); $this->assertSame($parameter->description, sprintf('Query parameter %s description', $name)); } From 21559053a562ad7effad3ca428ea071e892af7ad Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sun, 13 Aug 2023 20:41:46 +0200 Subject: [PATCH 088/120] Add MapQueryParameter functional tests --- .../Functional/Controller/ApiController81.php | 39 ++++++++++++- Tests/Functional/SymfonyFunctionalTest.php | 56 +++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/Tests/Functional/Controller/ApiController81.php b/Tests/Functional/Controller/ApiController81.php index 9b1c275b5..a9855e767 100644 --- a/Tests/Functional/Controller/ApiController81.php +++ b/Tests/Functional/Controller/ApiController81.php @@ -18,6 +18,7 @@ use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article81; use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyMapQueryString; use OpenApi\Attributes as OA; +use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Attribute\MapQueryString; use Symfony\Component\Routing\Annotation\Route; @@ -106,13 +107,47 @@ public function fetchArticleFromMapQueryStringNullable( description: 'Query parameter nullableName description' )] #[OA\Parameter( - name: 'article81Enum', + name: 'articleType81', in: 'query', - description: 'Query parameter article81Enum description' + description: 'Query parameter articleType81 description' )] #[OA\Response(response: '200', description: '')] public function fetchArticleFromMapQueryStringOverwriteParameters( #[MapQueryString] SymfonyMapQueryString $article81Query ) { } + + #[Route('/article_map_query_parameter')] + #[OA\Response(response: '200', description: '')] + public function fetchArticleFromMapQueryParameter( + #[MapQueryParameter] int $id, + ) { + } + + #[Route('/article_map_query_parameter_nullable')] + #[OA\Response(response: '200', description: '')] + public function fetchArticleFromMapQueryParameterNullable( + #[MapQueryParameter] ?int $id, + ) { + } + + #[Route('/article_map_query_parameter_default')] + #[OA\Response(response: '200', description: '')] + public function fetchArticleFromMapQueryParameterDefault( + #[MapQueryParameter] int $id = 123, + ) { + } + + #[Route('/article_map_query_parameter_overwrite_parameters')] + #[OA\Parameter( + name: 'id', + in: 'query', + description: 'Query parameter id description', + example: 123, + )] + #[OA\Response(response: '200', description: '')] + public function fetchArticleFromMapQueryParameterOverwriteParameters( + #[MapQueryParameter] ?int $id, + ) { + } } diff --git a/Tests/Functional/SymfonyFunctionalTest.php b/Tests/Functional/SymfonyFunctionalTest.php index 83be70dba..03a148e22 100644 --- a/Tests/Functional/SymfonyFunctionalTest.php +++ b/Tests/Functional/SymfonyFunctionalTest.php @@ -11,6 +11,7 @@ namespace Nelmio\ApiDocBundle\Tests\Functional; +use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Attribute\MapQueryString; class SymfonyFunctionalTest extends WebTestCase @@ -117,4 +118,59 @@ public function testMapQueryStringParametersOverwriteParameters(): void $this->assertSame($parameter->description, sprintf('Query parameter %s description', $name)); } } + + public function testMapQueryParameter(): void + { + if (!class_exists(MapQueryParameter::class)) { + self::markTestSkipped('Symfony 6.3 MapQueryParameter attribute not found'); + } + + $operation = $this->getOperation('/api/article_map_query_parameter', 'get'); + $in = 'query'; + + $parameter = $this->getParameter($operation, 'id', $in); + $this->assertTrue($parameter->required); + $this->assertSame('integer', $parameter->schema->type); + } + + public function testMapQueryParameterHandlesNullable(): void + { + if (!class_exists(MapQueryParameter::class)) { + self::markTestSkipped('Symfony 6.3 MapQueryParameter attribute not found'); + } + + $operation = $this->getOperation('/api/article_map_query_parameter_nullable', 'get'); + $in = 'query'; + + $parameter = $this->getParameter($operation, 'id', $in); + $this->assertFalse($parameter->required); + } + + public function testMapQueryParameterHandlesDefault(): void + { + if (!class_exists(MapQueryParameter::class)) { + self::markTestSkipped('Symfony 6.3 MapQueryParameter attribute not found'); + } + + $operation = $this->getOperation('/api/article_map_query_parameter_default', 'get'); + $in = 'query'; + + $parameter = $this->getParameter($operation, 'id', $in); + $this->assertFalse($parameter->required); + $this->assertSame(123, $parameter->schema->default); + } + + public function testMapQueryParameterOverwriteParameter(): void + { + if (!class_exists(MapQueryParameter::class)) { + self::markTestSkipped('Symfony 6.3 MapQueryParameter attribute not found'); + } + + $operation = $this->getOperation('/api/article_map_query_parameter_overwrite_parameters', 'get'); + $in = 'query'; + + $parameter = $this->getParameter($operation, 'id', $in); + $this->assertSame(123, $parameter->example); + $this->assertSame('Query parameter id description', $parameter->description); + } } From 9e629a09c9f07208b1b93039c1d6dd123bceefee Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sun, 13 Aug 2023 21:15:46 +0200 Subject: [PATCH 089/120] Update required statement --- .../SymfonyMapQueryParameterDescriber.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php index c4d3ab55c..1bae338ca 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php @@ -26,7 +26,7 @@ public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionPar $operationParameter = Util::getOperationParameter($operation, $attribute->name ?? $parameter->getName(), 'query'); - SymfonyAnnotationHelper::modifyAnnotationValue($operationParameter, 'required', !$parameter->isDefaultValueAvailable() && !$parameter->allowsNull()); + SymfonyAnnotationHelper::modifyAnnotationValue($operationParameter, 'required', !($parameter->isDefaultValueAvailable() || $parameter->allowsNull())); /** @var OA\Schema $schema */ $schema = Util::getChild($operationParameter, OA\Schema::class); From f2dafb73f653414348532776ed0ef7740b0f2531 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sun, 13 Aug 2023 21:15:54 +0200 Subject: [PATCH 090/120] Set requestBody required --- .../SymfonyMapRequestPayloadDescriber.php | 1 + 1 file changed, 1 insertion(+) diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php index e31160b59..da3d3c0f7 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php @@ -28,6 +28,7 @@ public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionPar /** @var OA\RequestBody $requestBody */ $requestBody = Util::getChild($operation, OA\RequestBody::class); + SymfonyAnnotationHelper::modifyAnnotationValue($requestBody, 'required', !($parameter->isDefaultValueAvailable() || $parameter->allowsNull())); if (!is_array($attribute->acceptFormat)) { $this->describeRequestBody($requestBody, $parameter, $attribute->acceptFormat ?? 'json'); From 37f1a4e1b9ffff181d07f79bf3f1cc2bc2023e9b Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sun, 13 Aug 2023 21:22:27 +0200 Subject: [PATCH 091/120] Add MapRequestPayload functional tests --- .../Functional/Controller/ApiController81.php | 25 ++++++ Tests/Functional/SymfonyFunctionalTest.php | 82 +++++++++++++++---- 2 files changed, 90 insertions(+), 17 deletions(-) diff --git a/Tests/Functional/Controller/ApiController81.php b/Tests/Functional/Controller/ApiController81.php index a9855e767..14e2aa067 100644 --- a/Tests/Functional/Controller/ApiController81.php +++ b/Tests/Functional/Controller/ApiController81.php @@ -20,6 +20,7 @@ use OpenApi\Attributes as OA; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Attribute\MapQueryString; +use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\Routing\Annotation\Route; class ApiController81 extends ApiController80 @@ -150,4 +151,28 @@ public function fetchArticleFromMapQueryParameterOverwriteParameters( #[MapQueryParameter] ?int $id, ) { } + + #[Route('/article_map_request_payload', methods: ['POST'])] + #[OA\Response(response: '200', description: '')] + public function createArticleFromMapRequestPayload( + #[MapRequestPayload] Article81 $article81, + ) { + } + + #[Route('/article_map_request_payload_nullable', methods: ['POST'])] + #[OA\Response(response: '200', description: '')] + public function createArticleFromMapRequestPayloadNullable( + #[MapRequestPayload] ?Article81 $article81, + ) { + } + + #[Route('/article_map_request_payload_overwrite', methods: ['POST'])] + #[OA\RequestBody( + description: 'Request body description', + )] + #[OA\Response(response: '200', description: '')] + public function createArticleFromMapRequestPayloadOverwrite( + #[MapRequestPayload] Article81 $article81, + ) { + } } diff --git a/Tests/Functional/SymfonyFunctionalTest.php b/Tests/Functional/SymfonyFunctionalTest.php index 03a148e22..56f998aaa 100644 --- a/Tests/Functional/SymfonyFunctionalTest.php +++ b/Tests/Functional/SymfonyFunctionalTest.php @@ -11,8 +11,10 @@ namespace Nelmio\ApiDocBundle\Tests\Functional; +use OpenApi\Annotations\Components; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Attribute\MapQueryString; +use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; class SymfonyFunctionalTest extends WebTestCase { @@ -67,19 +69,19 @@ public function testMapQueryString(): void $in = 'query'; $parameter = $this->getParameter($operation, 'id', $in); - $this->assertTrue($parameter->required); + self::assertTrue($parameter->required); $parameter = $this->getParameter($operation, 'name', $in); - $this->assertTrue($parameter->required); + self::assertTrue($parameter->required); $parameter = $this->getParameter($operation, 'nullableName', $in); - $this->assertFalse($parameter->required); + self::assertFalse($parameter->required); $parameter = $this->getParameter($operation, 'articleType81', $in); $property = $this->getProperty($this->getModel('SymfonyMapQueryString'), 'articleType81'); - $this->assertTrue($parameter->required); - $this->assertEquals($property, $parameter->schema); + self::assertTrue($parameter->required); + self::assertEquals($property, $parameter->schema); } public function testMapQueryStringParametersAreOptional(): void @@ -93,16 +95,16 @@ public function testMapQueryStringParametersAreOptional(): void $in = 'query'; $parameter = $this->getParameter($operation, 'id', $in); - $this->assertFalse($parameter->required); + self::assertFalse($parameter->required); $parameter = $this->getParameter($operation, 'name', $in); - $this->assertFalse($parameter->required); + self::assertFalse($parameter->required); $parameter = $this->getParameter($operation, 'nullableName', $in); - $this->assertFalse($parameter->required); + self::assertFalse($parameter->required); $parameter = $this->getParameter($operation, 'articleType81', $in); - $this->assertFalse($parameter->required); + self::assertFalse($parameter->required); } public function testMapQueryStringParametersOverwriteParameters(): void @@ -115,7 +117,7 @@ public function testMapQueryStringParametersOverwriteParameters(): void foreach (['id', 'name', 'nullableName', 'articleType81'] as $name) { $parameter = $this->getParameter($operation, $name, 'query'); - $this->assertSame($parameter->description, sprintf('Query parameter %s description', $name)); + self::assertSame($parameter->description, sprintf('Query parameter %s description', $name)); } } @@ -129,8 +131,8 @@ public function testMapQueryParameter(): void $in = 'query'; $parameter = $this->getParameter($operation, 'id', $in); - $this->assertTrue($parameter->required); - $this->assertSame('integer', $parameter->schema->type); + self::assertTrue($parameter->required); + self::assertSame('integer', $parameter->schema->type); } public function testMapQueryParameterHandlesNullable(): void @@ -143,7 +145,7 @@ public function testMapQueryParameterHandlesNullable(): void $in = 'query'; $parameter = $this->getParameter($operation, 'id', $in); - $this->assertFalse($parameter->required); + self::assertFalse($parameter->required); } public function testMapQueryParameterHandlesDefault(): void @@ -156,8 +158,8 @@ public function testMapQueryParameterHandlesDefault(): void $in = 'query'; $parameter = $this->getParameter($operation, 'id', $in); - $this->assertFalse($parameter->required); - $this->assertSame(123, $parameter->schema->default); + self::assertFalse($parameter->required); + self::assertSame(123, $parameter->schema->default); } public function testMapQueryParameterOverwriteParameter(): void @@ -170,7 +172,53 @@ public function testMapQueryParameterOverwriteParameter(): void $in = 'query'; $parameter = $this->getParameter($operation, 'id', $in); - $this->assertSame(123, $parameter->example); - $this->assertSame('Query parameter id description', $parameter->description); + self::assertSame(123, $parameter->example); + self::assertSame('Query parameter id description', $parameter->description); + } + + public function testMapRequestPayload(): void + { + if (!class_exists(MapRequestPayload::class)) { + self::markTestSkipped('Symfony 6.3 MapRequestPayload attribute not found'); + } + + $operation = $this->getOperation('/api/article_map_request_payload', 'post'); + + $requestBody = $operation->requestBody; + self::assertTrue($requestBody->required); + + self::assertCount(1, $requestBody->content); + self::assertArrayHasKey('application/json', $requestBody->content); + + $media = $requestBody->content['application/json']; + + self::assertSame('application/json', $media->mediaType); + + $model = $this->getModel('Article81'); + self::assertSame(Components::SCHEMA_REF . $model->schema, $media->schema->ref); + } + + public function testMapRequestPayloadNullable(): void + { + if (!class_exists(MapRequestPayload::class)) { + self::markTestSkipped('Symfony 6.3 MapRequestPayload attribute not found'); + } + + $operation = $this->getOperation('/api/article_map_request_payload_nullable', 'post'); + + $requestBody = $operation->requestBody; + self::assertFalse($requestBody->required); + } + + public function testMapRequestPayloadOverwriteRequestBody(): void + { + if (!class_exists(MapRequestPayload::class)) { + self::markTestSkipped('Symfony 6.3 MapRequestPayload attribute not found'); + } + + $operation = $this->getOperation('/api/article_map_request_payload_overwrite', 'post'); + + $requestBody = $operation->requestBody; + self::assertSame( 'Request body description', $requestBody->description); } } From a7de308dc2d1a6661365ee38199cf9294f82e1a7 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sun, 13 Aug 2023 21:23:38 +0200 Subject: [PATCH 092/120] Fix style --- Tests/Functional/SymfonyFunctionalTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Functional/SymfonyFunctionalTest.php b/Tests/Functional/SymfonyFunctionalTest.php index 56f998aaa..7eea1f3b0 100644 --- a/Tests/Functional/SymfonyFunctionalTest.php +++ b/Tests/Functional/SymfonyFunctionalTest.php @@ -195,7 +195,7 @@ public function testMapRequestPayload(): void self::assertSame('application/json', $media->mediaType); $model = $this->getModel('Article81'); - self::assertSame(Components::SCHEMA_REF . $model->schema, $media->schema->ref); + self::assertSame(Components::SCHEMA_REF.$model->schema, $media->schema->ref); } public function testMapRequestPayloadNullable(): void @@ -219,6 +219,6 @@ public function testMapRequestPayloadOverwriteRequestBody(): void $operation = $this->getOperation('/api/article_map_request_payload_overwrite', 'post'); $requestBody = $operation->requestBody; - self::assertSame( 'Request body description', $requestBody->description); + self::assertSame('Request body description', $requestBody->description); } } From 398357827c594143ae5891fbeedc64fd5131cbe3 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sun, 13 Aug 2023 21:27:05 +0200 Subject: [PATCH 093/120] Cleanup array format check --- .../SymfonyMapRequestPayloadDescriber.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php index da3d3c0f7..3c00117dd 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php +++ b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php @@ -30,12 +30,13 @@ public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionPar $requestBody = Util::getChild($operation, OA\RequestBody::class); SymfonyAnnotationHelper::modifyAnnotationValue($requestBody, 'required', !($parameter->isDefaultValueAvailable() || $parameter->allowsNull())); - if (!is_array($attribute->acceptFormat)) { - $this->describeRequestBody($requestBody, $parameter, $attribute->acceptFormat ?? 'json'); - } else { - foreach ($attribute->acceptFormat as $format) { - $this->describeRequestBody($requestBody, $parameter, $format); - } + $formats = $attribute->acceptFormat; + if (!is_array($formats)) { + $formats = [$attribute->acceptFormat ?? 'json']; + } + + foreach ($formats as $format) { + $this->describeRequestBody($requestBody, $parameter, $format); } } From 7cc33079d0aa42bbe4788006fc9df7eb9bf24853 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Tue, 2 Jan 2024 20:01:52 +0100 Subject: [PATCH 094/120] add required field to test --- Tests/Functional/SymfonyFunctionalTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Tests/Functional/SymfonyFunctionalTest.php b/Tests/Functional/SymfonyFunctionalTest.php index 7eea1f3b0..97ef86a3c 100644 --- a/Tests/Functional/SymfonyFunctionalTest.php +++ b/Tests/Functional/SymfonyFunctionalTest.php @@ -37,6 +37,11 @@ public function testMapQueryStringModelGetsCreated(): void $expected = [ 'schema' => 'SymfonyMapQueryString', + 'required' => [ + 'id', + 'name', + 'articleType81', + ], 'properties' => [ 'id' => [ 'type' => 'integer', From b694819c71ade2740480419549f8b5109425ec58 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Tue, 2 Jan 2024 20:09:11 +0100 Subject: [PATCH 095/120] fix baseline --- phpunit-baseline.json | 165 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/phpunit-baseline.json b/phpunit-baseline.json index f999c7fc4..631b34dd8 100644 --- a/phpunit-baseline.json +++ b/phpunit-baseline.json @@ -6918,5 +6918,170 @@ "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\FunctionalTest::testContextPassedToNameConverter", "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_file\" service is deprecated since version 5.2", "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryStringModelGetsCreated", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_class\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryStringModelGetsCreated", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_dir\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryStringModelGetsCreated", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_file\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryString", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_class\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryString", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_dir\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryString", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_file\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryStringParametersAreOptional", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_class\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryStringParametersAreOptional", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_dir\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryStringParametersAreOptional", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_file\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryStringParametersOverwriteParameters", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_class\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryStringParametersOverwriteParameters", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_dir\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryStringParametersOverwriteParameters", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_file\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryParameter", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_class\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryParameter", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_dir\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryParameter", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_file\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryParameterHandlesNullable", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_class\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryParameterHandlesNullable", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_dir\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryParameterHandlesNullable", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_file\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryParameterHandlesDefault", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_class\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryParameterHandlesDefault", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_dir\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryParameterHandlesDefault", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_file\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryParameterOverwriteParameter", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_class\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryParameterOverwriteParameter", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_dir\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapQueryParameterOverwriteParameter", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_file\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapRequestPayload", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_class\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapRequestPayload", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_dir\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapRequestPayload", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_file\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapRequestPayloadNullable", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_class\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapRequestPayloadNullable", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_dir\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapRequestPayloadNullable", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_file\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapRequestPayloadOverwriteRequestBody", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_class\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapRequestPayloadOverwriteRequestBody", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_dir\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapRequestPayloadOverwriteRequestBody", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_file\" service is deprecated since version 5.2", + "count": 1 } ] From e1fc5378e46170852d2be338fde6227bad1b1dcc Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Tue, 2 Jan 2024 23:24:36 +0100 Subject: [PATCH 096/120] refactor logic to use symfony metadata instead of reflection --- DependencyInjection/NelmioApiDocExtension.php | 5 + OpenApiPhp/Util.php | 12 ++ Resources/config/symfony.xml | 15 +- RouteDescriber/InlineParameterDescriber.php | 55 ++++++ .../InlineParameterDescriberInterface.php | 15 ++ .../SymfonyMapQueryParameterDescriber.php | 47 ++++++ .../SymfonyMapQueryStringDescriber.php | 66 ++++++++ .../SymfonyMapRequestPayloadDescriber.php | 39 ++--- .../SymfonyAnnotationDescriber.php | 15 -- .../SymfonyAnnotationHelper.php | 54 ------ .../SymfonyMapQueryParameterDescriber.php | 40 ----- .../SymfonyMapQueryStringDescriber.php | 112 ------------- RouteDescriber/SymfonyDescriber.php | 62 ------- .../SymfonyMapQueryParameterDescriberTest.php | 105 ------------ .../SymfonyMapQueryStringDescriberTest.php | 158 ------------------ .../SymfonyMapRequestPayloadDescriberTest.php | 94 ----------- Tests/RouteDescriber/SymfonyDescriberTest.php | 125 -------------- 17 files changed, 224 insertions(+), 795 deletions(-) create mode 100644 RouteDescriber/InlineParameterDescriber.php create mode 100644 RouteDescriber/InlineParameterDescriber/InlineParameterDescriberInterface.php create mode 100644 RouteDescriber/InlineParameterDescriber/SymfonyMapQueryParameterDescriber.php create mode 100644 RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php rename RouteDescriber/{SymfonyAnnotationDescriber => InlineParameterDescriber}/SymfonyMapRequestPayloadDescriber.php (54%) delete mode 100644 RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationDescriber.php delete mode 100644 RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationHelper.php delete mode 100644 RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php delete mode 100644 RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php delete mode 100644 RouteDescriber/SymfonyDescriber.php delete mode 100644 Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php delete mode 100644 Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php delete mode 100644 Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php delete mode 100644 Tests/RouteDescriber/SymfonyDescriberTest.php diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index b661e9293..060bb574d 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -21,6 +21,7 @@ use Nelmio\ApiDocBundle\ModelDescriber\BazingaHateoasModelDescriber; use Nelmio\ApiDocBundle\ModelDescriber\JMSModelDescriber; use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface; +use Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber\InlineParameterDescriberInterface; use Nelmio\ApiDocBundle\Routing\FilteredRouteCollectionBuilder; use OpenApi\Generator; use Symfony\Component\Config\FileLocator; @@ -180,6 +181,10 @@ public function load(array $configs, ContainerBuilder $container): void && class_exists(MapQueryString::class) ) { $loader->load('symfony.xml'); + + // Add autoconfiguration for inline parameter describer + $container->registerForAutoconfiguration(InlineParameterDescriberInterface::class) + ->addTag('nelmio_api_doc.inline_parameter_describer'); } $bundles = $container->getParameter('kernel.bundles'); diff --git a/OpenApiPhp/Util.php b/OpenApiPhp/Util.php index dc0db90e8..49f537f5d 100644 --- a/OpenApiPhp/Util.php +++ b/OpenApiPhp/Util.php @@ -504,4 +504,16 @@ function ($value) { $class::$_nested )); } + + /** + * Helper method to modify an annotation value only if its value has not yet been set. + */ + public static function modifyAnnotationValue(OA\AbstractAnnotation $parameter, string $property, $value): void + { + if (!Generator::isDefault($parameter->{$property})) { + return; + } + + $parameter->{$property} = $value; + } } diff --git a/Resources/config/symfony.xml b/Resources/config/symfony.xml index 0a9c6fe5e..1ac1bc988 100644 --- a/Resources/config/symfony.xml +++ b/Resources/config/symfony.xml @@ -4,21 +4,22 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - - + + - + - + - + - + - + + diff --git a/RouteDescriber/InlineParameterDescriber.php b/RouteDescriber/InlineParameterDescriber.php new file mode 100644 index 000000000..f7e2a2380 --- /dev/null +++ b/RouteDescriber/InlineParameterDescriber.php @@ -0,0 +1,55 @@ +getDefault('_controller'); + + $argumentMetaDataList = $this->argumentMetadataFactory->createArgumentMetadata($controller, $reflectionMethod); + + if (!$argumentMetaDataList) { + return; + } + + foreach ($this->getOperations($api, $route) as $operation) { + foreach ($argumentMetaDataList as $argumentMetadata) { + foreach ($this->inlineParameterDescribers as $inlineParameterDescriber) { + if ($inlineParameterDescriber instanceof ModelRegistryAwareInterface) { + $inlineParameterDescriber->setModelRegistry($this->modelRegistry); + } + + if (!$inlineParameterDescriber->supports($argumentMetadata)) { + continue; + } + + $inlineParameterDescriber->describe($api, $operation, $argumentMetadata); + } + } + } + } +} diff --git a/RouteDescriber/InlineParameterDescriber/InlineParameterDescriberInterface.php b/RouteDescriber/InlineParameterDescriber/InlineParameterDescriberInterface.php new file mode 100644 index 000000000..28e1d8d90 --- /dev/null +++ b/RouteDescriber/InlineParameterDescriber/InlineParameterDescriberInterface.php @@ -0,0 +1,15 @@ +getAttributes(MapQueryParameter::class, ArgumentMetadata::IS_INSTANCEOF)) { + return false; + } + + return $argumentMetadata->getType() !== null; + } + + public function describe(OA\OpenApi $api, OA\Operation $operation, ArgumentMetadata $argumentMetadata): void + { + $attribute = $argumentMetadata->getAttributes(MapQueryParameter::class, ArgumentMetadata::IS_INSTANCEOF)[0]; + + $operationParameter = Util::getOperationParameter($operation, $attribute->name ?? $argumentMetadata->getName(), 'query'); + + Util::modifyAnnotationValue($operationParameter, 'required', !($argumentMetadata->hasDefaultValue() || $argumentMetadata->isNullable())); + + /** @var OA\Schema $schema */ + $schema = Util::getChild($operationParameter, OA\Schema::class); + + if (FILTER_VALIDATE_REGEXP === $attribute->filter) { + Util::modifyAnnotationValue($schema, 'pattern', $attribute->options['regexp']); + } + + if ($argumentMetadata->hasDefaultValue()) { + Util::modifyAnnotationValue($schema, 'default', $argumentMetadata->getDefaultValue()); + } + + $this->mapNativeType($schema, $argumentMetadata->getType()); + } +} diff --git a/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php new file mode 100644 index 000000000..bb7fab038 --- /dev/null +++ b/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php @@ -0,0 +1,66 @@ +getAttributes(MapQueryString::class, ArgumentMetadata::IS_INSTANCEOF)) { + return false; + } + + return $argumentMetadata->getType() && class_exists($argumentMetadata->getType()); + } + + public function describe(OA\OpenApi $api, OA\Operation $operation, ArgumentMetadata $argumentMetadata): void + { + $model = new Model(new Type(Type::BUILTIN_TYPE_OBJECT, $argumentMetadata->isNullable(), $argumentMetadata->getType())); + + $modelRef = $this->modelRegistry->register($model); + $this->modelRegistry->registerSchemas($model->getHash()); + + $nativeModelName = str_replace(OA\Components::SCHEMA_REF, '', $modelRef); + + $schemaModel = Util::getSchema($api, $nativeModelName); + + // There are no properties to map to query parameters + if (Generator::UNDEFINED === $schemaModel->properties) { + return; + } + + $isModelOptional = $argumentMetadata->hasDefaultValue() || $argumentMetadata->isNullable(); + + foreach ($schemaModel->properties as $property) { + $operationParameter = Util::getOperationParameter($operation, $property->property, 'query'); + Util::modifyAnnotationValue($operationParameter, 'schema', $property); + Util::modifyAnnotationValue($operationParameter, 'name', $property->property); + Util::modifyAnnotationValue($operationParameter, 'description', $property->description); + Util::modifyAnnotationValue($operationParameter, 'required', $property->required); + Util::modifyAnnotationValue($operationParameter, 'deprecated', $property->deprecated); + Util::modifyAnnotationValue($operationParameter, 'example', $property->example); + + if ($isModelOptional) { + Util::modifyAnnotationValue($operationParameter, 'required', false); + } elseif (is_array($schemaModel->required) && in_array($property->property, $schemaModel->required, true)) { + Util::modifyAnnotationValue($operationParameter, 'required', true); + } else { + Util::modifyAnnotationValue($operationParameter, 'required', false); + } + } + } +} diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php b/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php similarity index 54% rename from RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php rename to RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php index 3c00117dd..de95f5adf 100644 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriber.php +++ b/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php @@ -2,33 +2,33 @@ declare(strict_types=1); -namespace Nelmio\ApiDocBundle\RouteDescriber\SymfonyAnnotationDescriber; +namespace Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber; use InvalidArgumentException; use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; -use ReflectionParameter; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; -final class SymfonyMapRequestPayloadDescriber implements SymfonyAnnotationDescriber +final class SymfonyMapRequestPayloadDescriber implements InlineParameterDescriberInterface { - public function supports(ReflectionParameter $parameter): bool + public function supports(ArgumentMetadata $argumentMetadata): bool { - if (!SymfonyAnnotationHelper::getAttribute($parameter, MapRequestPayload::class)) { + if (!$argumentMetadata->getAttributes(MapRequestPayload::class, ArgumentMetadata::IS_INSTANCEOF)) { return false; } - return $parameter->hasType(); + return $argumentMetadata->getType() && class_exists($argumentMetadata->getType()); } - public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionParameter $parameter): void + public function describe(OA\OpenApi $api, OA\Operation $operation, ArgumentMetadata $argumentMetadata): void { - $attribute = SymfonyAnnotationHelper::getAttribute($parameter, MapRequestPayload::class); + $attribute = $argumentMetadata->getAttributes(MapRequestPayload::class, ArgumentMetadata::IS_INSTANCEOF)[0]; /** @var OA\RequestBody $requestBody */ $requestBody = Util::getChild($operation, OA\RequestBody::class); - SymfonyAnnotationHelper::modifyAnnotationValue($requestBody, 'required', !($parameter->isDefaultValueAvailable() || $parameter->allowsNull())); + Util::modifyAnnotationValue($requestBody, 'required', !($argumentMetadata->hasDefaultValue() || $argumentMetadata->isNullable())); $formats = $attribute->acceptFormat; if (!is_array($formats)) { @@ -36,24 +36,17 @@ public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionPar } foreach ($formats as $format) { - $this->describeRequestBody($requestBody, $parameter, $format); - } - } - - private function describeRequestBody(OA\RequestBody $requestBody, ReflectionParameter $parameter, string $format): void - { - $contentSchema = $this->getContentSchemaForType($requestBody, $format); - SymfonyAnnotationHelper::modifyAnnotationValue($contentSchema, 'ref', new Model(type: $parameter->getType()->getName())); - SymfonyAnnotationHelper::modifyAnnotationValue($contentSchema, 'type', 'object'); - - $schema = Util::getProperty($contentSchema, $parameter->getName()); + $contentSchema = $this->getContentSchemaForType($requestBody, $format); + Util::modifyAnnotationValue($contentSchema, 'ref', new Model(type: $argumentMetadata->getType())); + Util::modifyAnnotationValue($contentSchema, 'type', 'object'); - SymfonyAnnotationHelper::describeCommonSchemaFromParameter($schema, $parameter); + Util::getProperty($contentSchema, $argumentMetadata->getName()); + } } private function getContentSchemaForType(OA\RequestBody $requestBody, string $type): OA\Schema { - SymfonyAnnotationHelper::modifyAnnotationValue($requestBody, 'content', []); + Util::modifyAnnotationValue($requestBody, 'content', []); switch ($type) { case 'json': $contentType = 'application/json'; @@ -81,7 +74,7 @@ private function getContentSchemaForType(OA\RequestBody $requestBody, string $ty $requestBody->content[$contentType], OA\Schema::class ); - SymfonyAnnotationHelper::modifyAnnotationValue($schema, 'type', 'object'); + Util::modifyAnnotationValue($schema, 'type', 'object'); } return Util::getChild( diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationDescriber.php deleted file mode 100644 index 6d73938aa..000000000 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyAnnotationDescriber.php +++ /dev/null @@ -1,15 +0,0 @@ - $attribute - * - * @return T|null - * - * @template T of object - */ - public static function getAttribute(ReflectionParameter $parameter, string $attribute): ?object - { - if ($attribute = $parameter->getAttributes($attribute, \ReflectionAttribute::IS_INSTANCEOF)) { - return $attribute[0]->newInstance(); - } - - return null; - } - - public static function describeCommonSchemaFromParameter(OA\Schema $schema, ReflectionParameter $parameter): void - { - if ($parameter->isDefaultValueAvailable()) { - self::modifyAnnotationValue($schema, 'default', $parameter->getDefaultValue()); - } - - if ($parameter->getType()->isBuiltin()) { - $type = $parameter->getType()->getName(); - - if (in_array($type, ['int', 'integer'], true)) { - $type = 'integer'; - } - - self::modifyAnnotationValue($schema, 'type', $type); - } - } - - public static function modifyAnnotationValue(OA\AbstractAnnotation $parameter, string $property, $value): void - { - if (!Generator::isDefault($parameter->{$property})) { - return; - } - - $parameter->{$property} = $value; - } -} diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php deleted file mode 100644 index 1bae338ca..000000000 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriber.php +++ /dev/null @@ -1,40 +0,0 @@ -hasType(); - } - - public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionParameter $parameter): void - { - $attribute = SymfonyAnnotationHelper::getAttribute($parameter, MapQueryParameter::class); - - $operationParameter = Util::getOperationParameter($operation, $attribute->name ?? $parameter->getName(), 'query'); - - SymfonyAnnotationHelper::modifyAnnotationValue($operationParameter, 'required', !($parameter->isDefaultValueAvailable() || $parameter->allowsNull())); - - /** @var OA\Schema $schema */ - $schema = Util::getChild($operationParameter, OA\Schema::class); - - if (FILTER_VALIDATE_REGEXP === $attribute->filter) { - SymfonyAnnotationHelper::modifyAnnotationValue($schema, 'pattern', $attribute->options['regexp']); - } - - SymfonyAnnotationHelper::describeCommonSchemaFromParameter($schema, $parameter); - } -} diff --git a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php deleted file mode 100644 index f8cf9c9e8..000000000 --- a/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriber.php +++ /dev/null @@ -1,112 +0,0 @@ -hasType(); - } - - public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionParameter $parameter): void - { - $model = new Model(new Type(Type::BUILTIN_TYPE_OBJECT, $parameter->allowsNull(), $parameter->getType()->getName())); - $modelRef = $this->modelRegistry->register($model); - - $nativeModelName = str_replace(OA\Components::SCHEMA_REF, '', $modelRef); - - $schemaModel = Util::getSchema($api, $nativeModelName); - - foreach ($this->modelDescribers as $modelDescriber) { - if ($modelDescriber instanceof ModelRegistryAwareInterface) { - $modelDescriber->setModelRegistry($this->modelRegistry); - } - - if ($modelDescriber->supports($model)) { - $modelDescriber->describe($model, $schemaModel); - - break; - } - } - - // There are no properties to map to query parameters - if (Generator::UNDEFINED === $schemaModel->properties) { - return; - } - - $isModelOptional = $parameter->isDefaultValueAvailable() || $parameter->allowsNull(); - - foreach ($schemaModel->properties as $property) { - $constructorParameter = $this->getConstructorReflectionParameterForProperty($parameter, $property); - - $operationParameter = Util::getOperationParameter($operation, $property->property, 'query'); - $this->addParameterValuesFromProperty($operationParameter, $property); - - $isQueryOptional = (Generator::UNDEFINED !== $property->nullable && $property->nullable) - || $constructorParameter?->isDefaultValueAvailable() - || $isModelOptional; - - SymfonyAnnotationHelper::modifyAnnotationValue($operationParameter, 'required', !$isQueryOptional); - - if ($constructorParameter?->isDefaultValueAvailable()) { - SymfonyAnnotationHelper::modifyAnnotationValue($operationParameter, 'example', $constructorParameter->getDefaultValue()); - } - } - } - - private function getConstructorReflectionParameterForProperty(ReflectionParameter $parameter, OA\Property $property): ?ReflectionParameter - { - $reflectionClass = new ReflectionClass($parameter->getType()->getName()); - - if (!$contructor = $reflectionClass->getConstructor()) { - return null; - } - - foreach ($contructor->getParameters() as $parameter) { - if ($property->property === $parameter->getName()) { - return $parameter; - } - } - - return null; - } - - private function addParameterValuesFromProperty(OA\Parameter $parameter, OA\Property $property): void - { - SymfonyAnnotationHelper::modifyAnnotationValue($parameter, 'schema', $property); - SymfonyAnnotationHelper::modifyAnnotationValue($parameter, 'name', $property->property); - SymfonyAnnotationHelper::modifyAnnotationValue($parameter, 'description', $property->description); - SymfonyAnnotationHelper::modifyAnnotationValue($parameter, 'required', $property->required); - SymfonyAnnotationHelper::modifyAnnotationValue($parameter, 'deprecated', $property->deprecated); - SymfonyAnnotationHelper::modifyAnnotationValue($parameter, 'example', $property->example); - } -} diff --git a/RouteDescriber/SymfonyDescriber.php b/RouteDescriber/SymfonyDescriber.php deleted file mode 100644 index 55ccd6e82..000000000 --- a/RouteDescriber/SymfonyDescriber.php +++ /dev/null @@ -1,62 +0,0 @@ -getMethodParameter($reflectionMethod); - - foreach ($this->getOperations($api, $route) as $operation) { - foreach ($parameters as $parameter) { - foreach ($this->annotationDescribers as $annotationDescriber) { - if ($annotationDescriber instanceof ModelRegistryAwareInterface) { - $annotationDescriber->setModelRegistry($this->modelRegistry); - } - - if (!$annotationDescriber->supports($parameter)) { - continue; - } - - $annotationDescriber->describe($api, $operation, $parameter); - } - } - } - } - - /** - * @return ReflectionParameter[] - */ - private function getMethodParameter(ReflectionMethod $reflectionMethod): array - { - $parameters = []; - - foreach ($reflectionMethod->getParameters() as $parameter) { - $parameters[] = $parameter; - } - - return $parameters; - } -} diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php deleted file mode 100644 index b711023f7..000000000 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php +++ /dev/null @@ -1,105 +0,0 @@ -symfonyMapQueryParameterDescriber = new SymfonyMapQueryParameterDescriber(); - } - - /** - * @dataProvider provideMapQueryParameterTestData - */ - public function testMapQueryParameter(callable $function): void - { - $parameter = new ReflectionParameter($function, 'parameter1'); - - $this->symfonyMapQueryParameterDescriber->describe( - new OpenApi([]), - $operation = new Operation([]), - $parameter - ); - - /** @var MapQueryParameter $mapQueryParameter */ - $mapQueryParameter = $parameter->getAttributes(MapQueryParameter::class, ReflectionAttribute::IS_INSTANCEOF)[0]->newInstance(); - - $documentationParameter = $operation->parameters[0]; - self::assertSame($mapQueryParameter->name ?? $parameter->getName(), $documentationParameter->name); - self::assertSame('query', $documentationParameter->in); - self::assertSame(!$parameter->isDefaultValueAvailable() && !$parameter->allowsNull(), $documentationParameter->required); - - $schema = $documentationParameter->schema; - self::assertSame('integer', $schema->type); - if ($parameter->isDefaultValueAvailable()) { - self::assertSame($parameter->getDefaultValue(), $schema->default); - } - - if (FILTER_VALIDATE_REGEXP === $mapQueryParameter->filter) { - self::assertSame($mapQueryParameter->options['regexp'], $schema->pattern); - } - } - - public static function provideMapQueryParameterTestData(): iterable - { - yield 'it documents query parameters' => [ - function ( - #[MapQueryParameter] int $parameter1, - ) { - }, - ]; - - yield 'it documents query parameters with default values' => [ - function ( - #[MapQueryParameter] int $parameter1 = 123, - ) { - }, - ]; - - yield 'it documents query parameters with nullable types' => [ - function ( - #[MapQueryParameter] ?int $parameter1, - ) { - }, - ]; - - yield 'it uses MapQueryParameter name argument as name' => [ - function ( - #[MapQueryParameter('someOtherParameter1Name')] int $parameter1, - ) { - }, - ]; - - yield 'it documents regex pattern' => [ - function ( - #[MapQueryParameter(filter: FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^\d+$/'])] int $parameter1, - ) { - }, - ]; - } -} diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php deleted file mode 100644 index 79d9c8ee1..000000000 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php +++ /dev/null @@ -1,158 +0,0 @@ -openApi = new OpenApi([]); - - $this->symfonyMapQueryStringDescriber = new SymfonyMapQueryStringDescriber([new SelfDescribingModelDescriber()]); - - $registry = new ModelRegistry([], $this->openApi, []); - - $this->symfonyMapQueryStringDescriber->setModelRegistry($registry); - } - - /** - * @dataProvider provideMapQueryStringTestData - * - * @param array{optional: bool} $expectations - */ - public function testMapQueryString(callable $function, array $expectations): void - { - $parameter = new ReflectionParameter($function, 'parameter1'); - - $this->symfonyMapQueryStringDescriber->describe( - $this->openApi, - $operation = new Operation([]), - $parameter - ); - - // Test it registers the model - $modelSchema = $this->openApi->components->schemas[0]; - $expectedModelProperties = SymfonyDescriberMapQueryStringClass::getProperties(); - - self::assertSame(SymfonyDescriberMapQueryStringClass::SCHEMA, $modelSchema->schema); - self::assertSame(SymfonyDescriberMapQueryStringClass::TITLE, $modelSchema->title); - self::assertSame(SymfonyDescriberMapQueryStringClass::TYPE, $modelSchema->type); - self::assertEquals($expectedModelProperties, $modelSchema->properties); - - foreach ($expectedModelProperties as $key => $expectedModelProperty) { - $queryParameter = $operation->parameters[$key]; - - self::assertSame('query', $queryParameter->in); - self::assertSame($expectedModelProperty->property, $queryParameter->name); - self::assertSame(!$expectations['optional'], $queryParameter->required); - } - } - - public static function provideMapQueryStringTestData(): iterable - { - yield 'it documents query string parameters' => [ - function ( - #[MapQueryString] SymfonyDescriberMapQueryStringClass $parameter1, - ) { - }, - [ - 'optional' => false, - ], - ]; - - yield 'it documents a nullable type as optional' => [ - function ( - #[MapQueryString] ?SymfonyDescriberMapQueryStringClass $parameter1, - ) { - }, - [ - 'optional' => true, - ], - ]; - - yield 'it documents a default value as optional' => [ - function ( - #[MapQueryString] ?SymfonyDescriberMapQueryStringClass $parameter1, - ) { - }, - [ - 'optional' => true, - ], - ]; - } - - public function testItDescribesProperties(): void - { - $function = function ( - #[MapQueryString] DTO $DTO, - ) { - }; - - $parameter = new ReflectionParameter($function, 'DTO'); - - $this->symfonyMapQueryStringDescriber->describe( - $this->openApi, - $operation = new Operation([]), - $parameter - ); - - // Test it registers the model - $modelSchema = $this->openApi->components->schemas[0]; - $expectedModelProperties = DTO::getProperties(); - - self::assertEquals($expectedModelProperties, $modelSchema->properties); - - self::assertSame('id', $operation->parameters[0]->name); - self::assertSame('int', $operation->parameters[0]->schema->type); - - self::assertSame('name', $operation->parameters[1]->name); - - self::assertSame('nullableName', $operation->parameters[2]->name); - self::assertSame('string', $operation->parameters[2]->schema->type); - self::assertSame(false, $operation->parameters[2]->required); - self::assertSame(true, $operation->parameters[2]->schema->nullable); - - self::assertSame('nameWithExample', $operation->parameters[3]->name); - self::assertSame('string', $operation->parameters[3]->schema->type); - self::assertSame(true, $operation->parameters[3]->required); - self::assertSame(DTO::EXAMPLE_NAME, $operation->parameters[3]->schema->example); - self::assertSame(DTO::EXAMPLE_NAME, $operation->parameters[3]->example); - - self::assertSame('nameWithDescription', $operation->parameters[4]->name); - self::assertSame('string', $operation->parameters[4]->schema->type); - self::assertSame(true, $operation->parameters[4]->required); - self::assertSame(DTO::DESCRIPTION, $operation->parameters[4]->schema->description); - self::assertSame(DTO::DESCRIPTION, $operation->parameters[4]->description); - } -} diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php deleted file mode 100644 index 14bebba81..000000000 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php +++ /dev/null @@ -1,94 +0,0 @@ -symfonyMapRequestPayloadDescriber = new SymfonyMapRequestPayloadDescriber(); - } - - /** - * @dataProvider provideMapRequestPayloadTestData - * - * @param string[] $expectedMediaTypes - */ - public function testMapRequestPayload(callable $function, array $expectedMediaTypes): void - { - $parameter = new ReflectionParameter($function, 'payload'); - - $this->symfonyMapRequestPayloadDescriber->describe( - new OpenApi([]), - $operation = new Operation([]), - $parameter - ); - - foreach ($expectedMediaTypes as $expectedMediaType) { - $requestBodyContent = $operation->requestBody->content[$expectedMediaType]; - - self::assertSame($expectedMediaType, $requestBodyContent->mediaType); - self::assertSame('object', $requestBodyContent->schema->type); - self::assertSame(stdClass::class, $requestBodyContent->schema->ref->type); - } - } - - public static function provideMapRequestPayloadTestData(): iterable - { - yield 'it sets default mediaType to json' => [ - function ( - #[MapRequestPayload] stdClass $payload - ) { - }, - ['application/json'], - ]; - - yield 'it sets mediaType to json' => [ - function ( - #[MapRequestPayload('json')] stdClass $payload - ) { - }, - ['application/json'], - ]; - - yield 'it sets mediaType to xml' => [ - function ( - #[MapRequestPayload('xml')] stdClass $payload - ) { - }, - ['application/xml'], - ]; - - yield 'it sets multiple mediaTypes' => [ - function ( - #[MapRequestPayload(['json', 'xml'])] stdClass $payload - ) { - }, - ['application/json', 'application/xml'], - ]; - } -} diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php deleted file mode 100644 index fab4f508b..000000000 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ /dev/null @@ -1,125 +0,0 @@ -symfonyAnnotationDescriberMock = $this->createMock(SymfonyAnnotationDescriber::class); - - $this->modelRegistry = new ModelRegistry( - [], - $this->openApi = new OpenApi([]), - [] - ); - - $this->symfonyDescriber = new SymfonyDescriber( - [$this->symfonyAnnotationDescriberMock] - ); - - $this->symfonyDescriber->setModelRegistry($this->modelRegistry); - } - - public function testDescribe(): void - { - $reflectionParameter = $this->createStub(ReflectionParameter::class); - - $reflectionMethodStub = $this->createStub(ReflectionMethod::class); - $reflectionMethodStub->method('getParameters')->willReturn([$reflectionParameter]); - - $this->symfonyAnnotationDescriberMock - ->expects(self::exactly(count(Util::OPERATIONS))) - ->method('supports') - ->with($reflectionParameter) - ->willReturn(true) - ; - - $this->symfonyAnnotationDescriberMock - ->expects(self::exactly(count(Util::OPERATIONS))) - ->method('describe') - ->with( - $this->openApi, - self::isInstanceOf(Operation::class), - $reflectionParameter - ) - ; - - $this->symfonyDescriber->describe( - $this->openApi, - new Route('/'), - $reflectionMethodStub - ); - } - - public function testDescribeSkipsUnsupportedDescribers(): void - { - $reflectionParameter = $this->createStub(ReflectionParameter::class); - - $reflectionMethodStub = $this->createStub(ReflectionMethod::class); - $reflectionMethodStub->method('getParameters')->willReturn([$reflectionParameter]); - - $this->symfonyAnnotationDescriberMock - ->expects(self::exactly(count(Util::OPERATIONS))) - ->method('supports') - ->with($reflectionParameter) - ->willReturn(false) - ; - - $this->symfonyAnnotationDescriberMock - ->expects(self::never()) - ->method('describe') - ; - - $this->symfonyDescriber->describe( - $this->openApi, - new Route('/'), - $reflectionMethodStub - ); - } -} From f3d6af2a3975ba220ddbb6d077923536f23cc9bd Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Tue, 2 Jan 2024 23:25:29 +0100 Subject: [PATCH 097/120] style fix --- .../SymfonyMapQueryParameterDescriber.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryParameterDescriber.php b/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryParameterDescriber.php index b59638ec6..ef34a00d3 100644 --- a/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryParameterDescriber.php +++ b/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryParameterDescriber.php @@ -20,7 +20,7 @@ public function supports(ArgumentMetadata $argumentMetadata): bool return false; } - return $argumentMetadata->getType() !== null; + return null !== $argumentMetadata->getType(); } public function describe(OA\OpenApi $api, OA\Operation $operation, ArgumentMetadata $argumentMetadata): void From c07abe0fd4eece7e6d4e99e8f0609ee10a1fd231 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Tue, 2 Jan 2024 23:38:54 +0100 Subject: [PATCH 098/120] re-add manually iterating over describers --- .../SymfonyMapQueryStringDescriber.php | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php index bb7fab038..9d34fe6f8 100644 --- a/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php +++ b/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php @@ -7,6 +7,7 @@ use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; use Nelmio\ApiDocBundle\Model\Model; +use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface; use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; use OpenApi\Generator; @@ -18,6 +19,14 @@ final class SymfonyMapQueryStringDescriber implements InlineParameterDescriberIn { use ModelRegistryAwareTrait; + /** + * @param ModelDescriberInterface[] $modelDescribers + */ + public function __construct( + private iterable $modelDescribers + ) { + } + public function supports(ArgumentMetadata $argumentMetadata): bool { if (!$argumentMetadata->getAttributes(MapQueryString::class, ArgumentMetadata::IS_INSTANCEOF)) { @@ -32,12 +41,23 @@ public function describe(OA\OpenApi $api, OA\Operation $operation, ArgumentMetad $model = new Model(new Type(Type::BUILTIN_TYPE_OBJECT, $argumentMetadata->isNullable(), $argumentMetadata->getType())); $modelRef = $this->modelRegistry->register($model); - $this->modelRegistry->registerSchemas($model->getHash()); $nativeModelName = str_replace(OA\Components::SCHEMA_REF, '', $modelRef); $schemaModel = Util::getSchema($api, $nativeModelName); + foreach ($this->modelDescribers as $modelDescriber) { + if ($modelDescriber instanceof ModelRegistryAwareInterface) { + $modelDescriber->setModelRegistry($this->modelRegistry); + } + + if ($modelDescriber->supports($model)) { + $modelDescriber->describe($model, $schemaModel); + + break; + } + } + // There are no properties to map to query parameters if (Generator::UNDEFINED === $schemaModel->properties) { return; From c86bd627cf9bd6bd7ab3c0677952abaaddaae484 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Wed, 3 Jan 2024 00:14:39 +0100 Subject: [PATCH 099/120] re-add unit tests --- Tests/RouteDescriber/Fixtures/DTO.php | 25 ++- .../SymfonyDescriberMapQueryStringClass.php | 3 + .../SymfonyMapQueryParameterDescriberTest.php | 115 +++++++++++++ .../SymfonyMapQueryStringDescriberTest.php | 161 ++++++++++++++++++ .../SymfonyMapRequestPayloadDescriberTest.php | 106 ++++++++++++ Tests/RouteDescriber/SymfonyDescriberTest.php | 146 ++++++++++++++++ 6 files changed, 551 insertions(+), 5 deletions(-) create mode 100644 Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php create mode 100644 Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php create mode 100644 Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php create mode 100644 Tests/RouteDescriber/SymfonyDescriberTest.php diff --git a/Tests/RouteDescriber/Fixtures/DTO.php b/Tests/RouteDescriber/Fixtures/DTO.php index a37be818e..2a6d8f9e8 100644 --- a/Tests/RouteDescriber/Fixtures/DTO.php +++ b/Tests/RouteDescriber/Fixtures/DTO.php @@ -16,25 +16,40 @@ class DTO implements SelfDescribingModelInterface public const DESCRIPTION = 'some description'; public function __construct( - private int $id, - private string $name, - private ?string $nullableName, + public int $id, + public string $name, + public ?string $nullableName, #[OA\Property( example: self::EXAMPLE_NAME, )] - private string $nameWithExample, + public string $nameWithExample, #[OA\Property( description: self::DESCRIPTION, )] - private string $nameWithDescription, + public string $nameWithDescription, ) { } public static function describe(Schema $schema, Model $model): void { + $schema->type = 'object'; + $schema->required = self::getRequired(); $schema->properties = self::getProperties(); } + /** + * @return string[] + */ + public static function getRequired(): array + { + return [ + 'id', + 'name', + 'nameWithExample', + 'nameWithDescription', + ]; + } + /** * @return Property[] */ diff --git a/Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php b/Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php index 5482e91cd..fb659a970 100644 --- a/Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php +++ b/Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php @@ -22,6 +22,9 @@ public static function describe(Schema $schema, Model $model): void $schema->description = $model->getType()->getClassName(); $schema->type = self::TYPE; + $schema->required = [ + 'id', + ]; $schema->properties = self::getProperties(); } diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php new file mode 100644 index 000000000..970478c73 --- /dev/null +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php @@ -0,0 +1,115 @@ +argumentMetadataFactory = self::getContainer()->get('argument_metadata_factory'); + + $this->symfonyMapQueryParameterDescriber = new SymfonyMapQueryParameterDescriber(); + } + + /** + * @dataProvider provideMapQueryParameterTestData + */ + public function testMapQueryParameter(callable $function): void + { + $argumentMetaData = $this->argumentMetadataFactory->createArgumentMetadata($function)[0]; + + $this->symfonyMapQueryParameterDescriber->describe( + new OpenApi([]), + $operation = new Operation([]), + $argumentMetaData + ); + + /** @var MapQueryParameter $mapQueryParameter */ + $mapQueryParameter = $argumentMetaData->getAttributes(MapQueryParameter::class, ArgumentMetadata::IS_INSTANCEOF)[0]; + + $documentationParameter = $operation->parameters[0]; + self::assertSame($mapQueryParameter->name ?? $argumentMetaData->getName(), $documentationParameter->name); + self::assertSame('query', $documentationParameter->in); + self::assertSame(!$argumentMetaData->hasDefaultValue() && !$argumentMetaData->isNullable(), $documentationParameter->required); + + $schema = $documentationParameter->schema; + self::assertSame('integer', $schema->type); + if ($argumentMetaData->hasDefaultValue()) { + self::assertSame($argumentMetaData->getDefaultValue(), $schema->default); + } + + if (FILTER_VALIDATE_REGEXP === $mapQueryParameter->filter) { + self::assertSame($mapQueryParameter->options['regexp'], $schema->pattern); + } + } + + public static function provideMapQueryParameterTestData(): iterable + { + yield 'it documents query parameters' => [ + function ( + #[MapQueryParameter] int $parameter1, + ) { + }, + ]; + + yield 'it documents query parameters with default values' => [ + function ( + #[MapQueryParameter] int $parameter1 = 123, + ) { + }, + ]; + + yield 'it documents query parameters with nullable types' => [ + function ( + #[MapQueryParameter] ?int $parameter1, + ) { + }, + ]; + + yield 'it uses MapQueryParameter name argument as name' => [ + function ( + #[MapQueryParameter('someOtherParameter1Name')] int $parameter1, + ) { + }, + ]; + + yield 'it documents regex pattern' => [ + function ( + #[MapQueryParameter(filter: FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^\d+$/'])] int $parameter1, + ) { + }, + ]; + } +} diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php new file mode 100644 index 000000000..924689619 --- /dev/null +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php @@ -0,0 +1,161 @@ +argumentMetadataFactory = self::getContainer()->get('argument_metadata_factory'); + + $this->openApi = new OpenApi([]); + + $this->symfonyMapQueryStringDescriber = new SymfonyMapQueryStringDescriber([new SelfDescribingModelDescriber()]); + + $registry = new ModelRegistry([], $this->openApi, []); + + $this->symfonyMapQueryStringDescriber->setModelRegistry($registry); + } + + /** + * @dataProvider provideMapQueryStringTestData + */ + public function testMapQueryString(callable $function, bool $required): void + { + $argumentMetaData = $this->argumentMetadataFactory->createArgumentMetadata($function)[0]; + + $this->symfonyMapQueryStringDescriber->describe( + $this->openApi, + $operation = new Operation([]), + $argumentMetaData + ); + + // Test it registers the model + $modelSchema = $this->openApi->components->schemas[0]; + $expectedModelProperties = SymfonyDescriberMapQueryStringClass::getProperties(); + + self::assertSame(SymfonyDescriberMapQueryStringClass::SCHEMA, $modelSchema->schema); + self::assertSame(SymfonyDescriberMapQueryStringClass::TITLE, $modelSchema->title); + self::assertSame(SymfonyDescriberMapQueryStringClass::TYPE, $modelSchema->type); + self::assertEquals($expectedModelProperties, $modelSchema->properties); + + foreach ($expectedModelProperties as $key => $expectedModelProperty) { + $queryParameter = $operation->parameters[$key]; + + self::assertSame('query', $queryParameter->in); + self::assertSame($expectedModelProperty->property, $queryParameter->name); + self::assertSame($required, $queryParameter->required); + } + } + + public static function provideMapQueryStringTestData(): iterable + { + yield 'it documents query string parameters' => [ + function ( + #[MapQueryString] SymfonyDescriberMapQueryStringClass $parameter1, + ) { + }, + true + ]; + + yield 'it documents a nullable type as optional' => [ + function ( + #[MapQueryString] ?SymfonyDescriberMapQueryStringClass $parameter1, + ) { + }, + false + ]; + + yield 'it documents a default value as optional' => [ + function ( + #[MapQueryString] ?SymfonyDescriberMapQueryStringClass $parameter1, + ) { + }, + false + ]; + } + + public function testItDescribesProperties(): void + { + $function = function ( + #[MapQueryString] DTO $DTO, + ) { + }; + + $argumentMetaData = $this->argumentMetadataFactory->createArgumentMetadata($function)[0]; + + $this->symfonyMapQueryStringDescriber->describe( + $this->openApi, + $operation = new Operation([]), + $argumentMetaData + ); + + // Test it registers the model + $modelSchema = $this->openApi->components->schemas[0]; + + self::assertEquals('object', $modelSchema->type); + self::assertEquals(DTO::getRequired(), $modelSchema->required); + self::assertEquals(DTO::getProperties(), $modelSchema->properties); + + self::assertSame('id', $operation->parameters[0]->name); + self::assertSame('int', $operation->parameters[0]->schema->type); + + self::assertSame('name', $operation->parameters[1]->name); + + self::assertSame('nullableName', $operation->parameters[2]->name); + self::assertSame('string', $operation->parameters[2]->schema->type); + self::assertSame(false, $operation->parameters[2]->required); + self::assertSame(true, $operation->parameters[2]->schema->nullable); + + self::assertSame('nameWithExample', $operation->parameters[3]->name); + self::assertSame('string', $operation->parameters[3]->schema->type); + self::assertSame(true, $operation->parameters[3]->required); + self::assertSame(DTO::EXAMPLE_NAME, $operation->parameters[3]->schema->example); + self::assertSame(DTO::EXAMPLE_NAME, $operation->parameters[3]->example); + + self::assertSame('nameWithDescription', $operation->parameters[4]->name); + self::assertSame('string', $operation->parameters[4]->schema->type); + self::assertSame(true, $operation->parameters[4]->required); + self::assertSame(DTO::DESCRIPTION, $operation->parameters[4]->schema->description); + self::assertSame(DTO::DESCRIPTION, $operation->parameters[4]->description); + } +} diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php new file mode 100644 index 000000000..f5cdbaf31 --- /dev/null +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php @@ -0,0 +1,106 @@ +argumentMetadataFactory = self::getContainer()->get('argument_metadata_factory'); + + $this->symfonyMapRequestPayloadDescriber = new SymfonyMapRequestPayloadDescriber(); + } + + /** + * @dataProvider provideMapRequestPayloadTestData + * + * @param string[] $expectedMediaTypes + */ + public function testMapRequestPayload(callable $function, array $expectedMediaTypes): void + { + $argumentMetaData = $this->argumentMetadataFactory->createArgumentMetadata($function)[0]; + + $this->symfonyMapRequestPayloadDescriber->describe( + new OpenApi([]), + $operation = new Operation([]), + $argumentMetaData + ); + + foreach ($expectedMediaTypes as $expectedMediaType) { + $requestBodyContent = $operation->requestBody->content[$expectedMediaType]; + + self::assertSame($expectedMediaType, $requestBodyContent->mediaType); + self::assertSame('object', $requestBodyContent->schema->type); + self::assertSame(stdClass::class, $requestBodyContent->schema->ref->type); + } + } + + public static function provideMapRequestPayloadTestData(): iterable + { + yield 'it sets default mediaType to json' => [ + function ( + #[MapRequestPayload] stdClass $payload + ) { + }, + ['application/json'], + ]; + + yield 'it sets mediaType to json' => [ + function ( + #[MapRequestPayload('json')] stdClass $payload + ) { + }, + ['application/json'], + ]; + + yield 'it sets mediaType to xml' => [ + function ( + #[MapRequestPayload('xml')] stdClass $payload + ) { + }, + ['application/xml'], + ]; + + yield 'it sets multiple mediaTypes' => [ + function ( + #[MapRequestPayload(['json', 'xml'])] stdClass $payload + ) { + }, + ['application/json', 'application/xml'], + ]; + } +} diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php new file mode 100644 index 000000000..ff555ee89 --- /dev/null +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -0,0 +1,146 @@ +argumentMetadataFactoryInterface = $this->createMock(ArgumentMetadataFactoryInterface::class); + $this->inlineParameterDescriberInterfaceMock = $this->createMock(InlineParameterDescriberInterface::class); + + $this->modelRegistry = new ModelRegistry( + [], + $this->openApi = new OpenApi([]), + [] + ); + + $this->inlineParameterDescriber = new InlineParameterDescriber( + $this->argumentMetadataFactoryInterface, + [$this->inlineParameterDescriberInterfaceMock] + ); + + $this->inlineParameterDescriber->setModelRegistry($this->modelRegistry); + } + + public function testDescribe(): void + { + $argumentMetaData = $this->createStub(ArgumentMetadata::class); + + $reflectionMethodStub = $this->createStub(ReflectionMethod::class); + + $this->argumentMetadataFactoryInterface + ->expects(self::once()) + ->method('createArgumentMetadata') + ->with('foo', $reflectionMethodStub) + ->willReturn([$argumentMetaData]) + ; + + $this->inlineParameterDescriberInterfaceMock + ->expects(self::exactly(count(Util::OPERATIONS))) + ->method('supports') + ->with($argumentMetaData) + ->willReturn(true) + ; + + $this->inlineParameterDescriberInterfaceMock + ->expects(self::exactly(count(Util::OPERATIONS))) + ->method('describe') + ->with( + $this->openApi, + self::isInstanceOf(Operation::class), + $argumentMetaData + ) + ; + + $this->inlineParameterDescriber->describe( + $this->openApi, + new Route('/', defaults: ['_controller' => 'foo']), + $reflectionMethodStub + ); + } + + public function testDescribeSkipsUnsupportedDescribers(): void + { + $argumentMetaData = $this->createStub(ArgumentMetadata::class); + + $reflectionMethodStub = $this->createStub(ReflectionMethod::class); + + $this->argumentMetadataFactoryInterface + ->expects(self::once()) + ->method('createArgumentMetadata') + ->with('foo', $reflectionMethodStub) + ->willReturn([$argumentMetaData]) + ; + + $this->inlineParameterDescriberInterfaceMock + ->expects(self::exactly(count(Util::OPERATIONS))) + ->method('supports') + ->with($argumentMetaData) + ->willReturn(false) + ; + + $this->inlineParameterDescriberInterfaceMock + ->expects(self::never()) + ->method('describe') + ; + + $this->inlineParameterDescriber->describe( + $this->openApi, + new Route('/', defaults: ['_controller' => 'foo']), + $reflectionMethodStub + ); + } +} From 56f2341b19255b764493603084e90f611d22e63a Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Wed, 3 Jan 2024 00:16:06 +0100 Subject: [PATCH 100/120] style fix --- .../SymfonyMapQueryParameterDescriberTest.php | 3 --- .../SymfonyMapQueryStringDescriberTest.php | 9 +++------ .../SymfonyMapRequestPayloadDescriberTest.php | 5 ----- Tests/RouteDescriber/SymfonyDescriberTest.php | 1 - 4 files changed, 3 insertions(+), 15 deletions(-) diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php index 970478c73..5a75d0a4a 100644 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php @@ -8,9 +8,6 @@ use Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber\SymfonyMapQueryParameterDescriber; use Nelmio\ApiDocBundle\Tests\Functional\WebTestCase; use OpenApi\Annotations\OpenApi; -use PHPUnit\Framework\TestCase; -use ReflectionAttribute; -use ReflectionParameter; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactoryInterface; diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php index 924689619..3f6ebcbc2 100644 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php @@ -12,9 +12,6 @@ use Nelmio\ApiDocBundle\Tests\RouteDescriber\Fixtures\DTO; use Nelmio\ApiDocBundle\Tests\RouteDescriber\Fixtures\SymfonyDescriberMapQueryStringClass; use OpenApi\Annotations\OpenApi; -use PHPUnit\Framework\TestCase; -use ReflectionParameter; -use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\HttpKernel\Attribute\MapQueryString; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactoryInterface; use const PHP_VERSION_ID; @@ -94,7 +91,7 @@ function ( #[MapQueryString] SymfonyDescriberMapQueryStringClass $parameter1, ) { }, - true + true, ]; yield 'it documents a nullable type as optional' => [ @@ -102,7 +99,7 @@ function ( #[MapQueryString] ?SymfonyDescriberMapQueryStringClass $parameter1, ) { }, - false + false, ]; yield 'it documents a default value as optional' => [ @@ -110,7 +107,7 @@ function ( #[MapQueryString] ?SymfonyDescriberMapQueryStringClass $parameter1, ) { }, - false + false, ]; } diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php index f5cdbaf31..bb97af4d3 100644 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php @@ -6,15 +6,10 @@ use Nelmio\ApiDocBundle\Annotation\Operation; use Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber\SymfonyMapRequestPayloadDescriber; -use Nelmio\ApiDocBundle\Tests\Functional\TestKernel; use Nelmio\ApiDocBundle\Tests\Functional\WebTestCase; use OpenApi\Annotations\OpenApi; -use PHPUnit\Framework\TestCase; -use ReflectionParameter; use stdClass; -use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; -use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactoryInterface; use const PHP_VERSION_ID; diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index ff555ee89..00b557327 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -20,7 +20,6 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use ReflectionMethod; -use ReflectionParameter; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactoryInterface; use Symfony\Component\Routing\Route; From 1ecb9a0110f1f79a1f140228094b04bf11bb5c8d Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Wed, 3 Jan 2024 00:17:32 +0100 Subject: [PATCH 101/120] remove named parameter --- Tests/RouteDescriber/SymfonyDescriberTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index 00b557327..2866804e2 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -106,7 +106,7 @@ public function testDescribe(): void $this->inlineParameterDescriber->describe( $this->openApi, - new Route('/', defaults: ['_controller' => 'foo']), + new Route('/', ['_controller' => 'foo']), $reflectionMethodStub ); } @@ -138,7 +138,7 @@ public function testDescribeSkipsUnsupportedDescribers(): void $this->inlineParameterDescriber->describe( $this->openApi, - new Route('/', defaults: ['_controller' => 'foo']), + new Route('/', ['_controller' => 'foo']), $reflectionMethodStub ); } From 035db3fba7214a9f832a4038a3afcbeb790aef03 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Wed, 3 Jan 2024 00:35:52 +0100 Subject: [PATCH 102/120] move xml load logic to describers --- DependencyInjection/NelmioApiDocExtension.php | 12 ++---------- .../config/{symfony.xml => inline_parameter.xml} | 0 .../SymfonyMapQueryParameterDescriber.php | 4 ++++ .../SymfonyMapQueryStringDescriber.php | 4 ++++ .../SymfonyMapRequestPayloadDescriber.php | 4 ++++ 5 files changed, 14 insertions(+), 10 deletions(-) rename Resources/config/{symfony.xml => inline_parameter.xml} (100%) diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index 060bb574d..fdd6f4b8f 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -33,9 +33,6 @@ use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; -use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; -use Symfony\Component\HttpKernel\Attribute\MapQueryString; -use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Routing\RouteCollection; @@ -174,13 +171,8 @@ public function load(array $configs, ContainerBuilder $container): void ->setArgument(1, $config['media_types']); } - if ( - PHP_VERSION_ID > 80100 - && class_exists(MapRequestPayload::class) - && class_exists(MapQueryParameter::class) - && class_exists(MapQueryString::class) - ) { - $loader->load('symfony.xml'); + if (PHP_VERSION_ID > 80100) { + $loader->load('inline_parameter.xml'); // Add autoconfiguration for inline parameter describer $container->registerForAutoconfiguration(InlineParameterDescriberInterface::class) diff --git a/Resources/config/symfony.xml b/Resources/config/inline_parameter.xml similarity index 100% rename from Resources/config/symfony.xml rename to Resources/config/inline_parameter.xml diff --git a/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryParameterDescriber.php b/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryParameterDescriber.php index ef34a00d3..a9dfa4a62 100644 --- a/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryParameterDescriber.php +++ b/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryParameterDescriber.php @@ -16,6 +16,10 @@ final class SymfonyMapQueryParameterDescriber implements InlineParameterDescribe public function supports(ArgumentMetadata $argumentMetadata): bool { + if (!class_exists(MapQueryParameter::class)) { + return false; + } + if (!$argumentMetadata->getAttributes(MapQueryParameter::class, ArgumentMetadata::IS_INSTANCEOF)) { return false; } diff --git a/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php index 9d34fe6f8..32e8f7fc2 100644 --- a/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php +++ b/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php @@ -29,6 +29,10 @@ public function __construct( public function supports(ArgumentMetadata $argumentMetadata): bool { + if (!class_exists(MapQueryString::class)) { + return false; + } + if (!$argumentMetadata->getAttributes(MapQueryString::class, ArgumentMetadata::IS_INSTANCEOF)) { return false; } diff --git a/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php b/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php index de95f5adf..316173af5 100644 --- a/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php +++ b/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php @@ -15,6 +15,10 @@ final class SymfonyMapRequestPayloadDescriber implements InlineParameterDescribe { public function supports(ArgumentMetadata $argumentMetadata): bool { + if (!class_exists(MapRequestPayload::class)) { + return false; + } + if (!$argumentMetadata->getAttributes(MapRequestPayload::class, ArgumentMetadata::IS_INSTANCEOF)) { return false; } From cfa47e0c1fba9f075f0463f51ec6b971d5ca4fd1 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Wed, 3 Jan 2024 00:43:01 +0100 Subject: [PATCH 103/120] Revert "move xml load logic to describers" This reverts commit 035db3fba7214a9f832a4038a3afcbeb790aef03. --- DependencyInjection/NelmioApiDocExtension.php | 12 ++++++++++-- .../config/{inline_parameter.xml => symfony.xml} | 0 .../SymfonyMapQueryParameterDescriber.php | 4 ---- .../SymfonyMapQueryStringDescriber.php | 4 ---- .../SymfonyMapRequestPayloadDescriber.php | 4 ---- 5 files changed, 10 insertions(+), 14 deletions(-) rename Resources/config/{inline_parameter.xml => symfony.xml} (100%) diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index fdd6f4b8f..060bb574d 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -33,6 +33,9 @@ use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; +use Symfony\Component\HttpKernel\Attribute\MapQueryString; +use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Routing\RouteCollection; @@ -171,8 +174,13 @@ public function load(array $configs, ContainerBuilder $container): void ->setArgument(1, $config['media_types']); } - if (PHP_VERSION_ID > 80100) { - $loader->load('inline_parameter.xml'); + if ( + PHP_VERSION_ID > 80100 + && class_exists(MapRequestPayload::class) + && class_exists(MapQueryParameter::class) + && class_exists(MapQueryString::class) + ) { + $loader->load('symfony.xml'); // Add autoconfiguration for inline parameter describer $container->registerForAutoconfiguration(InlineParameterDescriberInterface::class) diff --git a/Resources/config/inline_parameter.xml b/Resources/config/symfony.xml similarity index 100% rename from Resources/config/inline_parameter.xml rename to Resources/config/symfony.xml diff --git a/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryParameterDescriber.php b/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryParameterDescriber.php index a9dfa4a62..ef34a00d3 100644 --- a/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryParameterDescriber.php +++ b/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryParameterDescriber.php @@ -16,10 +16,6 @@ final class SymfonyMapQueryParameterDescriber implements InlineParameterDescribe public function supports(ArgumentMetadata $argumentMetadata): bool { - if (!class_exists(MapQueryParameter::class)) { - return false; - } - if (!$argumentMetadata->getAttributes(MapQueryParameter::class, ArgumentMetadata::IS_INSTANCEOF)) { return false; } diff --git a/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php index 32e8f7fc2..9d34fe6f8 100644 --- a/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php +++ b/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php @@ -29,10 +29,6 @@ public function __construct( public function supports(ArgumentMetadata $argumentMetadata): bool { - if (!class_exists(MapQueryString::class)) { - return false; - } - if (!$argumentMetadata->getAttributes(MapQueryString::class, ArgumentMetadata::IS_INSTANCEOF)) { return false; } diff --git a/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php b/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php index 316173af5..de95f5adf 100644 --- a/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php +++ b/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php @@ -15,10 +15,6 @@ final class SymfonyMapRequestPayloadDescriber implements InlineParameterDescribe { public function supports(ArgumentMetadata $argumentMetadata): bool { - if (!class_exists(MapRequestPayload::class)) { - return false; - } - if (!$argumentMetadata->getAttributes(MapRequestPayload::class, ArgumentMetadata::IS_INSTANCEOF)) { return false; } From 4fd32d3554dd55b37cb74f536e1bc541fbf57891 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 5 Jan 2024 15:47:52 +0100 Subject: [PATCH 104/120] major refactor --- DependencyInjection/NelmioApiDocExtension.php | 51 ++++++++++--- Processors/MapQueryStringProcessor.php | 73 ++++++++++++++++++ Resources/config/symfony.xml | 25 ------- .../InlineParameterDescriberInterface.php | 15 ---- .../RouteArgumentDescriberInterface.php | 13 ++++ .../SymfonyMapQueryParameterDescriber.php | 16 ++-- .../SymfonyMapQueryStringDescriber.php | 75 ++++--------------- .../SymfonyMapRequestPayloadDescriber.php | 34 ++++----- ...scriber.php => RouteArgumentDescriber.php} | 12 +-- Tests/RouteDescriber/SymfonyDescriberTest.php | 12 +-- 10 files changed, 171 insertions(+), 155 deletions(-) create mode 100644 Processors/MapQueryStringProcessor.php delete mode 100644 Resources/config/symfony.xml delete mode 100644 RouteDescriber/InlineParameterDescriber/InlineParameterDescriberInterface.php create mode 100644 RouteDescriber/InlineParameterDescriber/RouteArgumentDescriberInterface.php rename RouteDescriber/{InlineParameterDescriber.php => RouteArgumentDescriber.php} (77%) diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index 060bb574d..d5156d8d9 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -21,7 +21,12 @@ use Nelmio\ApiDocBundle\ModelDescriber\BazingaHateoasModelDescriber; use Nelmio\ApiDocBundle\ModelDescriber\JMSModelDescriber; use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface; -use Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber\InlineParameterDescriberInterface; +use Nelmio\ApiDocBundle\Processors\MapQueryStringProcessor; +use Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber\RouteArgumentDescriberInterface; +use Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber\SymfonyMapQueryParameterDescriber; +use Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber\SymfonyMapQueryStringDescriber; +use Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber\SymfonyMapRequestPayloadDescriber; +use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber; use Nelmio\ApiDocBundle\Routing\FilteredRouteCollectionBuilder; use OpenApi\Generator; use Symfony\Component\Config\FileLocator; @@ -174,17 +179,41 @@ public function load(array $configs, ContainerBuilder $container): void ->setArgument(1, $config['media_types']); } - if ( - PHP_VERSION_ID > 80100 - && class_exists(MapRequestPayload::class) - && class_exists(MapQueryParameter::class) - && class_exists(MapQueryString::class) - ) { - $loader->load('symfony.xml'); - + if (PHP_VERSION_ID > 80100) { // Add autoconfiguration for inline parameter describer - $container->registerForAutoconfiguration(InlineParameterDescriberInterface::class) - ->addTag('nelmio_api_doc.inline_parameter_describer'); + $container->registerForAutoconfiguration(RouteArgumentDescriberInterface::class) + ->addTag('nelmio_api_doc.route_argument_describer'); + + $container->register('nelmio_api_doc.route_describers.route_argument', RouteArgumentDescriber::class) + ->setPublic(false) + ->addTag('nelmio_api_doc.route_describer', ['priority' => -225]) + ->setArguments([ + new Reference('argument_metadata_factory'), + new TaggedIteratorArgument('nelmio_api_doc.route_argument_describer') + ]) + ; + + if (class_exists(MapQueryString::class)) { + $container->register('nelmio_api_doc.route_argument_describer.map_query_string', SymfonyMapQueryStringDescriber::class) + ->setPublic(false) + ->addTag('nelmio_api_doc.route_argument_describer', ['priority' => 0]); + + $container->register('nelmio_api_doc.swagger.processor.map_query_string', MapQueryStringProcessor::class) + ->setPublic(false) + ->addTag('nelmio_api_doc.swagger.processor', ['priority' => 0]); + } + + if (class_exists(MapRequestPayload::class)) { + $container->register('nelmio_api_doc.route_argument_describer.map_request_payload', SymfonyMapRequestPayloadDescriber::class) + ->setPublic(false) + ->addTag('nelmio_api_doc.route_argument_describer', ['priority' => 0]); + } + + if (class_exists(MapQueryParameter::class)) { + $container->register('nelmio_api_doc.route_argument_describer.map_query_parameter', SymfonyMapQueryParameterDescriber::class) + ->setPublic(false) + ->addTag('nelmio_api_doc.route_argument_describer', ['priority' => 0]); + } } $bundles = $container->getParameter('kernel.bundles'); diff --git a/Processors/MapQueryStringProcessor.php b/Processors/MapQueryStringProcessor.php new file mode 100644 index 000000000..115eabfca --- /dev/null +++ b/Processors/MapQueryStringProcessor.php @@ -0,0 +1,73 @@ +getAnnotationsOfType(OA\Operation::class); + + foreach ($operations as $operation) { + if (!isset($operation->_context->{SymfonyMapQueryStringDescriber::CONTEXT_ARGUMENT_METADATA})) { + continue; + } + + $argumentMetaData = $operation->_context->{SymfonyMapQueryStringDescriber::CONTEXT_ARGUMENT_METADATA}; + if (!$argumentMetaData instanceof ArgumentMetadata) { + continue; + } + + $modelRef = $operation->_context->{SymfonyMapQueryStringDescriber::CONTEXT_MODEL_REF}; + if (!isset($modelRef)) { + throw new \LogicException(sprintf('MapQueryString Model reference not found for operation "%s"', $operation->operationId)); + } + + $nativeModelName = str_replace(OA\Components::SCHEMA_REF, '', $modelRef); + + $schemaModel = Util::getSchema($analysis->openapi, $nativeModelName); + + // There are no properties to map to query parameters + if (Generator::UNDEFINED === $schemaModel->properties) { + return; + } + + $isModelOptional = $argumentMetaData->hasDefaultValue() || $argumentMetaData->isNullable(); + + foreach ($schemaModel->properties as $property) { + $operationParameter = Util::getOperationParameter($operation, $property->property, 'query'); + Util::modifyAnnotationValue($operationParameter, 'schema', $property); + Util::modifyAnnotationValue($operationParameter, 'name', $property->property); + Util::modifyAnnotationValue($operationParameter, 'description', $property->description); + Util::modifyAnnotationValue($operationParameter, 'required', $property->required); + Util::modifyAnnotationValue($operationParameter, 'deprecated', $property->deprecated); + Util::modifyAnnotationValue($operationParameter, 'example', $property->example); + + if ($isModelOptional) { + Util::modifyAnnotationValue($operationParameter, 'required', false); + } elseif (is_array($schemaModel->required) && in_array($property->property, $schemaModel->required, true)) { + Util::modifyAnnotationValue($operationParameter, 'required', true); + } else { + Util::modifyAnnotationValue($operationParameter, 'required', false); + } + } + } + } +} diff --git a/Resources/config/symfony.xml b/Resources/config/symfony.xml deleted file mode 100644 index 1ac1bc988..000000000 --- a/Resources/config/symfony.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/RouteDescriber/InlineParameterDescriber/InlineParameterDescriberInterface.php b/RouteDescriber/InlineParameterDescriber/InlineParameterDescriberInterface.php deleted file mode 100644 index 28e1d8d90..000000000 --- a/RouteDescriber/InlineParameterDescriber/InlineParameterDescriberInterface.php +++ /dev/null @@ -1,15 +0,0 @@ -getAttributes(MapQueryParameter::class, ArgumentMetadata::IS_INSTANCEOF)) { - return false; + /** @var MapQueryParameter $attribute */ + if (!$attribute = $argumentMetadata->getAttributes(MapQueryParameter::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null) { + return; } - return null !== $argumentMetadata->getType(); - } - - public function describe(OA\OpenApi $api, OA\Operation $operation, ArgumentMetadata $argumentMetadata): void - { - $attribute = $argumentMetadata->getAttributes(MapQueryParameter::class, ArgumentMetadata::IS_INSTANCEOF)[0]; - $operationParameter = Util::getOperationParameter($operation, $attribute->name ?? $argumentMetadata->getName(), 'query'); Util::modifyAnnotationValue($operationParameter, 'required', !($argumentMetadata->hasDefaultValue() || $argumentMetadata->isNullable())); diff --git a/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php index 9d34fe6f8..81b222415 100644 --- a/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php +++ b/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php @@ -7,80 +7,31 @@ use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; use Nelmio\ApiDocBundle\Model\Model; -use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface; -use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; -use OpenApi\Generator; use Symfony\Component\HttpKernel\Attribute\MapQueryString; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\PropertyInfo\Type; -final class SymfonyMapQueryStringDescriber implements InlineParameterDescriberInterface, ModelRegistryAwareInterface +final class SymfonyMapQueryStringDescriber implements RouteArgumentDescriberInterface, ModelRegistryAwareInterface { - use ModelRegistryAwareTrait; - - /** - * @param ModelDescriberInterface[] $modelDescribers - */ - public function __construct( - private iterable $modelDescribers - ) { - } + public const CONTEXT_ARGUMENT_METADATA = 'nelmio_api_doc_bundle.map_query_string.argument_metadata'; + public const CONTEXT_MODEL_REF = 'nelmio_api_doc_bundle.map_query_string.model_ref'; - public function supports(ArgumentMetadata $argumentMetadata): bool - { - if (!$argumentMetadata->getAttributes(MapQueryString::class, ArgumentMetadata::IS_INSTANCEOF)) { - return false; - } - - return $argumentMetadata->getType() && class_exists($argumentMetadata->getType()); - } + use ModelRegistryAwareTrait; - public function describe(OA\OpenApi $api, OA\Operation $operation, ArgumentMetadata $argumentMetadata): void + public function describe(ArgumentMetadata $argumentMetadata, OA\Operation $operation): void { - $model = new Model(new Type(Type::BUILTIN_TYPE_OBJECT, $argumentMetadata->isNullable(), $argumentMetadata->getType())); - - $modelRef = $this->modelRegistry->register($model); - - $nativeModelName = str_replace(OA\Components::SCHEMA_REF, '', $modelRef); - - $schemaModel = Util::getSchema($api, $nativeModelName); - - foreach ($this->modelDescribers as $modelDescriber) { - if ($modelDescriber instanceof ModelRegistryAwareInterface) { - $modelDescriber->setModelRegistry($this->modelRegistry); - } - - if ($modelDescriber->supports($model)) { - $modelDescriber->describe($model, $schemaModel); - - break; - } - } - - // There are no properties to map to query parameters - if (Generator::UNDEFINED === $schemaModel->properties) { + /** @var MapQueryString $attribute */ + if (!$attribute = $argumentMetadata->getAttributes(MapQueryString::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null) { return; } - $isModelOptional = $argumentMetadata->hasDefaultValue() || $argumentMetadata->isNullable(); + $modelRef = $this->modelRegistry->register(new Model( + new Type(Type::BUILTIN_TYPE_OBJECT, $argumentMetadata->isNullable(), $argumentMetadata->getType()), + serializationContext: $attribute->serializationContext, + )); - foreach ($schemaModel->properties as $property) { - $operationParameter = Util::getOperationParameter($operation, $property->property, 'query'); - Util::modifyAnnotationValue($operationParameter, 'schema', $property); - Util::modifyAnnotationValue($operationParameter, 'name', $property->property); - Util::modifyAnnotationValue($operationParameter, 'description', $property->description); - Util::modifyAnnotationValue($operationParameter, 'required', $property->required); - Util::modifyAnnotationValue($operationParameter, 'deprecated', $property->deprecated); - Util::modifyAnnotationValue($operationParameter, 'example', $property->example); - - if ($isModelOptional) { - Util::modifyAnnotationValue($operationParameter, 'required', false); - } elseif (is_array($schemaModel->required) && in_array($property->property, $schemaModel->required, true)) { - Util::modifyAnnotationValue($operationParameter, 'required', true); - } else { - Util::modifyAnnotationValue($operationParameter, 'required', false); - } - } + $operation->_context->{self::CONTEXT_ARGUMENT_METADATA} = $argumentMetadata; + $operation->_context->{self::CONTEXT_MODEL_REF} = $modelRef; } } diff --git a/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php b/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php index de95f5adf..91c036ce7 100644 --- a/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php +++ b/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php @@ -4,27 +4,30 @@ namespace Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber; -use InvalidArgumentException; -use Nelmio\ApiDocBundle\Annotation\Model; +use Nelmio\ApiDocBundle\Model\Model; +use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; +use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\PropertyInfo\Type; -final class SymfonyMapRequestPayloadDescriber implements InlineParameterDescriberInterface +final class SymfonyMapRequestPayloadDescriber implements RouteArgumentDescriberInterface, ModelRegistryAwareInterface { - public function supports(ArgumentMetadata $argumentMetadata): bool + use ModelRegistryAwareTrait; + + public function describe(ArgumentMetadata $argumentMetadata, OA\Operation $operation): void { - if (!$argumentMetadata->getAttributes(MapRequestPayload::class, ArgumentMetadata::IS_INSTANCEOF)) { - return false; + /** @var MapRequestPayload $attribute */ + if (!$attribute = $argumentMetadata->getAttributes(MapRequestPayload::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null) { + return; } - return $argumentMetadata->getType() && class_exists($argumentMetadata->getType()); - } - - public function describe(OA\OpenApi $api, OA\Operation $operation, ArgumentMetadata $argumentMetadata): void - { - $attribute = $argumentMetadata->getAttributes(MapRequestPayload::class, ArgumentMetadata::IS_INSTANCEOF)[0]; + $model = $this->modelRegistry->register(new Model( + new Type(Type::BUILTIN_TYPE_OBJECT, $argumentMetadata->isNullable(), $argumentMetadata->getType()), + serializationContext: $attribute->serializationContext, + )); /** @var OA\RequestBody $requestBody */ $requestBody = Util::getChild($operation, OA\RequestBody::class); @@ -37,10 +40,7 @@ public function describe(OA\OpenApi $api, OA\Operation $operation, ArgumentMetad foreach ($formats as $format) { $contentSchema = $this->getContentSchemaForType($requestBody, $format); - Util::modifyAnnotationValue($contentSchema, 'ref', new Model(type: $argumentMetadata->getType())); - Util::modifyAnnotationValue($contentSchema, 'type', 'object'); - - Util::getProperty($contentSchema, $argumentMetadata->getName()); + Util::modifyAnnotationValue($contentSchema, 'ref', $model); } } @@ -57,7 +57,7 @@ private function getContentSchemaForType(OA\RequestBody $requestBody, string $ty break; default: - throw new InvalidArgumentException('Unsupported media type'); + throw new \InvalidArgumentException('Unsupported media type'); } if (!isset($requestBody->content[$contentType])) { diff --git a/RouteDescriber/InlineParameterDescriber.php b/RouteDescriber/RouteArgumentDescriber.php similarity index 77% rename from RouteDescriber/InlineParameterDescriber.php rename to RouteDescriber/RouteArgumentDescriber.php index f7e2a2380..196ea7879 100644 --- a/RouteDescriber/InlineParameterDescriber.php +++ b/RouteDescriber/RouteArgumentDescriber.php @@ -6,19 +6,19 @@ use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; -use Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber\InlineParameterDescriberInterface; +use Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber\RouteArgumentDescriberInterface; use OpenApi\Annotations as OA; use ReflectionMethod; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactoryInterface; use Symfony\Component\Routing\Route; -final class InlineParameterDescriber implements RouteDescriberInterface, ModelRegistryAwareInterface +final class RouteArgumentDescriber implements RouteDescriberInterface, ModelRegistryAwareInterface { use RouteDescriberTrait; use ModelRegistryAwareTrait; /** - * @param InlineParameterDescriberInterface[] $inlineParameterDescribers + * @param RouteArgumentDescriberInterface[] $inlineParameterDescribers */ public function __construct( private ArgumentMetadataFactoryInterface $argumentMetadataFactory, @@ -43,11 +43,7 @@ public function describe(OA\OpenApi $api, Route $route, ReflectionMethod $reflec $inlineParameterDescriber->setModelRegistry($this->modelRegistry); } - if (!$inlineParameterDescriber->supports($argumentMetadata)) { - continue; - } - - $inlineParameterDescriber->describe($api, $operation, $argumentMetadata); + $inlineParameterDescriber->describe($argumentMetadata, $operation); } } } diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php index 2866804e2..31970adda 100644 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -13,8 +13,8 @@ use Nelmio\ApiDocBundle\Model\ModelRegistry; use Nelmio\ApiDocBundle\OpenApiPhp\Util; -use Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber; -use Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber\InlineParameterDescriberInterface; +use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber; +use Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber\RouteArgumentDescriberInterface; use OpenApi\Annotations\OpenApi; use OpenApi\Annotations\Operation; use PHPUnit\Framework\MockObject\MockObject; @@ -33,12 +33,12 @@ class SymfonyDescriberTest extends TestCase private $argumentMetadataFactoryInterface; /** - * @var MockObject&InlineParameterDescriberInterface + * @var MockObject&RouteArgumentDescriberInterface */ private $inlineParameterDescriberInterfaceMock; /** - * @var InlineParameterDescriber + * @var RouteArgumentDescriber */ private $inlineParameterDescriber; @@ -58,7 +58,7 @@ protected function setUp(): void } $this->argumentMetadataFactoryInterface = $this->createMock(ArgumentMetadataFactoryInterface::class); - $this->inlineParameterDescriberInterfaceMock = $this->createMock(InlineParameterDescriberInterface::class); + $this->inlineParameterDescriberInterfaceMock = $this->createMock(RouteArgumentDescriberInterface::class); $this->modelRegistry = new ModelRegistry( [], @@ -66,7 +66,7 @@ protected function setUp(): void [] ); - $this->inlineParameterDescriber = new InlineParameterDescriber( + $this->inlineParameterDescriber = new RouteArgumentDescriber( $this->argumentMetadataFactoryInterface, [$this->inlineParameterDescriberInterfaceMock] ); From f8d9d8ecbdeaaccc1a0647712b7d8c70275e46b3 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 5 Jan 2024 15:51:22 +0100 Subject: [PATCH 105/120] remove tests --- Tests/RouteDescriber/Fixtures/DTO.php | 84 ---------- .../SymfonyDescriberMapQueryStringClass.php | 45 ----- .../SymfonyMapQueryParameterDescriberTest.php | 112 ------------- .../SymfonyMapQueryStringDescriberTest.php | 158 ------------------ .../SymfonyMapRequestPayloadDescriberTest.php | 101 ----------- Tests/RouteDescriber/SymfonyDescriberTest.php | 145 ---------------- 6 files changed, 645 deletions(-) delete mode 100644 Tests/RouteDescriber/Fixtures/DTO.php delete mode 100644 Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php delete mode 100644 Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php delete mode 100644 Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php delete mode 100644 Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php delete mode 100644 Tests/RouteDescriber/SymfonyDescriberTest.php diff --git a/Tests/RouteDescriber/Fixtures/DTO.php b/Tests/RouteDescriber/Fixtures/DTO.php deleted file mode 100644 index 2a6d8f9e8..000000000 --- a/Tests/RouteDescriber/Fixtures/DTO.php +++ /dev/null @@ -1,84 +0,0 @@ -type = 'object'; - $schema->required = self::getRequired(); - $schema->properties = self::getProperties(); - } - - /** - * @return string[] - */ - public static function getRequired(): array - { - return [ - 'id', - 'name', - 'nameWithExample', - 'nameWithDescription', - ]; - } - - /** - * @return Property[] - */ - public static function getProperties(): array - { - return [ - new Property([ - 'property' => 'id', - 'type' => 'int', - ]), - new Property([ - 'property' => 'name', - 'type' => 'string', - ]), - new Property([ - 'property' => 'nullableName', - 'type' => 'string', - 'nullable' => true, - ]), - new Property([ - 'property' => 'nameWithExample', - 'type' => 'string', - 'example' => self::EXAMPLE_NAME, - ]), - new Property([ - 'property' => 'nameWithDescription', - 'type' => 'string', - 'description' => self::DESCRIPTION, - ]), - ]; - } -} diff --git a/Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php b/Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php deleted file mode 100644 index fb659a970..000000000 --- a/Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php +++ /dev/null @@ -1,45 +0,0 @@ -schema = self::SCHEMA; - $schema->title = self::TITLE; - $schema->description = $model->getType()->getClassName(); - $schema->type = self::TYPE; - - $schema->required = [ - 'id', - ]; - $schema->properties = self::getProperties(); - } - - /** - * @return Property[] - */ - public static function getProperties(): array - { - return [ - new Property([ - 'property' => 'id', - 'type' => 'int', - 'nullable' => false, - 'default' => 123, - ]), - ]; - } -} diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php deleted file mode 100644 index 5a75d0a4a..000000000 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php +++ /dev/null @@ -1,112 +0,0 @@ -argumentMetadataFactory = self::getContainer()->get('argument_metadata_factory'); - - $this->symfonyMapQueryParameterDescriber = new SymfonyMapQueryParameterDescriber(); - } - - /** - * @dataProvider provideMapQueryParameterTestData - */ - public function testMapQueryParameter(callable $function): void - { - $argumentMetaData = $this->argumentMetadataFactory->createArgumentMetadata($function)[0]; - - $this->symfonyMapQueryParameterDescriber->describe( - new OpenApi([]), - $operation = new Operation([]), - $argumentMetaData - ); - - /** @var MapQueryParameter $mapQueryParameter */ - $mapQueryParameter = $argumentMetaData->getAttributes(MapQueryParameter::class, ArgumentMetadata::IS_INSTANCEOF)[0]; - - $documentationParameter = $operation->parameters[0]; - self::assertSame($mapQueryParameter->name ?? $argumentMetaData->getName(), $documentationParameter->name); - self::assertSame('query', $documentationParameter->in); - self::assertSame(!$argumentMetaData->hasDefaultValue() && !$argumentMetaData->isNullable(), $documentationParameter->required); - - $schema = $documentationParameter->schema; - self::assertSame('integer', $schema->type); - if ($argumentMetaData->hasDefaultValue()) { - self::assertSame($argumentMetaData->getDefaultValue(), $schema->default); - } - - if (FILTER_VALIDATE_REGEXP === $mapQueryParameter->filter) { - self::assertSame($mapQueryParameter->options['regexp'], $schema->pattern); - } - } - - public static function provideMapQueryParameterTestData(): iterable - { - yield 'it documents query parameters' => [ - function ( - #[MapQueryParameter] int $parameter1, - ) { - }, - ]; - - yield 'it documents query parameters with default values' => [ - function ( - #[MapQueryParameter] int $parameter1 = 123, - ) { - }, - ]; - - yield 'it documents query parameters with nullable types' => [ - function ( - #[MapQueryParameter] ?int $parameter1, - ) { - }, - ]; - - yield 'it uses MapQueryParameter name argument as name' => [ - function ( - #[MapQueryParameter('someOtherParameter1Name')] int $parameter1, - ) { - }, - ]; - - yield 'it documents regex pattern' => [ - function ( - #[MapQueryParameter(filter: FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^\d+$/'])] int $parameter1, - ) { - }, - ]; - } -} diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php deleted file mode 100644 index 3f6ebcbc2..000000000 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php +++ /dev/null @@ -1,158 +0,0 @@ -argumentMetadataFactory = self::getContainer()->get('argument_metadata_factory'); - - $this->openApi = new OpenApi([]); - - $this->symfonyMapQueryStringDescriber = new SymfonyMapQueryStringDescriber([new SelfDescribingModelDescriber()]); - - $registry = new ModelRegistry([], $this->openApi, []); - - $this->symfonyMapQueryStringDescriber->setModelRegistry($registry); - } - - /** - * @dataProvider provideMapQueryStringTestData - */ - public function testMapQueryString(callable $function, bool $required): void - { - $argumentMetaData = $this->argumentMetadataFactory->createArgumentMetadata($function)[0]; - - $this->symfonyMapQueryStringDescriber->describe( - $this->openApi, - $operation = new Operation([]), - $argumentMetaData - ); - - // Test it registers the model - $modelSchema = $this->openApi->components->schemas[0]; - $expectedModelProperties = SymfonyDescriberMapQueryStringClass::getProperties(); - - self::assertSame(SymfonyDescriberMapQueryStringClass::SCHEMA, $modelSchema->schema); - self::assertSame(SymfonyDescriberMapQueryStringClass::TITLE, $modelSchema->title); - self::assertSame(SymfonyDescriberMapQueryStringClass::TYPE, $modelSchema->type); - self::assertEquals($expectedModelProperties, $modelSchema->properties); - - foreach ($expectedModelProperties as $key => $expectedModelProperty) { - $queryParameter = $operation->parameters[$key]; - - self::assertSame('query', $queryParameter->in); - self::assertSame($expectedModelProperty->property, $queryParameter->name); - self::assertSame($required, $queryParameter->required); - } - } - - public static function provideMapQueryStringTestData(): iterable - { - yield 'it documents query string parameters' => [ - function ( - #[MapQueryString] SymfonyDescriberMapQueryStringClass $parameter1, - ) { - }, - true, - ]; - - yield 'it documents a nullable type as optional' => [ - function ( - #[MapQueryString] ?SymfonyDescriberMapQueryStringClass $parameter1, - ) { - }, - false, - ]; - - yield 'it documents a default value as optional' => [ - function ( - #[MapQueryString] ?SymfonyDescriberMapQueryStringClass $parameter1, - ) { - }, - false, - ]; - } - - public function testItDescribesProperties(): void - { - $function = function ( - #[MapQueryString] DTO $DTO, - ) { - }; - - $argumentMetaData = $this->argumentMetadataFactory->createArgumentMetadata($function)[0]; - - $this->symfonyMapQueryStringDescriber->describe( - $this->openApi, - $operation = new Operation([]), - $argumentMetaData - ); - - // Test it registers the model - $modelSchema = $this->openApi->components->schemas[0]; - - self::assertEquals('object', $modelSchema->type); - self::assertEquals(DTO::getRequired(), $modelSchema->required); - self::assertEquals(DTO::getProperties(), $modelSchema->properties); - - self::assertSame('id', $operation->parameters[0]->name); - self::assertSame('int', $operation->parameters[0]->schema->type); - - self::assertSame('name', $operation->parameters[1]->name); - - self::assertSame('nullableName', $operation->parameters[2]->name); - self::assertSame('string', $operation->parameters[2]->schema->type); - self::assertSame(false, $operation->parameters[2]->required); - self::assertSame(true, $operation->parameters[2]->schema->nullable); - - self::assertSame('nameWithExample', $operation->parameters[3]->name); - self::assertSame('string', $operation->parameters[3]->schema->type); - self::assertSame(true, $operation->parameters[3]->required); - self::assertSame(DTO::EXAMPLE_NAME, $operation->parameters[3]->schema->example); - self::assertSame(DTO::EXAMPLE_NAME, $operation->parameters[3]->example); - - self::assertSame('nameWithDescription', $operation->parameters[4]->name); - self::assertSame('string', $operation->parameters[4]->schema->type); - self::assertSame(true, $operation->parameters[4]->required); - self::assertSame(DTO::DESCRIPTION, $operation->parameters[4]->schema->description); - self::assertSame(DTO::DESCRIPTION, $operation->parameters[4]->description); - } -} diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php deleted file mode 100644 index bb97af4d3..000000000 --- a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php +++ /dev/null @@ -1,101 +0,0 @@ -argumentMetadataFactory = self::getContainer()->get('argument_metadata_factory'); - - $this->symfonyMapRequestPayloadDescriber = new SymfonyMapRequestPayloadDescriber(); - } - - /** - * @dataProvider provideMapRequestPayloadTestData - * - * @param string[] $expectedMediaTypes - */ - public function testMapRequestPayload(callable $function, array $expectedMediaTypes): void - { - $argumentMetaData = $this->argumentMetadataFactory->createArgumentMetadata($function)[0]; - - $this->symfonyMapRequestPayloadDescriber->describe( - new OpenApi([]), - $operation = new Operation([]), - $argumentMetaData - ); - - foreach ($expectedMediaTypes as $expectedMediaType) { - $requestBodyContent = $operation->requestBody->content[$expectedMediaType]; - - self::assertSame($expectedMediaType, $requestBodyContent->mediaType); - self::assertSame('object', $requestBodyContent->schema->type); - self::assertSame(stdClass::class, $requestBodyContent->schema->ref->type); - } - } - - public static function provideMapRequestPayloadTestData(): iterable - { - yield 'it sets default mediaType to json' => [ - function ( - #[MapRequestPayload] stdClass $payload - ) { - }, - ['application/json'], - ]; - - yield 'it sets mediaType to json' => [ - function ( - #[MapRequestPayload('json')] stdClass $payload - ) { - }, - ['application/json'], - ]; - - yield 'it sets mediaType to xml' => [ - function ( - #[MapRequestPayload('xml')] stdClass $payload - ) { - }, - ['application/xml'], - ]; - - yield 'it sets multiple mediaTypes' => [ - function ( - #[MapRequestPayload(['json', 'xml'])] stdClass $payload - ) { - }, - ['application/json', 'application/xml'], - ]; - } -} diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php deleted file mode 100644 index 31970adda..000000000 --- a/Tests/RouteDescriber/SymfonyDescriberTest.php +++ /dev/null @@ -1,145 +0,0 @@ -argumentMetadataFactoryInterface = $this->createMock(ArgumentMetadataFactoryInterface::class); - $this->inlineParameterDescriberInterfaceMock = $this->createMock(RouteArgumentDescriberInterface::class); - - $this->modelRegistry = new ModelRegistry( - [], - $this->openApi = new OpenApi([]), - [] - ); - - $this->inlineParameterDescriber = new RouteArgumentDescriber( - $this->argumentMetadataFactoryInterface, - [$this->inlineParameterDescriberInterfaceMock] - ); - - $this->inlineParameterDescriber->setModelRegistry($this->modelRegistry); - } - - public function testDescribe(): void - { - $argumentMetaData = $this->createStub(ArgumentMetadata::class); - - $reflectionMethodStub = $this->createStub(ReflectionMethod::class); - - $this->argumentMetadataFactoryInterface - ->expects(self::once()) - ->method('createArgumentMetadata') - ->with('foo', $reflectionMethodStub) - ->willReturn([$argumentMetaData]) - ; - - $this->inlineParameterDescriberInterfaceMock - ->expects(self::exactly(count(Util::OPERATIONS))) - ->method('supports') - ->with($argumentMetaData) - ->willReturn(true) - ; - - $this->inlineParameterDescriberInterfaceMock - ->expects(self::exactly(count(Util::OPERATIONS))) - ->method('describe') - ->with( - $this->openApi, - self::isInstanceOf(Operation::class), - $argumentMetaData - ) - ; - - $this->inlineParameterDescriber->describe( - $this->openApi, - new Route('/', ['_controller' => 'foo']), - $reflectionMethodStub - ); - } - - public function testDescribeSkipsUnsupportedDescribers(): void - { - $argumentMetaData = $this->createStub(ArgumentMetadata::class); - - $reflectionMethodStub = $this->createStub(ReflectionMethod::class); - - $this->argumentMetadataFactoryInterface - ->expects(self::once()) - ->method('createArgumentMetadata') - ->with('foo', $reflectionMethodStub) - ->willReturn([$argumentMetaData]) - ; - - $this->inlineParameterDescriberInterfaceMock - ->expects(self::exactly(count(Util::OPERATIONS))) - ->method('supports') - ->with($argumentMetaData) - ->willReturn(false) - ; - - $this->inlineParameterDescriberInterfaceMock - ->expects(self::never()) - ->method('describe') - ; - - $this->inlineParameterDescriber->describe( - $this->openApi, - new Route('/', ['_controller' => 'foo']), - $reflectionMethodStub - ); - } -} From 5355511c3d6c5a928eb87273a5bf051de22af31f Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 5 Jan 2024 15:52:18 +0100 Subject: [PATCH 106/120] style fix --- DependencyInjection/NelmioApiDocExtension.php | 2 +- .../SymfonyMapRequestPayloadDescriber.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index d5156d8d9..5710cd913 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -189,7 +189,7 @@ public function load(array $configs, ContainerBuilder $container): void ->addTag('nelmio_api_doc.route_describer', ['priority' => -225]) ->setArguments([ new Reference('argument_metadata_factory'), - new TaggedIteratorArgument('nelmio_api_doc.route_argument_describer') + new TaggedIteratorArgument('nelmio_api_doc.route_argument_describer'), ]) ; diff --git a/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php b/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php index 91c036ce7..4dc7f86ad 100644 --- a/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php +++ b/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php @@ -4,9 +4,9 @@ namespace Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber; -use Nelmio\ApiDocBundle\Model\Model; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; +use Nelmio\ApiDocBundle\Model\Model; use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; From 2102421c63fde9e74af00ada3507f1398123ac74 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 5 Jan 2024 16:18:32 +0100 Subject: [PATCH 107/120] expand symfony map attribute tests --- .../Functional/Controller/ApiController81.php | 5 + .../Entity/SymfonyMapQueryString.php | 1 + Tests/Functional/SymfonyFunctionalTest.php | 409 ++++++++++++++---- 3 files changed, 330 insertions(+), 85 deletions(-) diff --git a/Tests/Functional/Controller/ApiController81.php b/Tests/Functional/Controller/ApiController81.php index bfe65bf09..b976145fe 100644 --- a/Tests/Functional/Controller/ApiController81.php +++ b/Tests/Functional/Controller/ApiController81.php @@ -470,6 +470,11 @@ public function fetchArticleFromMapQueryStringNullable( in: 'query', description: 'Query parameter articleType81 description' )] + #[OA\Parameter( + name: 'nullableArticleType81', + in: 'query', + description: 'Query parameter nullableArticleType81 description' + )] #[OA\Response(response: '200', description: '')] public function fetchArticleFromMapQueryStringOverwriteParameters( #[MapQueryString] SymfonyMapQueryString $article81Query diff --git a/Tests/Functional/Entity/SymfonyMapQueryString.php b/Tests/Functional/Entity/SymfonyMapQueryString.php index 99d682a27..4f9713025 100644 --- a/Tests/Functional/Entity/SymfonyMapQueryString.php +++ b/Tests/Functional/Entity/SymfonyMapQueryString.php @@ -11,6 +11,7 @@ public function __construct( public string $name, public ?string $nullableName, public ArticleType81 $articleType81, + public ?ArticleType81 $nullableArticleType81, ) { } } diff --git a/Tests/Functional/SymfonyFunctionalTest.php b/Tests/Functional/SymfonyFunctionalTest.php index 97ef86a3c..e2fd39daf 100644 --- a/Tests/Functional/SymfonyFunctionalTest.php +++ b/Tests/Functional/SymfonyFunctionalTest.php @@ -11,7 +11,6 @@ namespace Nelmio\ApiDocBundle\Tests\Functional; -use OpenApi\Annotations\Components; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Attribute\MapQueryString; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; @@ -56,6 +55,12 @@ public function testMapQueryStringModelGetsCreated(): void 'articleType81' => [ '$ref' => '#/components/schemas/ArticleType81', ], + 'nullableArticleType81' => [ + 'nullable' => true, + 'allOf' => [ + ['$ref' => '#/components/schemas/ArticleType81'] + ], + ], ], 'type' => 'object', ]; @@ -69,24 +74,65 @@ public function testMapQueryString(): void self::markTestSkipped('Symfony 6.3 MapQueryString attribute not found'); } - $operation = $this->getOperation('/api/article_map_query_string', 'get'); - - $in = 'query'; - - $parameter = $this->getParameter($operation, 'id', $in); - self::assertTrue($parameter->required); - - $parameter = $this->getParameter($operation, 'name', $in); - self::assertTrue($parameter->required); - - $parameter = $this->getParameter($operation, 'nullableName', $in); - self::assertFalse($parameter->required); - - $parameter = $this->getParameter($operation, 'articleType81', $in); - - $property = $this->getProperty($this->getModel('SymfonyMapQueryString'), 'articleType81'); - self::assertTrue($parameter->required); - self::assertEquals($property, $parameter->schema); + self::assertEquals([ + 'operationId' => 'get_api_nelmio_apidoc_tests_functional_api_fetcharticlefrommapquerystring', + 'parameters' => [ + [ + 'name' => 'id', + 'in' => 'query', + 'required' => true, + 'schema' => [ + 'type' => 'integer', + 'property' => 'id', + ], + ], + [ + 'name' => 'name', + 'in' => 'query', + 'required' => true, + 'schema' => [ + 'type' => 'string', + 'property' => 'name', + ], + ], + [ + 'name' => 'nullableName', + 'in' => 'query', + 'required' => false, + 'schema' => [ + 'type' => 'string', + 'nullable' => true, + 'property' => 'nullableName', + ], + ], + [ + 'name' => 'articleType81', + 'in' => 'query', + 'required' => true, + 'schema' => [ + '$ref' => '#/components/schemas/ArticleType81', + 'property' => 'articleType81', + ], + ], + [ + 'name' => 'nullableArticleType81', + 'in' => 'query', + 'required' => false, + 'schema' => [ + 'nullable' => true, + 'allOf' => [ + ['$ref' => '#/components/schemas/ArticleType81'] + ], + 'property' => 'nullableArticleType81', + ], + ], + ], + 'responses' => [ + '200' => [ + 'description' => '', + ], + ], + ], json_decode($this->getOperation('/api/article_map_query_string', 'get')->toJson(), true)); } public function testMapQueryStringParametersAreOptional(): void @@ -95,21 +141,65 @@ public function testMapQueryStringParametersAreOptional(): void self::markTestSkipped('Symfony 6.3 MapQueryString attribute not found'); } - $operation = $this->getOperation('/api/article_map_query_string_nullable', 'get'); - - $in = 'query'; - - $parameter = $this->getParameter($operation, 'id', $in); - self::assertFalse($parameter->required); - - $parameter = $this->getParameter($operation, 'name', $in); - self::assertFalse($parameter->required); - - $parameter = $this->getParameter($operation, 'nullableName', $in); - self::assertFalse($parameter->required); - - $parameter = $this->getParameter($operation, 'articleType81', $in); - self::assertFalse($parameter->required); + self::assertEquals([ + 'operationId' => 'get_api_nelmio_apidoc_tests_functional_api_fetcharticlefrommapquerystringnullable', + 'parameters' => [ + [ + 'name' => 'id', + 'in' => 'query', + 'required' => false, + 'schema' => [ + 'type' => 'integer', + 'property' => 'id', + ], + ], + [ + 'name' => 'name', + 'in' => 'query', + 'required' => false, + 'schema' => [ + 'type' => 'string', + 'property' => 'name', + ], + ], + [ + 'name' => 'nullableName', + 'in' => 'query', + 'required' => false, + 'schema' => [ + 'type' => 'string', + 'nullable' => true, + 'property' => 'nullableName', + ], + ], + [ + 'name' => 'articleType81', + 'in' => 'query', + 'required' => false, + 'schema' => [ + '$ref' => '#/components/schemas/ArticleType81', + 'property' => 'articleType81', + ], + ], + [ + 'name' => 'nullableArticleType81', + 'in' => 'query', + 'required' => false, + 'schema' => [ + 'nullable' => true, + 'allOf' => [ + ['$ref' => '#/components/schemas/ArticleType81'] + ], + 'property' => 'nullableArticleType81', + ], + ], + ], + 'responses' => [ + '200' => [ + 'description' => '', + ], + ], + ], json_decode($this->getOperation('/api/article_map_query_string_nullable', 'get')->toJson(), true)); } public function testMapQueryStringParametersOverwriteParameters(): void @@ -118,12 +208,70 @@ public function testMapQueryStringParametersOverwriteParameters(): void self::markTestSkipped('Symfony 6.3 MapQueryString attribute not found'); } - $operation = $this->getOperation('/api/article_map_query_string_overwrite_parameters', 'get'); - - foreach (['id', 'name', 'nullableName', 'articleType81'] as $name) { - $parameter = $this->getParameter($operation, $name, 'query'); - self::assertSame($parameter->description, sprintf('Query parameter %s description', $name)); - } + self::assertEquals([ + 'operationId' => 'get_api_nelmio_apidoc_tests_functional_api_fetcharticlefrommapquerystringoverwriteparameters', + 'parameters' => [ + [ + 'name' => 'id', + 'in' => 'query', + 'required' => true, + 'schema' => [ + 'type' => 'integer', + 'property' => 'id', + ], + 'description' => 'Query parameter id description', + ], + [ + 'name' => 'name', + 'in' => 'query', + 'required' => true, + 'schema' => [ + 'type' => 'string', + 'property' => 'name', + ], + 'description' => 'Query parameter name description', + ], + [ + 'name' => 'nullableName', + 'in' => 'query', + 'required' => false, + 'schema' => [ + 'type' => 'string', + 'nullable' => true, + 'property' => 'nullableName', + ], + 'description' => 'Query parameter nullableName description', + ], + [ + 'name' => 'articleType81', + 'in' => 'query', + 'required' => true, + 'schema' => [ + '$ref' => '#/components/schemas/ArticleType81', + 'property' => 'articleType81', + ], + 'description' => 'Query parameter articleType81 description', + ], + [ + 'name' => 'nullableArticleType81', + 'in' => 'query', + 'required' => false, + 'schema' => [ + 'nullable' => true, + 'allOf' => [ + ['$ref' => '#/components/schemas/ArticleType81'] + ], + 'property' => 'nullableArticleType81', + ], + 'description' => 'Query parameter nullableArticleType81 description', + ], + ], + 'responses' => [ + '200' => [ + 'description' => '', + ], + ], + ], json_decode($this->getOperation('/api/article_map_query_string_overwrite_parameters', 'get')->toJson(), true)); } public function testMapQueryParameter(): void @@ -132,12 +280,24 @@ public function testMapQueryParameter(): void self::markTestSkipped('Symfony 6.3 MapQueryParameter attribute not found'); } - $operation = $this->getOperation('/api/article_map_query_parameter', 'get'); - $in = 'query'; - - $parameter = $this->getParameter($operation, 'id', $in); - self::assertTrue($parameter->required); - self::assertSame('integer', $parameter->schema->type); + self::assertEquals([ + 'operationId' => 'get_api_nelmio_apidoc_tests_functional_api_fetcharticlefrommapqueryparameter', + 'parameters' => [ + [ + 'name' => 'id', + 'in' => 'query', + 'required' => true, + 'schema' => [ + 'type' => 'integer', + ], + ], + ], + 'responses' => [ + '200' => [ + 'description' => '', + ], + ], + ], json_decode($this->getOperation('/api/article_map_query_parameter', 'get')->toJson(), true)); } public function testMapQueryParameterHandlesNullable(): void @@ -146,11 +306,25 @@ public function testMapQueryParameterHandlesNullable(): void self::markTestSkipped('Symfony 6.3 MapQueryParameter attribute not found'); } - $operation = $this->getOperation('/api/article_map_query_parameter_nullable', 'get'); - $in = 'query'; - - $parameter = $this->getParameter($operation, 'id', $in); - self::assertFalse($parameter->required); + self::assertEquals([ + 'operationId' => 'get_api_nelmio_apidoc_tests_functional_api_fetcharticlefrommapqueryparameternullable', + 'parameters' => [ + [ + 'name' => 'id', + 'in' => 'query', + 'required' => false, + 'schema' => [ + 'type' => 'integer', + 'nullable' => true, + ], + ], + ], + 'responses' => [ + '200' => [ + 'description' => '', + ], + ], + ], json_decode($this->getOperation('/api/article_map_query_parameter_nullable', 'get')->toJson(), true)); } public function testMapQueryParameterHandlesDefault(): void @@ -159,12 +333,26 @@ public function testMapQueryParameterHandlesDefault(): void self::markTestSkipped('Symfony 6.3 MapQueryParameter attribute not found'); } - $operation = $this->getOperation('/api/article_map_query_parameter_default', 'get'); - $in = 'query'; - - $parameter = $this->getParameter($operation, 'id', $in); - self::assertFalse($parameter->required); - self::assertSame(123, $parameter->schema->default); + self::assertEquals([ + 'operationId' => 'get_api_nelmio_apidoc_tests_functional_api_fetcharticlefrommapqueryparameterdefault', + 'parameters' => [ + [ + 'name' => 'id', + 'in' => 'query', + 'required' => false, + 'schema' => [ + 'type' => 'integer', + 'nullable' => true, + 'default' => 123, + ], + ], + ], + 'responses' => [ + '200' => [ + 'description' => '', + ], + ], + ], json_decode($this->getOperation('/api/article_map_query_parameter_default', 'get')->toJson(), true)); } public function testMapQueryParameterOverwriteParameter(): void @@ -173,12 +361,27 @@ public function testMapQueryParameterOverwriteParameter(): void self::markTestSkipped('Symfony 6.3 MapQueryParameter attribute not found'); } - $operation = $this->getOperation('/api/article_map_query_parameter_overwrite_parameters', 'get'); - $in = 'query'; - - $parameter = $this->getParameter($operation, 'id', $in); - self::assertSame(123, $parameter->example); - self::assertSame('Query parameter id description', $parameter->description); + self::assertEquals([ + 'operationId' => 'get_api_nelmio_apidoc_tests_functional_api_fetcharticlefrommapqueryparameteroverwriteparameters', + 'parameters' => [ + [ + 'name' => 'id', + 'in' => 'query', + 'required' => false, + 'schema' => [ + 'type' => 'integer', + 'nullable' => true, + ], + 'description' => 'Query parameter id description', + 'example' => 123, + ], + ], + 'responses' => [ + '200' => [ + 'description' => '', + ], + ], + ], json_decode($this->getOperation('/api/article_map_query_parameter_overwrite_parameters', 'get')->toJson(), true)); } public function testMapRequestPayload(): void @@ -187,20 +390,24 @@ public function testMapRequestPayload(): void self::markTestSkipped('Symfony 6.3 MapRequestPayload attribute not found'); } - $operation = $this->getOperation('/api/article_map_request_payload', 'post'); - - $requestBody = $operation->requestBody; - self::assertTrue($requestBody->required); - - self::assertCount(1, $requestBody->content); - self::assertArrayHasKey('application/json', $requestBody->content); - - $media = $requestBody->content['application/json']; - - self::assertSame('application/json', $media->mediaType); - - $model = $this->getModel('Article81'); - self::assertSame(Components::SCHEMA_REF.$model->schema, $media->schema->ref); + self::assertEquals([ + 'operationId' => 'post_api_nelmio_apidoc_tests_functional_api_createarticlefrommaprequestpayload', + 'responses' => [ + '200' => [ + 'description' => '', + ], + ], + 'requestBody' => [ + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/Article81', + ], + ], + ], + 'required' => true, + ], + ], json_decode($this->getOperation('/api/article_map_request_payload', 'post')->toJson(), true)); } public function testMapRequestPayloadNullable(): void @@ -209,10 +416,27 @@ public function testMapRequestPayloadNullable(): void self::markTestSkipped('Symfony 6.3 MapRequestPayload attribute not found'); } - $operation = $this->getOperation('/api/article_map_request_payload_nullable', 'post'); - - $requestBody = $operation->requestBody; - self::assertFalse($requestBody->required); + self::assertEquals([ + 'operationId' => 'post_api_nelmio_apidoc_tests_functional_api_createarticlefrommaprequestpayloadnullable', + 'responses' => [ + '200' => [ + 'description' => '', + ], + ], + 'requestBody' => [ + 'content' => [ + 'application/json' => [ + 'schema' => [ + 'nullable' => true, + 'oneOf' => [ + ['$ref' => '#/components/schemas/Article81'], + ], + ], + ], + ], + 'required' => false, + ], + ], json_decode($this->getOperation('/api/article_map_request_payload_nullable', 'post')->toJson(), true)); } public function testMapRequestPayloadOverwriteRequestBody(): void @@ -221,9 +445,24 @@ public function testMapRequestPayloadOverwriteRequestBody(): void self::markTestSkipped('Symfony 6.3 MapRequestPayload attribute not found'); } - $operation = $this->getOperation('/api/article_map_request_payload_overwrite', 'post'); - - $requestBody = $operation->requestBody; - self::assertSame('Request body description', $requestBody->description); + self::assertEquals([ + 'operationId' => 'post_api_nelmio_apidoc_tests_functional_api_createarticlefrommaprequestpayloadoverwrite', + 'responses' => [ + '200' => [ + 'description' => '', + ], + ], + 'requestBody' => [ + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/Article81', + ], + ], + ], + 'required' => true, + 'description' => 'Request body description', + ], + ], json_decode($this->getOperation('/api/article_map_request_payload_overwrite', 'post')->toJson(), true)); } } From 74df5eb2a55cc404ad2d12759ceb00acebb21c33 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 5 Jan 2024 16:28:08 +0100 Subject: [PATCH 108/120] fix multiple models generated when null --- .../SymfonyMapRequestPayloadDescriber.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php b/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php index 4dc7f86ad..7dbd8b4ce 100644 --- a/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php +++ b/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php @@ -25,7 +25,7 @@ public function describe(ArgumentMetadata $argumentMetadata, OA\Operation $opera } $model = $this->modelRegistry->register(new Model( - new Type(Type::BUILTIN_TYPE_OBJECT, $argumentMetadata->isNullable(), $argumentMetadata->getType()), + new Type(Type::BUILTIN_TYPE_OBJECT, false, $argumentMetadata->getType()), serializationContext: $attribute->serializationContext, )); From ff938897dbd1ad2bc832adedb0f35482327faa2c Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 5 Jan 2024 16:28:33 +0100 Subject: [PATCH 109/120] generate proper nullable --- .../SymfonyMapQueryParameterDescriber.php | 4 ++++ .../SymfonyMapRequestPayloadDescriber.php | 11 ++++------- Tests/Functional/SymfonyFunctionalTest.php | 1 - 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryParameterDescriber.php b/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryParameterDescriber.php index afeb9f313..5644474d4 100644 --- a/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryParameterDescriber.php +++ b/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryParameterDescriber.php @@ -37,5 +37,9 @@ public function describe(ArgumentMetadata $argumentMetadata, OA\Operation $opera } $this->mapNativeType($schema, $argumentMetadata->getType()); + + if ($argumentMetadata->isNullable()) { + $schema->nullable = true; + } } } diff --git a/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php b/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php index 7dbd8b4ce..c554c07e1 100644 --- a/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php +++ b/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php @@ -41,6 +41,10 @@ public function describe(ArgumentMetadata $argumentMetadata, OA\Operation $opera foreach ($formats as $format) { $contentSchema = $this->getContentSchemaForType($requestBody, $format); Util::modifyAnnotationValue($contentSchema, 'ref', $model); + + if ($argumentMetadata->isNullable()) { + $contentSchema->nullable = true; + } } } @@ -68,13 +72,6 @@ private function getContentSchemaForType(OA\RequestBody $requestBody, string $ty '_context' => $weakContext, ] ); - - /** @var OA\Schema $schema */ - $schema = Util::getChild( - $requestBody->content[$contentType], - OA\Schema::class - ); - Util::modifyAnnotationValue($schema, 'type', 'object'); } return Util::getChild( diff --git a/Tests/Functional/SymfonyFunctionalTest.php b/Tests/Functional/SymfonyFunctionalTest.php index e2fd39daf..6805d1ad4 100644 --- a/Tests/Functional/SymfonyFunctionalTest.php +++ b/Tests/Functional/SymfonyFunctionalTest.php @@ -342,7 +342,6 @@ public function testMapQueryParameterHandlesDefault(): void 'required' => false, 'schema' => [ 'type' => 'integer', - 'nullable' => true, 'default' => 123, ], ], From 90ac4f5d124bf1d01249843d22dddcd0a695f7ba Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 5 Jan 2024 16:29:29 +0100 Subject: [PATCH 110/120] style fix --- Tests/Functional/SymfonyFunctionalTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/Functional/SymfonyFunctionalTest.php b/Tests/Functional/SymfonyFunctionalTest.php index 6805d1ad4..2923d4503 100644 --- a/Tests/Functional/SymfonyFunctionalTest.php +++ b/Tests/Functional/SymfonyFunctionalTest.php @@ -58,7 +58,7 @@ public function testMapQueryStringModelGetsCreated(): void 'nullableArticleType81' => [ 'nullable' => true, 'allOf' => [ - ['$ref' => '#/components/schemas/ArticleType81'] + ['$ref' => '#/components/schemas/ArticleType81'], ], ], ], @@ -121,7 +121,7 @@ public function testMapQueryString(): void 'schema' => [ 'nullable' => true, 'allOf' => [ - ['$ref' => '#/components/schemas/ArticleType81'] + ['$ref' => '#/components/schemas/ArticleType81'], ], 'property' => 'nullableArticleType81', ], @@ -188,7 +188,7 @@ public function testMapQueryStringParametersAreOptional(): void 'schema' => [ 'nullable' => true, 'allOf' => [ - ['$ref' => '#/components/schemas/ArticleType81'] + ['$ref' => '#/components/schemas/ArticleType81'], ], 'property' => 'nullableArticleType81', ], @@ -259,7 +259,7 @@ public function testMapQueryStringParametersOverwriteParameters(): void 'schema' => [ 'nullable' => true, 'allOf' => [ - ['$ref' => '#/components/schemas/ArticleType81'] + ['$ref' => '#/components/schemas/ArticleType81'], ], 'property' => 'nullableArticleType81', ], From db9c2840df5cd0c8df3a2292185a189f2280f596 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 5 Jan 2024 18:40:16 +0100 Subject: [PATCH 111/120] remove property property from schema --- Processors/MapQueryStringProcessor.php | 18 +++++++++++++----- Tests/Functional/SymfonyFunctionalTest.php | 15 --------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/Processors/MapQueryStringProcessor.php b/Processors/MapQueryStringProcessor.php index 115eabfca..872c18d63 100644 --- a/Processors/MapQueryStringProcessor.php +++ b/Processors/MapQueryStringProcessor.php @@ -53,12 +53,19 @@ public function __invoke(Analysis $analysis) foreach ($schemaModel->properties as $property) { $operationParameter = Util::getOperationParameter($operation, $property->property, 'query'); - Util::modifyAnnotationValue($operationParameter, 'schema', $property); + + // Remove incompatible properties + $propertyVars = get_object_vars($property); + unset($propertyVars['property']); + + $schema = new OA\Schema($propertyVars); + + $operationParameter->schema = $schema; Util::modifyAnnotationValue($operationParameter, 'name', $property->property); - Util::modifyAnnotationValue($operationParameter, 'description', $property->description); - Util::modifyAnnotationValue($operationParameter, 'required', $property->required); - Util::modifyAnnotationValue($operationParameter, 'deprecated', $property->deprecated); - Util::modifyAnnotationValue($operationParameter, 'example', $property->example); + Util::modifyAnnotationValue($operationParameter, 'description', $schema->description); + Util::modifyAnnotationValue($operationParameter, 'required', $schema->required); + Util::modifyAnnotationValue($operationParameter, 'deprecated', $schema->deprecated); + Util::modifyAnnotationValue($operationParameter, 'example', $schema->example); if ($isModelOptional) { Util::modifyAnnotationValue($operationParameter, 'required', false); @@ -68,6 +75,7 @@ public function __invoke(Analysis $analysis) Util::modifyAnnotationValue($operationParameter, 'required', false); } } + } } } diff --git a/Tests/Functional/SymfonyFunctionalTest.php b/Tests/Functional/SymfonyFunctionalTest.php index 2923d4503..aa54434ab 100644 --- a/Tests/Functional/SymfonyFunctionalTest.php +++ b/Tests/Functional/SymfonyFunctionalTest.php @@ -83,7 +83,6 @@ public function testMapQueryString(): void 'required' => true, 'schema' => [ 'type' => 'integer', - 'property' => 'id', ], ], [ @@ -92,7 +91,6 @@ public function testMapQueryString(): void 'required' => true, 'schema' => [ 'type' => 'string', - 'property' => 'name', ], ], [ @@ -102,7 +100,6 @@ public function testMapQueryString(): void 'schema' => [ 'type' => 'string', 'nullable' => true, - 'property' => 'nullableName', ], ], [ @@ -111,7 +108,6 @@ public function testMapQueryString(): void 'required' => true, 'schema' => [ '$ref' => '#/components/schemas/ArticleType81', - 'property' => 'articleType81', ], ], [ @@ -123,7 +119,6 @@ public function testMapQueryString(): void 'allOf' => [ ['$ref' => '#/components/schemas/ArticleType81'], ], - 'property' => 'nullableArticleType81', ], ], ], @@ -150,7 +145,6 @@ public function testMapQueryStringParametersAreOptional(): void 'required' => false, 'schema' => [ 'type' => 'integer', - 'property' => 'id', ], ], [ @@ -159,7 +153,6 @@ public function testMapQueryStringParametersAreOptional(): void 'required' => false, 'schema' => [ 'type' => 'string', - 'property' => 'name', ], ], [ @@ -169,7 +162,6 @@ public function testMapQueryStringParametersAreOptional(): void 'schema' => [ 'type' => 'string', 'nullable' => true, - 'property' => 'nullableName', ], ], [ @@ -178,7 +170,6 @@ public function testMapQueryStringParametersAreOptional(): void 'required' => false, 'schema' => [ '$ref' => '#/components/schemas/ArticleType81', - 'property' => 'articleType81', ], ], [ @@ -190,7 +181,6 @@ public function testMapQueryStringParametersAreOptional(): void 'allOf' => [ ['$ref' => '#/components/schemas/ArticleType81'], ], - 'property' => 'nullableArticleType81', ], ], ], @@ -217,7 +207,6 @@ public function testMapQueryStringParametersOverwriteParameters(): void 'required' => true, 'schema' => [ 'type' => 'integer', - 'property' => 'id', ], 'description' => 'Query parameter id description', ], @@ -227,7 +216,6 @@ public function testMapQueryStringParametersOverwriteParameters(): void 'required' => true, 'schema' => [ 'type' => 'string', - 'property' => 'name', ], 'description' => 'Query parameter name description', ], @@ -238,7 +226,6 @@ public function testMapQueryStringParametersOverwriteParameters(): void 'schema' => [ 'type' => 'string', 'nullable' => true, - 'property' => 'nullableName', ], 'description' => 'Query parameter nullableName description', ], @@ -248,7 +235,6 @@ public function testMapQueryStringParametersOverwriteParameters(): void 'required' => true, 'schema' => [ '$ref' => '#/components/schemas/ArticleType81', - 'property' => 'articleType81', ], 'description' => 'Query parameter articleType81 description', ], @@ -261,7 +247,6 @@ public function testMapQueryStringParametersOverwriteParameters(): void 'allOf' => [ ['$ref' => '#/components/schemas/ArticleType81'], ], - 'property' => 'nullableArticleType81', ], 'description' => 'Query parameter nullableArticleType81 description', ], From a82f5f6e7e6c90288a674e70fd4f787868d5f46e Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 5 Jan 2024 18:40:45 +0100 Subject: [PATCH 112/120] style fix --- Processors/MapQueryStringProcessor.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Processors/MapQueryStringProcessor.php b/Processors/MapQueryStringProcessor.php index 872c18d63..75ca1f1af 100644 --- a/Processors/MapQueryStringProcessor.php +++ b/Processors/MapQueryStringProcessor.php @@ -75,7 +75,6 @@ public function __invoke(Analysis $analysis) Util::modifyAnnotationValue($operationParameter, 'required', false); } } - } } } From 34e19d2b65d1643da36f42f01c7783a193e29a2e Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 5 Jan 2024 19:00:52 +0100 Subject: [PATCH 113/120] handle reflection exception --- RouteDescriber/RouteArgumentDescriber.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/RouteDescriber/RouteArgumentDescriber.php b/RouteDescriber/RouteArgumentDescriber.php index 196ea7879..d0259e3e2 100644 --- a/RouteDescriber/RouteArgumentDescriber.php +++ b/RouteDescriber/RouteArgumentDescriber.php @@ -30,7 +30,11 @@ public function describe(OA\OpenApi $api, Route $route, ReflectionMethod $reflec { $controller = $route->getDefault('_controller'); - $argumentMetaDataList = $this->argumentMetadataFactory->createArgumentMetadata($controller, $reflectionMethod); + try { + $argumentMetaDataList = $this->argumentMetadataFactory->createArgumentMetadata($controller, $reflectionMethod); + } catch (\ReflectionException) { + return; + } if (!$argumentMetaDataList) { return; From ffdb8ec26e2885c675decffaccfe00935ba38e88 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 5 Jan 2024 19:03:31 +0100 Subject: [PATCH 114/120] rename dir --- DependencyInjection/NelmioApiDocExtension.php | 8 ++++---- Processors/MapQueryStringProcessor.php | 4 ++-- RouteDescriber/RouteArgumentDescriber.php | 2 +- .../RouteArgumentDescriberInterface.php | 2 +- .../SymfonyMapQueryParameterDescriber.php | 2 +- .../SymfonyMapQueryStringDescriber.php | 2 +- .../SymfonyMapRequestPayloadDescriber.php | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) rename RouteDescriber/{InlineParameterDescriber => RouteArgumentDescriber}/RouteArgumentDescriberInterface.php (79%) rename RouteDescriber/{InlineParameterDescriber => RouteArgumentDescriber}/SymfonyMapQueryParameterDescriber.php (95%) rename RouteDescriber/{InlineParameterDescriber => RouteArgumentDescriber}/SymfonyMapQueryStringDescriber.php (95%) rename RouteDescriber/{InlineParameterDescriber => RouteArgumentDescriber}/SymfonyMapRequestPayloadDescriber.php (97%) diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index 5710cd913..a5c61a5cd 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -22,10 +22,10 @@ use Nelmio\ApiDocBundle\ModelDescriber\JMSModelDescriber; use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface; use Nelmio\ApiDocBundle\Processors\MapQueryStringProcessor; -use Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber\RouteArgumentDescriberInterface; -use Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber\SymfonyMapQueryParameterDescriber; -use Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber\SymfonyMapQueryStringDescriber; -use Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber\SymfonyMapRequestPayloadDescriber; +use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\RouteArgumentDescriberInterface; +use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapQueryParameterDescriber; +use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapQueryStringDescriber; +use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapRequestPayloadDescriber; use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber; use Nelmio\ApiDocBundle\Routing\FilteredRouteCollectionBuilder; use OpenApi\Generator; diff --git a/Processors/MapQueryStringProcessor.php b/Processors/MapQueryStringProcessor.php index 75ca1f1af..78d9c3fb9 100644 --- a/Processors/MapQueryStringProcessor.php +++ b/Processors/MapQueryStringProcessor.php @@ -5,7 +5,7 @@ namespace Nelmio\ApiDocBundle\Processors; use Nelmio\ApiDocBundle\OpenApiPhp\Util; -use Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber\SymfonyMapQueryStringDescriber; +use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapQueryStringDescriber; use OpenApi\Analysis; use OpenApi\Annotations as OA; use OpenApi\Generator; @@ -16,7 +16,7 @@ * A processor that adds query parameters to operations that have a MapQueryString attribute. * A processor is used to ensure that a Model is created. * - * @see \Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber\SymfonyMapQueryStringDescriber + * @see \Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapQueryStringDescriber */ final class MapQueryStringProcessor implements ProcessorInterface { diff --git a/RouteDescriber/RouteArgumentDescriber.php b/RouteDescriber/RouteArgumentDescriber.php index d0259e3e2..b95534ab7 100644 --- a/RouteDescriber/RouteArgumentDescriber.php +++ b/RouteDescriber/RouteArgumentDescriber.php @@ -6,7 +6,7 @@ use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; -use Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber\RouteArgumentDescriberInterface; +use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\RouteArgumentDescriberInterface; use OpenApi\Annotations as OA; use ReflectionMethod; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactoryInterface; diff --git a/RouteDescriber/InlineParameterDescriber/RouteArgumentDescriberInterface.php b/RouteDescriber/RouteArgumentDescriber/RouteArgumentDescriberInterface.php similarity index 79% rename from RouteDescriber/InlineParameterDescriber/RouteArgumentDescriberInterface.php rename to RouteDescriber/RouteArgumentDescriber/RouteArgumentDescriberInterface.php index 0f6683a6a..7932addc6 100644 --- a/RouteDescriber/InlineParameterDescriber/RouteArgumentDescriberInterface.php +++ b/RouteDescriber/RouteArgumentDescriber/RouteArgumentDescriberInterface.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber; +namespace Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber; use OpenApi\Annotations as OA; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; diff --git a/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryParameterDescriber.php b/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryParameterDescriber.php similarity index 95% rename from RouteDescriber/InlineParameterDescriber/SymfonyMapQueryParameterDescriber.php rename to RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryParameterDescriber.php index 5644474d4..b51649c89 100644 --- a/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryParameterDescriber.php +++ b/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryParameterDescriber.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber; +namespace Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber; use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; diff --git a/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryStringDescriber.php similarity index 95% rename from RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php rename to RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryStringDescriber.php index 81b222415..18ba946de 100644 --- a/RouteDescriber/InlineParameterDescriber/SymfonyMapQueryStringDescriber.php +++ b/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryStringDescriber.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber; +namespace Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; diff --git a/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php b/RouteDescriber/RouteArgumentDescriber/SymfonyMapRequestPayloadDescriber.php similarity index 97% rename from RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php rename to RouteDescriber/RouteArgumentDescriber/SymfonyMapRequestPayloadDescriber.php index c554c07e1..1b3f4c9c2 100644 --- a/RouteDescriber/InlineParameterDescriber/SymfonyMapRequestPayloadDescriber.php +++ b/RouteDescriber/RouteArgumentDescriber/SymfonyMapRequestPayloadDescriber.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Nelmio\ApiDocBundle\RouteDescriber\InlineParameterDescriber; +namespace Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; From 05f66dd84b72fc990db5e84d01963d02f316e17e Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 5 Jan 2024 19:39:06 +0100 Subject: [PATCH 115/120] style fix --- DependencyInjection/NelmioApiDocExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index a5c61a5cd..1c4c44ab4 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -22,11 +22,11 @@ use Nelmio\ApiDocBundle\ModelDescriber\JMSModelDescriber; use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface; use Nelmio\ApiDocBundle\Processors\MapQueryStringProcessor; +use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber; use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\RouteArgumentDescriberInterface; use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapQueryParameterDescriber; use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapQueryStringDescriber; use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapRequestPayloadDescriber; -use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber; use Nelmio\ApiDocBundle\Routing\FilteredRouteCollectionBuilder; use OpenApi\Generator; use Symfony\Component\Config\FileLocator; From 37418d511a60bd66183b8f4c281ef583115f16f9 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 5 Jan 2024 20:54:45 +0100 Subject: [PATCH 116/120] Move MapRequestPayload describing to swagger processor --- DependencyInjection/NelmioApiDocExtension.php | 5 + Processors/MapQueryStringProcessor.php | 4 +- Processors/MapRequestPayloadProcessor.php | 99 +++++++++++++++++++ .../SymfonyMapQueryStringDescriber.php | 4 +- .../SymfonyMapRequestPayloadDescriber.php | 57 ++--------- .../Functional/Controller/ApiController81.php | 13 +++ Tests/Functional/SymfonyFunctionalTest.php | 27 +++++ 7 files changed, 154 insertions(+), 55 deletions(-) create mode 100644 Processors/MapRequestPayloadProcessor.php diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index 1c4c44ab4..2bed2c2f6 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -22,6 +22,7 @@ use Nelmio\ApiDocBundle\ModelDescriber\JMSModelDescriber; use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface; use Nelmio\ApiDocBundle\Processors\MapQueryStringProcessor; +use Nelmio\ApiDocBundle\Processors\MapRequestPayloadProcessor; use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber; use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\RouteArgumentDescriberInterface; use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapQueryParameterDescriber; @@ -207,6 +208,10 @@ public function load(array $configs, ContainerBuilder $container): void $container->register('nelmio_api_doc.route_argument_describer.map_request_payload', SymfonyMapRequestPayloadDescriber::class) ->setPublic(false) ->addTag('nelmio_api_doc.route_argument_describer', ['priority' => 0]); + + $container->register('nelmio_api_doc.swagger.processor.map_request_payload', MapRequestPayloadProcessor::class) + ->setPublic(false) + ->addTag('nelmio_api_doc.swagger.processor', ['priority' => 0]); } if (class_exists(MapQueryParameter::class)) { diff --git a/Processors/MapQueryStringProcessor.php b/Processors/MapQueryStringProcessor.php index 78d9c3fb9..a32e6523a 100644 --- a/Processors/MapQueryStringProcessor.php +++ b/Processors/MapQueryStringProcessor.php @@ -16,7 +16,7 @@ * A processor that adds query parameters to operations that have a MapQueryString attribute. * A processor is used to ensure that a Model is created. * - * @see \Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapQueryStringDescriber + * @see SymfonyMapQueryStringDescriber */ final class MapQueryStringProcessor implements ProcessorInterface { @@ -32,7 +32,7 @@ public function __invoke(Analysis $analysis) $argumentMetaData = $operation->_context->{SymfonyMapQueryStringDescriber::CONTEXT_ARGUMENT_METADATA}; if (!$argumentMetaData instanceof ArgumentMetadata) { - continue; + throw new \LogicException(sprintf('MapQueryString ArgumentMetaData not found for operation "%s"', $operation->operationId)); } $modelRef = $operation->_context->{SymfonyMapQueryStringDescriber::CONTEXT_MODEL_REF}; diff --git a/Processors/MapRequestPayloadProcessor.php b/Processors/MapRequestPayloadProcessor.php new file mode 100644 index 000000000..0b5fb4be0 --- /dev/null +++ b/Processors/MapRequestPayloadProcessor.php @@ -0,0 +1,99 @@ +getAnnotationsOfType(OA\Operation::class); + + foreach ($operations as $operation) { + if (!isset($operation->_context->{SymfonyMapRequestPayloadDescriber::CONTEXT_ARGUMENT_METADATA})) { + continue; + } + + $argumentMetaData = $operation->_context->{SymfonyMapRequestPayloadDescriber::CONTEXT_ARGUMENT_METADATA}; + if (!$argumentMetaData instanceof ArgumentMetadata) { + throw new \LogicException(sprintf('MapRequestPayload ArgumentMetaData not found for operation "%s"', $operation->operationId)); + } + + /** @var MapRequestPayload $attribute */ + if (!$attribute = $argumentMetaData->getAttributes(MapRequestPayload::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null) { + throw new \LogicException(sprintf('Operation "%s" does not contain attribute of "%s', $operation->operationId, MapRequestPayload::class)); + } + + $modelRef = $operation->_context->{SymfonyMapRequestPayloadDescriber::CONTEXT_MODEL_REF}; + if (!isset($modelRef)) { + throw new \LogicException(sprintf('MapRequestPayload Model reference not found for operation "%s"', $operation->operationId)); + } + + /** @var OA\RequestBody $requestBody */ + $requestBody = Util::getChild($operation, OA\RequestBody::class); + Util::modifyAnnotationValue($requestBody, 'required', !($argumentMetaData->hasDefaultValue() || $argumentMetaData->isNullable())); + + $formats = $attribute->acceptFormat; + if (!is_array($formats)) { + $formats = [$attribute->acceptFormat ?? 'json']; + } + + foreach ($formats as $format) { + $contentSchema = $this->getContentSchemaForType($requestBody, $format); + Util::modifyAnnotationValue($contentSchema, 'ref', $modelRef); + + if ($argumentMetaData->isNullable()) { + $contentSchema->nullable = true; + } + } + } + } + + private function getContentSchemaForType(OA\RequestBody $requestBody, string $type): OA\Schema + { + Util::modifyAnnotationValue($requestBody, 'content', []); + switch ($type) { + case 'json': + $contentType = 'application/json'; + + break; + case 'xml': + $contentType = 'application/xml'; + + break; + default: + throw new \InvalidArgumentException('Unsupported media type'); + } + + if (!isset($requestBody->content[$contentType])) { + $weakContext = Util::createWeakContext($requestBody->_context); + $requestBody->content[$contentType] = new OA\MediaType( + [ + 'mediaType' => $contentType, + '_context' => $weakContext, + ] + ); + } + + return Util::getChild( + $requestBody->content[$contentType], + OA\Schema::class + ); + } +} diff --git a/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryStringDescriber.php index 18ba946de..95e998a88 100644 --- a/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryStringDescriber.php +++ b/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryStringDescriber.php @@ -14,8 +14,8 @@ final class SymfonyMapQueryStringDescriber implements RouteArgumentDescriberInterface, ModelRegistryAwareInterface { - public const CONTEXT_ARGUMENT_METADATA = 'nelmio_api_doc_bundle.map_query_string.argument_metadata'; - public const CONTEXT_MODEL_REF = 'nelmio_api_doc_bundle.map_query_string.model_ref'; + public const CONTEXT_ARGUMENT_METADATA = 'nelmio_api_doc_bundle.argument_metadata.'.self::class; + public const CONTEXT_MODEL_REF = 'nelmio_api_doc_bundle.model_ref.'.self::class; use ModelRegistryAwareTrait; diff --git a/RouteDescriber/RouteArgumentDescriber/SymfonyMapRequestPayloadDescriber.php b/RouteDescriber/RouteArgumentDescriber/SymfonyMapRequestPayloadDescriber.php index 1b3f4c9c2..5deb86744 100644 --- a/RouteDescriber/RouteArgumentDescriber/SymfonyMapRequestPayloadDescriber.php +++ b/RouteDescriber/RouteArgumentDescriber/SymfonyMapRequestPayloadDescriber.php @@ -7,7 +7,6 @@ use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; use Nelmio\ApiDocBundle\Model\Model; -use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; @@ -15,6 +14,9 @@ final class SymfonyMapRequestPayloadDescriber implements RouteArgumentDescriberInterface, ModelRegistryAwareInterface { + public const CONTEXT_ARGUMENT_METADATA = 'nelmio_api_doc_bundle.argument_metadata.'.self::class; + public const CONTEXT_MODEL_REF = 'nelmio_api_doc_bundle.model_ref.'.self::class; + use ModelRegistryAwareTrait; public function describe(ArgumentMetadata $argumentMetadata, OA\Operation $operation): void @@ -24,59 +26,12 @@ public function describe(ArgumentMetadata $argumentMetadata, OA\Operation $opera return; } - $model = $this->modelRegistry->register(new Model( + $modelRef = $this->modelRegistry->register(new Model( new Type(Type::BUILTIN_TYPE_OBJECT, false, $argumentMetadata->getType()), serializationContext: $attribute->serializationContext, )); - /** @var OA\RequestBody $requestBody */ - $requestBody = Util::getChild($operation, OA\RequestBody::class); - Util::modifyAnnotationValue($requestBody, 'required', !($argumentMetadata->hasDefaultValue() || $argumentMetadata->isNullable())); - - $formats = $attribute->acceptFormat; - if (!is_array($formats)) { - $formats = [$attribute->acceptFormat ?? 'json']; - } - - foreach ($formats as $format) { - $contentSchema = $this->getContentSchemaForType($requestBody, $format); - Util::modifyAnnotationValue($contentSchema, 'ref', $model); - - if ($argumentMetadata->isNullable()) { - $contentSchema->nullable = true; - } - } - } - - private function getContentSchemaForType(OA\RequestBody $requestBody, string $type): OA\Schema - { - Util::modifyAnnotationValue($requestBody, 'content', []); - switch ($type) { - case 'json': - $contentType = 'application/json'; - - break; - case 'xml': - $contentType = 'application/xml'; - - break; - default: - throw new \InvalidArgumentException('Unsupported media type'); - } - - if (!isset($requestBody->content[$contentType])) { - $weakContext = Util::createWeakContext($requestBody->_context); - $requestBody->content[$contentType] = new OA\MediaType( - [ - 'mediaType' => $contentType, - '_context' => $weakContext, - ] - ); - } - - return Util::getChild( - $requestBody->content[$contentType], - OA\Schema::class - ); + $operation->_context->{self::CONTEXT_ARGUMENT_METADATA} = $argumentMetadata; + $operation->_context->{self::CONTEXT_MODEL_REF} = $modelRef; } } diff --git a/Tests/Functional/Controller/ApiController81.php b/Tests/Functional/Controller/ApiController81.php index b976145fe..1e907ca49 100644 --- a/Tests/Functional/Controller/ApiController81.php +++ b/Tests/Functional/Controller/ApiController81.php @@ -538,4 +538,17 @@ public function createArticleFromMapRequestPayloadOverwrite( #[MapRequestPayload] Article81 $article81, ) { } + + #[Route('/article_map_request_payload_handles_already_set_content', methods: ['POST'])] + #[OA\RequestBody( + description: 'Request body description', + content: new OA\JsonContent( + ref: new Model(type: Article81::class) + ), + )] + #[OA\Response(response: '200', description: '')] + public function createArticleFromMapRequestPayloadHandlesAlreadySetContent( + #[MapRequestPayload] Article81 $article81, + ) { + } } diff --git a/Tests/Functional/SymfonyFunctionalTest.php b/Tests/Functional/SymfonyFunctionalTest.php index aa54434ab..a834c87a0 100644 --- a/Tests/Functional/SymfonyFunctionalTest.php +++ b/Tests/Functional/SymfonyFunctionalTest.php @@ -449,4 +449,31 @@ public function testMapRequestPayloadOverwriteRequestBody(): void ], ], json_decode($this->getOperation('/api/article_map_request_payload_overwrite', 'post')->toJson(), true)); } + + public function testMapRequestPayloadHandlesAlreadySetContent(): void + { + if (!class_exists(MapRequestPayload::class)) { + self::markTestSkipped('Symfony 6.3 MapRequestPayload attribute not found'); + } + + self::assertEquals([ + 'operationId' => 'post_api_nelmio_apidoc_tests_functional_api_createarticlefrommaprequestpayloadhandlesalreadysetcontent', + 'responses' => [ + '200' => [ + 'description' => '', + ], + ], + 'requestBody' => [ + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/Article81', + ], + ], + ], + 'required' => true, + 'description' => 'Request body description', + ], + ], json_decode($this->getOperation('/api/article_map_request_payload_handles_already_set_content', 'post')->toJson(), true)); + } } From 63f7c0942178172ab3883b7a34fc68771902ddc8 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 5 Jan 2024 20:57:15 +0100 Subject: [PATCH 117/120] fix baseline --- phpunit-baseline.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/phpunit-baseline.json b/phpunit-baseline.json index 631b34dd8..1db9fba4b 100644 --- a/phpunit-baseline.json +++ b/phpunit-baseline.json @@ -7083,5 +7083,20 @@ "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapRequestPayloadOverwriteRequestBody", "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_file\" service is deprecated since version 5.2", "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapRequestPayloadHandlesAlreadySetContent", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_class\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapRequestPayloadHandlesAlreadySetContent", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_dir\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SymfonyFunctionalTest::testMapRequestPayloadHandlesAlreadySetContent", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_file\" service is deprecated since version 5.2", + "count": 1 } ] From 77da33333a5fb1341094b63dfb3a8cb0e7cfa4da Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 5 Jan 2024 21:06:28 +0100 Subject: [PATCH 118/120] test overwriting to different model --- Tests/Functional/Controller/ApiController81.php | 1 + Tests/Functional/SymfonyFunctionalTest.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/Functional/Controller/ApiController81.php b/Tests/Functional/Controller/ApiController81.php index 1e907ca49..710992723 100644 --- a/Tests/Functional/Controller/ApiController81.php +++ b/Tests/Functional/Controller/ApiController81.php @@ -532,6 +532,7 @@ public function createArticleFromMapRequestPayloadNullable( #[Route('/article_map_request_payload_overwrite', methods: ['POST'])] #[OA\RequestBody( description: 'Request body description', + content: new Model(type: EntityWithNullableSchemaSet::class), )] #[OA\Response(response: '200', description: '')] public function createArticleFromMapRequestPayloadOverwrite( diff --git a/Tests/Functional/SymfonyFunctionalTest.php b/Tests/Functional/SymfonyFunctionalTest.php index a834c87a0..0005ecb24 100644 --- a/Tests/Functional/SymfonyFunctionalTest.php +++ b/Tests/Functional/SymfonyFunctionalTest.php @@ -440,7 +440,7 @@ public function testMapRequestPayloadOverwriteRequestBody(): void 'content' => [ 'application/json' => [ 'schema' => [ - '$ref' => '#/components/schemas/Article81', + '$ref' => '#/components/schemas/EntityWithNullableSchemaSet', ], ], ], From 1204c320e345c6c5d5dce33e9261a656c9762260 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Fri, 5 Jan 2024 21:21:13 +0100 Subject: [PATCH 119/120] query testing for schema overwriting --- Processors/MapQueryStringProcessor.php | 2 +- .../SymfonyMapQueryParameterDescriber.php | 9 ++++++--- Tests/Functional/Controller/ApiController81.php | 9 +++++++++ Tests/Functional/SymfonyFunctionalTest.php | 14 +++++++++++++- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/Processors/MapQueryStringProcessor.php b/Processors/MapQueryStringProcessor.php index a32e6523a..708dccdd0 100644 --- a/Processors/MapQueryStringProcessor.php +++ b/Processors/MapQueryStringProcessor.php @@ -60,7 +60,7 @@ public function __invoke(Analysis $analysis) $schema = new OA\Schema($propertyVars); - $operationParameter->schema = $schema; + Util::modifyAnnotationValue($operationParameter, 'schema', $schema); Util::modifyAnnotationValue($operationParameter, 'name', $property->property); Util::modifyAnnotationValue($operationParameter, 'description', $schema->description); Util::modifyAnnotationValue($operationParameter, 'required', $schema->required); diff --git a/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryParameterDescriber.php b/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryParameterDescriber.php index b51649c89..c9f454626 100644 --- a/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryParameterDescriber.php +++ b/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryParameterDescriber.php @@ -6,6 +6,7 @@ use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; +use OpenApi\Generator; use OpenApi\Processors\Concerns\TypesTrait; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; @@ -36,10 +37,12 @@ public function describe(ArgumentMetadata $argumentMetadata, OA\Operation $opera Util::modifyAnnotationValue($schema, 'default', $argumentMetadata->getDefaultValue()); } - $this->mapNativeType($schema, $argumentMetadata->getType()); + if (Generator::UNDEFINED === $schema->type) { + $this->mapNativeType($schema, $argumentMetadata->getType()); + } - if ($argumentMetadata->isNullable()) { - $schema->nullable = true; + if (Generator::UNDEFINED === $schema->nullable && $argumentMetadata->isNullable()) { + Util::modifyAnnotationValue($schema, 'nullable', true); } } } diff --git a/Tests/Functional/Controller/ApiController81.php b/Tests/Functional/Controller/ApiController81.php index 710992723..242826a5b 100644 --- a/Tests/Functional/Controller/ApiController81.php +++ b/Tests/Functional/Controller/ApiController81.php @@ -453,6 +453,7 @@ public function fetchArticleFromMapQueryStringNullable( #[OA\Parameter( name: 'id', in: 'query', + schema: new OA\Schema(type: 'string', nullable: true), description: 'Query parameter id description' )] #[OA\Parameter( @@ -509,9 +510,17 @@ public function fetchArticleFromMapQueryParameterDefault( description: 'Query parameter id description', example: 123, )] + #[OA\Parameter( + name: 'changedType', + in: 'query', + schema: new OA\Schema(type: 'int', nullable: false), + description: 'Incorrectly described query parameter', + example: 123, + )] #[OA\Response(response: '200', description: '')] public function fetchArticleFromMapQueryParameterOverwriteParameters( #[MapQueryParameter] ?int $id, + #[MapQueryParameter] ?string $changedType, ) { } diff --git a/Tests/Functional/SymfonyFunctionalTest.php b/Tests/Functional/SymfonyFunctionalTest.php index 0005ecb24..f318cd438 100644 --- a/Tests/Functional/SymfonyFunctionalTest.php +++ b/Tests/Functional/SymfonyFunctionalTest.php @@ -206,7 +206,8 @@ public function testMapQueryStringParametersOverwriteParameters(): void 'in' => 'query', 'required' => true, 'schema' => [ - 'type' => 'integer', + 'type' => 'string', + 'nullable' => true, ], 'description' => 'Query parameter id description', ], @@ -359,6 +360,17 @@ public function testMapQueryParameterOverwriteParameter(): void 'description' => 'Query parameter id description', 'example' => 123, ], + [ + 'name' => 'changedType', + 'in' => 'query', + 'required' => false, + 'schema' => [ + 'type' => 'int', + 'nullable' => false, + ], + 'description' => 'Incorrectly described query parameter', + 'example' => 123, + ], ], 'responses' => [ '200' => [ From a2f44ea6ceeb4baac0b31c9d8d8365f419576caa Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Sat, 6 Jan 2024 01:36:56 +0100 Subject: [PATCH 120/120] documentation update --- DependencyInjection/NelmioApiDocExtension.php | 2 +- Processors/MapQueryStringProcessor.php | 2 +- Processors/MapRequestPayloadProcessor.php | 2 +- Resources/doc/symfony_attributes.rst | 43 ++++++++++++++++++- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index 2bed2c2f6..40a8f3dc2 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -181,7 +181,7 @@ public function load(array $configs, ContainerBuilder $container): void } if (PHP_VERSION_ID > 80100) { - // Add autoconfiguration for inline parameter describer + // Add autoconfiguration for route argument describer $container->registerForAutoconfiguration(RouteArgumentDescriberInterface::class) ->addTag('nelmio_api_doc.route_argument_describer'); diff --git a/Processors/MapQueryStringProcessor.php b/Processors/MapQueryStringProcessor.php index 708dccdd0..5a419df75 100644 --- a/Processors/MapQueryStringProcessor.php +++ b/Processors/MapQueryStringProcessor.php @@ -14,7 +14,7 @@ /** * A processor that adds query parameters to operations that have a MapQueryString attribute. - * A processor is used to ensure that a Model is created. + * A processor is used to ensure that a Model has been created. * * @see SymfonyMapQueryStringDescriber */ diff --git a/Processors/MapRequestPayloadProcessor.php b/Processors/MapRequestPayloadProcessor.php index 0b5fb4be0..b524f9836 100644 --- a/Processors/MapRequestPayloadProcessor.php +++ b/Processors/MapRequestPayloadProcessor.php @@ -14,7 +14,7 @@ /** * A processor that adds query parameters to operations that have a MapRequestPayload attribute. - * A processor is used to ensure that a Model is created. + * A processor is used to ensure that a Model has been created. * * @see SymfonyMapRequestPayloadDescriber */ diff --git a/Resources/doc/symfony_attributes.rst b/Resources/doc/symfony_attributes.rst index f30e7100a..0f0c77594 100644 --- a/Resources/doc/symfony_attributes.rst +++ b/Resources/doc/symfony_attributes.rst @@ -149,11 +149,52 @@ Complete example } } +Customization +---------------------- + +Imagine you want to add, modify, or remove some documentation for a route argument. For that you will have to create your own describer which implements the :class:`RouteArgumentDescriberInterface`_ interface. + +Register your route argument describer +~~~~~~~ + +Before you can use your custom describer you must register it in your route argument describer as a service and tag it with ``nelmio_api_doc.route_argument_describer``. +Services implementing the :class:`RouteArgumentDescriberInterface`_ interface are automatically detected and used by NelmioApiDocBundle. + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + App\Describer\CustomRouteArgumentDescriber: + tags: + - { name: nelmio_api_doc.route_argument_describer } + + .. code-block:: xml + + + + + + + .. code-block:: php + + // config/services.php + use App\Describer\CustomRouteArgumentDescriber; + + return function (ContainerConfigurator $container) { + $container->services() + ->set(CustomRouteArgumentDescriber::class) + ->tag('nelmio_api_doc.route_argument_describer') + ; + }; + Disclaimer ---------------------- -Make sure to use at least php 8 (annotations) to make use of this functionality +Make sure to use at least php 8.1 (attribute support) to make use of this functionality. .. _`Symfony MapQueryString`: https://symfony.com/doc/current/controller.html#mapping-the-whole-query-string .. _`Symfony MapQueryParameter`: https://symfony.com/doc/current/controller.html#mapping-query-parameters-individually .. _`Symfony MapRequestPayload`: https://symfony.com/doc/current/controller.html#mapping-request-payload +.. _`RouteArgumentDescriberInterface`: https://github.com/DjordyKoert/NelmioApiDocBundle/blob/master/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryParameterDescriber.php