Skip to content

Instantly share code, notes, and snippets.

@axelvnk
Last active June 9, 2024 15:20
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!
@alexislefebvre
Copy link

alexislefebvre commented Dec 8, 2022

@UtechtDustin this code add or conditions so it should work with other filters. Did you try it?

Maybe you don't need this filter but instead create a filter to search in several properties: https://api-platform.com/docs/core/filters/#creating-custom-filters See also this answer on Stack Overflow.

@mislavjakopovic
Copy link

Unfortunately doesn't work with API Platform 3.0

@loicngr
Copy link

loicngr commented Aug 8, 2023

I write this if you want :

<?php

namespace App\Filter;

use ApiPlatform\Api\IdentifiersExtractorInterface;
use ApiPlatform\Api\IriConverterInterface;
use ApiPlatform\Doctrine\Common\Filter\SearchFilterInterface;
use ApiPlatform\Doctrine\Common\Filter\SearchFilterTrait;
use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\Operation;
use Closure;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
use Psr\Log\LoggerInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;

final class SearchMultiFieldsFilter extends AbstractFilter implements SearchFilterInterface
{
    use SearchFilterTrait;

    public function __construct(
        ManagerRegistry $managerRegistry,
        IriConverterInterface $iriConverter,
        ?PropertyAccessorInterface $propertyAccessor = null,
        ?LoggerInterface $logger = null,
        ?array $properties = null,
        ?IdentifiersExtractorInterface $identifiersExtractor = null,
        ?NameConverterInterface $nameConverter = null,
        public string $searchParameterName = 'search',
    ) {
        parent::__construct($managerRegistry, $logger, $properties, $nameConverter);

        $this->iriConverter = $iriConverter;
        $this->identifiersExtractor = $identifiersExtractor;
        $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
    }

    protected function getIriConverter(): IriConverterInterface
    {
        return $this->iriConverter;
    }

    protected function getPropertyAccessor(): PropertyAccessorInterface
    {
        return $this->propertyAccessor;
    }

    /**
     * {@inheritDoc}
     */
    protected function filterProperty(
        string $property,
        mixed $value,
        QueryBuilder $queryBuilder,
        QueryNameGeneratorInterface $queryNameGenerator,
        string $resourceClass,
        ?Operation $operation = null,
        array $context = [],
    ): void {
        if (
            null === $value
            || $property !== $this->searchParameterName
        ) {
            return;
        }

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

        foreach (($this->getProperties() ?? []) as $prop => $caseSensitive) {
            $filter = $this->generatePropertyOrWhere(
                $queryBuilder,
                $queryNameGenerator,
                $alias,
                $prop,
                $value,
                $resourceClass,
                $count,
                $caseSensitive ?? false,
            );

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

            [$expr, $exprParams] = $filter;
            $ors[] = $expr;

            $queryBuilder->setParameter($exprParams[1], $exprParams[0]);

            ++$count;
        }

        $queryBuilder->andWhere($queryBuilder->expr()->orX(...$ors));
    }

    protected function generatePropertyOrWhere(
        QueryBuilder $queryBuilder,
        QueryNameGeneratorInterface $queryNameGenerator,
        string $alias,
        string $property,
        string $value,
        string $resourceClass,
        int $key,
        bool $caseSensitive = false,
    ): ?array {
        if (
            !$this->isPropertyEnabled($property, $resourceClass)
            || !$this->isPropertyMapped($property, $resourceClass, true)
        ) {
            return null;
        }

        $field = $property;
        $associations = [];

        if ($this->isPropertyNested($property, $resourceClass)) {
            [$alias, $field, $associations] = $this->addJoinsForNestedProperty(
                $property,
                $alias,
                $queryBuilder,
                $queryNameGenerator,
                $resourceClass,
                Join::INNER_JOIN,
            );
        }

        $metadata = $this->getNestedMetadata($resourceClass, $associations);

        if (
            'id' === $field
            || !$metadata->hasField($field)
        ) {
            return null;
        }

        $wrapCase = $this->createWrapCase($caseSensitive);
        $valueParameter = ':' . $queryNameGenerator->generateParameterName($field);
        $aliasedField = sprintf('%s.%s', $alias, $field);
        $keyValueParameter = sprintf('%s_%s', $valueParameter, $key);

        return [
            $queryBuilder->expr()->like(
                $wrapCase($aliasedField),
                $wrapCase((string) $queryBuilder->expr()->concat("'%'", $keyValueParameter, "'%'")),
            ),
            [$caseSensitive ? $value : strtolower($value), $keyValueParameter],
        ];
    }

    protected function createWrapCase(bool $caseSensitive): Closure
    {
        return static function (string $expr) use ($caseSensitive): string {
            if ($caseSensitive) {
                return $expr;
            }

            return sprintf('LOWER(%s)', $expr);
        };
    }

    /**
     * {@inheritDoc}
     */
    protected function getType(string $doctrineType): string
    {
        return 'string';
    }

    public function getDescription(string $resourceClass): array
    {
        $props = $this->getProperties();

        if (null === $props) {
            throw new InvalidArgumentException('Properties must be specified');
        }

        return [
            $this->searchParameterName => [
                'property' => implode(', ', array_keys($props)),
                'type' => 'string',
                'required' => false,
                'description' => 'Recherche sur les propriétés spécifiées.',
            ],
        ];
    }
}
    ApiFilter(
        SearchMultiFieldsFilter::class,
        properties: [
            'name',
            'email',
            'manager.fullname',
            'manager.email',
        ],
    ),

@aesislabs
Copy link

@loicngr This is absolutely on point, thanks !!

@loicngr
Copy link

loicngr commented Oct 31, 2023

@aesislabs you're welcome

@salma-benn
Copy link

@loicngr please that is work only for string what i do if i want to search with int

@sfarkas1988
Copy link

Thank you very much! @loicngr

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