Skip to content

Commit 32c8b0e

Browse files
committed
fix(openapi): respect schema type for non-collection parameter documentation
When a filter parameter has an explicit schema type set to a non-array type (e.g., 'string'), the OpenAPI documentation now correctly generates a non-collection parameter instead of using array notation with '[]' suffix, deepObject style, and explode: true. Fixes #7548
1 parent c4d2c53 commit 32c8b0e

File tree

4 files changed

+99
-4
lines changed

4 files changed

+99
-4
lines changed

src/Doctrine/Common/Filter/OpenApiFilterTrait.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,19 @@ trait OpenApiFilterTrait
2323
{
2424
public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
2525
{
26+
$schema = $parameter->getSchema();
27+
$isArraySchema = 'array' === ($schema['type'] ?? null);
28+
$castToArray = $parameter->getCastToArray();
29+
30+
// Use non-array notation if:
31+
// 1. Schema type is explicitly set to a non-array type (string, number, etc.)
32+
// 2. OR castToArray is explicitly false
33+
$hasNonArraySchema = null !== $schema && !$isArraySchema;
34+
35+
if ($hasNonArraySchema || false === $castToArray) {
36+
return new OpenApiParameter(name: $parameter->getKey(), in: 'query');
37+
}
38+
2639
return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true);
2740
}
2841
}

src/OpenApi/Factory/OpenApiFactory.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -726,13 +726,14 @@ private function getFilterParameter(string $name, array $description, string $sh
726726
$arrayValueType = method_exists(PropertyInfoExtractor::class, 'getType') ? TypeIdentifier::ARRAY->value : LegacyType::BUILTIN_TYPE_ARRAY;
727727
$objectValueType = method_exists(PropertyInfoExtractor::class, 'getType') ? TypeIdentifier::OBJECT->value : LegacyType::BUILTIN_TYPE_OBJECT;
728728

729-
$style = 'array' === ($schema['type'] ?? null) && \in_array(
729+
$isArraySchema = 'array' === ($schema['type'] ?? null);
730+
$style = $isArraySchema && \in_array(
730731
$description['type'],
731732
[$arrayValueType, $objectValueType],
732733
true
733734
) ? 'deepObject' : 'form';
734735

735-
$parameter = isset($description['openapi']) && $description['openapi'] instanceof Parameter ? $description['openapi'] : new Parameter(in: 'query', name: $name, style: $style, explode: $description['is_collection'] ?? false);
736+
$parameter = isset($description['openapi']) && $description['openapi'] instanceof Parameter ? $description['openapi'] : new Parameter(in: 'query', name: $name, style: $style, explode: $isArraySchema ? ($description['is_collection'] ?? false) : false);
736737

737738
if ('' === $parameter->getDescription() && ($str = $description['description'] ?? '')) {
738739
$parameter = $parameter->withDescription($str);
@@ -771,6 +772,8 @@ private function getFilterParameter(string $name, array $description, string $sh
771772
$arrayValueType = method_exists(PropertyInfoExtractor::class, 'getType') ? TypeIdentifier::ARRAY->value : LegacyType::BUILTIN_TYPE_ARRAY;
772773
$objectValueType = method_exists(PropertyInfoExtractor::class, 'getType') ? TypeIdentifier::OBJECT->value : LegacyType::BUILTIN_TYPE_OBJECT;
773774

775+
$isArraySchema = 'array' === $schema['type'];
776+
774777
return new Parameter(
775778
$name,
776779
'query',
@@ -779,12 +782,12 @@ private function getFilterParameter(string $name, array $description, string $sh
779782
$description['openapi']['deprecated'] ?? false,
780783
$description['openapi']['allowEmptyValue'] ?? null,
781784
$schema,
782-
'array' === $schema['type'] && \in_array(
785+
$isArraySchema && \in_array(
783786
$description['type'],
784787
[$arrayValueType, $objectValueType],
785788
true
786789
) ? 'deepObject' : 'form',
787-
$description['openapi']['explode'] ?? ('array' === $schema['type']),
790+
$description['openapi']['explode'] ?? $isArraySchema,
788791
$description['openapi']['allowReserved'] ?? null,
789792
$description['openapi']['example'] ?? null,
790793
isset(

tests/Fixtures/TestBundle/Entity/ProductWithQueryParameter.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@
4242
filter: new OrderFilter(),
4343
properties: ['rating']
4444
),
45+
'exactBrand' => new QueryParameter(
46+
filter: new ExactFilter(),
47+
property: 'brand',
48+
schema: ['type' => 'string']
49+
),
50+
'exactCategory' => new QueryParameter(
51+
filter: new ExactFilter(),
52+
property: 'category',
53+
castToArray: false
54+
),
4555
]
4656
),
4757
]

tests/Functional/Parameters/DoctrineTest.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,4 +300,73 @@ private function loadProductFixtures(string $resourceClass): void
300300

301301
$manager->flush();
302302
}
303+
304+
#[DataProvider('openApiParameterDocumentationProvider')]
305+
public function testOpenApiParameterDocumentation(string $parameterName, bool $shouldHaveArrayNotation, string $expectedStyle, bool $expectedExplode, ?string $expectedSchemaType = null): void
306+
{
307+
if ($this->isMongoDB()) {
308+
$this->markTestSkipped('Not tested with mongodb.');
309+
}
310+
311+
$resource = ProductWithQueryParameter::class;
312+
$this->recreateSchema([$resource]);
313+
314+
$response = self::createClient()->request('GET', '/docs', [
315+
'headers' => ['Accept' => 'application/vnd.openapi+json'],
316+
]);
317+
318+
$this->assertResponseIsSuccessful();
319+
$openApiDoc = $response->toArray();
320+
321+
$parameters = $openApiDoc['paths']['/product_with_query_parameters']['get']['parameters'];
322+
$foundParameter = null;
323+
$expectedName = $shouldHaveArrayNotation ? $parameterName.'[]' : $parameterName;
324+
$alternativeName = $shouldHaveArrayNotation ? $parameterName : $parameterName.'[]';
325+
326+
foreach ($parameters as $parameter) {
327+
if ($parameter['name'] === $expectedName || $parameter['name'] === $alternativeName) {
328+
$foundParameter = $parameter;
329+
break;
330+
}
331+
}
332+
333+
$this->assertNotNull($foundParameter, sprintf('%s parameter should be present in OpenAPI documentation', $parameterName));
334+
$this->assertSame($expectedName, $foundParameter['name'], sprintf('Parameter name should%s have [] suffix', $shouldHaveArrayNotation ? '' : ' NOT'));
335+
$this->assertSame('query', $foundParameter['in']);
336+
$this->assertFalse($foundParameter['required']);
337+
338+
if ($expectedSchemaType) {
339+
$this->assertSame($expectedSchemaType, $foundParameter['schema']['type'], sprintf('Parameter schema type should be %s', $expectedSchemaType));
340+
}
341+
342+
$this->assertSame($expectedStyle, $foundParameter['style'] ?? 'form', sprintf('Style should be %s', $expectedStyle));
343+
$this->assertSame($expectedExplode, $foundParameter['explode'] ?? false, sprintf('Explode should be %s', $expectedExplode ? 'true' : 'false'));
344+
}
345+
346+
public static function openApiParameterDocumentationProvider(): array
347+
{
348+
return [
349+
'default behavior (no castToArray, no schema) should use array notation' => [
350+
'parameterName' => 'brand',
351+
'shouldHaveArrayNotation' => true,
352+
'expectedStyle' => 'deepObject',
353+
'expectedExplode' => true,
354+
'expectedSchemaType' => 'string',
355+
],
356+
'explicit schema type string should not use array notation' => [
357+
'parameterName' => 'exactBrand',
358+
'shouldHaveArrayNotation' => false,
359+
'expectedStyle' => 'form',
360+
'expectedExplode' => false,
361+
'expectedSchemaType' => 'string',
362+
],
363+
'castToArray false should not use array notation' => [
364+
'parameterName' => 'exactCategory',
365+
'shouldHaveArrayNotation' => false,
366+
'expectedStyle' => 'form',
367+
'expectedExplode' => false,
368+
'expectedSchemaType' => 'string',
369+
],
370+
];
371+
}
303372
}

0 commit comments

Comments
 (0)