Last active
May 18, 2021 14:09
-
-
Save bizley/a4aef0ec04994a9832fcdeba6fb44aa5 to your computer and use it in GitHub Desktop.
API Platform custom filter for multiple alternative properties' values (ORM)
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 | |
declare(strict_types=1); | |
namespace App\Entity; | |
use ApiPlatform\Core\Annotation\ApiFilter; | |
use ApiPlatform\Core\Annotation\ApiResource; | |
use App\Doctrine\Filter\OrSearchFilter; | |
/** | |
* @ApiResource() | |
* @ApiFilter( | |
* OrSearchFilter::class, | |
* properties={ | |
* "search"={"property1", "property2"}, | |
* "searchSecond"={"property3"} | |
* } | |
* ) | |
* Usage: | |
* ?search=aaa => WHERE property1 LIKE '%aaa%' OR property2 LIKE '%aaa%' | |
* ?search=%aaa => WHERE property1 LIKE '%aaa' OR property2 LIKE '%aaa' | |
* ?search=aaa% => WHERE property1 LIKE 'aaa%' OR property2 LIKE 'aaa%' | |
* ?searchSecond=bbb => WHERE property3 LIKE '%bbb%' | |
* ?search=aaa&searchSecond=bbb => WHERE property3 LIKE '%bbb%' AND (property1 LIKE '%aaa%' OR property2 LIKE '%aaa%') | |
*/ | |
class Entity | |
{ | |
} |
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 | |
declare(strict_types=1); | |
namespace App\Doctrine\Filter; | |
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter; | |
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; | |
use Doctrine\ORM\QueryBuilder; | |
use function array_key_exists; | |
use function array_map; | |
use function count; | |
use function explode; | |
use function implode; | |
use function in_array; | |
use function strpos; | |
use function substr; | |
use function trim; | |
final class OrSearchFilter extends AbstractContextAwareFilter | |
{ | |
private const LIKE_PRE = 'pre'; | |
private const LIKE_POST = 'post'; | |
private const LIKE_BOTH = 'both'; | |
public function apply( | |
QueryBuilder $queryBuilder, | |
QueryNameGeneratorInterface $queryNameGenerator, | |
string $resourceClass, | |
string $operationName = null, | |
array $context = [] | |
): void { | |
foreach ($this->properties as $searchParameter => $fields) { | |
if (array_key_exists('filters', $context) && array_key_exists($searchParameter, $context['filters'])) { | |
$this->filterProperty( | |
$searchParameter, | |
$context['filters'][$searchParameter], | |
$queryBuilder, | |
$queryNameGenerator, | |
$resourceClass, | |
$operationName, | |
$context | |
); | |
} | |
} | |
} | |
protected function filterProperty( | |
string $property, | |
$value, | |
QueryBuilder $queryBuilder, | |
QueryNameGeneratorInterface $queryNameGenerator, | |
string $resourceClass, | |
string $operationName = null, | |
array $context = [] | |
): void { | |
if (null === $value) { | |
return; | |
} | |
$parameterName = $queryNameGenerator->generateParameterName($property); | |
$search = []; | |
$parameters = []; | |
foreach ($this->properties[$property] as $field) { | |
$likeMode = self::LIKE_BOTH; | |
if (strpos($field, '%') === 0) { | |
$likeMode = self::LIKE_PRE; | |
$field = substr($field, 1); | |
} | |
if (substr($field, -1) === '%') { | |
$likeMode = $likeMode === self::LIKE_PRE ? self::LIKE_BOTH : self::LIKE_POST; | |
$field = substr($field, 0, -1); | |
} | |
$joins = explode('.', $field); | |
$lastAlias = $queryBuilder->getRootAliases()[0]; | |
$count = count($joins); | |
foreach ($joins as $index => $joinPart) { | |
$currentAlias = $joinPart; | |
if ($index === $count - 1) { | |
$param = "{$likeMode}_{$parameterName}"; | |
if (!array_key_exists($likeMode, $parameters)) { | |
$parameters[$likeMode] = $param; | |
} | |
$search[] = "{$lastAlias}.{$currentAlias} LIKE :{$param}"; | |
} else { | |
$join = "{$lastAlias}.{$currentAlias}"; | |
if (!in_array($currentAlias, $queryBuilder->getAllAliases(), true)) { | |
$queryBuilder->leftJoin($join, $currentAlias); | |
} | |
} | |
$lastAlias = $currentAlias; | |
} | |
} | |
$queryBuilder->andWhere(implode(' OR ', $search)); | |
foreach ($parameters as $likeMode => $param) { | |
$valueFormat = "%{$value}%"; | |
if ($likeMode === self::LIKE_PRE) { | |
$valueFormat = "%{$value}"; | |
} elseif ($likeMode === self::LIKE_POST) { | |
$valueFormat = "{$value}%"; | |
} | |
$queryBuilder->setParameter($param, $valueFormat); | |
} | |
} | |
public function getDescription(string $resourceClass): array | |
{ | |
$description = []; | |
foreach ($this->properties as $searchParameter => $fields) { | |
$description[$searchParameter] = [ | |
'property' => $searchParameter, | |
'type' => 'string', | |
'required' => false, | |
'swagger' => [ | |
'description' => 'Combined filter on ' . implode(', ', array_map(static function ($value) { | |
return trim($value, '%'); | |
}, $fields)) | |
], | |
]; | |
} | |
return $description; | |
} | |
} |
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
App\Doctrine\Filter\OrSearchFilter: | |
tags: ['api_platform.filter'] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Update: fixed relation aliases collisions