New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add OrSearchFilter - filter on multiple conditions using OR #398
Comments
IMO if we add support for the @dunglas if we take this path it would be worth taking some work from LoopBackApiBundle. But this will require some work to be fair. Would you want to do it for v2 or v1 too? |
@theofidry ok for me to include LoopBackApiBundle in core. Will you target the v1.1 or the v2? |
Excellent news, this looks promising. Thank you both. |
@dunglas not sure I'll look more deeply this weekend. Basically I have two solutions:
The 2. is very unlikely without any BC. I need to check it a bit more to be able to tell the easiest path. But it requires a bit of work to finish it, and much more time to ease the integration and check the performance impact. |
BC means 2.0 (IMO it would be better to have it in 2.0 as 1.1 is very stable now). |
Yep, 1. possibly means merge-able in v1, 2. guaranteed to go be in v2. I'm gonna take a proper look at that this weekend (too busy this week~) |
@theofidry IMO having a case insensitive filter (#384) would concern the same topic, filters. The #384 proposal might not be adequate in the way it's configurated ( |
Good reminder, will look into that as well :) |
@theofidry what is missing on this ? |
I'm in the need for this feature in my API; I have to search for a given value in several fields at the same time, so an OR filter will be very much welcome. Is there any progress on this? some estimated release date? |
Best use a full-text search implementation then. If you're using PostgreSQL, the built-in Full Text Search is a good option. There are certainly use cases for an |
Are we going to implement it on core @api-platform/core-team |
IMO it's not really worth the work (because this is never only a matter of AND or OR). We should advise to build custom filters if someone needs this. |
i'm -1 on this too, we could help people building their custom one, but that will not be in core :) |
Ty for helping on this. Now i know how to use this simple basic functionality with 6 Lines of Configuration. Not. Now I am building a own filter. That regex example helps very well. Not. ^^ just saying ^^ |
<?php
namespace AppBundle\Filter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Common\Annotations\AnnotationReader;
final class SearchFilter extends AbstractFilter
{
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
{
if ($property === 'search') {
$this->logger->info('Search for: ' . $value);
} else {
return;
}
$reader = new AnnotationReader();
$annotation = $reader->getClassAnnotation(new \ReflectionClass(new $resourceClass), 'AppBundle\\Filter\\SearchAnnotation');
if (!$annotation) {
throw new \HttpInvalidParamException('No Search implemented.');
}
$parameterName = $queryNameGenerator->generateParameterName($property);
$search = [];
foreach ($annotation->fields as $field)
{
$search[] = "o.{$field} LIKE :{$parameterName}";
}
$queryBuilder->andWhere(implode(' OR ', $search));
$queryBuilder->setParameter($parameterName, '%' . $value . '%');
}
/**
* @param string $resourceClass
* @return array
*/
public function getDescription(string $resourceClass): array
{
$description['search'] = [
'property' => 'search',
'type' => 'string',
'required' => false,
'swagger' => ['description' => 'Searchfilter'],
];
return $description;
}
} <?php
namespace AppBundle\Filter;
use Doctrine\Common\Annotations\Annotation;
use Doctrine\Common\Annotations\Annotation\Target;
use Doctrine\Common\Annotations\AnnotationException;
/**
* @Annotation
* @Target("CLASS")
*/
final class SearchAnnotation
{
public $fields = [];
/**
* Constructor.
*
* @param array $data Key-value for properties to be defined in this class.
* @throws AnnotationException
*/
public function __construct(array $data)
{
if (!isset($data['value']) || !is_array($data['value'])) {
throw new AnnotationException('Options must be a array of strings.');
}
foreach ($data['value'] as $key => $value) {
if (is_string($value)) {
$this->fields[] = $value;
} else {
throw new AnnotationException('Options must be a array of strings.');
}
}
}
} services.yml 'AppBundle\Filter\SearchFilter':
class: 'AppBundle\Filter\SearchFilter'
autowire: true
tags: [ { name: 'api_platform.filter', id: 'search' } ] src/AppBundle/Entity/Product.php <?php
namespace AppBundle\Entity;
[...]
use AppBundle\Filter\SearchAnnotation as Searchable;
/**
* [...]
* @Searchable({"name", "description", "whatever"})
*/
class Product { [...] } |
Great work @hbroer, that helped me a lot. I changed few things for my usecase:
src/AppBundle/Filter/SearchFilter.php <?php
namespace AppBundle\Filter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Common\Annotations\AnnotationReader;
final class SearchFilter extends AbstractFilter
{
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
{
if ($property === 'search') {
$this->logger->info('Search for: ' . $value);
} else {
return;
}
$reader = new AnnotationReader();
$annotation = $reader->getClassAnnotation(new \ReflectionClass(new $resourceClass), \AppBundle\Filter\SearchAnnotation::class);
if (!$annotation) {
throw new \HttpInvalidParamException('No Search implemented.');
}
$parameterName = $queryNameGenerator->generateParameterName($property);
$search = [];
$mappedJoins = [];
foreach ($annotation->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 (false === array_search($join, $mappedJoins)) {
$queryBuilder->leftJoin($join, $currentAlias);
$mappedJoins[] = $join;
}
}
$lastAlias = $currentAlias;
}
}
$queryBuilder->andWhere(implode(' OR ', $search));
$queryBuilder->setParameter($parameterName, '%' . $value . '%');
}
/**
* @param string $resourceClass
* @return array
*/
public function getDescription(string $resourceClass): array
{
$reader = new AnnotationReader();
$annotation = $reader->getClassAnnotation(new \ReflectionClass(new $resourceClass), \AppBundle\Filter\SearchAnnotation::class);
$description['search'] = [
'property' => 'search',
'type' => 'string',
'required' => false,
'swagger' => ['description' => 'FullTextFilter on ' . implode(', ', $annotation->fields)],
];
return $description;
}
} |
Thank you so much @hbroer. This is exactly what I was looking for. At first I didn't got it working, because the filter definition in the So I have added this and it works: /**
* @ApiResource(
* attributes={
* "filters"={"search"}
* }
* )
* @Searchable({"name", "description", "whatever"})
*/
class Product { [...] } And I optimized the search. So you can enter final class SearchFilter extends AbstractFilter
{
// ... other code
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
{
if ($property === 'search') {
$this->logger->info('Search for: ' . $value);
} else {
return;
}
$reader = new AnnotationReader();
$annotation = $reader->getClassAnnotation(new \ReflectionClass(new $resourceClass), 'AppBundle\\Filter\\SearchAnnotation');
if (!$annotation) {
throw new \HttpInvalidParamException('No Search implemented.');
}
$parameterName = $queryNameGenerator->generateParameterName($property);
$searchItems = explode(' ', str_replace('-', ' ', $value));
if (is_array($searchItems)) {
$andx = $queryBuilder->expr()->andx();
foreach ($searchItems as $index => $searchItem) {
$orx = $queryBuilder->expr()->orx();
foreach ($annotation->fields as $field) {
$orx->add($queryBuilder->expr()->like('o.' . $field, ':' . $parameterName . '_' . $index));
}
if ($orx->count()) {
$queryBuilder->setParameter($parameterName . '_' . $index, '%' . $searchItem . '%');
$andx->add($orx);
}
}
if ($andx->count()) {
$queryBuilder->andWhere($andx);
}
}
}
// ... other code
} |
I am doing the above but the filterProperty method is never reached. Could anyone have a look at my detailed question regarding this at: https://stackoverflow.com/questions/49372971/custom-filter-api-platform-not-working |
I have same problem as @apfz - filter appears in documentation, but it's never applied. filterProperty() is never reached. |
unfortunately I did not find a solution for it, so I had to switch to another API framework :(. glad I did though, filtering is now a breeze. |
Thanks @hbroer @benjaminrau for your code. I've done a custom OR filter without an annotation https://gist.github.com/renta/b6ece3fec7896440fe52a9ec0e76571a |
looks good! Be careful with |
Hi @benjaminrau and @iCodr8, I liked both of your ways to do it. But I need to combine those two features :
You both chose different ways to do it and today, I am not able to merge it. How would you imagine it ? |
@dardinier Use full text search: https://www.postgresql.org/docs/10/textsearch.html |
@hbroer I am beginner to api platform and I want to achieve search in multiple properties i.e OR Filter. Can you please help me configuring it. |
@vipulw2011 Please use the proper channels for support questions. Thanks! You can ask on the #api-platform channel of Symfony Slack. |
Using of Please use |
@dardinier I think this is what you are looking for: https://gist.github.com/masseelch/47931f3a745409f8f44c69efa9ecb05c |
I tried to add the custom search filter to an abstract class but it doesn't work , i can't instantiate an abstract class, is any solution to prevent the new $resourceClass ? or maybe a custom search filter for an abstract class ? |
https://gist.github.com/WybrenKoelmans/81179b07ae0a10de30ba
I unfortunately do not have the time right now to make a proper PR, but using the gist above it should be straight forward what I intend.
The most important lines are
https://gist.github.com/WybrenKoelmans/81179b07ae0a10de30ba#file-orsearchfilter-php-L153
and the switch changes around
https://gist.github.com/WybrenKoelmans/81179b07ae0a10de30ba#file-orsearchfilter-php-L171
Should give me something like
The text was updated successfully, but these errors were encountered: