-
-
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; |
@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:
- Use db fulltext search. I think that it's a preferable way.
- 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.
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
I really appreciate all the work you did for this, it fits perfectly with my needs. However, I'm seeing that the filter isn't being hit. I've added the service and everything. Am I correct in that the query would be the likes of
api/user?fullName=john doe