From ef1d257e3e96f2f399e894656f6933e34ab24f58 Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 7 Apr 2026 14:28:06 +0200 Subject: [PATCH] fix(openapi): generate both singular and array parameter variants for filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit | Q | A | ------------- | --- | Branch? | main | Tickets | Fixes #7905 | License | MIT | Doc PR | ∅ When castToArray is null (default), OpenApiFilterTrait now returns both `param` and `param[]` variants, matching the old ApiFilter behavior. --- .../Common/Filter/OpenApiFilterTrait.php | 12 ++++- tests/Functional/Parameters/DoctrineTest.php | 44 ++++++++++++++++--- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/Doctrine/Common/Filter/OpenApiFilterTrait.php b/src/Doctrine/Common/Filter/OpenApiFilterTrait.php index 87ba6ef5d3..2af0d951df 100644 --- a/src/Doctrine/Common/Filter/OpenApiFilterTrait.php +++ b/src/Doctrine/Common/Filter/OpenApiFilterTrait.php @@ -34,6 +34,16 @@ public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|arr $arraySchema = ['type' => 'array', 'items' => $schema ?? ['type' => 'string']]; } - return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true, schema: $arraySchema); + $arrayParameter = new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true, schema: $arraySchema); + + // When castToArray is null (default), both singular and array forms are accepted + if (null === $parameter->getCastToArray()) { + return [ + new OpenApiParameter(name: $parameter->getKey(), in: 'query'), + $arrayParameter, + ]; + } + + return $arrayParameter; } } diff --git a/tests/Functional/Parameters/DoctrineTest.php b/tests/Functional/Parameters/DoctrineTest.php index 9631e08439..abce8d4344 100644 --- a/tests/Functional/Parameters/DoctrineTest.php +++ b/tests/Functional/Parameters/DoctrineTest.php @@ -342,7 +342,7 @@ private function loadProductFixtures(string $resourceClass): void } #[DataProvider('openApiParameterDocumentationProvider')] - public function testOpenApiParameterDocumentation(string $parameterName, bool $shouldHaveArrayNotation, string $expectedStyle, bool $expectedExplode, string $expectedDescription = '', ?array $expectedSchema = null): void + public function testOpenApiParameterDocumentation(string $parameterName, bool $shouldHaveArrayNotation, string $expectedStyle, bool $expectedExplode, string $expectedDescription = '', ?array $expectedSchema = null, bool $shouldHaveBothVariants = false): void { if ($this->isMongoDB()) { $this->markTestSkipped('Not tested with mongodb.'); @@ -359,12 +359,38 @@ public function testOpenApiParameterDocumentation(string $parameterName, bool $s $openApiDoc = $response->toArray(); $parameters = $openApiDoc['paths']['/product_with_query_parameters']['get']['parameters']; + + if ($shouldHaveBothVariants) { + $singularParameter = null; + $arrayParameter = null; + + foreach ($parameters as $parameter) { + if ($parameter['name'] === $parameterName) { + $singularParameter = $parameter; + } + if ($parameter['name'] === $parameterName.'[]') { + $arrayParameter = $parameter; + } + } + + $this->assertNotNull($singularParameter, \sprintf('%s singular parameter should be present in OpenAPI documentation', $parameterName)); + $this->assertNotNull($arrayParameter, \sprintf('%s[] array parameter should be present in OpenAPI documentation', $parameterName)); + $this->assertSame('query', $arrayParameter['in']); + $this->assertSame($expectedStyle, $arrayParameter['style'] ?? 'form'); + $this->assertSame($expectedExplode, $arrayParameter['explode'] ?? false); + + if ($expectedSchema) { + $this->assertSame($expectedSchema, $arrayParameter['schema'], 'Array parameter schema should match expected schema'); + } + + return; + } + $foundParameter = null; $expectedName = $shouldHaveArrayNotation ? $parameterName.'[]' : $parameterName; - $alternativeName = $shouldHaveArrayNotation ? $parameterName : $parameterName.'[]'; foreach ($parameters as $parameter) { - if ($parameter['name'] === $expectedName || $parameter['name'] === $alternativeName) { + if ($parameter['name'] === $expectedName) { $foundParameter = $parameter; break; } @@ -390,21 +416,23 @@ public function testOpenApiParameterDocumentation(string $parameterName, bool $s public static function openApiParameterDocumentationProvider(): array { return [ - 'default behavior (no castToArray, no schema) should use array notation' => [ + 'default behavior (no castToArray, no schema) should generate both singular and array parameters' => [ 'parameterName' => 'brand', 'shouldHaveArrayNotation' => true, 'expectedStyle' => 'deepObject', 'expectedExplode' => true, 'expectedDescription' => '', 'expectedSchema' => ['type' => 'array', 'items' => ['type' => 'string']], + 'shouldHaveBothVariants' => true, ], - 'default behavior with an extra description' => [ + 'default behavior with an extra description should generate both variants' => [ 'parameterName' => 'brandWithDescription', 'shouldHaveArrayNotation' => true, 'expectedStyle' => 'deepObject', 'expectedExplode' => true, 'expectedDescription' => 'Extra description about the filter', 'expectedSchema' => ['type' => 'array', 'items' => ['type' => 'string']], + 'shouldHaveBothVariants' => true, ], 'explicit schema type string should not use array notation' => [ 'parameterName' => 'exactBrand', @@ -422,21 +450,23 @@ public static function openApiParameterDocumentationProvider(): array 'expectedDescription' => '', 'expectedSchema' => ['type' => 'string'], ], - 'with schema and default castToArray should wrap schema in array type' => [ + 'with schema and default castToArray should generate both variants wrapping schema in array type' => [ 'parameterName' => 'tags', 'shouldHaveArrayNotation' => true, 'expectedStyle' => 'deepObject', 'expectedExplode' => true, 'expectedDescription' => '', 'expectedSchema' => ['type' => 'array', 'items' => ['anyOf' => [['type' => 'array', 'items' => ['type' => 'string']], ['type' => 'string']]]], + 'shouldHaveBothVariants' => true, ], - 'with schema and default castToArray should not wrap schema in array type if already an array' => [ + 'with schema and default castToArray should generate both variants without wrapping if already array' => [ 'parameterName' => 'listOfTags', 'shouldHaveArrayNotation' => true, 'expectedStyle' => 'deepObject', 'expectedExplode' => true, 'expectedDescription' => '', 'expectedSchema' => ['type' => 'array', 'items' => ['type' => 'string']], + 'shouldHaveBothVariants' => true, ], ]; }