Skip to content

Commit 06f47dd

Browse files
committed
feat(doctrine): search filters like laravel eloquent filters
1 parent 9389b4f commit 06f47dd

27 files changed

+1954
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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\Orm\Filter;
15+
16+
use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface;
17+
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
18+
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
19+
use ApiPlatform\Metadata\Operation;
20+
use ApiPlatform\Metadata\Parameter;
21+
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
22+
use Doctrine\ORM\QueryBuilder;
23+
use Doctrine\Persistence\ManagerRegistry;
24+
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
25+
26+
final class EndSearchFilter implements FilterInterface, ManagerRegistryAwareInterface, OpenApiParameterFilterInterface
27+
{
28+
use FilterInterfaceTrait;
29+
30+
public function __construct(
31+
private ?ManagerRegistry $managerRegistry = null,
32+
private readonly ?array $properties = null,
33+
private readonly ?NameConverterInterface $nameConverter = null,
34+
) {
35+
}
36+
37+
protected function filterProperty(string $property, mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
38+
{
39+
if (
40+
null === $value
41+
|| !$this->isPropertyEnabled($property, $resourceClass)
42+
|| !$this->isPropertyMapped($property, $resourceClass, true)
43+
) {
44+
return;
45+
}
46+
47+
$alias = $queryBuilder->getRootAliases()[0];
48+
$parameterName = $queryNameGenerator->generateParameterName($property);
49+
50+
$queryBuilder
51+
->andWhere(\sprintf('%s.%s LIKE :%s', $alias, $property, $parameterName))
52+
->setParameter($parameterName, '%'.$value);
53+
}
54+
55+
public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
56+
{
57+
return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true);
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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\Orm\Filter;
15+
16+
use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface;
17+
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
18+
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
19+
use ApiPlatform\Metadata\Operation;
20+
use ApiPlatform\Metadata\Parameter;
21+
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
22+
use Doctrine\ORM\QueryBuilder;
23+
use Doctrine\Persistence\ManagerRegistry;
24+
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
25+
26+
final class ExactSearchFilter implements FilterInterface, ManagerRegistryAwareInterface, OpenApiParameterFilterInterface
27+
{
28+
use FilterInterfaceTrait;
29+
30+
public function __construct(
31+
private ?ManagerRegistry $managerRegistry = null,
32+
private readonly ?array $properties = null,
33+
private readonly ?NameConverterInterface $nameConverter = null,
34+
) {
35+
}
36+
37+
protected function filterProperty(string $property, mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
38+
{
39+
if (
40+
null === $value
41+
|| !$this->isPropertyEnabled($property, $resourceClass)
42+
|| !$this->isPropertyMapped($property, $resourceClass, true)
43+
) {
44+
return;
45+
}
46+
47+
$alias = $queryBuilder->getRootAliases()[0];
48+
$parameterName = $queryNameGenerator->generateParameterName($property);
49+
50+
$queryBuilder
51+
->andWhere(\sprintf('%s.%s = :%s', $alias, $property, $parameterName))
52+
->setParameter($parameterName, $value);
53+
}
54+
55+
public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
56+
{
57+
return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true);
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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\Orm\Filter;
15+
16+
use ApiPlatform\Doctrine\Common\PropertyHelperTrait;
17+
use ApiPlatform\Doctrine\Orm\PropertyHelperTrait as OrmPropertyHelperTrait;
18+
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
19+
use ApiPlatform\Metadata\Exception\RuntimeException;
20+
use ApiPlatform\Metadata\Operation;
21+
use Doctrine\ORM\QueryBuilder;
22+
use Doctrine\Persistence\ManagerRegistry;
23+
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
24+
25+
trait FilterInterfaceTrait
26+
{
27+
use OrmPropertyHelperTrait;
28+
use PropertyHelperTrait;
29+
30+
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
31+
{
32+
foreach ($context['filters'] as $property => $value) {
33+
$this->filterProperty($this->denormalizePropertyName($property), $value, $queryBuilder, $queryNameGenerator, $resourceClass, $operation, $context);
34+
}
35+
}
36+
37+
public function getDescription(string $resourceClass): array
38+
{
39+
throw new RuntimeException('Not implemented.');
40+
}
41+
42+
/**
43+
* Determines whether the given property is enabled.
44+
*/
45+
protected function isPropertyEnabled(string $property, string $resourceClass): bool
46+
{
47+
if (null === $this->properties) {
48+
// to ensure sanity, nested properties must still be explicitly enabled
49+
return !$this->isPropertyNested($property, $resourceClass);
50+
}
51+
52+
return \array_key_exists($property, $this->properties);
53+
}
54+
55+
protected function denormalizePropertyName(string|int $property): string
56+
{
57+
if (!$this->nameConverter instanceof NameConverterInterface) {
58+
return (string) $property;
59+
}
60+
61+
return implode('.', array_map($this->nameConverter->denormalize(...), explode('.', (string) $property)));
62+
}
63+
64+
public function hasManagerRegistry(): bool
65+
{
66+
return $this->managerRegistry instanceof ManagerRegistry;
67+
}
68+
69+
public function getManagerRegistry(): ManagerRegistry
70+
{
71+
return $this->managerRegistry;
72+
}
73+
74+
public function setManagerRegistry(ManagerRegistry $managerRegistry): void
75+
{
76+
$this->managerRegistry = $managerRegistry;
77+
}
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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\Orm\Filter;
15+
16+
use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface;
17+
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
18+
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
19+
use ApiPlatform\Metadata\Operation;
20+
use ApiPlatform\Metadata\Parameter;
21+
use ApiPlatform\Metadata\ParameterProviderFilterInterface;
22+
use ApiPlatform\Metadata\PropertiesAwareInterface;
23+
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
24+
use ApiPlatform\State\Provider\IriConverterParameterProvider;
25+
use Doctrine\DBAL\Types\Types;
26+
use Doctrine\ORM\QueryBuilder;
27+
use Doctrine\Persistence\ManagerRegistry;
28+
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
29+
30+
final class IriSearchFilter implements FilterInterface, ManagerRegistryAwareInterface, OpenApiParameterFilterInterface, PropertiesAwareInterface, ParameterProviderFilterInterface
31+
{
32+
use FilterInterfaceTrait;
33+
34+
public function __construct(
35+
private ?ManagerRegistry $managerRegistry = null,
36+
private readonly ?array $properties = null,
37+
private readonly ?NameConverterInterface $nameConverter = null,
38+
) {
39+
}
40+
41+
protected function filterProperty(string $property, mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
42+
{
43+
if (
44+
null === $value
45+
|| !$this->isPropertyEnabled($property, $resourceClass)
46+
|| !$this->isPropertyMapped($property, $resourceClass, true)
47+
) {
48+
return;
49+
}
50+
51+
$value = $context['parameter']->getValue();
52+
53+
$alias = $queryBuilder->getRootAliases()[0];
54+
$parameterName = $queryNameGenerator->generateParameterName($property);
55+
56+
$queryBuilder
57+
->andWhere(\sprintf('%s.%s = :%s', $alias, $property, $parameterName))
58+
->setParameter($parameterName, $value);
59+
}
60+
61+
/**
62+
* {@inheritdoc}
63+
*/
64+
public function getType(string $doctrineType): string
65+
{
66+
// TODO: remove this test when doctrine/dbal:3 support is removed
67+
if (\defined(Types::class.'::ARRAY') && Types::ARRAY === $doctrineType) {
68+
return 'array';
69+
}
70+
71+
return match ($doctrineType) {
72+
Types::BIGINT, Types::INTEGER, Types::SMALLINT => 'int',
73+
Types::BOOLEAN => 'bool',
74+
Types::DATE_MUTABLE, Types::TIME_MUTABLE, Types::DATETIME_MUTABLE, Types::DATETIMETZ_MUTABLE, Types::DATE_IMMUTABLE, Types::TIME_IMMUTABLE, Types::DATETIME_IMMUTABLE, Types::DATETIMETZ_IMMUTABLE => \DateTimeInterface::class,
75+
Types::FLOAT => 'float',
76+
default => 'string',
77+
};
78+
}
79+
80+
public static function getParameterProvider(): string
81+
{
82+
return IriConverterParameterProvider::class;
83+
}
84+
85+
public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
86+
{
87+
return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true);
88+
}
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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\Orm\Filter;
15+
16+
use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface;
17+
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
18+
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
19+
use ApiPlatform\Metadata\Operation;
20+
use ApiPlatform\Metadata\Parameter;
21+
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
22+
use Doctrine\ORM\QueryBuilder;
23+
use Doctrine\Persistence\ManagerRegistry;
24+
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
25+
26+
final class PartialSearchFilter implements FilterInterface, ManagerRegistryAwareInterface, OpenApiParameterFilterInterface
27+
{
28+
use FilterInterfaceTrait;
29+
30+
public function __construct(
31+
private ?ManagerRegistry $managerRegistry = null,
32+
private readonly ?array $properties = null,
33+
private readonly ?NameConverterInterface $nameConverter = null,
34+
) {
35+
}
36+
37+
protected function filterProperty(string $property, mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
38+
{
39+
if (
40+
null === $value
41+
|| !$this->isPropertyEnabled($property, $resourceClass)
42+
|| !$this->isPropertyMapped($property, $resourceClass, true)
43+
) {
44+
return;
45+
}
46+
47+
$alias = $queryBuilder->getRootAliases()[0];
48+
$parameterName = $queryNameGenerator->generateParameterName($property);
49+
50+
$queryBuilder
51+
->andWhere(\sprintf('%s.%s LIKE :%s', $alias, $property, $parameterName))
52+
->setParameter($parameterName, '%'.$value.'%');
53+
}
54+
55+
public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
56+
{
57+
return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true);
58+
}
59+
}

0 commit comments

Comments
 (0)