Please provide a migration path for the ApiFilter attribute => query parameter.
Description
Hi @soyuka @vinceAmstoutz
I'm currently trying to migrate my ApiFilter attribute filters to avoid the futur deprecation, as explained in https://api-platform.com/docs/core/doctrine-filters/#introduction
But I'm currently having big trouble to understand
- How to migrate some of my filters
- What's the benefit
And I feel like I'm maybe not the only one, looking at some recent issue/discussions.
Also I think this Upgrade note could explain with more details why this changes, cause currently I didn't found satisfying explanations. I feel like the ApiFilter attribute will be replaced by
- The new filters classes (great)
- Declaring filter in the yaml/php config (why ? I hate having to maintain a config)
- Creating new dedicated filter (why having to maintain my own filter while ApiPlatform filters was doing the trick ?)
I understand writing doc is difficult and I'm open to do PR to improve it, but first I need to understand it ^^'
Example
I would expect a big list of BEFORE/AFTER example. I'm unsure how to write it currently, based on the issues I encounter.
Problem I still have
- I have an OrderFilter
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
that's I tried to migrated
parameters: ['order[:property]' => new QueryParameter(filter: new OrderFilter(), properties: ['name', 'createdAt'])],
it worked fine BUT got the same issue than https://github.com/api-platform/core/issues/7361
I follow the suggestion from soyuka (https://github.com/api-platform/core/issues/7361#issuecomment-3243315889) to use service id with .instance
parameters: ['order[:property]' => new QueryParameter(filter: 'api_platform.doctrine.orm.order_filter.instance', properties: ['name', 'createdAt'])],
but then the filter does nothing as reported by https://github.com/api-platform/core/issues/7361#issuecomment-3240681163
Looking at https://github.com/api-platform/core/issues/7361#issuecomment-3243835860 I feel like I'll have to declare a new service in my config. Is it the only way ? Is it plan to introduce a new OrderFilter ?
- I have a filter with nested property
#[ApiFilter(SearchFilter::class, properties: ['languagePair.target' => 'exact'])]
As reported https://github.com/api-platform/core/pull/7121#issuecomment-3575382644 and answered by soyuka, nested properties are (voluntary) not supported anymore for those filters. This is already discussed in https://github.com/api-platform/core/issues/7526
Filtering on nested properties requires joins which is hard to predict and may lead to unwanted behavior / performance issues, can't you create your own filter?
I have a big trouble understanding
- What was the issue before (since it works)
- How my own filter will avoid this issue (since I'll have to make my own JOIN !)
Wouldn't it be possible to still support nested properties ? Or to do somehting like OrFilter:
new NestedFilter(new ExactFilter())
Also soyuka told me I can still use the legacy (not recommended) SearchFilter but I got the same issue than the OrderFilter, something like
parameters: [
'targetLanguage' => new QueryParameter(
filter: 'api_platform.doctrine.orm.search_filter.instance',
property: 'languagePair.target'
),
]
does nothing...
Problem solved that might help someone else...
- I have a BooleanFilter on a live property I tried to migrate to
parameters: ['live' => new QueryParameter(filter: new BooleanFilter(), property: 'live')],
it worked fine BUT got the same issue than https://github.com/api-platform/core/issues/7361
I'm finally solve my issue with
parameters: ['live' => new QueryParameter(filter: new ExactFilter(), property: 'live')],
but I'm not sure of the changes.
- I have a SearchFilter
#[ApiFilter(SearchFilter::class, properties: ['name' => 'partial'])]
that I migrated into
parameters: ['name' => new QueryParameter(filter: new PartialSearchFilter(), property: 'name')],
I feel like it's the right way to do it.
OrderFilter
I have a POC with the following filter
Custom OrderFilter
class MyOrderFilter implements OrderFilterInterface, FilterInterface, ManagerRegistryAwareInterface, LoggerAwareInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface
{
use LoggerAwareTrait;
use ManagerRegistryAwareTrait;
use PropertyPlaceholderOpenApiParameterTrait;
/**
* @param array<string, mixed> $context
*/
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
{
$filter = new ApiPlatformOrderFilter(
$this->getManagerRegistry(),
'order',
$this->getLogger(),
);
$filter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, $context);
}
public function getDescription(string $resourceClass): array
{
return [];
}
/**
* @return array<string, mixed>
*/
public function getSchema(Parameter $parameter): array
{
return new ApiPlatformOrderFilter()->getSchema($parameter);
}
}
Which allows me to write
parameters: ['order[:property]' => new QueryParameter(filter: new MyOrderFilter(), properties: ['name', 'createdAt'])],
without having to declare a custom filter in service.yaml.
Maybe it's possible for ApiPlatform to expose a new OrderFilter which does not extract AbstractFilter ?
Nested property
For those interested I currently have a solution with a custom NestedFilter
Custom NestedFilter
class NestedFilter implements FilterInterface, ManagerRegistryAwareInterface, LoggerAwareInterface, OpenApiParameterFilterInterface
{
use LoggerAwareTrait;
use ManagerRegistryAwareTrait;
use OrmPropertyHelperTrait;
use PropertyHelperTrait;
public function __construct(
private readonly FilterInterface $filter,
) {
}
/**
* @param array<string, mixed> $context
*/
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
{
if ($this->filter instanceof ManagerRegistryAwareInterface) {
$this->filter->setManagerRegistry($this->getManagerRegistry());
}
if ($this->filter instanceof LoggerAwareInterface) {
$this->filter->setLogger($this->getLogger());
}
/** @var Parameter $parameter */
$parameter = $context['parameter'];
$property = $parameter->getProperty();
if (null === $property) {
throw new \InvalidArgumentException('Nested filter only supports single property.');
}
// Associations are camelCase but the property might be in snake case.
$camelCaseProperty = implode('.', array_map(
static fn (string $s) => new UnicodeString($s)->camel()->toString(),
explode('.', $property),
));
$alias = $queryBuilder->getRootAliases()[0];
if ($this->isPropertyNested($camelCaseProperty, $resourceClass)) {
[$alias, $camelCaseProperty] = $this->addJoinsForNestedProperty(
$camelCaseProperty,
$alias,
$queryBuilder,
$queryNameGenerator,
$resourceClass,
Join::INNER_JOIN,
);
$property = new UnicodeString($camelCaseProperty)->snake()->toString();
}
$qb = clone $queryBuilder;
$qb->resetDQLPart('from');
$qb->from(self::class, $alias); // Override the root alias of the queryBuilder
$qb->resetDQLPart('where');
$qb->setParameters(new ArrayCollection());
$newParameter = $parameter->withProperty($property)->withProperties([$property]);
$this->filter->apply(
$qb,
$queryNameGenerator,
$resourceClass,
$operation,
['parameter' => $newParameter] + $context
);
$queryBuilder->andWhere($qb->getDQLPart('where'));
$parameters = $queryBuilder->getParameters();
foreach ($qb->getParameters() as $p) {
$parameters->add($p);
}
$queryBuilder->setParameters($parameters);
}
public function getDescription(string $resourceClass): array
{
return [];
}
public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
{
return $this->filter instanceof OpenApiParameterFilterInterface
? $this->filter->getOpenApiParameters($parameter)
: null;
}
}
but it feels hacky.