Skip to content

Instantly share code, notes, and snippets.

@teohhanhui
Created July 8, 2019 10:36
Show Gist options
  • Save teohhanhui/6d77c2ef4cbdde7ecbee9b314ad27281 to your computer and use it in GitHub Desktop.
Save teohhanhui/6d77c2ef4cbdde7ecbee9b314ad27281 to your computer and use it in GitHub Desktop.
<?php
namespace App\Filter;
use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\ORM\QueryBuilder;
use Psr\Log\LoggerInterface;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
/**
* Filters the collection by match, using a property alias.
*/
class PropertyAliasSearchFilter extends SearchFilter
{
private $aliasedProperties;
public function __construct(ManagerRegistry $managerRegistry, array $properties, IriConverterInterface $iriConverter, PropertyAccessorInterface $propertyAccessor = null, LoggerInterface $logger = null)
{
$this->aliasedProperties = $properties;
$properties = [];
foreach ($this->aliasedProperties as $property => $propertyOptions) {
$properties[$propertyOptions['propertyName']] = $propertyOptions['strategy'];
}
parent::__construct($managerRegistry, null, $iriConverter, $propertyAccessor, $logger, $properties);
}
/**
* {@inheritdoc}
*/
public function getDescription(string $resourceClass): array
{
$description = [];
$properties = $this->aliasedProperties;
foreach ($properties as $property => $propertyOptions) {
$description[$property] = [
'property' => $propertyOptions['propertyName'],
'type' => 'string',
'required' => false,
'strategy' => $propertyOptions['strategy'],
];
}
return $description;
}
/**
* {@inheritdoc}
*/
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
{
if (!isset($this->aliasedProperties[$property])) {
return;
}
$property = $this->aliasedProperties[$property]['propertyName'];
parent::filterProperty($property, $value, $queryBuilder, $queryNameGenerator, $resourceClass, $operationName);
}
}
@coderpauloo
Copy link

Hi Teoh,

Your code seems amazing, and it seems to be doing what I want, but can you give me your services configuration please ? (services.yaml)
I didn't understand how it works with multiple strategies for a same field.

Thanks a lot mate ! :)

@byhoratiss
Copy link

I think it works as:

services:
    product.search_filter:
        class: App\Filter\PropertyAliasSearchFilter
        parent: 'api_platform.doctrine.orm.search_filter'
        arguments:
            - nameExact: { propertyName: 'name', strategy: 'exact' }
            - nameStart: { propertyName: 'name', strategy: 'start' }
        tags: [ 'api_platform.filter' ]

@coderpauloo
Copy link

Hey @byhoratiss thanks a lot for your help !

Did you manage to make the filter work for any other strategy than the very first one of the array (nameStart, ...) though ? I struggle understanding how the loop is made over it. It feels like the arguments are not interpreted as an array...

@teohhanhui
Copy link
Author

teohhanhui commented Sep 12, 2019

use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use App\Filter\PropertyAliasSearchFilter;

/**
 * @ApiFilter(SearchFilter::class, properties={
 *     "name"="exact",
 * })
 * @ApiFilter(PropertyAliasSearchFilter::class, properties={
 *     "nameStart"={"propertyName"="name", "strategy"="word_start"},
 * })
 */

(Don't use the PropertyAliasSearchFilter when you don't need to.)

Perhaps I'll write another version which allows the client to choose the strategy (from a whitelist). That should feel more natural.

@byhoratiss
Copy link

@coderpauloo
Copy link

Thank you for your help teoh, I will watch closely any of you update :)

Ok I see, like @byhoratiss said the thing is you have to register 1 service per propertyName that you want to filter and thus makes a lot of configuration.
We finally took his solution even if the code is bigger to maintain because the piped possibilites did exacltly what we intented to do (aka making annoying old filters for and old UI that offers possibilities like 'starts with', 'contains' ...).

@teohhanhui
Copy link
Author

teohhanhui commented Sep 12, 2019

the thing is you have to register 1 service per propertyName that you want to filter and thus makes a lot of configuration

That's entirely incorrect.

The service configuration example from @byhoratiss has a mistake. You need to pass them nested as the $properties argument.

@teohhanhui
Copy link
Author

@byhoratiss Your solution is not maintainable because you are duplicating the code from SearchFilter.

@byhoratiss
Copy link

byhoratiss commented Sep 12, 2019

@teohhanhui I know I'm duplicating code from parent class, because the method "filterProperty" does not have a parameter for Strategy, neither a "Configuration" object.

When the parent class filter allow me to set the strategy I will rewrite my implementation.

@nicotec
Copy link

nicotec commented Aug 31, 2022

Hello,

much simpler !!!

?page=1&firstname[exact]=mick

use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;

class SearchStrategyFilter extends SearchFilter
{
    protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
    {
        if (!is_array($value)) {
            return;
        }

        $first = array_key_first($value);

        if (!in_array($first, [
            self::STRATEGY_EXACT,
            self::STRATEGY_PARTIAL,
            self::STRATEGY_START,
            self::STRATEGY_END,
            self::STRATEGY_WORD_START,
        ])) {
            return;
        }

        $this->properties = [$property => $first];
        $value = $value[$first];

        parent::filterProperty($property, $value, $queryBuilder, $queryNameGenerator, $resourceClass, $operationName);
   }
}

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