Skip to content

Commit 26d2394

Browse files
feat(doctrine): new search filters (#7121)
Co-authored-by: soyuka <[email protected]>
1 parent f010fd4 commit 26d2394

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1802
-135
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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\Common\Filter;
15+
16+
use Psr\Log\LoggerInterface;
17+
18+
interface LoggerAwareInterface
19+
{
20+
public function hasLogger(): bool;
21+
22+
public function getLogger(): LoggerInterface;
23+
24+
public function setLogger(LoggerInterface $logger): void;
25+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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\Common\Filter;
15+
16+
use Psr\Log\LoggerInterface;
17+
use Psr\Log\NullLogger;
18+
19+
trait LoggerAwareTrait
20+
{
21+
private ?LoggerInterface $logger = null;
22+
23+
public function hasLogger(): bool
24+
{
25+
return $this->logger instanceof LoggerInterface;
26+
}
27+
28+
public function getLogger(): LoggerInterface
29+
{
30+
return $this->logger ??= new NullLogger();
31+
}
32+
33+
public function setLogger(LoggerInterface $logger): void
34+
{
35+
$this->logger = $logger;
36+
}
37+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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\Common\Filter;
15+
16+
use ApiPlatform\Metadata\Exception\RuntimeException;
17+
use Doctrine\Persistence\ManagerRegistry;
18+
19+
trait ManagerRegistryAwareTrait
20+
{
21+
private ?ManagerRegistry $managerRegistry = null;
22+
23+
public function hasManagerRegistry(): bool
24+
{
25+
return $this->managerRegistry instanceof ManagerRegistry;
26+
}
27+
28+
public function getManagerRegistry(): ManagerRegistry
29+
{
30+
if (!$this->hasManagerRegistry()) {
31+
throw new RuntimeException('ManagerRegistry must be initialized before accessing it.');
32+
}
33+
34+
return $this->managerRegistry;
35+
}
36+
37+
public function setManagerRegistry(ManagerRegistry $managerRegistry): void
38+
{
39+
$this->managerRegistry = $managerRegistry;
40+
}
41+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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\Common\Filter;
15+
16+
use ApiPlatform\Metadata\Parameter;
17+
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
18+
19+
/**
20+
* @author Vincent Amstoutz <[email protected]>
21+
*/
22+
trait OpenApiFilterTrait
23+
{
24+
public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
25+
{
26+
return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true);
27+
}
28+
}

src/Doctrine/Odm/Extension/ParameterExtension.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Doctrine\Odm\Extension;
1515

16+
use ApiPlatform\Doctrine\Common\Filter\LoggerAwareInterface;
1617
use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface;
1718
use ApiPlatform\Doctrine\Common\ParameterValueExtractorTrait;
1819
use ApiPlatform\Doctrine\Odm\Filter\AbstractFilter;
@@ -22,6 +23,7 @@
2223
use Doctrine\Bundle\MongoDBBundle\ManagerRegistry;
2324
use Doctrine\ODM\MongoDB\Aggregation\Builder;
2425
use Psr\Container\ContainerInterface;
26+
use Psr\Log\LoggerInterface;
2527

2628
/**
2729
* Reads operation parameters and execute its filter.
@@ -35,6 +37,7 @@ final class ParameterExtension implements AggregationCollectionExtensionInterfac
3537
public function __construct(
3638
private readonly ContainerInterface $filterLocator,
3739
private readonly ?ManagerRegistry $managerRegistry = null,
40+
private readonly ?LoggerInterface $logger = null,
3841
) {
3942
}
4043

@@ -67,6 +70,10 @@ private function applyFilter(Builder $aggregationBuilder, ?string $resourceClass
6770
$filter->setManagerRegistry($this->managerRegistry);
6871
}
6972

73+
if ($this->logger && $filter instanceof LoggerAwareInterface && !$filter->hasLogger()) {
74+
$filter->setLogger($this->logger);
75+
}
76+
7077
if ($filter instanceof AbstractFilter && !$filter->getProperties()) {
7178
$propertyKey = $parameter->getProperty() ?? $parameter->getKey();
7279

@@ -82,12 +89,19 @@ private function applyFilter(Builder $aggregationBuilder, ?string $resourceClass
8289
$filter->setProperties($properties ?? []);
8390
}
8491

85-
$filterContext = ['filters' => $values, 'parameter' => $parameter];
92+
$filterContext = ['filters' => $values, 'parameter' => $parameter, 'match' => $context['match'] ?? null];
8693
$filter->apply($aggregationBuilder, $resourceClass, $operation, $filterContext);
8794
// update by reference
8895
if (isset($filterContext['mongodb_odm_sort_fields'])) {
8996
$context['mongodb_odm_sort_fields'] = $filterContext['mongodb_odm_sort_fields'];
9097
}
98+
if (isset($filterContext['match'])) {
99+
$context['match'] = $filterContext['match'];
100+
}
101+
}
102+
103+
if (isset($context['match'])) {
104+
$aggregationBuilder->match()->addAnd($context['match']);
91105
}
92106
}
93107

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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\ManagerRegistryAwareInterface;
17+
use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareTrait;
18+
use ApiPlatform\Doctrine\Common\Filter\OpenApiFilterTrait;
19+
use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait;
20+
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
21+
use ApiPlatform\Metadata\Operation;
22+
use Doctrine\ODM\MongoDB\Aggregation\Builder;
23+
use Doctrine\ODM\MongoDB\DocumentManager;
24+
use Doctrine\ODM\MongoDB\LockException;
25+
use Doctrine\ODM\MongoDB\Mapping\MappingException;
26+
27+
/**
28+
* @author Vincent Amstoutz <[email protected]>
29+
*/
30+
final class ExactFilter implements FilterInterface, OpenApiParameterFilterInterface, ManagerRegistryAwareInterface
31+
{
32+
use BackwardCompatibleFilterDescriptionTrait;
33+
use ManagerRegistryAwareTrait;
34+
use OpenApiFilterTrait;
35+
36+
/**
37+
* @throws MappingException
38+
* @throws LockException
39+
*/
40+
public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void
41+
{
42+
$parameter = $context['parameter'];
43+
$property = $parameter->getProperty();
44+
$value = $parameter->getValue();
45+
$operator = $context['operator'] ?? 'addAnd';
46+
$match = $context['match'] = $context['match'] ??
47+
$aggregationBuilder
48+
->matchExpr();
49+
50+
$documentManager = $this->getManagerRegistry()->getManagerForClass($resourceClass);
51+
if (!$documentManager instanceof DocumentManager) {
52+
return;
53+
}
54+
55+
$classMetadata = $documentManager->getClassMetadata($resourceClass);
56+
57+
if (!$classMetadata->hasReference($property)) {
58+
$match
59+
->{$operator}($aggregationBuilder->matchExpr()->field($property)->{is_iterable($value) ? 'in' : 'equals'}($value));
60+
61+
return;
62+
}
63+
64+
$mapping = $classMetadata->getFieldMapping($property);
65+
$method = $classMetadata->isSingleValuedAssociation($property) ? 'references' : 'includesReferenceTo';
66+
67+
if (is_iterable($value)) {
68+
$or = $aggregationBuilder->matchExpr();
69+
70+
foreach ($value as $v) {
71+
$or->addOr($aggregationBuilder->matchExpr()->field($property)->{$method}($documentManager->getPartialReference($mapping['targetDocument'], $v)));
72+
}
73+
74+
$match->{$operator}($or);
75+
76+
return;
77+
}
78+
79+
$match
80+
->{$operator}(
81+
$aggregationBuilder->matchExpr()
82+
->field($property)
83+
->{$method}($documentManager->getPartialReference($mapping['targetDocument'], $value))
84+
);
85+
}
86+
}

src/Doctrine/Odm/Filter/FilterInterface.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use ApiPlatform\Metadata\FilterInterface as BaseFilterInterface;
1717
use ApiPlatform\Metadata\Operation;
18+
use ApiPlatform\Metadata\Parameter;
1819
use Doctrine\ODM\MongoDB\Aggregation\Builder;
1920

2021
/**
@@ -26,6 +27,8 @@ interface FilterInterface extends BaseFilterInterface
2627
{
2728
/**
2829
* Applies the filter.
30+
*
31+
* @param array|array{filters?: array<string, mixed>|array, parameter?: Parameter, mongodb_odm_sort_fields?: array, ...} $context
2932
*/
3033
public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void;
3134
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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\LoggerAwareInterface;
17+
use ApiPlatform\Doctrine\Common\Filter\LoggerAwareTrait;
18+
use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface;
19+
use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareTrait;
20+
use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait;
21+
use ApiPlatform\Metadata\Operation;
22+
use Doctrine\ODM\MongoDB\Aggregation\Builder;
23+
24+
final class FreeTextQueryFilter implements FilterInterface, ManagerRegistryAwareInterface, LoggerAwareInterface
25+
{
26+
use BackwardCompatibleFilterDescriptionTrait;
27+
use LoggerAwareTrait;
28+
use ManagerRegistryAwareTrait;
29+
30+
/**
31+
* @param list<string> $properties an array of properties, defaults to `parameter->getProperties()`
32+
*/
33+
public function __construct(private readonly FilterInterface $filter, private readonly ?array $properties = null)
34+
{
35+
}
36+
37+
public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void
38+
{
39+
if ($this->filter instanceof ManagerRegistryAwareInterface) {
40+
$this->filter->setManagerRegistry($this->getManagerRegistry());
41+
}
42+
43+
if ($this->filter instanceof LoggerAwareInterface) {
44+
$this->filter->setLogger($this->getLogger());
45+
}
46+
47+
$parameter = $context['parameter'];
48+
foreach ($this->properties ?? $parameter->getProperties() ?? [] as $property) {
49+
$newContext = ['parameter' => $parameter->withProperty($property), 'match' => $context['match'] ?? $aggregationBuilder->match()->expr()] + $context;
50+
$this->filter->apply(
51+
$aggregationBuilder,
52+
$resourceClass,
53+
$operation,
54+
$newContext,
55+
);
56+
57+
if (isset($newContext['match'])) {
58+
$context['match'] = $newContext['match'];
59+
}
60+
}
61+
}
62+
}

0 commit comments

Comments
 (0)