Skip to content

Commit a9d3bd6

Browse files
committed
feat(doctrine): search filters like laravel eloquent filters
1 parent f67f6f1 commit a9d3bd6

24 files changed

+1544
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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 IriSearchFilterTrait
20+
{
21+
public function getDescription(string $resourceClass): array
22+
{
23+
$description = [];
24+
25+
$properties = $this->getProperties();
26+
if (null === $properties) {
27+
$metadata = $this->getClassMetadata($resourceClass);
28+
$fieldNames = array_fill_keys($metadata->getFieldNames(), null);
29+
$associationNames = array_fill_keys($metadata->getAssociationNames(), null);
30+
31+
$properties = array_merge($fieldNames, $associationNames);
32+
}
33+
34+
foreach ($properties as $property => $strategy) {
35+
if (!$this->isPropertyMapped($property, $resourceClass, true)) {
36+
continue;
37+
}
38+
39+
if ($this->isPropertyNested($property, $resourceClass)) {
40+
$propertyParts = $this->splitPropertyParts($property, $resourceClass);
41+
$field = $propertyParts['field'];
42+
$metadata = $this->getNestedMetadata($resourceClass, $propertyParts['associations']);
43+
} else {
44+
$field = $property;
45+
$metadata = $this->getClassMetadata($resourceClass);
46+
}
47+
48+
$propertyName = $this->normalizePropertyName($property);
49+
if ($metadata->hasField($field)) {
50+
$typeOfField = $this->getType($metadata->getTypeOfField($field));
51+
$filterParameterNames = [$propertyName];
52+
53+
foreach ($filterParameterNames as $filterParameterName) {
54+
$description[$filterParameterName] = [
55+
'property' => $propertyName,
56+
'type' => $typeOfField,
57+
'required' => false,
58+
'strategy' => $strategy,
59+
'is_collection' => str_ends_with((string) $filterParameterName, '[]'),
60+
];
61+
}
62+
} elseif ($metadata->hasAssociation($field)) {
63+
$filterParameterNames = [
64+
$propertyName,
65+
$propertyName.'[]',
66+
];
67+
68+
foreach ($filterParameterNames as $filterParameterName) {
69+
$description[$filterParameterName] = [
70+
'property' => $propertyName,
71+
'type' => 'string',
72+
'required' => false,
73+
'strategy' => 'exact',
74+
'is_collection' => str_ends_with((string) $filterParameterName, '[]'),
75+
];
76+
}
77+
}
78+
}
79+
80+
return $description;
81+
}
82+
83+
public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
84+
{
85+
return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true);
86+
}
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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\IriSearchFilterTrait;
17+
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
18+
use ApiPlatform\Metadata\Operation;
19+
use ApiPlatform\Metadata\ParameterProviderFilterInterface;
20+
use ApiPlatform\Metadata\PropertiesFilterInterface;
21+
use ApiPlatform\State\Provider\IriConverterParameterProvider;
22+
use Doctrine\DBAL\Types\Types;
23+
use Doctrine\ODM\MongoDB\Aggregation\Builder;
24+
use Doctrine\ODM\MongoDB\Mapping\MappingException;
25+
use Doctrine\Persistence\ManagerRegistry;
26+
use Psr\Log\LoggerInterface;
27+
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
28+
29+
class ExactSearchFilter extends AbstractFilter implements OpenApiParameterFilterInterface, PropertiesFilterInterface, ParameterProviderFilterInterface
30+
{
31+
use IriSearchFilterTrait;
32+
33+
public function __construct(
34+
?ManagerRegistry $managerRegistry = null,
35+
?LoggerInterface $logger = null,
36+
?array $properties = null,
37+
?NameConverterInterface $nameConverter = null,
38+
) {
39+
parent::__construct($managerRegistry, $logger, $properties, $nameConverter);
40+
}
41+
42+
/**
43+
* @throws MappingException
44+
*/
45+
public function filterProperty(string $property, $value, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void
46+
{
47+
if (
48+
null === $value
49+
|| !$this->isPropertyEnabled($property, $resourceClass)
50+
|| !$this->isPropertyMapped($property, $resourceClass, true)
51+
) {
52+
return;
53+
}
54+
55+
$extraProperties = $operation?->getExtraProperties();
56+
$resource = $extraProperties['_value'] ?? null;
57+
if (!$resource) {
58+
$this->logger->warning(\sprintf('No resource found for property "%s".', $property));
59+
60+
return;
61+
}
62+
63+
$aggregationBuilder
64+
->match()
65+
->field($property)
66+
->equals($resource->getId());
67+
}
68+
69+
/**
70+
* {@inheritdoc}
71+
*/
72+
public function getType(string $doctrineType): string
73+
{
74+
// TODO: remove this test when doctrine/dbal:3 support is removed
75+
if (\defined(Types::class.'::ARRAY') && Types::ARRAY === $doctrineType) {
76+
return 'array';
77+
}
78+
79+
return match ($doctrineType) {
80+
Types::BIGINT, Types::INTEGER, Types::SMALLINT => 'int',
81+
Types::BOOLEAN => 'bool',
82+
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,
83+
Types::FLOAT => 'float',
84+
default => 'string',
85+
};
86+
}
87+
88+
public static function getParameterProvider(): string
89+
{
90+
return IriConverterParameterProvider::class;
91+
}
92+
}
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\Odm\Filter;
15+
16+
use ApiPlatform\Doctrine\Common\Filter\IriSearchFilterTrait;
17+
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
18+
use ApiPlatform\Metadata\Operation;
19+
use ApiPlatform\Metadata\ParameterProviderFilterInterface;
20+
use ApiPlatform\Metadata\PropertiesFilterInterface;
21+
use ApiPlatform\State\Provider\IriConverterParameterProvider;
22+
use Doctrine\ODM\MongoDB\Aggregation\Builder;
23+
use Doctrine\ODM\MongoDB\Mapping\MappingException;
24+
use Doctrine\ODM\MongoDB\Types\Type as MongoDbType;
25+
use Doctrine\Persistence\ManagerRegistry;
26+
use Psr\Log\LoggerInterface;
27+
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
28+
29+
class IriSearchFilter extends AbstractFilter implements OpenApiParameterFilterInterface, PropertiesFilterInterface, ParameterProviderFilterInterface
30+
{
31+
use IriSearchFilterTrait;
32+
33+
public function __construct(
34+
?ManagerRegistry $managerRegistry = null,
35+
?LoggerInterface $logger = null,
36+
?array $properties = null,
37+
?NameConverterInterface $nameConverter = null,
38+
) {
39+
parent::__construct($managerRegistry, $logger, $properties, $nameConverter);
40+
}
41+
42+
/**
43+
* @throws MappingException
44+
*/
45+
public function filterProperty(string $property, $value, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void
46+
{
47+
if (
48+
null === $value
49+
|| !$this->isPropertyEnabled($property, $resourceClass)
50+
|| !$this->isPropertyMapped($property, $resourceClass, true)
51+
) {
52+
return;
53+
}
54+
55+
$extraProperties = $operation?->getExtraProperties();
56+
$resource = $extraProperties['_value'] ?? null;
57+
58+
if (!$resource) {
59+
$this->logger->warning(\sprintf('No resource found for property "%s".', $property));
60+
61+
return;
62+
}
63+
64+
$aggregationBuilder
65+
->match()
66+
->field($property)
67+
->equals($resource->getId());
68+
}
69+
70+
/**
71+
* {@inheritdoc}
72+
*/
73+
protected function getType(string $doctrineType): string
74+
{
75+
// TODO: remove constantes deprecations when doctrine/dbal:3 support is removed
76+
return match ($doctrineType) {
77+
MongoDbType::INT, MongoDbType::INTEGER => 'int',
78+
MongoDbType::BOOL, MongoDbType::BOOLEAN => 'bool',
79+
MongoDbType::DATE, MongoDbType::DATE_IMMUTABLE => \DateTimeInterface::class,
80+
MongoDbType::FLOAT => 'float',
81+
default => 'string',
82+
};
83+
}
84+
85+
public static function getParameterProvider(): string
86+
{
87+
return IriConverterParameterProvider::class;
88+
}
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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\IriSearchFilterTrait;
17+
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
18+
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
19+
use ApiPlatform\Metadata\Operation;
20+
use ApiPlatform\Metadata\ParameterProviderFilterInterface;
21+
use ApiPlatform\Metadata\PropertiesFilterInterface;
22+
use ApiPlatform\State\Provider\IriConverterParameterProvider;
23+
use Doctrine\DBAL\Types\Types;
24+
use Doctrine\ORM\QueryBuilder;
25+
use Doctrine\Persistence\ManagerRegistry;
26+
use Psr\Log\LoggerInterface;
27+
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
28+
29+
class ExactSearchFilter extends AbstractFilter implements OpenApiParameterFilterInterface, PropertiesFilterInterface, ParameterProviderFilterInterface
30+
{
31+
use IriSearchFilterTrait;
32+
33+
public function __construct(
34+
?ManagerRegistry $managerRegistry = null,
35+
?LoggerInterface $logger = null,
36+
?array $properties = null,
37+
?NameConverterInterface $nameConverter = null,
38+
) {
39+
parent::__construct($managerRegistry, $logger, $properties, $nameConverter);
40+
}
41+
42+
protected function filterProperty(string $property, mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
43+
{
44+
if (
45+
null === $value
46+
|| !$this->isPropertyEnabled($property, $resourceClass)
47+
|| !$this->isPropertyMapped($property, $resourceClass, true)
48+
) {
49+
return;
50+
}
51+
52+
$extraProperties = $operation?->getExtraProperties();
53+
$resource = $extraProperties['_value'] ?? null;
54+
if (!$resource) {
55+
$this->logger->warning(\sprintf('No resource found for property "%s".', $property));
56+
57+
return;
58+
}
59+
60+
$alias = $queryBuilder->getRootAliases()[0];
61+
$parameterName = $queryNameGenerator->generateParameterName($property);
62+
63+
$queryBuilder
64+
->andWhere(\sprintf('%s.%s = :%s', $alias, $property, $parameterName))
65+
->setParameter($parameterName, $resource->getId());
66+
}
67+
68+
/**
69+
* {@inheritdoc}
70+
*/
71+
public function getType(string $doctrineType): string
72+
{
73+
// TODO: remove this test when doctrine/dbal:3 support is removed
74+
if (\defined(Types::class.'::ARRAY') && Types::ARRAY === $doctrineType) {
75+
return 'array';
76+
}
77+
78+
return match ($doctrineType) {
79+
Types::BIGINT, Types::INTEGER, Types::SMALLINT => 'int',
80+
Types::BOOLEAN => 'bool',
81+
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,
82+
Types::FLOAT => 'float',
83+
default => 'string',
84+
};
85+
}
86+
87+
public static function getParameterProvider(): string
88+
{
89+
return IriConverterParameterProvider::class;
90+
}
91+
}

0 commit comments

Comments
 (0)