From fdf41040070aca9e562d7f2a447f8a1995e16320 Mon Sep 17 00:00:00 2001 From: Pascal CESCON - Amoifr Date: Fri, 23 Oct 2026 14:25:29 +0200 Subject: [PATCH 1/2] fix(doctrine): document SortFilter parameters as a single value SortFilter only applies a string direction, but with the default castToArray OpenAPI also advertised an "order[]" array form that always fails validation. --- src/Doctrine/Odm/Filter/SortFilter.php | 13 +++++- .../Odm/Tests/Filter/SortFilterTest.php | 23 ++++++++++ src/Doctrine/Orm/Filter/SortFilter.php | 13 +++++- .../Orm/Tests/Filter/SortFilterTest.php | 44 +++++++++++++++++++ .../Functional/Parameters/SortFilterTest.php | 14 ++++++ 5 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 src/Doctrine/Orm/Tests/Filter/SortFilterTest.php diff --git a/src/Doctrine/Odm/Filter/SortFilter.php b/src/Doctrine/Odm/Filter/SortFilter.php index 4acad603409..37a2051fcb0 100644 --- a/src/Doctrine/Odm/Filter/SortFilter.php +++ b/src/Doctrine/Odm/Filter/SortFilter.php @@ -22,6 +22,7 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Parameter; use ApiPlatform\Metadata\SortFilterInterface; +use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; use Doctrine\ODM\MongoDB\Aggregation\Builder; /** @@ -38,7 +39,9 @@ final class SortFilter implements FilterInterface, JsonSchemaFilterInterface, Op { use BackwardCompatibleFilterDescriptionTrait; use NestedPropertyHelperTrait; - use OpenApiFilterTrait; + use OpenApiFilterTrait { + getOpenApiParameters as private getOpenApiFilterParameters; + } public function __construct( private readonly ?string $nullsComparison = null, @@ -97,4 +100,12 @@ public function getSchema(Parameter $parameter): array { return ['type' => 'string', 'enum' => ['asc', 'desc', 'ASC', 'DESC']]; } + + /** + * A sort direction is a single value, so the array form is only documented when castToArray is enabled explicitly. + */ + public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null + { + return $this->getOpenApiFilterParameters($parameter->withCastToArray($parameter->getCastToArray() ?? false)); + } } diff --git a/src/Doctrine/Odm/Tests/Filter/SortFilterTest.php b/src/Doctrine/Odm/Tests/Filter/SortFilterTest.php index d104bac3670..ea0660fea1c 100644 --- a/src/Doctrine/Odm/Tests/Filter/SortFilterTest.php +++ b/src/Doctrine/Odm/Tests/Filter/SortFilterTest.php @@ -19,7 +19,9 @@ use ApiPlatform\Doctrine\Odm\Tests\Fixtures\Document\RelatedDummy; use ApiPlatform\Doctrine\Odm\Tests\Fixtures\Document\ThirdLevel; use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; use Doctrine\ODM\MongoDB\DocumentManager; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class SortFilterTest extends TestCase @@ -337,4 +339,25 @@ public function testLookupDeduplication(): void $this->assertCount(1, $pipeline); $this->assertEquals(['$sort' => ['relatedDummy_lkup.name' => 1]], $pipeline[0]); } + + /** + * @param list $expectedNames + */ + #[DataProvider('provideCastToArray')] + public function testGetOpenApiParameters(?bool $castToArray, array $expectedNames): void + { + $parameter = new QueryParameter(key: 'orderName', property: 'name', castToArray: $castToArray); + + $openApiParameters = (new SortFilter())->getOpenApiParameters($parameter); + $openApiParameters = \is_array($openApiParameters) ? $openApiParameters : [$openApiParameters]; + + $this->assertSame($expectedNames, array_map(static fn (OpenApiParameter $openApiParameter): string => $openApiParameter->getName(), $openApiParameters)); + } + + public static function provideCastToArray(): iterable + { + yield 'default' => [null, ['orderName']]; + yield 'castToArray disabled' => [false, ['orderName']]; + yield 'castToArray enabled' => [true, ['orderName[]']]; + } } diff --git a/src/Doctrine/Orm/Filter/SortFilter.php b/src/Doctrine/Orm/Filter/SortFilter.php index d77da9131e2..266d4a2569d 100644 --- a/src/Doctrine/Orm/Filter/SortFilter.php +++ b/src/Doctrine/Orm/Filter/SortFilter.php @@ -23,6 +23,7 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Parameter; use ApiPlatform\Metadata\SortFilterInterface; +use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; @@ -40,7 +41,9 @@ final class SortFilter implements FilterInterface, JsonSchemaFilterInterface, Op { use BackwardCompatibleFilterDescriptionTrait; use NestedPropertyHelperTrait; - use OpenApiFilterTrait; + use OpenApiFilterTrait { + getOpenApiParameters as private getOpenApiFilterParameters; + } public function __construct( private readonly ?string $nullsComparison = null, @@ -88,4 +91,12 @@ public function getSchema(Parameter $parameter): array { return ['type' => 'string', 'enum' => ['asc', 'desc', 'ASC', 'DESC']]; } + + /** + * A sort direction is a single value, so the array form is only documented when castToArray is enabled explicitly. + */ + public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null + { + return $this->getOpenApiFilterParameters($parameter->withCastToArray($parameter->getCastToArray() ?? false)); + } } diff --git a/src/Doctrine/Orm/Tests/Filter/SortFilterTest.php b/src/Doctrine/Orm/Tests/Filter/SortFilterTest.php new file mode 100644 index 00000000000..190208ddca4 --- /dev/null +++ b/src/Doctrine/Orm/Tests/Filter/SortFilterTest.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Orm\Tests\Filter; + +use ApiPlatform\Doctrine\Orm\Filter\SortFilter; +use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; + +class SortFilterTest extends TestCase +{ + /** + * @param list $expectedNames + */ + #[DataProvider('provideCastToArray')] + public function testGetOpenApiParameters(?bool $castToArray, array $expectedNames): void + { + $parameter = new QueryParameter(key: 'orderName', property: 'name', castToArray: $castToArray); + + $openApiParameters = (new SortFilter())->getOpenApiParameters($parameter); + $openApiParameters = \is_array($openApiParameters) ? $openApiParameters : [$openApiParameters]; + + $this->assertSame($expectedNames, array_map(static fn (OpenApiParameter $openApiParameter): string => $openApiParameter->getName(), $openApiParameters)); + } + + public static function provideCastToArray(): iterable + { + yield 'default' => [null, ['orderName']]; + yield 'castToArray disabled' => [false, ['orderName']]; + yield 'castToArray enabled' => [true, ['orderName[]']]; + } +} diff --git a/tests/Functional/Parameters/SortFilterTest.php b/tests/Functional/Parameters/SortFilterTest.php index fb0bad15f97..d8040810038 100644 --- a/tests/Functional/Parameters/SortFilterTest.php +++ b/tests/Functional/Parameters/SortFilterTest.php @@ -178,6 +178,20 @@ public function testSortInvalidValueReturnsValidationError(): void $this->assertResponseStatusCodeSame(422); } + public function testOpenApiDocumentsSortParameterAsSingleValueOnly(): void + { + $response = self::createClient()->request('GET', '/docs', [ + 'headers' => ['Accept' => 'application/vnd.openapi+json'], + ]); + $this->assertResponseIsSuccessful(); + + $parameters = $response->toArray()['paths']['/filter_employees']['get']['parameters']; + $names = array_column($parameters, 'name'); + + $this->assertContains('orderName', $names); + $this->assertNotContains('orderName[]', $names); + } + public function testIriFilterOnDepartment(): void { $response = self::createClient()->request('GET', '/filter_departments'); From 7cdfb616c81393ebf2fb13712b719adc6c6caaf2 Mon Sep 17 00:00:00 2001 From: Pascal CESCON - Amoifr Date: Fri, 23 Oct 2026 14:58:22 +0200 Subject: [PATCH 2/2] test(doctrine): require api-platform/openapi to test SortFilter OpenAPI parameters --- src/Doctrine/Odm/composer.json | 1 + src/Doctrine/Orm/composer.json | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Doctrine/Odm/composer.json b/src/Doctrine/Odm/composer.json index 0389b571ee3..123398d46a6 100644 --- a/src/Doctrine/Odm/composer.json +++ b/src/Doctrine/Odm/composer.json @@ -34,6 +34,7 @@ "symfony/type-info": "^7.4 || ^8.0" }, "require-dev": { + "api-platform/openapi": "^4.4", "doctrine/doctrine-bundle": "^2.11 || ^3.1", "doctrine/mongodb-odm-bundle": "^5.0", "phpspec/prophecy-phpunit": "^2.2", diff --git a/src/Doctrine/Orm/composer.json b/src/Doctrine/Orm/composer.json index 2cfbc2eab48..4997802fb86 100644 --- a/src/Doctrine/Orm/composer.json +++ b/src/Doctrine/Orm/composer.json @@ -32,6 +32,7 @@ "doctrine/orm": "^2.17 || ^3.3" }, "require-dev": { + "api-platform/openapi": "^4.4", "doctrine/doctrine-bundle": "^2.11.1 || ^3.1", "phpspec/prophecy-phpunit": "^2.2", "phpunit/phpunit": "^11.5 || ^12.2",