Skip to content

Instantly share code, notes, and snippets.

@renta
Last active August 19, 2021 17:39
Show Gist options
  • Save renta/b6ece3fec7896440fe52a9ec0e76571a to your computer and use it in GitHub Desktop.
Save renta/b6ece3fec7896440fe52a9ec0e76571a to your computer and use it in GitHub Desktop.
Custom OR filter for an Api Platform
<?php
namespace App\Filter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;
final class OrSearchFilter extends AbstractContextAwareFilter
{
private const FILTER_KEY = 'orSearch';
/**
* Passes a property through the filter.
*
* @param string $property
* @param $value
* @param QueryBuilder $queryBuilder
* @param QueryNameGeneratorInterface $queryNameGenerator
* @param string $resourceClass
* @param string|null $operationName
*/
protected function filterProperty(
string $property,
$value,
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass,
string $operationName = null
): void {
if (null === $value || false === strpos($property, self::FILTER_KEY)) {
return;
}
$parameterName = $queryNameGenerator->generateParameterName($property);
$search = [];
$mappedJoins = [];
foreach ($this->properties as $groupName => $fields) {
foreach ($fields as $field) {
$joins = explode('.', $field);
for ($lastAlias = 'o', $i = 0, $num = \count($joins); $i < $num; $i++) {
$currentAlias = $joins[$i];
if ($i === $num - 1) {
$search[] = "LOWER({$lastAlias}.{$currentAlias}) LIKE LOWER(:{$parameterName})";
} else {
$join = "{$lastAlias}.{$currentAlias}";
if (!\in_array($join, $mappedJoins, true)) {
$queryBuilder->leftJoin($join, $currentAlias);
$mappedJoins[] = $join;
}
}
$lastAlias = $currentAlias;
}
}
}
$queryBuilder->andWhere(implode(' OR ', $search));
$queryBuilder->setParameter($parameterName, '%' . $value . '%');
}
/**
* Gets the description of this filter for the given resource.
*
* Returns an array with the filter parameter names as keys and array with the following data as values:
* - property: the property where the filter is applied
* - type: the type of the filter
* - required: if this filter is required
* - strategy: the used strategy
* - swagger (optional): additional parameters for the path operation,
* e.g. 'swagger' => [
* 'description' => 'My Description',
* 'name' => 'My Name',
* 'type' => 'integer',
* ]
* The description can contain additional data specific to a filter.
*
* @see \ApiPlatform\Core\Swagger\Serializer\DocumentationNormalizer::getFiltersParameters
*
* @param string $resourceClass
*
* @return array
*/
public function getDescription(string $resourceClass): array
{
$description = [];
foreach ($this->properties as $groupName => $fields) {
$description[self::FILTER_KEY . '_' . $groupName] = [
'property' => self::FILTER_KEY,
'type' => 'string',
'required' => false,
'swagger' => ['description' => 'OrSearchFilter on ' . implode(', ', $fields)],
];
}
return $description;
}
}
services:
#... other services
App\Filter\OrSearchFilter:
<?php
namespace App\Entity;
use App\Filter\OrSearchFilter;
use ApiPlatform\Core\Annotation\ApiFilter;
//...more imports here
/**
* @ApiFilter(
* OrSearchFilter::class, properties={
* "fullname": {"firstName", "lastName"}
* }
* )
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface
{
//...more fields here
/**
* @Assert\Length(min="1", max="100")
* @ORM\Column(type="string", length=100, nullable=true)
*/
private $firstName;
/**
* @Assert\Length(min="1", max="100")
* @ORM\Column(type="string", length=100, nullable=true)
*/
private $lastName;
@renta
Copy link
Author

renta commented Oct 2, 2018

@ELepolt
In my case a query string for the OrSearch full name filter looks like this:
https://localhost:8443/api/guides?orSearch_fullname=John
In the case of a first and a last name (https://localhost:8443/api/guides?orSearch_fullname=John%20Dow) this filter would not work, because it's construct the query something like:
FROM user u WHERE first_name LIKE "John Dow" OR last_name LIKE "John Dow"
And in the table there are only "John" or "Dow" in each column.

So, a task could be solved in several ways:

  1. Use db fulltext search. I think that it's a preferable way.
  2. Explode a query string and make something like:
    FROM user u WHERE first_name LIKE "John" OR first_name LIKE "Dow" OR last_name LIKE "John" OR last_name LIKE "Dow" (welcome, another custom filter!)
    I don't like the second one because it could lead to db-slow queries and looks to hacky and unprofessional.

@byhoratiss
Copy link

Thank you for all this @renta ! I've a issue trying to make it work with YML configuration files.
Can't figure out how to specify the "properties" option in YML, any ideas?

resources:
    App\Entity\Product:
        shortName: 'product'
        attributes:
            pagination_client_enabled: true
            filters:
              - 'App\Filter\OrSearchFilter': { properties: { fullname: { 'firstName', 'lastName' } } }

@byhoratiss
Copy link

byhoratiss commented Nov 12, 2018

I've found the answer about how to use this with YAML files and I'm leaving it here to other users.

  • You need to register the "OrSearchFilter" class as a Service.
services:
    'App\Filter\OrSearchFilter':
        tags: [ 'api_platform.filter' ]

If you use the @ApiFilter() annotation, you're done, but if you're using YAML files, you need to register a new Service for your Filter definition:

services:
    my_awesome_filters.or_filter:
        parent: 'App\Filter\OrSearchFilter'
        arguments: [ '@doctrine', '@request_stack', '@?logger', { propertyThatWillBeUsedAsParameter: [ 'propertyOne', 'anotherAwesomeProperty' ] } ]
        tags: [ 'api_platform.filter' ]

And use the ID of the service "my_awesome_filters.or_filter" as the example in your "filters" definition inside ApiPlatform resources.

resources:
    App\Entity\Product:
        shortName: 'product'
        attributes:
            filters:
              - "my_awesome_filters.or_filter"

@sneakyx
Copy link

sneakyx commented Mar 4, 2019

Hi,
thanks for your work, but can't get it running.
The array $fields on Line 41 is always null.
I wanted to filter by companies zip f.e. all companies with zip=12345 or 67890
My config:
services.yml
my.filter.or_filter: class: AppBundle\Filters\OrFilter tags: [ 'api_platform.filter' ]
my entity

<?php

namespace AppBundle\Entity\Blog;

use ApiPlatform\Core\Annotation\ApiFilter;
use AppBundle\Entity\Registration\User;
...
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\HasLifecycleCallbacks;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\NumericFilter;
use AppBundle\Filters\OrFilter;

/**
 * Post
 *
 * @ApiResource(attributes={
 *     "normalization_context"={"groups"={"api_read", "api_admin_read"}},
 *     "filters"={"post.search_filter"}
 * })
 * @ApiFilter(OrFilter::class, properties={"company.zip"})
 * @ORM\Table(name="post")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\Blog\PostRepository")
 * @ORM\HasLifecycleCallbacks
 *
 */

my request:
localhost:8000/api/v1/posts?orSearch_company.zip=12345.67890

the dump shows this:

array:1 [
"company.zip" => null
]

What am I doing wrong?

Thanks in advance!
sneaky

@kgonella
Copy link

kgonella commented Apr 2, 2019

thanks for your share !

@next-sentence
Copy link

@sneakyx

I wanted to filter by companies zip f.e. all companies with zip=12345 or 67890

you can use default SearchFilter

localhost:8000/api/v1/posts?company.zip[]=12345&company.zip[]=67890

read docs

@masacc
Copy link

masacc commented Aug 31, 2020

Thanks @renta !

In case someone need something a little bit different, I made a mix from this gist and another one (https://gist.github.com/masseelch/47931f3a745409f8f44c69efa9ecb05c) :

https://gist.github.com/masacc/94df641b3cb9814cbdaeb3f158d2e1f7

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