Skip to content

Instantly share code, notes, and snippets.

@bizley
Last active May 18, 2021 14:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bizley/a4aef0ec04994a9832fcdeba6fb44aa5 to your computer and use it in GitHub Desktop.
Save bizley/a4aef0ec04994a9832fcdeba6fb44aa5 to your computer and use it in GitHub Desktop.
API Platform custom filter for multiple alternative properties' values (ORM)
<?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
{
}
<?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;
}
}
App\Doctrine\Filter\OrSearchFilter:
tags: ['api_platform.filter']
@bizley
Copy link
Author

bizley commented May 18, 2021

Update: fixed relation aliases collisions

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