Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
[ci skip]
  • Loading branch information
soyuka committed Feb 1, 2024
1 parent dd2f9d6 commit e817b1d
Showing 1 changed file with 76 additions and 45 deletions.
121 changes: 76 additions & 45 deletions docs/adr/0006-filters.md
Expand Up @@ -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:
Expand Down Expand Up @@ -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 will need one interface per persistence layer supported.
* As usual this is either a callable either a symfony service.
*
* @param iterable<mixed>|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<HttpOperation>
*/
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

Expand Down

0 comments on commit e817b1d

Please sign in to comment.