Last active
September 19, 2022 19:43
-
-
Save masseelch/47931f3a745409f8f44c69efa9ecb05c to your computer and use it in GitHub Desktop.
(Kind of a) api-platform full-text search filter.
This file contains hidden or 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
// 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; | |
} | |
} |
This file contains hidden or 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
<?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)); | |
} | |
} | |
} |
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.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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