diff --git a/docs/adr/0006-filters.md b/docs/adr/0006-filters.md index 57f221b939..bfe46f2ae3 100644 --- a/docs/adr/0006-filters.md +++ b/docs/adr/0006-filters.md @@ -62,8 +62,6 @@ The idea of this ADR is to find a way to introduce more functionalities to API P 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: @@ -96,70 +94,103 @@ Also, if someone wants to implement the [loopback API](https://loopback.io/doc/e 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: - -```php -// how to give uidfilter the parameters it should declare? -// is it automatic if we find a property having the uid type? -#[Get(filters: [new SearchFilter(), new UidFilter()]) -#[Parameter('key', schema: ['type' => 'string'])] // add transform + validate extension points -class Book { +## Considered Options -} +Let's define a new Attribute `Parameter` that holds informations (filters, context, schema) tight to a parameter `key`. +```php final class Parameter { - mixed $value; - ?string $property; - ?string $class; - array $attributes; + public string $key; + public \ArrayObject schema; + public array $context; + public function provider(): Operation; + + /** + * The filters should be called within the API Platform state providers as they alter the Doctrine/Elasticsearch Query, + * therefore we probably will need one interface per persistence layer supported. + * As usual this is either a callable either a symfony service. + * + * @param iterable|mixed $value + */ + public function filter(Parameter $parameter, $value, array $context); } +``` -class FilterInterface {} +API Platform will continue to provide parsed query parameters and set an `_api_query_parameters` Request attribute, in the end the filter may or may not use it: -class UidFilter { - public function __construct(private readonly string $class) {} +```php +$queryString = RequestParser::getQueryString($request); +$request->attributes->set($queryString ? RequestParser::parseRequestParams($queryString) : []); +``` - public function parseQueryParameter(array $queryParameters = []): Parameter[] { - return [ - new Parameter(value: '', attributes: ['operation' => 'and']) - ]; - } +### Parameter Provider - // Query parameter type - public function getSchema(): array { - return ['type' => 'string']; - } +During the `Provider` phase (`RequestEvent::REQUEST`), we could use a `ParameterProvider`: - public function getOpenApiParameter(): OpenApi\Parameter { - return ...; - } +```php +/** + * Optionnaly transforms request parameters and provides modification to the current Operation. + * + * @implements ProviderInterface + */ +interface ParameterProvider extends ProviderInterface { + public function provider(Operation $operation, array $uriVariables = [], array $context = []): HttpOperation; } +``` -public function process(Operation $operation) { - $request = $context['request']; +This provider can: - foreach($operation->getFilters() as $filter) { - foreach ($filter->parseQueryParameter($request->query, $context) as $parameter) { - $this->queryParameterValidator->validate($filter, $parameter, $context); - $filter->execute($filter, $parameter, $context); - } +1. alter the HTTP Operation to provide additional context: + +```php +class GroupsParameterProvider implements ProviderInterface { + public function provider(Operation $operation, array $uriVariables = [], array $context = []): HttpOperation + { + $request = $context['request']; + return $operation->withNormalizationContext(['groups' => $request->query->all('groups')]); } } ``` +2. alter the parameter context: -TODO: -see SerializerFilterContextBuilder: public function apply(Request $request, bool $normalization, array $attributes, array &$context): void; -maybe something like: +```php +class UuidParameter implements ProviderInterface { + public function provider(Operation $operation, array $uriVariables = [], array $context = []): HttpOperation + { + $request = $context['request']; + $parameters = $request->attributes->get('_api_query_parameters'); + foreach ($parameters as $key => $value) { + $parameter = $operation->getParameter($key); + if (!$parameter) { + continue; + } + + if (!in_array('uuid', $parameter->getSchema()['type'])) { + continue; + } + + // TODO: should handle array values + try { + $parameters[$key] = Uuid::fromString($value); + } catch (\Exception $e) {} + + if ($parameter->getFilter() === SearchFilter::class) { + // Additionnaly, we are now sure we want an uuid filter so we could change it: + $operation->withParameter($key, $parameter->withFilter(UuidFilter::class)); + } + } -``` -class SerializerFilterInterface { - public function getNormalizationContext(...); - public function getDenormalizationContext(...); + return $operation; + } } ``` -## Decision Outcome +3. Validate parameters through the QueryParameterValidator. + +### Filters + +Filters should remain mostly unchanged, the current informations about the `property` to filter should be specified inside a `Parameter`'s `context`. ## Links