Skip to content

Commit

Permalink
feat(#2297): (Symfony 7.1) Add MapRequestPayload array parameter hand…
Browse files Browse the repository at this point in the history
…ling (#2298)

| Q | A |

|---------------|---------------------------------------------------------------------------------------------------------------------------|
| Bug fix? | no |
| New feature? | yes <!-- please update src/**/CHANGELOG.md files --> |
| Deprecations? | no <!-- please update UPGRADE-*.md and
src/**/CHANGELOG.md files --> |
| Issues | Fix #2297 <!-- prefix each issue number with "Fix #", no need
to create an issue if none exists, explain below instead --> |

With Symfony 7.1, a new `type` parameter has been introduced for
`MapRequestPayload` that allows array Controller parameters to be
resolved. This currently leads to an error when the Describer tries to
register a new Model, because the parameter type `array` is used in this
case. To resolve the issue, the given `type` parameter form the
`MapRequestPayload` should be used instead.

I tried to test this as gracefully as possible. Any more elegant
suggestions are welcome!

---------

Co-authored-by: Maximilian Zumbansen <maximilian.zumbansen@qossmic.com>
Co-authored-by: djordy <djordy.koert@yoursurprise.com>
  • Loading branch information
3 people committed Jun 19, 2024
1 parent d590880 commit 684391a
Show file tree
Hide file tree
Showing 16 changed files with 925 additions and 5,261 deletions.
13 changes: 11 additions & 2 deletions src/Processor/MapRequestPayloadProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,17 @@ public function __invoke(Analysis $analysis): void
}

$contentSchema = $this->getContentSchemaForType($requestBody, $format);

Util::modifyAnnotationValue($contentSchema, 'ref', $modelRef);
if ('array' === $argumentMetaData->getType()) {
$contentSchema->type = 'array';
$contentSchema->items = new OA\Items(
[
'ref' => $modelRef,
'_context' => Util::createWeakContext($contentSchema->_context),
]
);
} else {
Util::modifyAnnotationValue($contentSchema, 'ref', $modelRef);
}

if ($argumentMetaData->isNullable()) {
$contentSchema->nullable = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,15 @@ public function describe(ArgumentMetadata $argumentMetadata, OA\Operation $opera
return;
}

$typeClass = $argumentMetadata->getType();

$reflectionAttribute = new \ReflectionClass(MapRequestPayload::class);
if (Type::BUILTIN_TYPE_ARRAY === $typeClass && $reflectionAttribute->hasProperty('type') && null !== $attribute->type) {
$typeClass = $attribute->type;
}

$modelRef = $this->modelRegistry->register(new Model(
new Type(Type::BUILTIN_TYPE_OBJECT, false, $argumentMetadata->getType()),
new Type(Type::BUILTIN_TYPE_OBJECT, false, $typeClass),
groups: $this->getGroups($attribute),
serializationContext: $attribute->serializationContext,
));
Expand Down
188 changes: 0 additions & 188 deletions tests/Functional/Controller/ApiController81.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,11 @@
use Nelmio\ApiDocBundle\Tests\Functional\Entity\EntityWithObjectType;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\EntityWithRef;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\EntityWithUuid;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\QueryModel\ArrayQueryModel;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\QueryModel\FilterQueryModel;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\QueryModel\PaginationQueryModel;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\QueryModel\SortQueryModel;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\RangeInteger;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraints81;
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;
Expand All @@ -48,9 +43,6 @@
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
Expand Down Expand Up @@ -520,184 +512,4 @@ public function arbitraryArray()
public function dictionary()
{
}

#[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_passes_validation_groups')]
#[OA\Response(response: '200', description: '')]
public function fetchArticleFromMapQueryStringHandlesValidationGroups(
#[MapQueryString(validationGroups: ['test'])] SymfonyConstraintsWithValidationGroups $symfonyConstraintsWithValidationGroups,
) {
}

#[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_string_many_parameters')]
#[OA\Response(response: '200', description: '')]
public function fetchArticleWithManyParameters(
#[MapQueryString] FilterQueryModel $filterQuery,
#[MapQueryString] PaginationQueryModel $paginationQuery,
#[MapQueryString] SortQueryModel $sortQuery,
#[MapQueryString] ArrayQueryModel $arrayQuery,
) {
}

#[Route('/article_map_query_string_many_parameters_optional')]
#[OA\Response(response: '200', description: '')]
public function fetchArticleWithManyOptionalParameters(
#[MapQueryString] ?FilterQueryModel $filterQuery,
#[MapQueryString] ?PaginationQueryModel $paginationQuery,
#[MapQueryString] ?SortQueryModel $sortQuery,
#[MapQueryString] ?ArrayQueryModel $arrayQuery,
) {
}

#[Route('/article_map_query_parameter')]
#[OA\Response(response: '200', description: '')]
public function fetchArticleFromMapQueryParameter(
#[MapQueryParameter] int $someInt,
#[MapQueryParameter] float $someFloat,
#[MapQueryParameter] bool $someBool,
#[MapQueryParameter] string $someString,
#[MapQueryParameter] array $someArray,
) {
}

#[Route('/article_map_query_parameter_validate_filters')]
#[OA\Response(response: '200', description: '')]
public function fetchArticleFromMapQueryParameterValidateFilters(
#[MapQueryParameter(options: ['min_range' => 2, 'max_range' => 1234])] int $minMaxInt,
#[MapQueryParameter(filter: FILTER_VALIDATE_DOMAIN)] string $domain,
#[MapQueryParameter(filter: FILTER_VALIDATE_EMAIL)] string $email,
#[MapQueryParameter(filter: FILTER_VALIDATE_IP)] string $ip,
#[MapQueryParameter(filter: FILTER_VALIDATE_IP, flags: FILTER_FLAG_IPV4)] string $ipv4,
#[MapQueryParameter(filter: FILTER_VALIDATE_IP, flags: FILTER_FLAG_IPV6)] string $ipv6,
#[MapQueryParameter(filter: FILTER_VALIDATE_MAC)] string $macAddress,
#[MapQueryParameter(filter: FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^test/'])] string $regexp,
#[MapQueryParameter(filter: FILTER_VALIDATE_URL)] string $url,
) {
}

#[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,
) {
}

#[Route('/article_map_request_payload_validation_groups', methods: ['POST'])]
#[OA\Response(response: '200', description: '')]
public function createArticleFromMapRequestPayloadPassedValidationGroups(
#[MapRequestPayload(validationGroups: ['test'])] SymfonyConstraintsWithValidationGroups $symfonyConstraintsWithValidationGroups,
) {
}
}
79 changes: 79 additions & 0 deletions tests/Functional/Controller/MapQueryParameterController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;

use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
use Symfony\Component\Routing\Annotation\Route;

class MapQueryParameterController
{
#[Route('/article_map_query_parameter', methods: ['GET'])]
#[OA\Response(response: '200', description: '')]
public function fetchArticleFromMapQueryParameter(
#[MapQueryParameter] int $someInt,
#[MapQueryParameter] float $someFloat,
#[MapQueryParameter] bool $someBool,
#[MapQueryParameter] string $someString,
#[MapQueryParameter] array $someArray,
) {
}

#[Route('/article_map_query_parameter_validate_filters', methods: ['GET'])]
#[OA\Response(response: '200', description: '')]
public function fetchArticleFromMapQueryParameterValidateFilters(
#[MapQueryParameter(options: ['min_range' => 2, 'max_range' => 1234])] int $minMaxInt,
#[MapQueryParameter(filter: FILTER_VALIDATE_DOMAIN)] string $domain,
#[MapQueryParameter(filter: FILTER_VALIDATE_EMAIL)] string $email,
#[MapQueryParameter(filter: FILTER_VALIDATE_IP)] string $ip,
#[MapQueryParameter(filter: FILTER_VALIDATE_IP, flags: FILTER_FLAG_IPV4)] string $ipv4,
#[MapQueryParameter(filter: FILTER_VALIDATE_IP, flags: FILTER_FLAG_IPV6)] string $ipv6,
#[MapQueryParameter(filter: FILTER_VALIDATE_MAC)] string $macAddress,
#[MapQueryParameter(filter: FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^test/'])] string $regexp,
#[MapQueryParameter(filter: FILTER_VALIDATE_URL)] string $url,
) {
}

#[Route('/article_map_query_parameter_nullable', methods: ['GET'])]
#[OA\Response(response: '200', description: '')]
public function fetchArticleFromMapQueryParameterNullable(
#[MapQueryParameter] ?int $id,
) {
}

#[Route('/article_map_query_parameter_default', methods: ['GET'])]
#[OA\Response(response: '200', description: '')]
public function fetchArticleFromMapQueryParameterDefault(
#[MapQueryParameter] int $id = 123,
) {
}

#[Route('/article_map_query_parameter_overwrite_parameters', methods: ['GET'])]
#[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,
) {
}
}
10 changes: 5 additions & 5 deletions tests/Functional/Controller/MapQueryStringController.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,28 @@

class MapQueryStringController
{
#[Route('/article_map_query_string')]
#[Route('/article_map_query_string', methods: ['GET'])]
#[OA\Response(response: '200', description: '')]
public function fetchArticleFromMapQueryString(
#[MapQueryString] SymfonyMapQueryString $article81Query
) {
}

#[Route('/article_map_query_string_nullable')]
#[Route('/article_map_query_string_nullable', methods: ['GET'])]
#[OA\Response(response: '200', description: '')]
public function fetchArticleFromMapQueryStringNullable(
#[MapQueryString] ?SymfonyMapQueryString $article81Query
) {
}

#[Route('/article_map_query_string_passes_validation_groups')]
#[Route('/article_map_query_string_passes_validation_groups', methods: ['GET'])]
#[OA\Response(response: '200', description: '')]
public function fetchArticleFromMapQueryStringHandlesValidationGroups(
#[MapQueryString(validationGroups: ['test'])] SymfonyConstraintsWithValidationGroups $symfonyConstraintsWithValidationGroups,
) {
}

#[Route('/article_map_query_string_overwrite_parameters')]
#[Route('/article_map_query_string_overwrite_parameters', methods: ['GET'])]
#[OA\Parameter(
name: 'id',
in: 'query',
Expand Down Expand Up @@ -78,7 +78,7 @@ public function fetchArticleFromMapQueryStringOverwriteParameters(
) {
}

#[Route('/article_map_query_string_many_parameters')]
#[Route('/article_map_query_string_many_parameters', methods: ['GET'])]
#[OA\Response(response: '200', description: '')]
public function fetchArticleWithManyParameters(
#[MapQueryString] FilterQueryModel $filterQuery,
Expand Down
Loading

0 comments on commit 684391a

Please sign in to comment.