Skip to content

Commit 509939d

Browse files
vinceAmstoutzsoyuka
authored andcommitted
feat(doctrine): doctrine filters like laravel eloquent filters (api-platform#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 <soyuka@users.noreply.github.com>
1 parent 8697f21 commit 509939d

Some content is hidden

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

42 files changed

+1994
-74
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 <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\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 <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\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/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 <contact@alanpoulain.eu>
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 <teohhanhui@gmail.com>
105107
* @author Alan Poulain <contact@alanpoulain.eu>
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 <theo.fidry@gmail.com>
118123
* @author Alan Poulain <contact@alanpoulain.eu>
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
}

src/Doctrine/Odm/Filter/ExistsFilter.php

+19-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515

1616
use ApiPlatform\Doctrine\Common\Filter\ExistsFilterInterface;
1717
use ApiPlatform\Doctrine\Common\Filter\ExistsFilterTrait;
18+
use ApiPlatform\Doctrine\Common\Filter\PropertyPlaceholderOpenApiParameterTrait;
19+
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
20+
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
1821
use ApiPlatform\Metadata\Operation;
22+
use ApiPlatform\Metadata\Parameter;
1923
use Doctrine\ODM\MongoDB\Aggregation\Builder;
2024
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
2125
use Doctrine\Persistence\ManagerRegistry;
@@ -107,11 +111,12 @@
107111
* @author Teoh Han Hui <teohhanhui@gmail.com>
108112
* @author Alan Poulain <contact@alanpoulain.eu>
109113
*/
110-
final class ExistsFilter extends AbstractFilter implements ExistsFilterInterface
114+
final class ExistsFilter extends AbstractFilter implements ExistsFilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface
111115
{
112116
use ExistsFilterTrait;
117+
use PropertyPlaceholderOpenApiParameterTrait;
113118

114-
public function __construct(ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, ?array $properties = null, string $existsParameterName = self::QUERY_PARAMETER_KEY, ?NameConverterInterface $nameConverter = null)
119+
public function __construct(?ManagerRegistry $managerRegistry = null, ?LoggerInterface $logger = null, ?array $properties = null, string $existsParameterName = self::QUERY_PARAMETER_KEY, ?NameConverterInterface $nameConverter = null)
115120
{
116121
parent::__construct($managerRegistry, $logger, $properties, $nameConverter);
117122

@@ -123,6 +128,13 @@ public function __construct(ManagerRegistry $managerRegistry, ?LoggerInterface $
123128
*/
124129
public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void
125130
{
131+
$parameter = $context['parameter'] ?? null;
132+
if (null !== ($value = $context['filters'][$parameter?->getProperty()] ?? null)) {
133+
$this->filterProperty($this->denormalizePropertyName($parameter->getProperty()), $value, $aggregationBuilder, $resourceClass, $operation, $context);
134+
135+
return;
136+
}
137+
126138
foreach ($context['filters'][$this->existsParameterName] ?? [] as $property => $value) {
127139
$this->filterProperty($this->denormalizePropertyName($property), $value, $aggregationBuilder, $resourceClass, $operation, $context);
128140
}
@@ -167,4 +179,9 @@ protected function isNullableField(string $property, string $resourceClass): boo
167179

168180
return $metadata instanceof ClassMetadata && $metadata->hasField($field) ? $metadata->isNullable($field) : false;
169181
}
182+
183+
public function getSchema(Parameter $parameter): array
184+
{
185+
return ['type' => 'boolean'];
186+
}
170187
}

src/Doctrine/Odm/Filter/NumericFilter.php

+8-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\NumericFilterTrait;
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 <teohhanhui@gmail.com>
105107
* @author Alan Poulain <contact@alanpoulain.eu>
106108
*/
107-
final class NumericFilter extends AbstractFilter
109+
final class NumericFilter extends AbstractFilter implements JsonSchemaFilterInterface
108110
{
109111
use NumericFilterTrait;
110112

@@ -163,4 +165,9 @@ protected function getType(?string $doctrineType = null): string
163165

164166
return 'int';
165167
}
168+
169+
public function getSchema(Parameter $parameter): array
170+
{
171+
return ['type' => 'numeric'];
172+
}
166173
}

0 commit comments

Comments
 (0)