-
-
Save Tersoal/d45b0cc75cadf72cd7c0e49b892809b3 to your computer and use it in GitHub Desktop.
| <?php | |
| namespace App\Filter; | |
| use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter; | |
| use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; | |
| use ApiPlatform\Core\Exception\InvalidArgumentException; | |
| use Doctrine\ORM\QueryBuilder; | |
| /** | |
| * inspired from : | |
| * - https://gist.github.com/masseelch/47931f3a745409f8f44c69efa9ecb05c | |
| * - https://gist.github.com/renta/b6ece3fec7896440fe52a9ec0e76571a | |
| * - https://gist.github.com/masacc/94df641b3cb9814cbdaeb3f158d2e1f7 | |
| * | |
| * how to use : | |
| * - add classAnnotation : | |
| * ApiFilter(FullTextSearchFilter::class, properties={ | |
| * "search_example1"={ | |
| * "property1": "partial", | |
| * "property2": "exact" | |
| * }, | |
| * "search_example2"={ | |
| * "property1": "partial", | |
| * "property3": "partial" | |
| * } | |
| * }) | |
| * - use filter in query string as: | |
| * + `/api/myresources?search_example1=String%20with%20spaces` => this will search "String with spaces" | |
| * + `/api/myresources?search_example1%5B%5D=String%20with%20spaces` => this will search "String with spaces" | |
| * + `/api/myresources?search_example1%5B%5D=String&search_example1%5B%5D=with&search_example1%5B%5D=spaces` => this will search "String" or "with" or "spaces" | |
| */ | |
| class FullTextSearchFilter extends SearchFilter | |
| { | |
| private const PROPERTY_NAME_PREFIX = 'search_'; | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null) | |
| { | |
| if (0 !== strpos($property, self::PROPERTY_NAME_PREFIX)) { | |
| return; | |
| } | |
| if (false === isset($this->properties[$property])) { | |
| return; | |
| } | |
| $values = $this->normalizeValues((array) $value, $property); | |
| if (null === $values) { | |
| return; | |
| } | |
| $orExpressions = []; | |
| foreach ($values as $index => $value) { | |
| foreach ($this->properties[$property] as $propertyName => $strategy) { | |
| $strategy = $strategy ?? self::STRATEGY_EXACT; | |
| $alias = $queryBuilder->getRootAliases()[0]; | |
| $field = $propertyName; | |
| $associations = []; | |
| if ($this->isPropertyNested($propertyName, $resourceClass)) { | |
| [$alias, $field, $associations] = $this->addJoinsForNestedProperty($propertyName, $alias, $queryBuilder, $queryNameGenerator, $resourceClass); | |
| } | |
| $caseSensitive = true; | |
| $metadata = $this->getNestedMetadata($resourceClass, $associations); | |
| if ($metadata->hasField($field)) { | |
| if ('id' === $field) { | |
| $value = $this->getIdFromValue($value); | |
| } | |
| if (!$this->hasValidValues((array)$value, $this->getDoctrineFieldType($propertyName, $resourceClass))) { | |
| $this->logger->notice('Invalid filter ignored', [ | |
| 'exception' => new InvalidArgumentException(sprintf('Values for field "%s" are not valid according to the doctrine type.', $field)), | |
| ]); | |
| continue; | |
| } | |
| // prefixing the strategy with i makes it case insensitive | |
| if (0 === strpos($strategy, 'i')) { | |
| $strategy = substr($strategy, 1); | |
| $caseSensitive = false; | |
| } | |
| $orExpressions[] = $this->addWhereByStrategy($strategy, $queryBuilder, $queryNameGenerator, $alias, $field, $value, $caseSensitive); | |
| } | |
| } | |
| } | |
| $queryBuilder->andWhere($queryBuilder->expr()->orX(...$orExpressions)); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| protected function addWhereByStrategy(string $strategy, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $field, $value, bool $caseSensitive) | |
| { | |
| $wrapCase = $this->createWrapCase($caseSensitive); | |
| $valueParameter = $queryNameGenerator->generateParameterName($field); | |
| $exprBuilder = $queryBuilder->expr(); | |
| $queryBuilder->setParameter($valueParameter, $value); | |
| switch ($strategy) { | |
| case null: | |
| case self::STRATEGY_EXACT: | |
| return $exprBuilder->eq($wrapCase("$alias.$field"), $wrapCase(":$valueParameter")); | |
| case self::STRATEGY_PARTIAL: | |
| return $exprBuilder->like($wrapCase("$alias.$field"), $exprBuilder->concat("'%'", $wrapCase(":$valueParameter"), "'%'")); | |
| case self::STRATEGY_START: | |
| return $exprBuilder->like($wrapCase("$alias.$field"), $exprBuilder->concat($wrapCase(":$valueParameter"), "'%'")); | |
| case self::STRATEGY_END: | |
| return $exprBuilder->like($wrapCase("$alias.$field"), $exprBuilder->concat("'%'", $wrapCase(":$valueParameter"))); | |
| case self::STRATEGY_WORD_START: | |
| return $exprBuilder->orX( | |
| $exprBuilder->like($wrapCase("$alias.$field"), $exprBuilder->concat($wrapCase(":$valueParameter"), "'%'")), | |
| $exprBuilder->like($wrapCase("$alias.$field"), $exprBuilder->concat("'%'", $wrapCase(":$valueParameter"))) | |
| ); | |
| default: | |
| throw new InvalidArgumentException(sprintf('strategy %s does not exist.', $strategy)); | |
| } | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getDescription(string $resourceClass): array | |
| { | |
| $descriptions = []; | |
| foreach ($this->properties as $filterName => $properties) { | |
| $propertyNames = []; | |
| foreach ($properties as $property => $strategy) { | |
| if (!$this->isPropertyMapped($property, $resourceClass, true)) { | |
| continue; | |
| } | |
| $propertyNames[] = $this->normalizePropertyName($property); | |
| } | |
| $filterParameterName = $filterName . '[]'; | |
| $descriptions[$filterParameterName] = [ | |
| 'property' => $filterName, | |
| 'type' => 'string', | |
| 'required' => false, | |
| 'is_collection' => true, | |
| 'openapi' => [ | |
| 'description' => 'Search involves the fields: ' . implode(', ', $propertyNames), | |
| ], | |
| ]; | |
| } | |
| return $descriptions; | |
| } | |
| } |
Thank for your work, i use it now !
Thanks, I'll give it a try !
Hello,
I have implemented but the API returns me the whole records...
use App\Filter\FullTextSearchFilter;
...
/**
* ...
* @ApiFilter(FullTextSearchFilter::class, properties={
* "searchIt"={
* "familyName": "ipartial",
* "givenName": "ipartial",
* "email": "ipartial"
* }
* })
*/
API Request: http://localhost:8080/persons?searchIt%5B%5D=maq (or http://localhost:8080/persons?searchIt=maq&page=1) returns me the all the requests. I am supposed to receive one record.
Database: MySQL
API over Symfony 5.1.6
Any idea?
@maquejp it's because you must use the prefix "search_"
It's as this in the code :
private const PROPERTY_NAME_PREFIX = 'search_';
instead of "searchIt" use "search_it"
Indeed better with the right naming 👍
Thanks @Tersoal for this :) you saved a lot of work for me 🍺
If someone struggling to setup this filter in YAML here is an example
# services.yaml
# first define FullTextSearchFilter as service
app_fulltext_filter:
parent: 'api_platform.doctrine.orm.search_filter'
class: App\Filter\FullTextSearchFilter
autowire: false
autoconfigure: false
public: false
#next define filter service
user.search_filter:
parent: 'app_fulltext_filter'
arguments: [ { search_for: { email: 'ipartial', name: 'ipartial' } } ]
tags: [ 'api_platform.filter' ]
autowire: false
autoconfigure: false
public: false# in your resource configuration
# config/api_platform/resources/user.yaml
App\Entity\User:
itemOperations: ~
collectionOperations:
get:
filters: [ 'user.search_filter' ]@Tersoal it supports Filtering by a relation's property ? I tried
* "search_images"={
* "images.id": "exact"
* }
but it doesn't work
For me it works like a charm, I have relations in my app.
This makes no sense for me, you don't need a full text search for an only one field, you do?
@Tersoal it's because i want to use multiple id to get the result , I mean : WHERE id IN (value1, value2, ...)
is it possible without custom filter ?
I think so... Have you tried to send id param as array?
https://api-platform.com/docs/core/filters/#search-filter
Note: Search filters with the exact strategy can have multiple values for the same property (in this case the condition will be similar to a SQL IN clause).
Syntax: ?property[]=foo&property[]=bar
Does the trick nicely! Many thanks for your extended work on this :)
Happy to give this a try too !