Hi,
I work on an project with several filters.
One filter is an native filter :
services:
app.filter.ape:
parent: 'api_platform.doctrine.orm.search_filter'
arguments: [ { ape: 'exact' } ]
tags: [ { name: 'api_platform.filter', id: 'company.ape' } ]
The other one is an custom filter :
services:
...
app.filter.city:
class: AppBundle\Filter\CityFilter
arguments:
- '@doctrine'
- '@request_stack'
- '@?logger'
- null
- '@doctrine.orm.entity_manager'
- '@app.engine.company_maintainer'
tags: [ { name: 'api_platform.filter', id: 'company.city' } ]
<?php
namespace AppBundle\Filter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use AppBundle\Context\ContextFactory;
use AppBundle\Engine\CompanyMaintainer;
use AppBundle\Exception\NotImplementedException;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Doctrine\ManagerRegistry;
use Symfony\Component\HttpFoundation\RequestStack;
class CityFilter extends AbstractFilter
{
/**
* @var EntityManagerInterface
*/
private $em;
/**
* @var CompanyMaintainer
*/
private $companyMaintainer;
/**
* ApeFilter constructor.
*
* @param ManagerRegistry $managerRegistry
* @param RequestStack $requestStack
* @param LoggerInterface|null $logger
* @param array|null $properties
* @param EntityManagerInterface $em
* @param CompanyMaintainer $companyMaintainer
*/
public function __construct(ManagerRegistry $managerRegistry, RequestStack $requestStack, LoggerInterface $logger = null, array $properties = null, EntityManagerInterface $em, CompanyMaintainer $companyMaintainer)
{
$this->companyMaintainer = $companyMaintainer;
$this->em = $em;
parent::__construct($managerRegistry, $requestStack, $logger, $properties);
}
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
{
//TODO: implement this
throw new NotImplementedException('City filter not implement yet.');
}
// This function is only used to hook in documentation generators (supported by Swagger and Hydra)
public function getDescription(string $resourceClass): array
{
$description = [
'city' => [
'type' => 'string',
'required' => false,
'property' => 'city',
],
];
return $description;
}
}
When i get collection without filter (http://my_project.com/app_dev.php/companies) : no filter is trigger. _Which is good_.
When i get collection with the ape filter (http://my_project.com/app_dev.php/companies?ape=3212Z), the native ape filter AND the city custom filter are trigger (with an NotImplementedException). _Which is bad (the city filter shouldn't be called)_.
The city property is an "nested" property:
<?php
...
class Company
{
...
/**
* @var ArrayCollection
*
* @ORM\OneToMany(targetEntity="Location", mappedBy="company")
* @Groups({"company_free_data", "company_paid_data"})
*/
private $locations;
...
}
<?php
...
class Location
{
/**
* @var Company
*
* @ORM\ManyToOne(targetEntity="AppBundle\Entity\Company", inversedBy="locations")
* @ORM\JoinColumn(nullable=false)
*/
private $company;
...
/**
* @var string
*
* @ORM\Column(name="city", type="string", length=255)
* @Groups({"company_free_data", "company_paid_data"})
*/
private $city;
/**
* Set city.
*
* @param string $city
*
* @return Location
*/
public function setCity($city)
{
$this->city = $city;
return $this;
}
/**
* Get city.
*
* @return string
*/
public function getCity()
{
return $this->city;
}
}
To avoid the custom city filter to be called, i had declared my filter with the nested property like this :
app.filter.city:
class: AppBundle\Filter\CityFilter
arguments:
- '@doctrine'
- '@request_stack'
- '@?logger'
- { locations.postalCode }
- '@doctrine.orm.entity_manager'
- '@app.engine.company_maintainer'
tags: [ { name: 'api_platform.filter', id: 'company.city' } ]
But now AppBundle\Filter\CityFilter is never called even when it get collection with my filter (http://my_project.com/app_dev.php/companies?city=PARIS). _Which is bad_.
To solve this problem, i must add an if on every custom filter like this :
<?php
...
class CityFilter extends AbstractFilter
{
...
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
{
if ('city' === $property) {
//TODO: implement this
throw new NotImplementedException('City filter not implement yet.');
}
}
...
}
Is it a bug or an misunderstanding?
Filters are "applied" through what we call the FilterExtension (code may differ a bit between 2.0 and master).
Look at the condition there, it says: "if the filter is one of the declared resourceFilters, let's call ->apply()".
I don't see any ApiResource declaration above but if you don't want your custom filter to be used, you need to remove it from the filters section of the resource declaration.
Thanks for this quick answer!
<?php
...
/**
* Company.
*
* @ORM\Table(name="company")
* @ORM\Entity(repositoryClass="AppBundle\Repository\CompanyRepository")
* @ORM\HasLifecycleCallbacks()
* @ApiResource(collectionOperations={"get"={"method"="GET"}}, itemOperations={"get"={"method"="GET"}}, attributes={"filters"={"company.ape", "company.name", "company.postal_code", "company.city", "company.first_name", "company.last_name"}})
*/
class Company
{
...
}
company.city was declared
Yeah exactly, remove it to disable your filter :).
My bad : i misspoke.
Yeah, but i just want to disable city filter when i don't call it. If I remove his declaration, i cannot use this filter when i want it (eg. when i call http://my_project.com/app_dev.php/companies?city=PARIS).
I just don't want to trigger it when i call another filter, which is actually what append (eg. when i call http://my_project.com/app_dev.php/companies?ape=7010Z).
Oh I'm sorry about that misunderstood.
Got it now, this is the expected behavior.
Check how our core filters are built, for example BooleanFilter. We're calling isPropertyEnabled which checks if the given property belongs to the current filter (basically: array_key_exists($property, $this->properties)).
In your implementation, you could do:
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
{
if ('city' !== $property) {
return;
}
//this is executed only with the city property
}
FYI, filterProperty will be called for every property returned by the extractProperties method here.
Thanks !
I modified my filter like this :
...
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
{
if (!$this->isPropertyEnabled($property)) {
return;
}
//this is executed only with the city property
}
...
and his declaration like this :
services:
app.filter.city:
class: AppBundle\Filter\CityFilter
arguments:
- '@doctrine'
- '@request_stack'
- '@?logger'
- { city: ~ }
- '@doctrine.orm.entity_manager'
- '@app.engine.company_maintainer'
tags: [ { name: 'api_platform.filter', id: 'company.city' } ]
...
Now it works great!
Most helpful comment
Got it now, this is the expected behavior.
Check how our core filters are built, for example BooleanFilter. We're calling
isPropertyEnabledwhich checks if the given property belongs to the current filter (basically:array_key_exists($property, $this->properties)).In your implementation, you could do:
FYI,
filterPropertywill be called for every property returned by theextractPropertiesmethod here.