-
-
Save renta/b6ece3fec7896440fe52a9ec0e76571a to your computer and use it in GitHub Desktop.
<?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; |
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' } } }
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"
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
thanks for your share !
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
@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:
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.