From b2f38539db997690d13fd35963f9a250122b807b Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 29 Sep 2026 16:32:29 +0200 Subject: [PATCH] fix(doctrine): scalar schema for comparison params ComparisonFilter's per-operator OpenAPI parameters inherited the parent object schema and explode=true, so Postman expanded bare gt/lt/... params. Each operator param now carries the inner filter's scalar schema with explode=false. The inner schema is computed with castToArray=false, so UuidFilter and UlidFilter document a single uuid, not oneOf[uuid, array]. Fixes #8589 --- src/Doctrine/Odm/Filter/ComparisonFilter.php | 13 ++++++++----- src/Doctrine/Orm/Filter/ComparisonFilter.php | 13 ++++++++----- .../Parameters/ComparisonFilterTest.php | 17 +++++++++++++++++ .../Uuid/UuidComparisonFilterTest.php | 17 +++++++++++++++++ 4 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/Doctrine/Odm/Filter/ComparisonFilter.php b/src/Doctrine/Odm/Filter/ComparisonFilter.php index 53488ed6443..9e1f01e9005 100644 --- a/src/Doctrine/Odm/Filter/ComparisonFilter.php +++ b/src/Doctrine/Odm/Filter/ComparisonFilter.php @@ -84,13 +84,16 @@ public function getOpenApiParameters(Parameter $parameter): array { $in = $parameter instanceof QueryParameter ? 'query' : 'header'; $key = $parameter->getKey(); + $leafSchema = $this->filter instanceof JsonSchemaFilterInterface + ? $this->filter->getSchema($parameter->withCastToArray(false)) + : ['type' => 'string']; return [ - new OpenApiParameter(name: "{$key}[gt]", in: $in), - new OpenApiParameter(name: "{$key}[gte]", in: $in), - new OpenApiParameter(name: "{$key}[lt]", in: $in), - new OpenApiParameter(name: "{$key}[lte]", in: $in), - new OpenApiParameter(name: "{$key}[ne]", in: $in), + new OpenApiParameter(name: "{$key}[gt]", in: $in, explode: false, schema: $leafSchema), + new OpenApiParameter(name: "{$key}[gte]", in: $in, explode: false, schema: $leafSchema), + new OpenApiParameter(name: "{$key}[lt]", in: $in, explode: false, schema: $leafSchema), + new OpenApiParameter(name: "{$key}[lte]", in: $in, explode: false, schema: $leafSchema), + new OpenApiParameter(name: "{$key}[ne]", in: $in, explode: false, schema: $leafSchema), ]; } diff --git a/src/Doctrine/Orm/Filter/ComparisonFilter.php b/src/Doctrine/Orm/Filter/ComparisonFilter.php index c1362320b82..c47b4f96a76 100644 --- a/src/Doctrine/Orm/Filter/ComparisonFilter.php +++ b/src/Doctrine/Orm/Filter/ComparisonFilter.php @@ -84,13 +84,16 @@ public function getOpenApiParameters(Parameter $parameter): array { $in = $parameter instanceof QueryParameter ? 'query' : 'header'; $key = $parameter->getKey(); + $leafSchema = $this->filter instanceof JsonSchemaFilterInterface + ? $this->filter->getSchema($parameter->withCastToArray(false)) + : ['type' => 'string']; return [ - new OpenApiParameter(name: "{$key}[gt]", in: $in), - new OpenApiParameter(name: "{$key}[gte]", in: $in), - new OpenApiParameter(name: "{$key}[lt]", in: $in), - new OpenApiParameter(name: "{$key}[lte]", in: $in), - new OpenApiParameter(name: "{$key}[ne]", in: $in), + new OpenApiParameter(name: "{$key}[gt]", in: $in, explode: false, schema: $leafSchema), + new OpenApiParameter(name: "{$key}[gte]", in: $in, explode: false, schema: $leafSchema), + new OpenApiParameter(name: "{$key}[lt]", in: $in, explode: false, schema: $leafSchema), + new OpenApiParameter(name: "{$key}[lte]", in: $in, explode: false, schema: $leafSchema), + new OpenApiParameter(name: "{$key}[ne]", in: $in, explode: false, schema: $leafSchema), ]; } diff --git a/tests/Functional/Parameters/ComparisonFilterTest.php b/tests/Functional/Parameters/ComparisonFilterTest.php index 8caaef1e3dd..e42b993d891 100644 --- a/tests/Functional/Parameters/ComparisonFilterTest.php +++ b/tests/Functional/Parameters/ComparisonFilterTest.php @@ -153,6 +153,23 @@ public function testOpenApiDocumentation(): void } } + public function testOpenApiOperatorParametersAreScalar(): void + { + $response = self::createClient()->request('GET', '/docs', [ + 'headers' => ['Accept' => 'application/vnd.openapi+json'], + ]); + $this->assertResponseIsSuccessful(); + + $parameters = $response->toArray()['paths']['/chickens']['get']['parameters']; + $comparisonParams = array_filter($parameters, static fn ($p) => str_starts_with($p['name'], 'nameComparison[')); + $this->assertCount(5, $comparisonParams); + + foreach ($comparisonParams as $param) { + $this->assertSame(['type' => 'string'], $param['schema'], $param['name']); + $this->assertFalse($param['explode'], \sprintf('"%s" must not explode', $param['name'])); + } + } + private function loadFixtures(): void { $manager = $this->getManager(); diff --git a/tests/Functional/Uuid/UuidComparisonFilterTest.php b/tests/Functional/Uuid/UuidComparisonFilterTest.php index 016d617c706..ff9595aa6bc 100644 --- a/tests/Functional/Uuid/UuidComparisonFilterTest.php +++ b/tests/Functional/Uuid/UuidComparisonFilterTest.php @@ -43,6 +43,23 @@ protected function setUp(): void } } + public function testOpenApiOperatorParametersUseScalarUuidSchema(): void + { + $response = self::createClient()->request('GET', '/docs', [ + 'headers' => ['Accept' => 'application/vnd.openapi+json'], + ]); + $this->assertResponseIsSuccessful(); + + $parameters = $response->toArray()['paths']['/symfony_uuid_devices']['get']['parameters']; + $comparisonParams = array_filter($parameters, static fn ($p) => str_starts_with($p['name'], 'idComparison[')); + $this->assertCount(5, $comparisonParams); + + foreach ($comparisonParams as $param) { + $this->assertSame(['type' => 'string', 'format' => 'uuid'], $param['schema'], $param['name']); + $this->assertFalse($param['explode'], $param['name']); + } + } + public function testGtWithUuid(): void { $this->recreateSchema(static::getResources());