-
-
Notifications
You must be signed in to change notification settings - Fork 889
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(doctrine): search filters like laravel eloquent filters
- Loading branch information
1 parent
716a43b
commit e3b56c8
Showing
19 changed files
with
1,237 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
<?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\ExactSearchFilterTrait; | ||
use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface; | ||
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; | ||
use ApiPlatform\Metadata\OpenApiParameterFilterInterface; | ||
use ApiPlatform\Metadata\Operation; | ||
use Doctrine\ORM\QueryBuilder; | ||
use Doctrine\Persistence\ManagerRegistry; | ||
use Symfony\Component\Serializer\NameConverter\NameConverterInterface; | ||
|
||
final class ExactSearchFilter implements FilterInterface, ManagerRegistryAwareInterface, OpenApiParameterFilterInterface | ||
{ | ||
use ExactSearchFilterTrait; | ||
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 = :%s', $alias, $property, $parameterName)) | ||
->setParameter($parameterName, $value); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
{ | ||
// 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', | ||
}; | ||
} | ||
|
||
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
<?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\State\Provider; | ||
|
||
use ApiPlatform\Metadata\Exception\InvalidArgumentException; | ||
use ApiPlatform\Metadata\IdentifiersExtractor; | ||
use ApiPlatform\Metadata\IriConverterInterface; | ||
use ApiPlatform\Metadata\Operation; | ||
use ApiPlatform\Metadata\Parameter; | ||
use ApiPlatform\State\ParameterProviderInterface; | ||
use Symfony\Component\PropertyAccess\PropertyAccessorInterface; | ||
|
||
final readonly class IriConverterParameterProvider implements ParameterProviderInterface | ||
{ | ||
public function __construct( | ||
private IriConverterInterface $iriConverter, | ||
private PropertyAccessorInterface $propertyAccessor, | ||
private ?IdentifiersExtractor $identifiersExtractor = null, | ||
) { | ||
} | ||
|
||
public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?Operation | ||
{ | ||
$operation = $context['operation'] ?? null; | ||
$value = $parameter->getValue(); | ||
if (!$value) { | ||
return $operation; | ||
} | ||
|
||
$id = $this->getIdFromValue($value); | ||
$parameter->setValue($id); | ||
|
||
return $operation; | ||
} | ||
|
||
protected function getIdFromValue(string $value): mixed | ||
{ | ||
try { | ||
$item = $this->iriConverter->getResourceFromIri($value, ['fetch_data' => false]); | ||
|
||
if (null === $this->identifiersExtractor) { | ||
return $this->propertyAccessor->getValue($item, 'id'); | ||
} | ||
|
||
$identifiers = $this->identifiersExtractor->getIdentifiersFromItem($item); | ||
|
||
return 1 === \count($identifiers) ? array_pop($identifiers) : $identifiers; | ||
} catch (InvalidArgumentException) { | ||
// Do nothing, return the raw value | ||
} | ||
|
||
return $value; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.