Skip to content

Instantly share code, notes, and snippets.

@masacc
Last active October 1, 2020 09:05
Show Gist options
  • Save masacc/94df641b3cb9814cbdaeb3f158d2e1f7 to your computer and use it in GitHub Desktop.
Save masacc/94df641b3cb9814cbdaeb3f158d2e1f7 to your computer and use it in GitHub Desktop.
Yet Another API Platform "Or Search Filter".
<?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;
use function strlen;
use function strpos;
use function substr;
/**
* inspired from :
* - https://gist.github.com/masseelch/47931f3a745409f8f44c69efa9ecb05c
* - https://gist.github.com/renta/b6ece3fec7896440fe52a9ec0e76571a
*
* how to use :
* - add classAnnotation :
* ApiFilter(OrSearchFilter::class, properties={
* "or_myfiltername"={
* "property1": "partial",
* "property2": "exact"
* },
* "or_anotherfilter"={
* "property1": "partial",
* "property3": "partial"
* }
* })
* - use filter in query string : `/api/myresources?or_myfiltername=pony`
*/
class OrSearchFilter extends SearchFilter
{
private const PROPERTY_NAME_PREFIX = 'or_';
/**
* {@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;
}
$filterName = substr($property, strlen(self::PROPERTY_NAME_PREFIX));
if (false === isset($this->properties[$filterName])) {
return;
}
$orExpressions = [];
foreach ($this->properties[$filterName] 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));
}
}
}
@bamboriz
Copy link

bamboriz commented Sep 3, 2020

This seems not to be working for me. The results returned are not filtered for matches in property1 or property2. I tried different strategies. Any ideas as to what I might be missing.

NB: Didn't register it as a service (but even when I do result is the same)

@masacc
Copy link
Author

masacc commented Sep 9, 2020

@bamboriz could you share your code ?

@fkizewski
Copy link

I've make some modifications (I'm not sure that's the best method) and for me it's working now:

My explain, in the case that the annotation on the entity is:
ApiFilter(OrSearchFilter::class, properties={ "or_myfiltername"={ "property1": "partial", "property2": "exact" } })

I've comment this lines, because if i've no property in my entity with name "myfiltername", the return is executed.:

$filterName = substr($property, strlen(self::PROPERTY_NAME_PREFIX));
if (false === isset($this->properties[$filterName])) {
return;
}

And i've change the line 58 to : foreach ($this->properties[$property] as $propertyName => $strategy) {
My last problem: the filter does not appear into the documentation (I don't know how to solve it).

I'm on Symfony 5.1.2 and API Platform 2.5.6, and I hope you can fix your nice "OrSearchFilter" ;)

@Tersoal
Copy link

Tersoal commented Oct 1, 2020

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment