Skip to content

Commit fa3a068

Browse files
committed
feat(doctrine): add OrFilter for ORM and ODM
Continues the work at api-platform#7079 and before at api-platform#6865
1 parent 4da588e commit fa3a068

File tree

6 files changed

+255
-9
lines changed

6 files changed

+255
-9
lines changed

src/Doctrine/Odm/Filter/OrFilter.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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\Doctrine\Odm\Filter;
15+
16+
use ApiPlatform\Doctrine\Common\Filter\OpenApiFilterTrait;
17+
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
18+
use ApiPlatform\Metadata\Operation;
19+
use Doctrine\ODM\MongoDB\Aggregation\Builder;
20+
21+
final class OrFilter implements FilterInterface, OpenApiParameterFilterInterface
22+
{
23+
use OpenApiFilterTrait;
24+
/**
25+
* @var array<FilterInterface>
26+
*/
27+
private readonly array $filters;
28+
29+
public function __construct(FilterInterface ...$filters)
30+
{
31+
$this->filters = $filters;
32+
}
33+
34+
public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void
35+
{
36+
foreach ($this->filters as $filter) {
37+
$filter->apply($aggregationBuilder, $resourceClass, $operation, $context);
38+
}
39+
}
40+
}

src/Doctrine/Orm/Filter/OrFilter.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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\Doctrine\Orm\Filter;
15+
16+
use ApiPlatform\Doctrine\Common\Filter\OpenApiFilterTrait;
17+
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
18+
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
19+
use ApiPlatform\Metadata\Operation;
20+
use ApiPlatform\Metadata\ParameterProviderFilterInterface;
21+
use ApiPlatform\State\ParameterProvider\IriConverterParameterProvider;
22+
use Doctrine\ORM\QueryBuilder;
23+
24+
final class OrFilter implements FilterInterface, OpenApiParameterFilterInterface, ParameterProviderFilterInterface
25+
{
26+
use OpenApiFilterTrait;
27+
28+
/**
29+
* @var array<FilterInterface>
30+
*/
31+
private readonly array $filters;
32+
33+
public function __construct(FilterInterface ...$filters)
34+
{
35+
$this->filters = $filters;
36+
}
37+
38+
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
39+
{
40+
foreach ($this->filters as $filter) {
41+
$filter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, $context);
42+
}
43+
}
44+
45+
public static function getParameterProvider(): string
46+
{
47+
return IriConverterParameterProvider::class;
48+
}
49+
}

src/State/ParameterProvider/IriConverterParameterProvider.php

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
namespace ApiPlatform\State\ParameterProvider;
1515

16+
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
17+
use ApiPlatform\Metadata\Exception\ItemNotFoundException;
1618
use ApiPlatform\Metadata\IriConverterInterface;
1719
use ApiPlatform\Metadata\Operation;
1820
use ApiPlatform\Metadata\Parameter;
@@ -40,19 +42,23 @@ public function provide(Parameter $parameter, array $parameters = [], array $con
4042

4143
$iriConverterContext = ['fetch_data' => $parameter->getExtraProperties()['fetch_data'] ?? false];
4244

43-
if (\is_array($value)) {
44-
$entities = [];
45-
foreach ($value as $v) {
46-
$entities[] = $this->iriConverter->getResourceFromIri($v, $iriConverterContext);
47-
}
45+
try {
46+
if (\is_array($value)) {
47+
$entities = [];
48+
foreach ($value as $v) {
49+
$entities[] = $this->iriConverter->getResourceFromIri($v, $iriConverterContext);
50+
}
51+
52+
$parameter->setValue($entities);
4853

49-
$parameter->setValue($entities);
54+
return $operation;
55+
}
5056

57+
$parameter->setValue($this->iriConverter->getResourceFromIri($value, $iriConverterContext));
58+
} catch (InvalidArgumentException|ItemNotFoundException) {
5159
return $operation;
5260
}
5361

54-
$parameter->setValue($this->iriConverter->getResourceFromIri($value, $iriConverterContext));
55-
5662
return $operation;
5763
}
5864
}

tests/Fixtures/TestBundle/Document/Chicken.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
namespace ApiPlatform\Tests\Fixtures\TestBundle\Document;
1515

1616
use ApiPlatform\Doctrine\Odm\Filter\ExactFilter;
17+
use ApiPlatform\Doctrine\Odm\Filter\IriFilter;
18+
use ApiPlatform\Doctrine\Odm\Filter\OrFilter;
1719
use ApiPlatform\Doctrine\Odm\Filter\PartialSearchFilter;
1820
use ApiPlatform\Metadata\GetCollection;
1921
use ApiPlatform\Metadata\QueryParameter;
@@ -28,6 +30,10 @@
2830
filter: new PartialSearchFilter(),
2931
property: 'name',
3032
),
33+
'relation' => new QueryParameter(
34+
filter: new OrFilter(new IriFilter(), new ExactFilter()),
35+
property: 'chickenCoop'
36+
),
3137
],
3238
)]
3339
class Chicken
@@ -41,7 +47,7 @@ class Chicken
4147
#[ODM\ReferenceOne(targetDocument: ChickenCoop::class, inversedBy: 'chickens')]
4248
private ?ChickenCoop $chickenCoop = null;
4349

44-
public function getId(): ?int
50+
public function getId(): ?string
4551
{
4652
return $this->id;
4753
}

tests/Fixtures/TestBundle/Entity/Chicken.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity;
1515

1616
use ApiPlatform\Doctrine\Orm\Filter\ExactFilter;
17+
use ApiPlatform\Doctrine\Orm\Filter\IriFilter;
18+
use ApiPlatform\Doctrine\Orm\Filter\OrFilter;
1719
use ApiPlatform\Doctrine\Orm\Filter\PartialSearchFilter;
1820
use ApiPlatform\Metadata\GetCollection;
1921
use ApiPlatform\Metadata\QueryParameter;
@@ -28,6 +30,10 @@
2830
filter: new PartialSearchFilter(),
2931
property: 'name',
3032
),
33+
'relation' => new QueryParameter(
34+
filter: new OrFilter(new IriFilter(), new ExactFilter()),
35+
property: 'chickenCoop'
36+
),
3137
],
3238
)]
3339
class Chicken
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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\Parameters;
15+
16+
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
17+
use ApiPlatform\Tests\Fixtures\TestBundle\Document\Chicken as DocumentChicken;
18+
use ApiPlatform\Tests\Fixtures\TestBundle\Document\ChickenCoop as DocumentChickenCoop;
19+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Chicken;
20+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ChickenCoop;
21+
use ApiPlatform\Tests\RecreateSchemaTrait;
22+
use ApiPlatform\Tests\SetupClassResourcesTrait;
23+
use Doctrine\ODM\MongoDB\MongoDBException;
24+
use PHPUnit\Framework\Attributes\DataProvider;
25+
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
26+
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
27+
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
28+
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
29+
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
30+
31+
final class OrFilterTest extends ApiTestCase
32+
{
33+
use RecreateSchemaTrait;
34+
use SetupClassResourcesTrait;
35+
36+
public static function getResources(): array
37+
{
38+
return [Chicken::class, ChickenCoop::class];
39+
}
40+
41+
/**
42+
* @throws \Throwable
43+
* @throws MongoDBException
44+
*/
45+
protected function setUp(): void
46+
{
47+
$entities = $this->isMongoDB()
48+
? [DocumentChicken::class, DocumentChickenCoop::class]
49+
: [Chicken::class, ChickenCoop::class];
50+
51+
$this->recreateSchema($entities);
52+
$this->loadFixtures();
53+
}
54+
55+
/**
56+
* @throws RedirectionExceptionInterface
57+
* @throws DecodingExceptionInterface
58+
* @throws ClientExceptionInterface
59+
* @throws TransportExceptionInterface
60+
* @throws ServerExceptionInterface
61+
*/
62+
#[DataProvider('filterDataProvider')]
63+
public function testOrFilter(string $url, int $expectedCount, array $expectedNames): void
64+
{
65+
$client = self::createClient();
66+
$client->request('GET', $url);
67+
68+
$this->assertResponseIsSuccessful();
69+
$this->assertJsonContains(['hydra:totalItems' => $expectedCount]);
70+
71+
if ($expectedCount > 0) {
72+
$names = array_column($client->getResponse()->toArray()['hydra:member'], 'name');
73+
sort($names);
74+
sort($expectedNames);
75+
$this->assertSame($expectedNames, $names);
76+
}
77+
}
78+
79+
public static function filterDataProvider(): \Generator
80+
{
81+
yield 'filtre par ID du poulailler de Gertrude' => [
82+
'url' => '/chickens?relation=1',
83+
'expectedCount' => 1,
84+
'expectedNames' => ['Gertrude'],
85+
];
86+
87+
yield 'filtre par IRI du poulailler de Gertrude' => [
88+
'url' => '/chickens?relation=/chicken_coops/1',
89+
'expectedCount' => 1,
90+
'expectedNames' => ['Gertrude'],
91+
];
92+
93+
yield 'filtre par ID du poulailler de Henriette' => [
94+
'url' => '/chickens?relation=2',
95+
'expectedCount' => 1,
96+
'expectedNames' => ['Henriette'],
97+
];
98+
99+
yield 'filtre par IRI du poulailler de Henriette' => [
100+
'url' => '/chickens?relation=/chicken_coops/2',
101+
'expectedCount' => 1,
102+
'expectedNames' => ['Henriette'],
103+
];
104+
105+
yield 'filtre avec un ID inexistant' => [
106+
'url' => '/chickens?relation=999',
107+
'expectedCount' => 0,
108+
'expectedNames' => [],
109+
];
110+
}
111+
112+
/**
113+
* @throws \Throwable
114+
* @throws MongoDBException
115+
*/
116+
private function loadFixtures(): void
117+
{
118+
$manager = $this->getManager();
119+
$chickenClass = $this->isMongoDB() ? DocumentChicken::class : Chicken::class;
120+
$coopClass = $this->isMongoDB() ? DocumentChickenCoop::class : ChickenCoop::class;
121+
122+
$chickenCoop1 = new $coopClass();
123+
$chickenCoop2 = new $coopClass();
124+
$manager->persist($chickenCoop1);
125+
$manager->persist($chickenCoop2);
126+
127+
$chicken1 = new $chickenClass();
128+
$chicken1->setName('Gertrude');
129+
$chicken1->setChickenCoop($chickenCoop1);
130+
$manager->persist($chicken1);
131+
132+
$chicken2 = new $chickenClass();
133+
$chicken2->setName('Henriette');
134+
$chicken2->setChickenCoop($chickenCoop2);
135+
$manager->persist($chicken2);
136+
137+
$manager->flush();
138+
}
139+
}

0 commit comments

Comments
 (0)