Skip to content

Latest commit

 

History

History
170 lines (130 loc) · 6.4 KB

0006-filters.md

File metadata and controls

170 lines (130 loc) · 6.4 KB

Filtering system and query parameters

  • Deciders: @dunglas, @soyuka
  • Consulted: @aegypius, @mrossard, @metaclass-nl, @helyakin
  • Informed: @jdeniau, @bendavies

Context and Problem Statement

Over the year we collected lots of issues and behaviors around filter composition, query parameters documentation and validation. A Github issue tracks these problems or enhancements. Today, an API Filter is defined by this interface:

/**
 * Filters applicable on a resource.
 *
 * @author Kévin Dunglas <dunglas@gmail.com>
 */
interface FilterInterface
{
    /**
     * 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 (optional): the used strategy
     *   - is_collection (optional): if this filter is for collection
     *   - swagger (optional): additional parameters for the path operation,
     *     e.g. 'swagger' => [
     *       'description' => 'My Description',
     *       'name' => 'My Name',
     *       'type' => 'integer',
     *     ]
     *   - openapi (optional): additional parameters for the path operation in the version 3 spec,
     *     e.g. 'openapi' => [
     *       'description' => 'My Description',
     *       'name' => 'My Name',
     *       'schema' => [
     *          'type' => 'integer',
     *       ]
     *     ]
     *   - schema (optional): schema definition,
     *     e.g. 'schema' => [
     *       'type' => 'string',
     *       'enum' => ['value_1', 'value_2'],
     *     ]
     * The description can contain additional data specific to a filter.
     *
     * @see \ApiPlatform\OpenApi\Factory\OpenApiFactory::getFiltersParameters
     */
    public function getDescription(string $resourceClass): array;
}

The idea of this ADR is to find a way to introduce more functionalities to API Platform filters such as:

  • document query parameters for hydra, JSON Schema (OpenAPI being an extension of JSON Schema).
  • pilot the query parameter validation (current QueryParameterValidator bases itself on the given documentation schema) this is good but lacks flexibility when you need custom validation (created by @jdeniau)
  • compose with filters, which will naturally help creating an or/and filter
  • remove the relation between a query parameter and a property (they may have different names #5980), different types, a query parameter can have no link with a property (order filter)
  • provide a way to implement different query parameter syntaxes without changing the Filter implementation behind it

We will keep a BC layer with the current doctrine system as it shouldn't change much.

Considered Options

Filter composition

For this to work, we need to consider a 4 year old bug on searching with UIDs. Our SearchFilter allows to search by propertyName or by relation, using either a scalar or an IRI:

/books?author.id=1
/books?author.id=/author/1

Many attempts to fix these behavior on API Platform have lead to bugs and to be reverted. My proposal is to change how filters are applied to provide filters with less logic, that are easier to maintain and that do one thing good.

For the following example we will use an UUID to represent the stored identifier of an Author resource.

We know author is a property of Book, that represents a Resource. So it can be filtered by:

  • IRI
  • uid

We should therefore call both of these filters for each query parameter matched:

  • IriFilter (will do nothing if the value is not an IRI)
  • UuidFilter

With that in mind, an or filter would call a bunch of filters specifying the logic operation to execute.

Query parameter

The above shows that a query parameter key, which is a string may lead to multiple filters being called. This same can represent one or multiple values, and for a same key we can handle multiple types of data. Also, if someone wants to implement the loopback API ?filter[fields][vin]=false the link between the query parameter, the filter and the value gets more complex.

We need a way to instruct the program to parse query parameters and produce a link between filters, values and some context (property, logical operation, type etc.). The same system could be used to determine the type a filter must have to pilot query parameter validation and the JSON Schema.

Some code/thoughts:

#[Parameter(key: 'search', filter: new SearchFilter(property: 'foo'))]

// how to give uidfilter the paramters it should declare?
// is it automatic if we find a property having the uid type?
#[Get(filters: [new SearchFilter(), new UidFilter()])
class Book {

}

class SearchParameter {
    mixed $value;
    ?string $property;
    ?string $class;
    array $attributes;
}

class FilterInterface {}

class UidFilter {
    public function __construct(private readonly string $class) {}

    public function parseQueryParameter(array $queryParameters = []): Parameter[] {
        return [
            new Parameter(value: '', attributes: ['operation' => 'and'])
        ];
    }

    // Query parameter type
    public function getSchema(): array {
        return ['type' => 'string'];
    }

    public function getOpenApiParameter(): OpenApi\Parameter {
        return ...;
    }
}

public function process(Operation $operation) {
    $request = $context['request'];

    foreach($operation->getFilters() as $filter) {
        foreach ($filter->parseQueryParameter($request->query, $context) as $parameter) {
            $this->queryParameterValidator->validate($filter, $parameter, $context);
            $filter->execute($filter, $parameter, $context);
        }
    }
}

TODO: see SerializerFilterContextBuilder: public function apply(Request $request, bool $normalization, array $attributes, array &$context): void; maybe something like:

class SerializerFilterInterface {
    public function getNormalizationContext(...);
    public function getDenormalizationContext(...);
}

Decision Outcome

Links