-
-
Save masacc/94df641b3cb9814cbdaeb3f158d2e1f7 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; | |
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 could you share your code ?
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" ;)
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.
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)