-
-
Notifications
You must be signed in to change notification settings - Fork 893
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(doctrine): search filters like laravel eloquent filters #6865
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the API Platform project. | ||
* | ||
* (c) Kévin Dunglas <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace ApiPlatform\Doctrine\Orm\Filter; | ||
|
||
use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface; | ||
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; | ||
use ApiPlatform\Metadata\OpenApiParameterFilterInterface; | ||
use ApiPlatform\Metadata\Operation; | ||
use ApiPlatform\Metadata\Parameter; | ||
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; | ||
use Doctrine\ORM\QueryBuilder; | ||
use Doctrine\Persistence\ManagerRegistry; | ||
use Symfony\Component\Serializer\NameConverter\NameConverterInterface; | ||
|
||
final class EndSearchFilter implements FilterInterface, ManagerRegistryAwareInterface, OpenApiParameterFilterInterface | ||
{ | ||
use FilterInterfaceTrait; | ||
|
||
public function __construct( | ||
private ?ManagerRegistry $managerRegistry = null, | ||
private readonly ?array $properties = null, | ||
private readonly ?NameConverterInterface $nameConverter = null, | ||
) { | ||
} | ||
|
||
protected function filterProperty(string $property, mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void | ||
{ | ||
if ( | ||
null === $value | ||
|| !$this->isPropertyEnabled($property, $resourceClass) | ||
|| !$this->isPropertyMapped($property, $resourceClass, true) | ||
) { | ||
return; | ||
} | ||
|
||
$alias = $queryBuilder->getRootAliases()[0]; | ||
$parameterName = $queryNameGenerator->generateParameterName($property); | ||
|
||
$queryBuilder | ||
->andWhere(\sprintf('%s.%s LIKE :%s', $alias, $property, $parameterName)) | ||
->setParameter($parameterName, '%'.$value); | ||
} | ||
|
||
public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null | ||
{ | ||
return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the API Platform project. | ||
* | ||
* (c) Kévin Dunglas <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace ApiPlatform\Doctrine\Orm\Filter; | ||
|
||
use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface; | ||
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; | ||
use ApiPlatform\Metadata\OpenApiParameterFilterInterface; | ||
use ApiPlatform\Metadata\Operation; | ||
use ApiPlatform\Metadata\Parameter; | ||
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; | ||
use Doctrine\ORM\QueryBuilder; | ||
use Doctrine\Persistence\ManagerRegistry; | ||
use Symfony\Component\Serializer\NameConverter\NameConverterInterface; | ||
|
||
final class ExactSearchFilter implements FilterInterface, ManagerRegistryAwareInterface, OpenApiParameterFilterInterface | ||
{ | ||
use FilterInterfaceTrait; | ||
|
||
public function __construct( | ||
private ?ManagerRegistry $managerRegistry = null, | ||
private readonly ?array $properties = null, | ||
private readonly ?NameConverterInterface $nameConverter = null, | ||
) { | ||
} | ||
|
||
protected function filterProperty(string $property, mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void | ||
{ | ||
if ( | ||
null === $value | ||
|| !$this->isPropertyEnabled($property, $resourceClass) | ||
|| !$this->isPropertyMapped($property, $resourceClass, true) | ||
) { | ||
return; | ||
} | ||
Comment on lines
+39
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO: try to remove |
||
|
||
$alias = $queryBuilder->getRootAliases()[0]; | ||
$parameterName = $queryNameGenerator->generateParameterName($property); | ||
|
||
$queryBuilder | ||
->andWhere(\sprintf('%s.%s = :%s', $alias, $property, $parameterName)) | ||
->setParameter($parameterName, $value); | ||
} | ||
|
||
public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null | ||
{ | ||
return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the API Platform project. | ||
* | ||
* (c) Kévin Dunglas <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace ApiPlatform\Doctrine\Orm\Filter; | ||
|
||
use ApiPlatform\Doctrine\Common\PropertyHelperTrait; | ||
use ApiPlatform\Doctrine\Orm\PropertyHelperTrait as OrmPropertyHelperTrait; | ||
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; | ||
use ApiPlatform\Metadata\Exception\RuntimeException; | ||
use ApiPlatform\Metadata\Operation; | ||
use Doctrine\ORM\QueryBuilder; | ||
use Doctrine\Persistence\ManagerRegistry; | ||
use Symfony\Component\Serializer\NameConverter\NameConverterInterface; | ||
|
||
trait FilterInterfaceTrait | ||
{ | ||
use OrmPropertyHelperTrait; | ||
use PropertyHelperTrait; | ||
|
||
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void | ||
{ | ||
foreach ($context['filters'] as $property => $value) { | ||
$this->filterProperty($this->denormalizePropertyName($property), $value, $queryBuilder, $queryNameGenerator, $resourceClass, $operation, $context); | ||
} | ||
} | ||
|
||
public function getDescription(string $resourceClass): array | ||
{ | ||
throw new RuntimeException('Not implemented.'); | ||
} | ||
|
||
/** | ||
* Determines whether the given property is enabled. | ||
*/ | ||
protected function isPropertyEnabled(string $property, string $resourceClass): bool | ||
{ | ||
if (null === $this->properties) { | ||
// to ensure sanity, nested properties must still be explicitly enabled | ||
return !$this->isPropertyNested($property, $resourceClass); | ||
} | ||
|
||
return \array_key_exists($property, $this->properties); | ||
} | ||
|
||
protected function denormalizePropertyName(string|int $property): string | ||
{ | ||
if (!$this->nameConverter instanceof NameConverterInterface) { | ||
return (string) $property; | ||
} | ||
|
||
return implode('.', array_map($this->nameConverter->denormalize(...), explode('.', (string) $property))); | ||
} | ||
|
||
public function hasManagerRegistry(): bool | ||
{ | ||
return $this->managerRegistry instanceof ManagerRegistry; | ||
} | ||
|
||
public function getManagerRegistry(): ManagerRegistry | ||
{ | ||
return $this->managerRegistry; | ||
} | ||
|
||
public function setManagerRegistry(ManagerRegistry $managerRegistry): void | ||
{ | ||
$this->managerRegistry = $managerRegistry; | ||
Comment on lines
+32
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO: try to remove |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the API Platform project. | ||
* | ||
* (c) Kévin Dunglas <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace ApiPlatform\Doctrine\Orm\Filter; | ||
|
||
use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface; | ||
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; | ||
use ApiPlatform\Metadata\OpenApiParameterFilterInterface; | ||
use ApiPlatform\Metadata\Operation; | ||
use ApiPlatform\Metadata\Parameter; | ||
use ApiPlatform\Metadata\ParameterProviderFilterInterface; | ||
use ApiPlatform\Metadata\PropertiesAwareInterface; | ||
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; | ||
use ApiPlatform\State\Provider\IriConverterParameterProvider; | ||
use Doctrine\DBAL\Types\Types; | ||
use Doctrine\ORM\QueryBuilder; | ||
use Doctrine\Persistence\ManagerRegistry; | ||
use Symfony\Component\Serializer\NameConverter\NameConverterInterface; | ||
|
||
final class IriSearchFilter implements FilterInterface, ManagerRegistryAwareInterface, OpenApiParameterFilterInterface, PropertiesAwareInterface, ParameterProviderFilterInterface | ||
{ | ||
use FilterInterfaceTrait; | ||
|
||
public function __construct( | ||
private ?ManagerRegistry $managerRegistry = null, | ||
private readonly ?array $properties = null, | ||
private readonly ?NameConverterInterface $nameConverter = null, | ||
) { | ||
} | ||
|
||
protected function filterProperty(string $property, mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void | ||
{ | ||
if ( | ||
null === $value | ||
|| !$this->isPropertyEnabled($property, $resourceClass) | ||
|| !$this->isPropertyMapped($property, $resourceClass, true) | ||
) { | ||
return; | ||
} | ||
|
||
$value = $context['parameter']->getValue(); | ||
|
||
$alias = $queryBuilder->getRootAliases()[0]; | ||
$parameterName = $queryNameGenerator->generateParameterName($property); | ||
|
||
$queryBuilder | ||
->andWhere(\sprintf('%s.%s = :%s', $alias, $property, $parameterName)) | ||
->setParameter($parameterName, $value); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function getType(string $doctrineType): string | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TO DO: remove |
||
{ | ||
// TODO: remove this test when doctrine/dbal:3 support is removed | ||
if (\defined(Types::class.'::ARRAY') && Types::ARRAY === $doctrineType) { | ||
return 'array'; | ||
} | ||
|
||
return match ($doctrineType) { | ||
Types::BIGINT, Types::INTEGER, Types::SMALLINT => 'int', | ||
Types::BOOLEAN => 'bool', | ||
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, | ||
Types::FLOAT => 'float', | ||
default => 'string', | ||
vinceAmstoutz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}; | ||
} | ||
|
||
public static function getParameterProvider(): string | ||
{ | ||
return IriConverterParameterProvider::class; | ||
} | ||
|
||
public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null | ||
{ | ||
return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the API Platform project. | ||
* | ||
* (c) Kévin Dunglas <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace ApiPlatform\Doctrine\Orm\Filter; | ||
|
||
use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface; | ||
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; | ||
use ApiPlatform\Metadata\OpenApiParameterFilterInterface; | ||
use ApiPlatform\Metadata\Operation; | ||
use ApiPlatform\Metadata\Parameter; | ||
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; | ||
use Doctrine\ORM\QueryBuilder; | ||
use Doctrine\Persistence\ManagerRegistry; | ||
use Symfony\Component\Serializer\NameConverter\NameConverterInterface; | ||
|
||
final class PartialSearchFilter implements FilterInterface, ManagerRegistryAwareInterface, OpenApiParameterFilterInterface | ||
{ | ||
use FilterInterfaceTrait; | ||
|
||
public function __construct( | ||
private ?ManagerRegistry $managerRegistry = null, | ||
private readonly ?array $properties = null, | ||
private readonly ?NameConverterInterface $nameConverter = null, | ||
) { | ||
} | ||
|
||
protected function filterProperty(string $property, mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void | ||
{ | ||
if ( | ||
null === $value | ||
|| !$this->isPropertyEnabled($property, $resourceClass) | ||
|| !$this->isPropertyMapped($property, $resourceClass, true) | ||
) { | ||
return; | ||
} | ||
|
||
$alias = $queryBuilder->getRootAliases()[0]; | ||
$parameterName = $queryNameGenerator->generateParameterName($property); | ||
|
||
$queryBuilder | ||
->andWhere(\sprintf('%s.%s LIKE :%s', $alias, $property, $parameterName)) | ||
->setParameter($parameterName, '%'.$value.'%'); | ||
} | ||
|
||
public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null | ||
{ | ||
return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should hardcode parameter value style in the filter, I would rather be able to define the default strategy to use in the configuration and fallback to the default
should be suffisant for most of the cases.
Any thoughts @soyuka ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you define your own OpenApiParameter this will not get called AFAIK
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it's the final step (before supporting Odm) @soyuka @aegypius