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 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 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)); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I do not use graphql, therefore i do not know how to achieve this. I am sorry.