Skip to content

Commit 19809c6

Browse files
soyukaclaude
andauthored
feat(doctrine): ne (not equal) operator for ComparisonFilter (#7814)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5a876cc commit 19809c6

File tree

5 files changed

+217
-1
lines changed

5 files changed

+217
-1
lines changed

src/Doctrine/Odm/Filter/ComparisonFilter.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ final class ComparisonFilter implements FilterInterface, OpenApiParameterFilterI
4444
'gte' => 'gte',
4545
'lt' => 'lt',
4646
'lte' => 'lte',
47+
'ne' => 'notEqual',
4748
];
4849

4950
public function __construct(private readonly FilterInterface $filter)
@@ -91,6 +92,7 @@ public function getOpenApiParameters(Parameter $parameter): array
9192
new OpenApiParameter(name: "{$key}[gte]", in: $in),
9293
new OpenApiParameter(name: "{$key}[lt]", in: $in),
9394
new OpenApiParameter(name: "{$key}[lte]", in: $in),
95+
new OpenApiParameter(name: "{$key}[ne]", in: $in),
9496
];
9597
}
9698

@@ -108,6 +110,7 @@ public function getSchema(Parameter $parameter): array
108110
'gte' => $innerSchema,
109111
'lt' => $innerSchema,
110112
'lte' => $innerSchema,
113+
'ne' => $innerSchema,
111114
],
112115
];
113116
}

src/Doctrine/Orm/Filter/ComparisonFilter.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ final class ComparisonFilter implements FilterInterface, OpenApiParameterFilterI
4545
'gte' => '>=',
4646
'lt' => '<',
4747
'lte' => '<=',
48+
'ne' => '!=',
4849
];
4950

5051
public const ALLOWED_DQL_OPERATORS = ['=', '>', '>=', '<', '<=', '!=', '<>'];
@@ -91,6 +92,7 @@ public function getOpenApiParameters(Parameter $parameter): array
9192
new OpenApiParameter(name: "{$key}[gte]", in: $in),
9293
new OpenApiParameter(name: "{$key}[lt]", in: $in),
9394
new OpenApiParameter(name: "{$key}[lte]", in: $in),
95+
new OpenApiParameter(name: "{$key}[ne]", in: $in),
9496
];
9597
}
9698

@@ -108,6 +110,7 @@ public function getSchema(Parameter $parameter): array
108110
'gte' => $innerSchema,
109111
'lt' => $innerSchema,
110112
'lte' => $innerSchema,
113+
'ne' => $innerSchema,
111114
],
112115
];
113116
}

tests/Fixtures/TestBundle/Entity/Uuid/SymfonyUuidDevice.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity\Uuid;
1515

16+
use ApiPlatform\Doctrine\Orm\Filter\ComparisonFilter;
1617
use ApiPlatform\Doctrine\Orm\Filter\UuidFilter;
1718
use ApiPlatform\Metadata\ApiResource;
1819
use ApiPlatform\Metadata\Get;
@@ -29,6 +30,10 @@
2930
'id' => new QueryParameter(
3031
filter: new UuidFilter(),
3132
),
33+
'idComparison' => new QueryParameter(
34+
filter: new ComparisonFilter(new UuidFilter()),
35+
property: 'id',
36+
),
3237
]
3338
),
3439
new Post(),

tests/Functional/Parameters/ComparisonFilterTest.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,24 @@ public function testCombinedGtAndLt(): void
9898
$this->assertSame(['Bravo', 'Charlie'], $names);
9999
}
100100

101+
public function testNe(): void
102+
{
103+
// ne "Bravo": all names except "Bravo" → Alpha, Charlie, Delta
104+
$response = self::createClient()->request('GET', '/chickens?nameComparison[ne]=Bravo');
105+
$this->assertResponseIsSuccessful();
106+
$names = array_map(static fn ($c) => $c['name'], $response->toArray()['member']);
107+
sort($names);
108+
$this->assertSame(['Alpha', 'Charlie', 'Delta'], $names);
109+
}
110+
111+
public function testNeNoMatch(): void
112+
{
113+
// ne "Unknown": no chicken has that name, so all are returned
114+
$response = self::createClient()->request('GET', '/chickens?nameComparison[ne]=Unknown&itemsPerPage=10');
115+
$this->assertResponseIsSuccessful();
116+
$this->assertCount(4, $response->toArray()['member']);
117+
}
118+
101119
public function testGtNoResults(): void
102120
{
103121
// gt "ZZZZ": no name is alphabetically after "ZZZZ"
@@ -125,7 +143,7 @@ public function testOpenApiDocumentation(): void
125143
$parameters = $openApiDoc['paths']['/chickens']['get']['parameters'];
126144
$parameterNames = array_column($parameters, 'name');
127145

128-
foreach (['nameComparison[gt]', 'nameComparison[gte]', 'nameComparison[lt]', 'nameComparison[lte]'] as $expectedName) {
146+
foreach (['nameComparison[gt]', 'nameComparison[gte]', 'nameComparison[lt]', 'nameComparison[lte]', 'nameComparison[ne]'] as $expectedName) {
129147
$this->assertContains($expectedName, $parameterNames, \sprintf('Expected parameter "%s" in OpenAPI documentation', $expectedName));
130148
}
131149

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Functional\Uuid;
15+
16+
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
17+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Uuid\SymfonyUuidDevice;
18+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Uuid\SymfonyUuidDeviceEndpoint;
19+
use ApiPlatform\Tests\RecreateSchemaTrait;
20+
use ApiPlatform\Tests\SetupClassResourcesTrait;
21+
22+
final class UuidComparisonFilterTest extends ApiTestCase
23+
{
24+
use RecreateSchemaTrait;
25+
use SetupClassResourcesTrait;
26+
27+
protected static ?bool $alwaysBootKernel = false;
28+
29+
/**
30+
* @return class-string[]
31+
*/
32+
public static function getResources(): array
33+
{
34+
return [SymfonyUuidDevice::class, SymfonyUuidDeviceEndpoint::class];
35+
}
36+
37+
protected function setUp(): void
38+
{
39+
parent::setUp();
40+
41+
if ($this->isMongoDB()) {
42+
$this->markTestSkipped('UuidFilter is ORM only.');
43+
}
44+
}
45+
46+
public function testGtWithUuid(): void
47+
{
48+
$this->recreateSchema(static::getResources());
49+
50+
$manager = $this->getManager();
51+
$manager->persist($device1 = new SymfonyUuidDevice());
52+
usleep(1000);
53+
$manager->persist($device2 = new SymfonyUuidDevice());
54+
usleep(1000);
55+
$manager->persist($device3 = new SymfonyUuidDevice());
56+
$manager->flush();
57+
58+
$uuids = [(string) $device1->id, (string) $device2->id, (string) $device3->id];
59+
sort($uuids);
60+
61+
$response = self::createClient()->request('GET', '/symfony_uuid_devices', [
62+
'query' => [
63+
'idComparison' => ['gt' => $uuids[1]],
64+
],
65+
]);
66+
67+
self::assertResponseIsSuccessful();
68+
$json = $response->toArray();
69+
self::assertSame(1, $json['hydra:totalItems']);
70+
self::assertSame($uuids[2], $json['hydra:member'][0]['id']);
71+
}
72+
73+
public function testGteWithUuid(): void
74+
{
75+
$this->recreateSchema(static::getResources());
76+
77+
$manager = $this->getManager();
78+
$manager->persist($device1 = new SymfonyUuidDevice());
79+
usleep(1000);
80+
$manager->persist($device2 = new SymfonyUuidDevice());
81+
usleep(1000);
82+
$manager->persist($device3 = new SymfonyUuidDevice());
83+
$manager->flush();
84+
85+
$uuids = [(string) $device1->id, (string) $device2->id, (string) $device3->id];
86+
sort($uuids);
87+
88+
$response = self::createClient()->request('GET', '/symfony_uuid_devices', [
89+
'query' => [
90+
'idComparison' => ['gte' => $uuids[1]],
91+
],
92+
]);
93+
94+
self::assertResponseIsSuccessful();
95+
$json = $response->toArray();
96+
self::assertSame(2, $json['hydra:totalItems']);
97+
}
98+
99+
public function testLtWithUuid(): void
100+
{
101+
$this->recreateSchema(static::getResources());
102+
103+
$manager = $this->getManager();
104+
$manager->persist($device1 = new SymfonyUuidDevice());
105+
usleep(1000);
106+
$manager->persist($device2 = new SymfonyUuidDevice());
107+
usleep(1000);
108+
$manager->persist($device3 = new SymfonyUuidDevice());
109+
$manager->flush();
110+
111+
$uuids = [(string) $device1->id, (string) $device2->id, (string) $device3->id];
112+
sort($uuids);
113+
114+
$response = self::createClient()->request('GET', '/symfony_uuid_devices', [
115+
'query' => [
116+
'idComparison' => ['lt' => $uuids[1]],
117+
],
118+
]);
119+
120+
self::assertResponseIsSuccessful();
121+
$json = $response->toArray();
122+
self::assertSame(1, $json['hydra:totalItems']);
123+
self::assertSame($uuids[0], $json['hydra:member'][0]['id']);
124+
}
125+
126+
public function testCombinedGteAndLteWithUuid(): void
127+
{
128+
$this->recreateSchema(static::getResources());
129+
130+
$manager = $this->getManager();
131+
$manager->persist($device1 = new SymfonyUuidDevice());
132+
usleep(1000);
133+
$manager->persist($device2 = new SymfonyUuidDevice());
134+
usleep(1000);
135+
$manager->persist($device3 = new SymfonyUuidDevice());
136+
usleep(1000);
137+
$manager->persist($device4 = new SymfonyUuidDevice());
138+
$manager->flush();
139+
140+
$uuids = [(string) $device1->id, (string) $device2->id, (string) $device3->id, (string) $device4->id];
141+
sort($uuids);
142+
143+
$response = self::createClient()->request('GET', '/symfony_uuid_devices', [
144+
'query' => [
145+
'idComparison' => ['gte' => $uuids[1], 'lte' => $uuids[2]],
146+
],
147+
]);
148+
149+
self::assertResponseIsSuccessful();
150+
$json = $response->toArray();
151+
self::assertSame(2, $json['hydra:totalItems']);
152+
}
153+
154+
public function testNeWithUuid(): void
155+
{
156+
$this->recreateSchema(static::getResources());
157+
158+
$manager = $this->getManager();
159+
$manager->persist($device1 = new SymfonyUuidDevice());
160+
$manager->persist($device2 = new SymfonyUuidDevice());
161+
$manager->persist($device3 = new SymfonyUuidDevice());
162+
$manager->flush();
163+
164+
$response = self::createClient()->request('GET', '/symfony_uuid_devices', [
165+
'query' => [
166+
'idComparison' => ['ne' => (string) $device2->id],
167+
],
168+
]);
169+
170+
self::assertResponseIsSuccessful();
171+
$json = $response->toArray();
172+
self::assertSame(2, $json['hydra:totalItems']);
173+
174+
$returnedIds = array_map(static fn ($m) => $m['id'], $json['hydra:member']);
175+
self::assertNotContains((string) $device2->id, $returnedIds);
176+
}
177+
178+
protected function tearDown(): void
179+
{
180+
if ($this->isMongoDB()) {
181+
return;
182+
}
183+
184+
$this->recreateSchema(static::getResources());
185+
parent::tearDown();
186+
}
187+
}

0 commit comments

Comments
 (0)