diff --git a/src/Doctrine/Odm/Filter/ComparisonFilter.php b/src/Doctrine/Odm/Filter/ComparisonFilter.php index 3c822f1b3f3..1593fc473ed 100644 --- a/src/Doctrine/Odm/Filter/ComparisonFilter.php +++ b/src/Doctrine/Odm/Filter/ComparisonFilter.php @@ -44,6 +44,7 @@ final class ComparisonFilter implements FilterInterface, OpenApiParameterFilterI 'gte' => 'gte', 'lt' => 'lt', 'lte' => 'lte', + 'ne' => 'notEqual', ]; public function __construct(private readonly FilterInterface $filter) @@ -91,6 +92,7 @@ public function getOpenApiParameters(Parameter $parameter): array 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), ]; } @@ -108,6 +110,7 @@ public function getSchema(Parameter $parameter): array 'gte' => $innerSchema, 'lt' => $innerSchema, 'lte' => $innerSchema, + 'ne' => $innerSchema, ], ]; } diff --git a/src/Doctrine/Orm/Filter/ComparisonFilter.php b/src/Doctrine/Orm/Filter/ComparisonFilter.php index 41a1bce6e89..474973d72fd 100644 --- a/src/Doctrine/Orm/Filter/ComparisonFilter.php +++ b/src/Doctrine/Orm/Filter/ComparisonFilter.php @@ -45,6 +45,7 @@ final class ComparisonFilter implements FilterInterface, OpenApiParameterFilterI 'gte' => '>=', 'lt' => '<', 'lte' => '<=', + 'ne' => '!=', ]; public const ALLOWED_DQL_OPERATORS = ['=', '>', '>=', '<', '<=', '!=', '<>']; @@ -91,6 +92,7 @@ public function getOpenApiParameters(Parameter $parameter): array 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), ]; } @@ -108,6 +110,7 @@ public function getSchema(Parameter $parameter): array 'gte' => $innerSchema, 'lt' => $innerSchema, 'lte' => $innerSchema, + 'ne' => $innerSchema, ], ]; } diff --git a/tests/Fixtures/TestBundle/Entity/Uuid/SymfonyUuidDevice.php b/tests/Fixtures/TestBundle/Entity/Uuid/SymfonyUuidDevice.php index f61100fa76b..3795c222004 100644 --- a/tests/Fixtures/TestBundle/Entity/Uuid/SymfonyUuidDevice.php +++ b/tests/Fixtures/TestBundle/Entity/Uuid/SymfonyUuidDevice.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity\Uuid; +use ApiPlatform\Doctrine\Orm\Filter\ComparisonFilter; use ApiPlatform\Doctrine\Orm\Filter\UuidFilter; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; @@ -29,6 +30,10 @@ 'id' => new QueryParameter( filter: new UuidFilter(), ), + 'idComparison' => new QueryParameter( + filter: new ComparisonFilter(new UuidFilter()), + property: 'id', + ), ] ), new Post(), diff --git a/tests/Functional/Parameters/ComparisonFilterTest.php b/tests/Functional/Parameters/ComparisonFilterTest.php index 34ae583e7e0..8caaef1e3dd 100644 --- a/tests/Functional/Parameters/ComparisonFilterTest.php +++ b/tests/Functional/Parameters/ComparisonFilterTest.php @@ -98,6 +98,24 @@ public function testCombinedGtAndLt(): void $this->assertSame(['Bravo', 'Charlie'], $names); } + public function testNe(): void + { + // ne "Bravo": all names except "Bravo" → Alpha, Charlie, Delta + $response = self::createClient()->request('GET', '/chickens?nameComparison[ne]=Bravo'); + $this->assertResponseIsSuccessful(); + $names = array_map(static fn ($c) => $c['name'], $response->toArray()['member']); + sort($names); + $this->assertSame(['Alpha', 'Charlie', 'Delta'], $names); + } + + public function testNeNoMatch(): void + { + // ne "Unknown": no chicken has that name, so all are returned + $response = self::createClient()->request('GET', '/chickens?nameComparison[ne]=Unknown&itemsPerPage=10'); + $this->assertResponseIsSuccessful(); + $this->assertCount(4, $response->toArray()['member']); + } + public function testGtNoResults(): void { // gt "ZZZZ": no name is alphabetically after "ZZZZ" @@ -125,7 +143,7 @@ public function testOpenApiDocumentation(): void $parameters = $openApiDoc['paths']['/chickens']['get']['parameters']; $parameterNames = array_column($parameters, 'name'); - foreach (['nameComparison[gt]', 'nameComparison[gte]', 'nameComparison[lt]', 'nameComparison[lte]'] as $expectedName) { + foreach (['nameComparison[gt]', 'nameComparison[gte]', 'nameComparison[lt]', 'nameComparison[lte]', 'nameComparison[ne]'] as $expectedName) { $this->assertContains($expectedName, $parameterNames, \sprintf('Expected parameter "%s" in OpenAPI documentation', $expectedName)); } diff --git a/tests/Functional/Uuid/UuidComparisonFilterTest.php b/tests/Functional/Uuid/UuidComparisonFilterTest.php new file mode 100644 index 00000000000..016d617c706 --- /dev/null +++ b/tests/Functional/Uuid/UuidComparisonFilterTest.php @@ -0,0 +1,187 @@ + + * + * 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\Tests\Functional\Uuid; + +use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Uuid\SymfonyUuidDevice; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Uuid\SymfonyUuidDeviceEndpoint; +use ApiPlatform\Tests\RecreateSchemaTrait; +use ApiPlatform\Tests\SetupClassResourcesTrait; + +final class UuidComparisonFilterTest extends ApiTestCase +{ + use RecreateSchemaTrait; + use SetupClassResourcesTrait; + + protected static ?bool $alwaysBootKernel = false; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [SymfonyUuidDevice::class, SymfonyUuidDeviceEndpoint::class]; + } + + protected function setUp(): void + { + parent::setUp(); + + if ($this->isMongoDB()) { + $this->markTestSkipped('UuidFilter is ORM only.'); + } + } + + public function testGtWithUuid(): void + { + $this->recreateSchema(static::getResources()); + + $manager = $this->getManager(); + $manager->persist($device1 = new SymfonyUuidDevice()); + usleep(1000); + $manager->persist($device2 = new SymfonyUuidDevice()); + usleep(1000); + $manager->persist($device3 = new SymfonyUuidDevice()); + $manager->flush(); + + $uuids = [(string) $device1->id, (string) $device2->id, (string) $device3->id]; + sort($uuids); + + $response = self::createClient()->request('GET', '/symfony_uuid_devices', [ + 'query' => [ + 'idComparison' => ['gt' => $uuids[1]], + ], + ]); + + self::assertResponseIsSuccessful(); + $json = $response->toArray(); + self::assertSame(1, $json['hydra:totalItems']); + self::assertSame($uuids[2], $json['hydra:member'][0]['id']); + } + + public function testGteWithUuid(): void + { + $this->recreateSchema(static::getResources()); + + $manager = $this->getManager(); + $manager->persist($device1 = new SymfonyUuidDevice()); + usleep(1000); + $manager->persist($device2 = new SymfonyUuidDevice()); + usleep(1000); + $manager->persist($device3 = new SymfonyUuidDevice()); + $manager->flush(); + + $uuids = [(string) $device1->id, (string) $device2->id, (string) $device3->id]; + sort($uuids); + + $response = self::createClient()->request('GET', '/symfony_uuid_devices', [ + 'query' => [ + 'idComparison' => ['gte' => $uuids[1]], + ], + ]); + + self::assertResponseIsSuccessful(); + $json = $response->toArray(); + self::assertSame(2, $json['hydra:totalItems']); + } + + public function testLtWithUuid(): void + { + $this->recreateSchema(static::getResources()); + + $manager = $this->getManager(); + $manager->persist($device1 = new SymfonyUuidDevice()); + usleep(1000); + $manager->persist($device2 = new SymfonyUuidDevice()); + usleep(1000); + $manager->persist($device3 = new SymfonyUuidDevice()); + $manager->flush(); + + $uuids = [(string) $device1->id, (string) $device2->id, (string) $device3->id]; + sort($uuids); + + $response = self::createClient()->request('GET', '/symfony_uuid_devices', [ + 'query' => [ + 'idComparison' => ['lt' => $uuids[1]], + ], + ]); + + self::assertResponseIsSuccessful(); + $json = $response->toArray(); + self::assertSame(1, $json['hydra:totalItems']); + self::assertSame($uuids[0], $json['hydra:member'][0]['id']); + } + + public function testCombinedGteAndLteWithUuid(): void + { + $this->recreateSchema(static::getResources()); + + $manager = $this->getManager(); + $manager->persist($device1 = new SymfonyUuidDevice()); + usleep(1000); + $manager->persist($device2 = new SymfonyUuidDevice()); + usleep(1000); + $manager->persist($device3 = new SymfonyUuidDevice()); + usleep(1000); + $manager->persist($device4 = new SymfonyUuidDevice()); + $manager->flush(); + + $uuids = [(string) $device1->id, (string) $device2->id, (string) $device3->id, (string) $device4->id]; + sort($uuids); + + $response = self::createClient()->request('GET', '/symfony_uuid_devices', [ + 'query' => [ + 'idComparison' => ['gte' => $uuids[1], 'lte' => $uuids[2]], + ], + ]); + + self::assertResponseIsSuccessful(); + $json = $response->toArray(); + self::assertSame(2, $json['hydra:totalItems']); + } + + public function testNeWithUuid(): void + { + $this->recreateSchema(static::getResources()); + + $manager = $this->getManager(); + $manager->persist($device1 = new SymfonyUuidDevice()); + $manager->persist($device2 = new SymfonyUuidDevice()); + $manager->persist($device3 = new SymfonyUuidDevice()); + $manager->flush(); + + $response = self::createClient()->request('GET', '/symfony_uuid_devices', [ + 'query' => [ + 'idComparison' => ['ne' => (string) $device2->id], + ], + ]); + + self::assertResponseIsSuccessful(); + $json = $response->toArray(); + self::assertSame(2, $json['hydra:totalItems']); + + $returnedIds = array_map(static fn ($m) => $m['id'], $json['hydra:member']); + self::assertNotContains((string) $device2->id, $returnedIds); + } + + protected function tearDown(): void + { + if ($this->isMongoDB()) { + return; + } + + $this->recreateSchema(static::getResources()); + parent::tearDown(); + } +}