Skip to content

Commit f67f6f1

Browse files
feat(doctrine): doctrine filters like laravel eloquent filters (#6775)
* feat(doctrine): doctrine filters like laravel eloquent filters * fix: allow multiple validation with :property placeholder * fix: correct escape filter condition * fix: remove duplicated block --------- Co-authored-by: soyuka <[email protected]>
1 parent 81a0f85 commit f67f6f1

Some content is hidden

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

45 files changed

+2111
-96
lines changed
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 Doctrine\Persistence\ManagerRegistry;
17+
18+
interface ManagerRegistryAwareInterface
19+
{
20+
public function hasManagerRegistry(): bool;
21+
22+
public function getManagerRegistry(): ManagerRegistry;
23+
24+
public function setManagerRegistry(ManagerRegistry $managerRegistry): void;
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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+
trait PropertyPlaceholderOpenApiParameterTrait
20+
{
21+
/**
22+
* @return array<OpenApiParameter>|null
23+
*/
24+
public function getOpenApiParameters(Parameter $parameter): ?array
25+
{
26+
if (str_contains($parameter->getKey(), ':property')) {
27+
$parameters = [];
28+
$key = str_replace('[:property]', '', $parameter->getKey());
29+
foreach (array_keys($parameter->getExtraProperties()['_properties'] ?? []) as $property) {
30+
$parameters[] = new OpenApiParameter(name: \sprintf('%s[%s]', $key, $property), in: 'query');
31+
}
32+
33+
return $parameters;
34+
}
35+
36+
return null;
37+
}
38+
}

src/Doctrine/Odm/Extension/ParameterExtension.php

+45-10
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@
1313

1414
namespace ApiPlatform\Doctrine\Odm\Extension;
1515

16+
use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface;
1617
use ApiPlatform\Doctrine\Common\ParameterValueExtractorTrait;
18+
use ApiPlatform\Doctrine\Odm\Filter\AbstractFilter;
1719
use ApiPlatform\Doctrine\Odm\Filter\FilterInterface;
1820
use ApiPlatform\Metadata\Operation;
1921
use ApiPlatform\State\ParameterNotFound;
22+
use Doctrine\Bundle\MongoDBBundle\ManagerRegistry;
2023
use Doctrine\ODM\MongoDB\Aggregation\Builder;
2124
use Psr\Container\ContainerInterface;
2225

@@ -29,14 +32,20 @@ final class ParameterExtension implements AggregationCollectionExtensionInterfac
2932
{
3033
use ParameterValueExtractorTrait;
3134

32-
public function __construct(private readonly ContainerInterface $filterLocator)
33-
{
35+
public function __construct(
36+
private readonly ContainerInterface $filterLocator,
37+
private readonly ?ManagerRegistry $managerRegistry = null,
38+
) {
3439
}
3540

41+
/**
42+
* @param array<string, mixed> $context
43+
*/
3644
private function applyFilter(Builder $aggregationBuilder, ?string $resourceClass = null, ?Operation $operation = null, array &$context = []): void
3745
{
3846
foreach ($operation->getParameters() ?? [] as $parameter) {
39-
if (!($v = $parameter->getValue()) || $v instanceof ParameterNotFound) {
47+
// TODO: remove the null equality as a parameter can have a null value
48+
if (null === ($v = $parameter->getValue()) || $v instanceof ParameterNotFound) {
4049
continue;
4150
}
4251

@@ -45,14 +54,40 @@ private function applyFilter(Builder $aggregationBuilder, ?string $resourceClass
4554
continue;
4655
}
4756

48-
$filter = $this->filterLocator->has($filterId) ? $this->filterLocator->get($filterId) : null;
49-
if ($filter instanceof FilterInterface) {
50-
$filterContext = ['filters' => $values, 'parameter' => $parameter];
51-
$filter->apply($aggregationBuilder, $resourceClass, $operation, $filterContext);
52-
// update by reference
53-
if (isset($filterContext['mongodb_odm_sort_fields'])) {
54-
$context['mongodb_odm_sort_fields'] = $filterContext['mongodb_odm_sort_fields'];
57+
$filter = match (true) {
58+
$filterId instanceof FilterInterface => $filterId,
59+
\is_string($filterId) && $this->filterLocator->has($filterId) => $this->filterLocator->get($filterId),
60+
default => null,
61+
};
62+
63+
if (!$filter instanceof FilterInterface) {
64+
continue;
65+
}
66+
67+
if ($this->managerRegistry && $filter instanceof ManagerRegistryAwareInterface && !$filter->hasManagerRegistry()) {
68+
$filter->setManagerRegistry($this->managerRegistry);
69+
}
70+
71+
if ($filter instanceof AbstractFilter && !$filter->getProperties()) {
72+
$propertyKey = $parameter->getProperty() ?? $parameter->getKey();
73+
74+
if (str_contains($propertyKey, ':property')) {
75+
$extraProperties = $parameter->getExtraProperties()['_properties'] ?? [];
76+
foreach (array_keys($extraProperties) as $property) {
77+
$properties[$property] = $parameter->getFilterContext();
78+
}
79+
} else {
80+
$properties = [$propertyKey => $parameter->getFilterContext()];
5581
}
82+
83+
$filter->setProperties($properties ?? []);
84+
}
85+
86+
$filterContext = ['filters' => $values, 'parameter' => $parameter];
87+
$filter->apply($aggregationBuilder, $resourceClass, $operation, $filterContext);
88+
// update by reference
89+
if (isset($filterContext['mongodb_odm_sort_fields'])) {
90+
$context['mongodb_odm_sort_fields'] = $filterContext['mongodb_odm_sort_fields'];
5691
}
5792
}
5893
}

src/Doctrine/Odm/Filter/AbstractFilter.php

+29-6
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313

1414
namespace ApiPlatform\Doctrine\Odm\Filter;
1515

16+
use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface;
1617
use ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface;
1718
use ApiPlatform\Doctrine\Common\PropertyHelperTrait;
1819
use ApiPlatform\Doctrine\Odm\PropertyHelperTrait as MongoDbOdmPropertyHelperTrait;
20+
use ApiPlatform\Metadata\Exception\RuntimeException;
1921
use ApiPlatform\Metadata\Operation;
2022
use Doctrine\ODM\MongoDB\Aggregation\Builder;
2123
use Doctrine\Persistence\ManagerRegistry;
@@ -30,14 +32,18 @@
3032
*
3133
* @author Alan Poulain <[email protected]>
3234
*/
33-
abstract class AbstractFilter implements FilterInterface, PropertyAwareFilterInterface
35+
abstract class AbstractFilter implements FilterInterface, PropertyAwareFilterInterface, ManagerRegistryAwareInterface
3436
{
3537
use MongoDbOdmPropertyHelperTrait;
3638
use PropertyHelperTrait;
3739
protected LoggerInterface $logger;
3840

39-
public function __construct(protected ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, protected ?array $properties = null, protected ?NameConverterInterface $nameConverter = null)
40-
{
41+
public function __construct(
42+
protected ?ManagerRegistry $managerRegistry = null,
43+
?LoggerInterface $logger = null,
44+
protected ?array $properties = null,
45+
protected ?NameConverterInterface $nameConverter = null,
46+
) {
4147
$this->logger = $logger ?? new NullLogger();
4248
}
4349

@@ -56,18 +62,35 @@ public function apply(Builder $aggregationBuilder, string $resourceClass, ?Opera
5662
*/
5763
abstract protected function filterProperty(string $property, $value, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void;
5864

59-
protected function getManagerRegistry(): ManagerRegistry
65+
public function hasManagerRegistry(): bool
66+
{
67+
return $this->managerRegistry instanceof ManagerRegistry;
68+
}
69+
70+
public function getManagerRegistry(): ManagerRegistry
6071
{
72+
if (!$this->hasManagerRegistry()) {
73+
throw new RuntimeException('ManagerRegistry must be initialized before accessing it.');
74+
}
75+
6176
return $this->managerRegistry;
6277
}
6378

64-
protected function getProperties(): ?array
79+
public function setManagerRegistry(ManagerRegistry $managerRegistry): void
80+
{
81+
$this->managerRegistry = $managerRegistry;
82+
}
83+
84+
/**
85+
* @return array<string, mixed>|null
86+
*/
87+
public function getProperties(): ?array
6588
{
6689
return $this->properties;
6790
}
6891

6992
/**
70-
* @param string[] $properties
93+
* @param array<string, mixed> $properties
7194
*/
7295
public function setProperties(array $properties): void
7396
{

src/Doctrine/Odm/Filter/BooleanFilter.php

+11-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
namespace ApiPlatform\Doctrine\Odm\Filter;
1515

1616
use ApiPlatform\Doctrine\Common\Filter\BooleanFilterTrait;
17+
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
1718
use ApiPlatform\Metadata\Operation;
19+
use ApiPlatform\Metadata\Parameter;
1820
use Doctrine\ODM\MongoDB\Aggregation\Builder;
1921
use Doctrine\ODM\MongoDB\Types\Type as MongoDbType;
2022

@@ -104,7 +106,7 @@
104106
* @author Teoh Han Hui <[email protected]>
105107
* @author Alan Poulain <[email protected]>
106108
*/
107-
final class BooleanFilter extends AbstractFilter
109+
final class BooleanFilter extends AbstractFilter implements JsonSchemaFilterInterface
108110
{
109111
use BooleanFilterTrait;
110112

@@ -139,4 +141,12 @@ protected function filterProperty(string $property, $value, Builder $aggregation
139141

140142
$aggregationBuilder->match()->field($matchField)->equals($value);
141143
}
144+
145+
/**
146+
* @return array<string, string>
147+
*/
148+
public function getSchema(Parameter $parameter): array
149+
{
150+
return ['type' => 'boolean'];
151+
}
142152
}

src/Doctrine/Odm/Filter/DateFilter.php

+38-12
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
1717
use ApiPlatform\Doctrine\Common\Filter\DateFilterTrait;
1818
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
19+
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
20+
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
1921
use ApiPlatform\Metadata\Operation;
22+
use ApiPlatform\Metadata\Parameter;
23+
use ApiPlatform\Metadata\QueryParameter;
24+
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
2025
use Doctrine\ODM\MongoDB\Aggregation\Builder;
2126
use Doctrine\ODM\MongoDB\Types\Type as MongoDbType;
2227

@@ -117,7 +122,7 @@
117122
* @author Théo FIDRY <[email protected]>
118123
* @author Alan Poulain <[email protected]>
119124
*/
120-
final class DateFilter extends AbstractFilter implements DateFilterInterface
125+
final class DateFilter extends AbstractFilter implements DateFilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface
121126
{
122127
use DateFilterTrait;
123128

@@ -129,11 +134,11 @@ final class DateFilter extends AbstractFilter implements DateFilterInterface
129134
/**
130135
* {@inheritdoc}
131136
*/
132-
protected function filterProperty(string $property, $values, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void
137+
protected function filterProperty(string $property, $value, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void
133138
{
134-
// Expect $values to be an array having the period as keys and the date value as values
139+
// Expect $value to be an array having the period as keys and the date value as values
135140
if (
136-
!\is_array($values)
141+
!\is_array($value)
137142
|| !$this->isPropertyEnabled($property, $resourceClass)
138143
|| !$this->isPropertyMapped($property, $resourceClass)
139144
|| !$this->isDateField($property, $resourceClass)
@@ -153,42 +158,42 @@ protected function filterProperty(string $property, $values, Builder $aggregatio
153158
$aggregationBuilder->match()->field($matchField)->notEqual(null);
154159
}
155160

156-
if (isset($values[self::PARAMETER_BEFORE])) {
161+
if (isset($value[self::PARAMETER_BEFORE])) {
157162
$this->addMatch(
158163
$aggregationBuilder,
159164
$matchField,
160165
self::PARAMETER_BEFORE,
161-
$values[self::PARAMETER_BEFORE],
166+
$value[self::PARAMETER_BEFORE],
162167
$nullManagement
163168
);
164169
}
165170

166-
if (isset($values[self::PARAMETER_STRICTLY_BEFORE])) {
171+
if (isset($value[self::PARAMETER_STRICTLY_BEFORE])) {
167172
$this->addMatch(
168173
$aggregationBuilder,
169174
$matchField,
170175
self::PARAMETER_STRICTLY_BEFORE,
171-
$values[self::PARAMETER_STRICTLY_BEFORE],
176+
$value[self::PARAMETER_STRICTLY_BEFORE],
172177
$nullManagement
173178
);
174179
}
175180

176-
if (isset($values[self::PARAMETER_AFTER])) {
181+
if (isset($value[self::PARAMETER_AFTER])) {
177182
$this->addMatch(
178183
$aggregationBuilder,
179184
$matchField,
180185
self::PARAMETER_AFTER,
181-
$values[self::PARAMETER_AFTER],
186+
$value[self::PARAMETER_AFTER],
182187
$nullManagement
183188
);
184189
}
185190

186-
if (isset($values[self::PARAMETER_STRICTLY_AFTER])) {
191+
if (isset($value[self::PARAMETER_STRICTLY_AFTER])) {
187192
$this->addMatch(
188193
$aggregationBuilder,
189194
$matchField,
190195
self::PARAMETER_STRICTLY_AFTER,
191-
$values[self::PARAMETER_STRICTLY_AFTER],
196+
$value[self::PARAMETER_STRICTLY_AFTER],
192197
$nullManagement
193198
);
194199
}
@@ -237,4 +242,25 @@ private function addMatch(Builder $aggregationBuilder, string $field, string $op
237242

238243
$aggregationBuilder->match()->addAnd($aggregationBuilder->matchExpr()->field($field)->operator($operatorValue[$operator], $value));
239244
}
245+
246+
/**
247+
* @return array<string, string>
248+
*/
249+
public function getSchema(Parameter $parameter): array
250+
{
251+
return ['type' => 'date'];
252+
}
253+
254+
public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
255+
{
256+
$in = $parameter instanceof QueryParameter ? 'query' : 'header';
257+
$key = $parameter->getKey();
258+
259+
return [
260+
new OpenApiParameter(name: $key.'[after]', in: $in),
261+
new OpenApiParameter(name: $key.'[before]', in: $in),
262+
new OpenApiParameter(name: $key.'[strictly_after]', in: $in),
263+
new OpenApiParameter(name: $key.'[strictly_before]', in: $in),
264+
];
265+
}
240266
}

0 commit comments

Comments
 (0)