Skip to content

Commit fd6f1fa

Browse files
committed
feat(doctrine): boolean filter like laravel filters
1 parent 1ac0c3d commit fd6f1fa

File tree

12 files changed

+369
-67
lines changed

12 files changed

+369
-67
lines changed

src/Doctrine/Orm/Extension/FilterExtension.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGenerator
5151
$orderFilters = [];
5252

5353
foreach ($resourceFilters as $filterId) {
54-
$filter = $this->filterLocator->has($filterId) ? $this->filterLocator->get($filterId) : null;
54+
$filter = $this->resolveFilter($filterId);
5555
if ($filter instanceof FilterInterface) {
5656
// Apply the OrderFilter after every other filter to avoid an edge case where OrderFilter would do a LEFT JOIN instead of an INNER JOIN
5757
if ($filter instanceof OrderFilter) {
@@ -69,4 +69,17 @@ public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGenerator
6969
$orderFilter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, $context);
7070
}
7171
}
72+
73+
private function resolveFilter(object|string|null $filterId): ?FilterInterface
74+
{
75+
if (\is_object($filterId)) {
76+
return $filterId instanceof FilterInterface ? $filterId : null;
77+
}
78+
79+
if (\is_string($filterId) && $this->filterLocator->has($filterId)) {
80+
return $this->filterLocator->get($filterId);
81+
}
82+
83+
return null;
84+
}
7285
}

src/Doctrine/Orm/Extension/ParameterExtension.php

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use ApiPlatform\State\ParameterNotFound;
2121
use Doctrine\ORM\QueryBuilder;
2222
use Psr\Container\ContainerInterface;
23+
use Symfony\Bridge\Doctrine\ManagerRegistry;
2324

2425
/**
2526
* Reads operation parameters and execute its filter.
@@ -30,17 +31,21 @@ final class ParameterExtension implements QueryCollectionExtensionInterface, Que
3031
{
3132
use ParameterValueExtractorTrait;
3233

33-
public function __construct(private readonly ContainerInterface $filterLocator)
34-
{
34+
public function __construct(
35+
private readonly ContainerInterface $filterLocator,
36+
private readonly ?ManagerRegistry $managerRegistry = null,
37+
) {
3538
}
3639

3740
/**
3841
* @param array<string, mixed> $context
3942
*/
4043
private function applyFilter(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
4144
{
45+
$filter = null;
46+
4247
foreach ($operation?->getParameters() ?? [] as $parameter) {
43-
if (!($v = $parameter->getValue()) || $v instanceof ParameterNotFound) {
48+
if (($v = $parameter->getValue()) === null || $v instanceof ParameterNotFound) {
4449
continue;
4550
}
4651

@@ -49,9 +54,20 @@ private function applyFilter(QueryBuilder $queryBuilder, QueryNameGeneratorInter
4954
continue;
5055
}
5156

52-
$filter = $this->filterLocator->has($filterId) ? $this->filterLocator->get($filterId) : null;
57+
if (\is_string($filterId) && $this->filterLocator->has($filterId)) {
58+
$filter = $this->filterLocator->has($filterId) ? $this->filterLocator->get($filterId) : null;
59+
}
60+
61+
if (\is_object($filterId)) {
62+
$filter = $filterId;
63+
$filter->setManagerRegistry($this->managerRegistry);
64+
$filter->setProperties($values);
65+
}
66+
5367
if ($filter instanceof FilterInterface) {
54-
$filter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, ['filters' => $values, 'parameter' => $parameter] + $context);
68+
$filter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation,
69+
array_merge(['filters' => $values, 'parameter' => $parameter], $context)
70+
);
5571
}
5672
}
5773
}

src/Doctrine/Orm/Filter/AbstractFilter.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ abstract class AbstractFilter implements FilterInterface, PropertyAwareFilterInt
3030
use PropertyHelperTrait;
3131
protected LoggerInterface $logger;
3232

33-
public function __construct(protected ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, protected ?array $properties = null, protected ?NameConverterInterface $nameConverter = null)
33+
public function __construct(protected ?ManagerRegistry $managerRegistry = null, ?LoggerInterface $logger = null, protected ?array $properties = null, protected ?NameConverterInterface $nameConverter = null)
3434
{
3535
$this->logger = $logger ?? new NullLogger();
3636
}
@@ -53,12 +53,17 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q
5353
*/
5454
abstract protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void;
5555

56-
protected function getManagerRegistry(): ManagerRegistry
56+
protected function getManagerRegistry(): ?ManagerRegistry
5757
{
5858
return $this->managerRegistry;
5959
}
6060

61-
protected function getProperties(): ?array
61+
public function setManagerRegistry(?ManagerRegistry $managerRegistry): ?ManagerRegistry
62+
{
63+
return $this->managerRegistry = $managerRegistry;
64+
}
65+
66+
public function getProperties(): ?array
6267
{
6368
return $this->properties;
6469
}

src/Doctrine/Orm/Filter/BooleanFilter.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515

1616
use ApiPlatform\Doctrine\Common\Filter\BooleanFilterTrait;
1717
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
18+
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
19+
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
1820
use ApiPlatform\Metadata\Operation;
21+
use ApiPlatform\Metadata\Parameter;
22+
use ApiPlatform\Metadata\QueryParameter;
23+
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
1924
use Doctrine\DBAL\Types\Types;
2025
use Doctrine\ORM\Query\Expr\Join;
2126
use Doctrine\ORM\QueryBuilder;
@@ -106,7 +111,7 @@
106111
* @author Amrouche Hamza <[email protected]>
107112
* @author Teoh Han Hui <[email protected]>
108113
*/
109-
final class BooleanFilter extends AbstractFilter
114+
final class BooleanFilter extends AbstractFilter implements OpenApiParameterFilterInterface, JsonSchemaFilterInterface
110115
{
111116
use BooleanFilterTrait;
112117

@@ -145,4 +150,27 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB
145150
->andWhere(\sprintf('%s.%s = :%s', $alias, $field, $valueParameter))
146151
->setParameter($valueParameter, $value);
147152
}
153+
154+
/**
155+
* @return array<string, mixed>
156+
*/
157+
public function getSchema(Parameter $parameter): array
158+
{
159+
return ['type' => 'boolean'];
160+
}
161+
162+
public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
163+
{
164+
$in = $parameter instanceof QueryParameter ? 'query' : 'header';
165+
$key = $parameter->getKey();
166+
167+
return [
168+
new OpenApiParameter(
169+
name: $key,
170+
in: $in,
171+
required: false,
172+
schema: ['type' => 'boolean'],
173+
),
174+
];
175+
}
148176
}

src/Doctrine/Orm/PropertyHelperTrait.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
*/
3030
trait PropertyHelperTrait
3131
{
32-
abstract protected function getManagerRegistry(): ManagerRegistry;
32+
abstract protected function getManagerRegistry(): ?ManagerRegistry;
3333

3434
/**
3535
* Splits the given property into parts.
@@ -43,7 +43,7 @@ protected function getClassMetadata(string $resourceClass): ClassMetadata
4343
{
4444
$manager = $this
4545
->getManagerRegistry()
46-
->getManagerForClass($resourceClass);
46+
?->getManagerForClass($resourceClass);
4747

4848
if ($manager) {
4949
return $manager->getClassMetadata($resourceClass);

src/Hydra/Serializer/CollectionFiltersNormalizer.php

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ public function normalize(mixed $object, ?string $format = null, array $context
9797
}
9898
$currentFilters = [];
9999
foreach ($resourceFilters as $filterId) {
100-
if ($filter = $this->getFilter($filterId)) {
100+
$filter = $this->resolveFilter($filterId);
101+
if ($filter) {
101102
$currentFilters[] = $filter;
102103
}
103104
}
@@ -153,7 +154,7 @@ private function getSearch(string $resourceClass, array $parts, array $filters,
153154
continue;
154155
}
155156

156-
if (!($property = $parameter->getProperty()) && ($filterId = $parameter->getFilter()) && ($filter = $this->getFilter($filterId))) {
157+
if (!($property = $parameter->getProperty()) && ($filterId = $parameter->getFilter()) && ($filter = $this->resolveFilter($filterId))) {
157158
foreach ($filter->getDescription($resourceClass) as $variable => $description) {
158159
// This is a practice induced by PHP and is not necessary when implementing URI template
159160
if (str_ends_with((string) $variable, '[]')) {
@@ -189,12 +190,13 @@ private function getSearch(string $resourceClass, array $parts, array $filters,
189190
return ['@type' => $hydraPrefix.'IriTemplate', $hydraPrefix.'template' => \sprintf('%s{?%s}', $parts['path'], implode(',', $variables)), $hydraPrefix.'variableRepresentation' => 'BasicRepresentation', $hydraPrefix.'mapping' => $mapping];
190191
}
191192

192-
/**
193-
* Gets a filter with a backward compatibility.
194-
*/
195-
private function getFilter(string $filterId): ?FilterInterface
193+
private function resolveFilter(object|string|null $filterId): ?\ApiPlatform\Doctrine\Orm\Filter\FilterInterface
196194
{
197-
if ($this->filterLocator && $this->filterLocator->has($filterId)) {
195+
if (\is_object($filterId)) {
196+
return $filterId instanceof \ApiPlatform\Doctrine\Orm\Filter\FilterInterface ? $filterId : null;
197+
}
198+
199+
if (\is_string($filterId) && $this->filterLocator->has($filterId)) {
198200
return $this->filterLocator->get($filterId);
199201
}
200202

src/Serializer/SerializerFilterContextBuilder.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,25 @@ public function createFromRequest(Request $request, bool $normalization, ?array
5454
}
5555

5656
foreach ($resourceFilters as $filterId) {
57-
if ($this->filterLocator->has($filterId) && ($filter = $this->filterLocator->get($filterId)) instanceof FilterInterface) {
57+
$filter = $this->resolveFilter($filterId);
58+
if ($filter instanceof FilterInterface) {
5859
$filter->apply($request, $normalization, $attributes, $context);
5960
}
6061
}
6162

6263
return $context;
6364
}
65+
66+
private function resolveFilter(object|string|null $filterId): ?\ApiPlatform\Doctrine\Orm\Filter\FilterInterface
67+
{
68+
if (\is_object($filterId)) {
69+
return $filterId instanceof \ApiPlatform\Doctrine\Orm\Filter\FilterInterface ? $filterId : null;
70+
}
71+
72+
if (\is_string($filterId) && $this->filterLocator->has($filterId)) {
73+
return $this->filterLocator->get($filterId);
74+
}
75+
76+
return null;
77+
}
6478
}

src/Symfony/Bundle/Resources/config/doctrine_orm.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@
150150

151151
<service id="api_platform.doctrine.orm.extension.parameter_extension" class="ApiPlatform\Doctrine\Orm\Extension\ParameterExtension" public="false">
152152
<argument type="service" id="api_platform.filter_locator" />
153+
<argument type="service" id="doctrine" />
153154
<tag name="api_platform.doctrine.orm.query_extension.collection" priority="-16" />
154155
<tag name="api_platform.doctrine.orm.query_extension.item" priority="-9" />
155156
</service>

src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php

Lines changed: 1 addition & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,8 @@
1313

1414
namespace ApiPlatform\Validator\Metadata\Resource\Factory;
1515

16-
use ApiPlatform\Metadata\HttpOperation;
1716
use ApiPlatform\Metadata\Parameter;
1817
use ApiPlatform\Metadata\Parameters;
19-
use ApiPlatform\Metadata\QueryParameter;
2018
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2119
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
2220
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
@@ -52,15 +50,10 @@ public function create(string $resourceClass): ResourceMetadataCollection
5250

5351
foreach ($operations as $operationName => $operation) {
5452
$parameters = $operation->getParameters() ?? new Parameters();
55-
foreach ($parameters as $key => $parameter) {
53+
foreach ($operation->getParameters() ?? [] as $key => $parameter) {
5654
$parameters->add($key, $this->addSchemaValidation($parameter));
5755
}
5856

59-
// As we deprecate the parameter validator, we declare a parameter for each filter transfering validation to the new system
60-
if ($operation->getFilters() && 0 === $parameters->count()) {
61-
$parameters = $this->addFilterValidation($operation);
62-
}
63-
6457
if (\count($parameters) > 0) {
6558
$operations->add($operationName, $operation->withParameters($parameters));
6659
}
@@ -167,43 +160,4 @@ private function addSchemaValidation(Parameter $parameter, ?array $schema = null
167160

168161
return $parameter->withConstraints($assertions);
169162
}
170-
171-
private function addFilterValidation(HttpOperation $operation): Parameters
172-
{
173-
$parameters = new Parameters();
174-
$internalPriority = -1;
175-
176-
foreach ($operation->getFilters() as $filter) {
177-
if (!$this->filterLocator->has($filter)) {
178-
continue;
179-
}
180-
181-
$filter = $this->filterLocator->get($filter);
182-
foreach ($filter->getDescription($operation->getClass()) as $parameterName => $definition) {
183-
$key = $parameterName;
184-
$required = $definition['required'] ?? false;
185-
$schema = $definition['schema'] ?? null;
186-
187-
$openApi = null;
188-
if (isset($definition['openapi']) && $definition['openapi'] instanceof OpenApiParameter) {
189-
$openApi = $definition['openapi'];
190-
}
191-
192-
// The query parameter validator forced this, lets maintain BC on filters
193-
if (true === $required && !$openApi) {
194-
$openApi = new OpenApiParameter(name: $key, in: 'query', allowEmptyValue: false);
195-
}
196-
197-
$parameters->add($key, $this->addSchemaValidation(
198-
// we disable openapi and hydra on purpose as their docs comes from filters see the condition for addFilterValidation above
199-
new QueryParameter(key: $key, property: $definition['property'] ?? null, priority: $internalPriority--, schema: $schema, openApi: false, hydra: false),
200-
$schema,
201-
$required,
202-
$openApi
203-
));
204-
}
205-
}
206-
207-
return $parameters;
208-
}
209163
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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\Tests\Fixtures\TestBundle\Document;
15+
16+
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
17+
use ApiPlatform\Metadata\ApiResource;
18+
use ApiPlatform\Metadata\GetCollection;
19+
use ApiPlatform\Metadata\QueryParameter;
20+
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
21+
22+
#[ApiResource]
23+
#[GetCollection(
24+
filters: [
25+
'active' => new QueryParameter(
26+
filter: new BooleanFilter(),
27+
),
28+
'enabled' => new QueryParameter(
29+
filter: new BooleanFilter(),
30+
property: 'active',
31+
),
32+
],
33+
)]
34+
#[ODM\Document]
35+
class FilteredBooleanParameter
36+
{
37+
public function __construct(
38+
#[ODM\Id(type: 'int', strategy: 'INCREMENT')]
39+
public ?int $id = null,
40+
41+
#[ODM\Field(type: 'bool', nullable: true)]
42+
public ?bool $active = null,
43+
) {
44+
}
45+
46+
public function getId(): ?int
47+
{
48+
return $this->id;
49+
}
50+
51+
public function isActive(): bool
52+
{
53+
return $this->active;
54+
}
55+
56+
public function setActive(?bool $active): void
57+
{
58+
$this->active = $active;
59+
}
60+
}

0 commit comments

Comments
 (0)