Skip to content

Instantly share code, notes, and snippets.

@axelvnk
Last active November 1, 2024 23:08
Show Gist options
  • Save axelvnk/edf879af5c7dbd9616a4eeb77c7181a3 to your computer and use it in GitHub Desktop.
Save axelvnk/edf879af5c7dbd9616a4eeb77c7181a3 to your computer and use it in GitHub Desktop.
Api platform OR search filter
<?php
namespace Axelvkn\AppBundle\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 OrSearchFilter extends SearchFilter
{
/**
* {@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);
switch ($strategy) {
case null:
case self::STRATEGY_EXACT:
$queryBuilder
->orWhere(sprintf($wrapCase('%s.%s').' = '.$wrapCase(':%s'), $alias, $field, $valueParameter))
->setParameter($valueParameter, $value);
break;
case self::STRATEGY_PARTIAL:
$queryBuilder
->orWhere(sprintf($wrapCase('%s.%s').' LIKE '.$wrapCase('CONCAT(\'%%\', :%s, \'%%\')'), $alias, $field, $valueParameter))
->setParameter($valueParameter, $value);
break;
case self::STRATEGY_START:
$queryBuilder
->orWhere(sprintf($wrapCase('%s.%s').' LIKE '.$wrapCase('CONCAT(:%s, \'%%\')'), $alias, $field, $valueParameter))
->setParameter($valueParameter, $value);
break;
case self::STRATEGY_END:
$queryBuilder
->orWhere(sprintf($wrapCase('%s.%s').' LIKE '.$wrapCase('CONCAT(\'%%\', :%s)'), $alias, $field, $valueParameter))
->setParameter($valueParameter, $value);
break;
case self::STRATEGY_WORD_START:
$queryBuilder
->orWhere(sprintf($wrapCase('%1$s.%2$s').' LIKE '.$wrapCase('CONCAT(:%3$s, \'%%\')').' OR '.$wrapCase('%1$s.%2$s').' LIKE '.$wrapCase('CONCAT(\'%% \', :%3$s, \'%%\')'), $alias, $field, $valueParameter))
->setParameter($valueParameter, $value);
break;
default:
throw new InvalidArgumentException(sprintf('strategy %s does not exist.', $strategy));
}
}
}
services:
axelvnk.filter.or_search_filter:
class: Axelvnk\AppBundle\Filter\OrSearchFilter
parent: "api_platform.doctrine.orm.search_filter"
axelvnk.filter.customer:
parent: axelvnk.filter.or_search_filter
arguments:
- { name: "partial", vatNumber: "partial" }
tags:
- { name: "api_platform.filter", id: "customer.search" }
#now you can search /api/customers?name=0844.010.460&vatNumber=0844.010.460 and your filter will be applied with all or conditions!
@nicolasbonnici
Copy link

nicolasbonnici commented Nov 1, 2024

Here's another exemple using APIP 4

https://gist.github.com/nicolasbonnici/3d598c8981ec1ac9769b4b7f5a9031f6

<?php

namespace App\Filter;

use ApiPlatform\Doctrine\Orm\Filter\FilterInterface;
use Doctrine\ORM\Query\Expr\Comparison;
use Doctrine\ORM\QueryBuilder;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;

class SearchOrFilter implements FilterInterface
{
    private ?CamelCaseToSnakeCaseNameConverter $nameConverter = null;

    public function __construct(private readonly RequestStack $requestStack, private readonly array $properties = [])
    {
    }

    private function getNameConverter(): CamelCaseToSnakeCaseNameConverter
    {
        return $this->nameConverter ??= new CamelCaseToSnakeCaseNameConverter();
    }

    public function apply(
        QueryBuilder $queryBuilder,
        QueryNameGeneratorInterface $queryNameGenerator,
        string $resourceClass,
        $operation = null,
        array $context = []
    ): void
    {
        $request = $this->requestStack->getCurrentRequest();
        if (!$request) {
            return;
        }

        $alias = $queryBuilder->getRootAliases()[0];
        $orConditions = [];

        foreach ($this->properties as $property => $settings) {
            $type = is_array($settings) ? $settings['type'] : $settings;
            $value = $request->get($this->getNameConverter()->normalize($property));

            if ($value === null) {
                continue;
            }

            $parameterName = $queryNameGenerator->generateParameterName($property);

            $orConditions[] = $this->buildCondition(
                $queryBuilder,
                $alias,
                $property,
                $parameterName,
                $value,
                $type
            );
        }

        if (!empty($orConditions)) {
            $queryBuilder->andWhere($queryBuilder->expr()->orX(...$orConditions));
        }
    }

    private function buildCondition(
        QueryBuilder $queryBuilder,
        string $alias,
        string $property,
        string $parameterName,
        string $value,
        string $type
    ): Comparison {
        $isPartial = in_array($type, ['partial', 'ipartial'], true);
        $isInsensitive = in_array($type, ['ipartial', 'iexact'], true);
        $value = $isPartial ? '%' . $value . '%' : $value;

        $queryBuilder->setParameter($parameterName, $value);

        $column = $isInsensitive
            ? sprintf('LOWER(%s.%s)', $alias, $property)
            : sprintf('%s.%s', $alias, $property);
        $param = $isInsensitive
            ? sprintf('LOWER(:%s)', $parameterName)
            : sprintf(':%s', $parameterName);

        return $isPartial
            ? $queryBuilder->expr()->like($column, $param)
            : $queryBuilder->expr()->eq($column, $param);
    }

    public function getDescription(string $resourceClass): array
    {
        return array_map(fn($settings, $property) => [
            'property' => $property,
            'type' => Type::BUILTIN_TYPE_STRING,
            'required' => false,
            'description' => sprintf('Filter using OR condition on %s', $property),
        ], $this->properties, array_keys($this->properties));
    }
}

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