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!
@UtechtDustin
Copy link

UtechtDustin commented Dec 7, 2022

Is it possible to use this Filter and the Searchfilter at the same time ?
We have on the left side a few filters e.g. city, but on the top we also want the OrSearchFilter to search on almsot any property of the Entity.

So if someone search for e.g. test123 on the top search we see want to see all entities which has test123 in at least one of the defined properties (orSearchFilter).
If we also set the city filter e.g. new york on the left side we want to see all Entities which are located in new york AND have test123 in at least one of the defined properties (orSearchFilter).

@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