From 9b642dffb5a9a68595af3e62c2772f3c7e9a1272 Mon Sep 17 00:00:00 2001 From: Djordy Koert Date: Mon, 15 Jan 2024 15:14:07 +0100 Subject: [PATCH] Add Symfony attribute describers (#2112) * Add SymfonyDescriber * Add SymfonyDescriber dependency injection * Fix codestyle * fixup! Fix codestyle * Add php 8 checks * Add symfony.xml loading * Temp: increase max self deprecations * Add Exception throw when invalid php version is used * Fix codestyle * Add describeRequestBody method * Use elseif * Only check for php version once * Add SymfonyDescriberTest for MapRequestPayload * Fix annotation * Skip test if attributes don't exist * Skip test based on php version * Move $mapRequestPayload type to annotation * Fix annotation style * Fix SymfonyDescriberTest for older symfony versions * Remove version check * Remove usage of in_array to check for attribute * Change elseif to separate if statement * Fix testMapRequestPayloadParamRegistersRequestBody for split up if statement * Add testMapQueryParameter * Fix codestyle * Remove newline * Expand docs for symfony controller mapping * Add backticks * Clarify docs * Upgrade major_version to 6 * Upgrade versionadded_directive_min_version to 6.0 * Revert max allowed self deprecations * Revert phpunit.xml.dist changes * Revert "Revert max allowed self deprecations" This reverts commit c75b5396007bb46a24f43c30aaf44805d13f0ba8. * Remove not working generator bypass and replace with iterable * Update testMapQueryParameter to work with 'controller' classes * Update testMapRequestPayload to work with 'controller' classes * Remove check for MapQueryString existence * Fix codestyle * Swap comparison order * initial MapQueryString setup * Move annotation describe methods to their own SymfonyAnnotationDescriber classes * Cleanup * Add annotation describer services * Move to own test files * Cleanup * Call setModelRegistry on annotation describers * Use accessible values for tests * Add SymfonyMapQueryStringDescriberTest * Only check availability of needed attribute * Fix message * Fix styleci * Fix styleci * Expand SymfonyMapQueryStringDescriber to copy property data to query parameter data * Fix style * Add missing newline * Fix test php 7.2 compatability * Remove annotation var name * Fix missing values * Fix missing values * Add DTO testing class * Expand SymfonyMapQueryStringDescriberTest * Add SymfonyDescriberTest tests * Remove unused import * Remove trailing commas * Copy ref * Remove setting allowEmptyValue * Remove empty value test * Update documentation * Merge documentation instead of overwriting * Expand symfony controller mapping attribute documentation * Fix RST * Fix RST (missing blank line) * Revert max self deprecations * Use modelDescriber to describe model instead of registering all models * Create weak context * Get schema from property instead of manually setting every property * Add newline at end of file * Fix style * Prevent overwriting non-default values * Add functional test for MapQueryString * Use modifyAnnotationValue helper method instead of overwriting * Fix incorrect name is used for query * Transform int to integer * Remove allowEmptyValue * Fix type comparison * Fix enum not being used in test * Add MapQueryParameter functional tests * Update required statement * Set requestBody required * Add MapRequestPayload functional tests * Fix style * Cleanup array format check * add required field to test * fix baseline * refactor logic to use symfony metadata instead of reflection * style fix * re-add manually iterating over describers * re-add unit tests * style fix * remove named parameter * move xml load logic to describers * Revert "move xml load logic to describers" This reverts commit 035db3fba7214a9f832a4038a3afcbeb790aef03. * major refactor * remove tests * style fix * expand symfony map attribute tests * fix multiple models generated when null * generate proper nullable * style fix * remove property property from schema * style fix * handle reflection exception * rename dir * style fix * Move MapRequestPayload describing to swagger processor * fix baseline * test overwriting to different model * query testing for schema overwriting * documentation update --- .doctor-rst.yaml | 6 +- DependencyInjection/NelmioApiDocExtension.php | 51 ++ OpenApiPhp/Util.php | 12 + Processors/MapQueryStringProcessor.php | 80 +++ Processors/MapRequestPayloadProcessor.php | 99 ++++ Resources/doc/index.rst | 9 +- Resources/doc/symfony_attributes.rst | 200 +++++++ RouteDescriber/RouteArgumentDescriber.php | 55 ++ .../RouteArgumentDescriberInterface.php | 13 + .../SymfonyMapQueryParameterDescriber.php | 48 ++ .../SymfonyMapQueryStringDescriber.php | 37 ++ .../SymfonyMapRequestPayloadDescriber.php | 37 ++ .../Functional/Controller/ApiController81.php | 131 +++++ .../Entity/SymfonyMapQueryString.php | 17 + Tests/Functional/SymfonyFunctionalTest.php | 491 ++++++++++++++++++ phpunit-baseline.json | 180 +++++++ 16 files changed, 1462 insertions(+), 4 deletions(-) create mode 100644 Processors/MapQueryStringProcessor.php create mode 100644 Processors/MapRequestPayloadProcessor.php create mode 100644 Resources/doc/symfony_attributes.rst create mode 100644 RouteDescriber/RouteArgumentDescriber.php create mode 100644 RouteDescriber/RouteArgumentDescriber/RouteArgumentDescriberInterface.php create mode 100644 RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryParameterDescriber.php create mode 100644 RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryStringDescriber.php create mode 100644 RouteDescriber/RouteArgumentDescriber/SymfonyMapRequestPayloadDescriber.php create mode 100644 Tests/Functional/Entity/SymfonyMapQueryString.php create mode 100644 Tests/Functional/SymfonyFunctionalTest.php 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' diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index bf93ef8c2..5dc202738 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -21,6 +21,13 @@ use Nelmio\ApiDocBundle\ModelDescriber\BazingaHateoasModelDescriber; 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; +use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapQueryStringDescriber; +use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapRequestPayloadDescriber; use Nelmio\ApiDocBundle\Routing\FilteredRouteCollectionBuilder; use OpenApi\Generator; use Symfony\Component\Config\FileLocator; @@ -32,6 +39,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; @@ -170,6 +180,47 @@ public function load(array $configs, ContainerBuilder $container): void ->setArgument(1, $config['media_types']); } + if (PHP_VERSION_ID > 80100) { + // Add autoconfiguration for route argument 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]); + + $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)) { + $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'); if (!isset($bundles['TwigBundle']) || !class_exists('Symfony\Component\Asset\Packages')) { $container->removeDefinition('nelmio_api_doc.controller.swagger_ui'); diff --git a/OpenApiPhp/Util.php b/OpenApiPhp/Util.php index 8a3537f56..b03727a47 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/Processors/MapQueryStringProcessor.php b/Processors/MapQueryStringProcessor.php new file mode 100644 index 000000000..5a419df75 --- /dev/null +++ b/Processors/MapQueryStringProcessor.php @@ -0,0 +1,80 @@ +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) { + throw new \LogicException(sprintf('MapQueryString ArgumentMetaData not found for operation "%s"', $operation->operationId)); + } + + $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'); + + // Remove incompatible properties + $propertyVars = get_object_vars($property); + unset($propertyVars['property']); + + $schema = new OA\Schema($propertyVars); + + Util::modifyAnnotationValue($operationParameter, 'schema', $schema); + Util::modifyAnnotationValue($operationParameter, 'name', $property->property); + 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); + } 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/Processors/MapRequestPayloadProcessor.php b/Processors/MapRequestPayloadProcessor.php new file mode 100644 index 000000000..b524f9836 --- /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/Resources/doc/index.rst b/Resources/doc/index.rst index 3b21567c3..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, 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 @@ -239,6 +239,12 @@ 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** 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 ---------- @@ -576,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 diff --git a/Resources/doc/symfony_attributes.rst b/Resources/doc/symfony_attributes.rst new file mode 100644 index 000000000..0f0c77594 --- /dev/null +++ b/Resources/doc/symfony_attributes.rst @@ -0,0 +1,200 @@ +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\UserDTO; + use AppBundle\UserQuery; + 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) + { + // ... + } + } + +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.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 diff --git a/RouteDescriber/RouteArgumentDescriber.php b/RouteDescriber/RouteArgumentDescriber.php new file mode 100644 index 000000000..b95534ab7 --- /dev/null +++ b/RouteDescriber/RouteArgumentDescriber.php @@ -0,0 +1,55 @@ +getDefault('_controller'); + + try { + $argumentMetaDataList = $this->argumentMetadataFactory->createArgumentMetadata($controller, $reflectionMethod); + } catch (\ReflectionException) { + return; + } + + 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); + } + + $inlineParameterDescriber->describe($argumentMetadata, $operation); + } + } + } + } +} diff --git a/RouteDescriber/RouteArgumentDescriber/RouteArgumentDescriberInterface.php b/RouteDescriber/RouteArgumentDescriber/RouteArgumentDescriberInterface.php new file mode 100644 index 000000000..7932addc6 --- /dev/null +++ b/RouteDescriber/RouteArgumentDescriber/RouteArgumentDescriberInterface.php @@ -0,0 +1,13 @@ +getAttributes(MapQueryParameter::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null) { + return; + } + + $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()); + } + + if (Generator::UNDEFINED === $schema->type) { + $this->mapNativeType($schema, $argumentMetadata->getType()); + } + + if (Generator::UNDEFINED === $schema->nullable && $argumentMetadata->isNullable()) { + Util::modifyAnnotationValue($schema, 'nullable', true); + } + } +} diff --git a/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryStringDescriber.php b/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryStringDescriber.php new file mode 100644 index 000000000..95e998a88 --- /dev/null +++ b/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryStringDescriber.php @@ -0,0 +1,37 @@ +getAttributes(MapQueryString::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null) { + return; + } + + $modelRef = $this->modelRegistry->register(new Model( + new Type(Type::BUILTIN_TYPE_OBJECT, $argumentMetadata->isNullable(), $argumentMetadata->getType()), + serializationContext: $attribute->serializationContext, + )); + + $operation->_context->{self::CONTEXT_ARGUMENT_METADATA} = $argumentMetadata; + $operation->_context->{self::CONTEXT_MODEL_REF} = $modelRef; + } +} diff --git a/RouteDescriber/RouteArgumentDescriber/SymfonyMapRequestPayloadDescriber.php b/RouteDescriber/RouteArgumentDescriber/SymfonyMapRequestPayloadDescriber.php new file mode 100644 index 000000000..5deb86744 --- /dev/null +++ b/RouteDescriber/RouteArgumentDescriber/SymfonyMapRequestPayloadDescriber.php @@ -0,0 +1,37 @@ +getAttributes(MapRequestPayload::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null) { + return; + } + + $modelRef = $this->modelRegistry->register(new Model( + new Type(Type::BUILTIN_TYPE_OBJECT, false, $argumentMetadata->getType()), + serializationContext: $attribute->serializationContext, + )); + + $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 f9d52acfe..3797cb343 100644 --- a/Tests/Functional/Controller/ApiController81.php +++ b/Tests/Functional/Controller/ApiController81.php @@ -30,6 +30,7 @@ use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraintsWithValidationGroups; use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyDiscriminator81; use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyDiscriminatorFileMapping; +use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyMapQueryString; use Nelmio\ApiDocBundle\Tests\Functional\Entity\User; use Nelmio\ApiDocBundle\Tests\Functional\EntityExcluded\Symfony7\SerializedNameEntity; use Nelmio\ApiDocBundle\Tests\Functional\Form\DummyType; @@ -38,6 +39,9 @@ use Nelmio\ApiDocBundle\Tests\Functional\Form\FormWithRefType; use Nelmio\ApiDocBundle\Tests\Functional\Form\UserType; 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 @@ -459,4 +463,131 @@ public function nameConverterContext() public function arbitraryArray() { } + + #[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', + schema: new OA\Schema(type: 'string', nullable: true), + 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: 'articleType81', + 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 + ) { + } + + #[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\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, + ) { + } + + #[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', + content: new Model(type: EntityWithNullableSchemaSet::class), + )] + #[OA\Response(response: '200', description: '')] + 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/Entity/SymfonyMapQueryString.php b/Tests/Functional/Entity/SymfonyMapQueryString.php new file mode 100644 index 000000000..4f9713025 --- /dev/null +++ b/Tests/Functional/Entity/SymfonyMapQueryString.php @@ -0,0 +1,17 @@ + 'api.example.com']); + } + + public function testMapQueryStringModelGetsCreated(): void + { + if (!class_exists(MapQueryString::class)) { + self::markTestSkipped('Symfony 6.3 MapQueryString attribute not found'); + } + + $expected = [ + 'schema' => 'SymfonyMapQueryString', + 'required' => [ + 'id', + 'name', + 'articleType81', + ], + 'properties' => [ + 'id' => [ + 'type' => 'integer', + ], + 'name' => [ + 'type' => 'string', + ], + 'nullableName' => [ + 'type' => 'string', + 'nullable' => true, + ], + 'articleType81' => [ + '$ref' => '#/components/schemas/ArticleType81', + ], + 'nullableArticleType81' => [ + 'nullable' => true, + 'allOf' => [ + ['$ref' => '#/components/schemas/ArticleType81'], + ], + ], + ], + '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'); + } + + self::assertEquals([ + 'operationId' => 'get_api_nelmio_apidoc_tests_functional_api_fetcharticlefrommapquerystring', + 'parameters' => [ + [ + 'name' => 'id', + 'in' => 'query', + 'required' => true, + 'schema' => [ + 'type' => 'integer', + ], + ], + [ + 'name' => 'name', + 'in' => 'query', + 'required' => true, + 'schema' => [ + 'type' => 'string', + ], + ], + [ + 'name' => 'nullableName', + 'in' => 'query', + 'required' => false, + 'schema' => [ + 'type' => 'string', + 'nullable' => true, + ], + ], + [ + 'name' => 'articleType81', + 'in' => 'query', + 'required' => true, + 'schema' => [ + '$ref' => '#/components/schemas/ArticleType81', + ], + ], + [ + 'name' => 'nullableArticleType81', + 'in' => 'query', + 'required' => false, + 'schema' => [ + 'nullable' => true, + 'allOf' => [ + ['$ref' => '#/components/schemas/ArticleType81'], + ], + ], + ], + ], + 'responses' => [ + '200' => [ + 'description' => '', + ], + ], + ], json_decode($this->getOperation('/api/article_map_query_string', 'get')->toJson(), true)); + } + + public function testMapQueryStringParametersAreOptional(): void + { + if (!class_exists(MapQueryString::class)) { + self::markTestSkipped('Symfony 6.3 MapQueryString attribute not found'); + } + + self::assertEquals([ + 'operationId' => 'get_api_nelmio_apidoc_tests_functional_api_fetcharticlefrommapquerystringnullable', + 'parameters' => [ + [ + 'name' => 'id', + 'in' => 'query', + 'required' => false, + 'schema' => [ + 'type' => 'integer', + ], + ], + [ + 'name' => 'name', + 'in' => 'query', + 'required' => false, + 'schema' => [ + 'type' => 'string', + ], + ], + [ + 'name' => 'nullableName', + 'in' => 'query', + 'required' => false, + 'schema' => [ + 'type' => 'string', + 'nullable' => true, + ], + ], + [ + 'name' => 'articleType81', + 'in' => 'query', + 'required' => false, + 'schema' => [ + '$ref' => '#/components/schemas/ArticleType81', + ], + ], + [ + 'name' => 'nullableArticleType81', + 'in' => 'query', + 'required' => false, + 'schema' => [ + 'nullable' => true, + 'allOf' => [ + ['$ref' => '#/components/schemas/ArticleType81'], + ], + ], + ], + ], + 'responses' => [ + '200' => [ + 'description' => '', + ], + ], + ], json_decode($this->getOperation('/api/article_map_query_string_nullable', 'get')->toJson(), true)); + } + + public function testMapQueryStringParametersOverwriteParameters(): void + { + if (!class_exists(MapQueryString::class)) { + self::markTestSkipped('Symfony 6.3 MapQueryString attribute not found'); + } + + self::assertEquals([ + 'operationId' => 'get_api_nelmio_apidoc_tests_functional_api_fetcharticlefrommapquerystringoverwriteparameters', + 'parameters' => [ + [ + 'name' => 'id', + 'in' => 'query', + 'required' => true, + 'schema' => [ + 'type' => 'string', + 'nullable' => true, + ], + 'description' => 'Query parameter id description', + ], + [ + 'name' => 'name', + 'in' => 'query', + 'required' => true, + 'schema' => [ + 'type' => 'string', + ], + 'description' => 'Query parameter name description', + ], + [ + 'name' => 'nullableName', + 'in' => 'query', + 'required' => false, + 'schema' => [ + 'type' => 'string', + 'nullable' => true, + ], + 'description' => 'Query parameter nullableName description', + ], + [ + 'name' => 'articleType81', + 'in' => 'query', + 'required' => true, + 'schema' => [ + '$ref' => '#/components/schemas/ArticleType81', + ], + 'description' => 'Query parameter articleType81 description', + ], + [ + 'name' => 'nullableArticleType81', + 'in' => 'query', + 'required' => false, + 'schema' => [ + 'nullable' => true, + 'allOf' => [ + ['$ref' => '#/components/schemas/ArticleType81'], + ], + ], + '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 + { + if (!class_exists(MapQueryParameter::class)) { + self::markTestSkipped('Symfony 6.3 MapQueryParameter attribute not found'); + } + + 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 + { + if (!class_exists(MapQueryParameter::class)) { + self::markTestSkipped('Symfony 6.3 MapQueryParameter attribute not found'); + } + + 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 + { + if (!class_exists(MapQueryParameter::class)) { + self::markTestSkipped('Symfony 6.3 MapQueryParameter attribute not found'); + } + + self::assertEquals([ + 'operationId' => 'get_api_nelmio_apidoc_tests_functional_api_fetcharticlefrommapqueryparameterdefault', + 'parameters' => [ + [ + 'name' => 'id', + 'in' => 'query', + 'required' => false, + 'schema' => [ + 'type' => 'integer', + 'default' => 123, + ], + ], + ], + 'responses' => [ + '200' => [ + 'description' => '', + ], + ], + ], json_decode($this->getOperation('/api/article_map_query_parameter_default', 'get')->toJson(), true)); + } + + public function testMapQueryParameterOverwriteParameter(): void + { + if (!class_exists(MapQueryParameter::class)) { + self::markTestSkipped('Symfony 6.3 MapQueryParameter attribute not found'); + } + + 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, + ], + [ + 'name' => 'changedType', + 'in' => 'query', + 'required' => false, + 'schema' => [ + 'type' => 'int', + 'nullable' => false, + ], + 'description' => 'Incorrectly described query parameter', + 'example' => 123, + ], + ], + 'responses' => [ + '200' => [ + 'description' => '', + ], + ], + ], json_decode($this->getOperation('/api/article_map_query_parameter_overwrite_parameters', 'get')->toJson(), true)); + } + + public function testMapRequestPayload(): 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_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 + { + if (!class_exists(MapRequestPayload::class)) { + self::markTestSkipped('Symfony 6.3 MapRequestPayload attribute not found'); + } + + 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 + { + if (!class_exists(MapRequestPayload::class)) { + self::markTestSkipped('Symfony 6.3 MapRequestPayload attribute not found'); + } + + self::assertEquals([ + 'operationId' => 'post_api_nelmio_apidoc_tests_functional_api_createarticlefrommaprequestpayloadoverwrite', + 'responses' => [ + '200' => [ + 'description' => '', + ], + ], + 'requestBody' => [ + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/EntityWithNullableSchemaSet', + ], + ], + ], + 'required' => true, + 'description' => 'Request body description', + ], + ], 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)); + } +} diff --git a/phpunit-baseline.json b/phpunit-baseline.json index e5a54aaaf..7f1f2acfe 100644 --- a/phpunit-baseline.json +++ b/phpunit-baseline.json @@ -6963,5 +6963,185 @@ "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\FunctionalTest::testEntityWithFalsyDefaults", "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 + }, + { + "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 } ]