-
-
Save masseelch/47931f3a745409f8f44c69efa9ecb05c to your computer and use it in GitHub Desktop.
// Use it like this. | |
/api?search=this_is_my_search_string%20this_is_the_second_term_of_my_search_string | |
<?php | |
/** | |
* @ApiResource() | |
* @ApiFilter(FullTextSearchFilter::class, properties={ | |
* "id": "exact", | |
* "task": "partial", | |
* "client.name": "start" | |
* }) | |
* @ORM\Entity(repositoryClass=EntityRepository::class) | |
*/ | |
class Job | |
{ | |
/** | |
* @ORM\Id() | |
* @ORM\GeneratedValue() | |
* @ORM\Column(type="integer") | |
*/ | |
private $id; | |
/** | |
* @ORM\ManyToOne(targetEntity=Client::class, inversedBy="entites") | |
* @ORM\JoinColumn(nullable=false) | |
*/ | |
private $client; | |
public function getId(): ?int | |
{ | |
return $this->id; | |
} | |
public function getTask(): ?string | |
{ | |
return $this->task; | |
} | |
public function setTask(string $task): self | |
{ | |
$this->task = $task; | |
return $this; | |
} | |
public function getClient(): ?Client | |
{ | |
return $this->client; | |
} | |
public function setClient(?Client $client): self | |
{ | |
$this->client = $client; | |
return $this; | |
} | |
} |
<?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; | |
class FullTextSearchFilter extends SearchFilter | |
{ | |
private const PROPERTY_NAME = 'search'; | |
/** | |
* {@inheritdoc} | |
*/ | |
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null) | |
{ | |
// This filter will work with the 'search'-query-parameter only. | |
if ($property !== self::PROPERTY_NAME) { | |
return; | |
} | |
$orExpressions = []; | |
// Split the $value at spaces. | |
// For each term 'or' all given properties by strategy. | |
// 'And' all 'or'-parts. | |
$terms = explode(" ", $value); | |
foreach ($terms as $index => $term) { | |
foreach ($this->properties as $property => $strategy) { | |
$strategy = $strategy ?? self::STRATEGY_EXACT; | |
$alias = $queryBuilder->getRootAliases()[0]; | |
$field = $property; | |
$associations = []; | |
if ($this->isPropertyNested($property, $resourceClass)) { | |
[$alias, $field, $associations] = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass); | |
} | |
$caseSensitive = true; | |
$metadata = $this->getNestedMetadata($resourceClass, $associations); | |
if ($metadata->hasField($field)) { | |
if ('id' === $field) { | |
$term = $this->getIdFromValue($term); | |
} | |
if (!$this->hasValidValues((array)$term, $this->getDoctrineFieldType($property, $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[$index][] = $this->addWhereByStrategy($strategy, $queryBuilder, $queryNameGenerator, $alias, $field, $term, $caseSensitive); | |
} | |
} | |
} | |
$exprBuilder = $queryBuilder->expr(); | |
foreach ($orExpressions as $expr) { | |
$queryBuilder->andWhere($exprBuilder->orX(...$expr)); | |
} | |
} | |
/** | |
* {@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)); | |
} | |
} | |
} |
@masseelch Thanks a lot! It saved my life.
small typo: the comment block starting with
Split the $value at spaces.
should be moved next to$terms = explode(" ", $value);
: https://gist.github.com/alexislefebvre/fcbbb9104c787b9ccb739ce3bb5cfe06/revisions#diff-6aef44b2f91b3e1e0fa1602c78c91e92
You are welcome. Thanks for the hint. Updated it.
Thanks @masseelch !
In case someone need something a little bit different, I made a mix from this gist and another one (https://gist.github.com/renta/b6ece3fec7896440fe52a9ec0e76571a) :
https://gist.github.com/masacc/94df641b3cb9814cbdaeb3f158d2e1f7
Great !
very usefull
thank's for sharing ;)
It works like a charm. Thanks a lot!
Hi. Great job! I've done some refactoring to allow to search a string with spaces, or multiple strings.
EDITED: the code was wrong.
I've created a gist with correct one, allowing multiple string to search and multiple search options. Also the filters are added to swagger doc.
https://gist.github.com/Tersoal/d45b0cc75cadf72cd7c0e49b892809b3
This is inspired in this gist and also:
https://gist.github.com/renta/b6ece3fec7896440fe52a9ec0e76571a
https://gist.github.com/masacc/94df641b3cb9814cbdaeb3f158d2e1f7
It works well in REST. Is there any way to support this filter in graphql?
It works well in REST. Is there any way to support this filter in graphql?
I do not use graphql, therefore i do not know how to achieve this. I am sorry.
@masseelch Thanks a lot! It saved my life.
small typo: the comment block starting with
Split the $value at spaces.
should be moved next to$terms = explode(" ", $value);
: https://gist.github.com/alexislefebvre/fcbbb9104c787b9ccb739ce3bb5cfe06/revisions#diff-6aef44b2f91b3e1e0fa1602c78c91e92