Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(doctrine): parameter filter extension #6248

Merged
merged 2 commits into from Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/Doctrine/Common/Filter/PropertyAwareFilterInterface.php
@@ -0,0 +1,27 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Doctrine\Common\Filter;

/**
* @author Antoine Bluchet <soyuka@gmail.com>
*
* @experimental
*/
interface PropertyAwareFilterInterface
{
/**
* @param string[] $properties
*/
public function setProperties(array $properties): void;
}
71 changes: 71 additions & 0 deletions src/Doctrine/Odm/Extension/ParameterExtension.php
@@ -0,0 +1,71 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Doctrine\Odm\Extension;

use ApiPlatform\Doctrine\Odm\Filter\FilterInterface;
use ApiPlatform\Metadata\Operation;
use Doctrine\ODM\MongoDB\Aggregation\Builder;
use Psr\Container\ContainerInterface;

/**
* Reads operation parameters and execute its filter.
*
* @author Antoine Bluchet <soyuka@gmail.com>
*/
final class ParameterExtension implements AggregationCollectionExtensionInterface, AggregationItemExtensionInterface
{
public function __construct(private readonly ContainerInterface $filterLocator)

Check warning on line 28 in src/Doctrine/Odm/Extension/ParameterExtension.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Extension/ParameterExtension.php#L28

Added line #L28 was not covered by tests
{
}

Check warning on line 30 in src/Doctrine/Odm/Extension/ParameterExtension.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Extension/ParameterExtension.php#L30

Added line #L30 was not covered by tests

private function applyFilter(Builder $aggregationBuilder, ?string $resourceClass = null, ?Operation $operation = null, array &$context = []): void

Check warning on line 32 in src/Doctrine/Odm/Extension/ParameterExtension.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Extension/ParameterExtension.php#L32

Added line #L32 was not covered by tests
{
foreach ($operation->getParameters() ?? [] as $parameter) {
$values = $parameter->getExtraProperties()['_api_values'] ?? [];
if (!$values) {
continue;

Check warning on line 37 in src/Doctrine/Odm/Extension/ParameterExtension.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Extension/ParameterExtension.php#L34-L37

Added lines #L34 - L37 were not covered by tests
}

if (null === ($filterId = $parameter->getFilter())) {
continue;

Check warning on line 41 in src/Doctrine/Odm/Extension/ParameterExtension.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Extension/ParameterExtension.php#L40-L41

Added lines #L40 - L41 were not covered by tests
}

$filter = $this->filterLocator->has($filterId) ? $this->filterLocator->get($filterId) : null;
if ($filter instanceof FilterInterface) {
$filterContext = ['filters' => $values];
$filter->apply($aggregationBuilder, $resourceClass, $operation, $filterContext);

Check warning on line 47 in src/Doctrine/Odm/Extension/ParameterExtension.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Extension/ParameterExtension.php#L44-L47

Added lines #L44 - L47 were not covered by tests
// update by reference
if (isset($filterContext['mongodb_odm_sort_fields'])) {
$context['mongodb_odm_sort_fields'] = $filterContext['mongodb_odm_sort_fields'];

Check warning on line 50 in src/Doctrine/Odm/Extension/ParameterExtension.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Extension/ParameterExtension.php#L49-L50

Added lines #L49 - L50 were not covered by tests
}
}
}
}

/**
* {@inheritdoc}
*/
public function applyToCollection(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void

Check warning on line 59 in src/Doctrine/Odm/Extension/ParameterExtension.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Extension/ParameterExtension.php#L59

Added line #L59 was not covered by tests
{
$this->applyFilter($aggregationBuilder, $resourceClass, $operation, $context);

Check warning on line 61 in src/Doctrine/Odm/Extension/ParameterExtension.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Extension/ParameterExtension.php#L61

Added line #L61 was not covered by tests
}

/**
* {@inheritdoc}
*/
public function applyToItem(Builder $aggregationBuilder, string $resourceClass, array $identifiers, ?Operation $operation = null, array &$context = []): void

Check warning on line 67 in src/Doctrine/Odm/Extension/ParameterExtension.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Extension/ParameterExtension.php#L67

Added line #L67 was not covered by tests
{
$this->applyFilter($aggregationBuilder, $resourceClass, $operation, $context);

Check warning on line 69 in src/Doctrine/Odm/Extension/ParameterExtension.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Extension/ParameterExtension.php#L69

Added line #L69 was not covered by tests
}
}
11 changes: 10 additions & 1 deletion src/Doctrine/Odm/Filter/AbstractFilter.php
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Doctrine\Odm\Filter;

use ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface;
use ApiPlatform\Doctrine\Common\PropertyHelperTrait;
use ApiPlatform\Doctrine\Odm\PropertyHelperTrait as MongoDbOdmPropertyHelperTrait;
use ApiPlatform\Metadata\Operation;
Expand All @@ -29,7 +30,7 @@
*
* @author Alan Poulain <contact@alanpoulain.eu>
*/
abstract class AbstractFilter implements FilterInterface
abstract class AbstractFilter implements FilterInterface, PropertyAwareFilterInterface
{
use MongoDbOdmPropertyHelperTrait;
use PropertyHelperTrait;
Expand Down Expand Up @@ -65,6 +66,14 @@
return $this->properties;
}

/**
* @param string[] $properties
*/
public function setProperties(array $properties): void

Check warning on line 72 in src/Doctrine/Odm/Filter/AbstractFilter.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Filter/AbstractFilter.php#L72

Added line #L72 was not covered by tests
{
$this->properties = $properties;

Check warning on line 74 in src/Doctrine/Odm/Filter/AbstractFilter.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Odm/Filter/AbstractFilter.php#L74

Added line #L74 was not covered by tests
}

protected function getLogger(): LoggerInterface
{
return $this->logger;
Expand Down
70 changes: 70 additions & 0 deletions src/Doctrine/Orm/Extension/ParameterExtension.php
@@ -0,0 +1,70 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Doctrine\Orm\Extension;

use ApiPlatform\Doctrine\Orm\Filter\FilterInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\QueryBuilder;
use Psr\Container\ContainerInterface;

/**
* Reads operation parameters and execute its filter.
*
* @author Antoine Bluchet <soyuka@gmail.com>
*/
final class ParameterExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
public function __construct(private readonly ContainerInterface $filterLocator)
{
}

/**
* @param array<string, mixed> $context
*/
private function applyFilter(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
{
foreach ($operation->getParameters() ?? [] as $parameter) {
$values = $parameter->getExtraProperties()['_api_values'] ?? [];
if (!$values) {
continue;

Check warning on line 41 in src/Doctrine/Orm/Extension/ParameterExtension.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Orm/Extension/ParameterExtension.php#L39-L41

Added lines #L39 - L41 were not covered by tests
}

if (null === ($filterId = $parameter->getFilter())) {
continue;

Check warning on line 45 in src/Doctrine/Orm/Extension/ParameterExtension.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Orm/Extension/ParameterExtension.php#L44-L45

Added lines #L44 - L45 were not covered by tests
}

$filter = $this->filterLocator->has($filterId) ? $this->filterLocator->get($filterId) : null;
if ($filter instanceof FilterInterface) {
$filter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, ['filters' => $values] + $context);

Check warning on line 50 in src/Doctrine/Orm/Extension/ParameterExtension.php

View check run for this annotation

Codecov / codecov/patch

src/Doctrine/Orm/Extension/ParameterExtension.php#L48-L50

Added lines #L48 - L50 were not covered by tests
}
}
}

/**
* {@inheritdoc}
*/
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
{
$this->applyFilter($queryBuilder, $queryNameGenerator, $resourceClass, $operation, $context);
}

/**
* {@inheritdoc}
*/
public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, ?Operation $operation = null, array $context = []): void
{
$this->applyFilter($queryBuilder, $queryNameGenerator, $resourceClass, $operation, $context);
}
}
11 changes: 10 additions & 1 deletion src/Doctrine/Orm/Filter/AbstractFilter.php
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Doctrine\Orm\Filter;

use ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface;
use ApiPlatform\Doctrine\Common\PropertyHelperTrait;
use ApiPlatform\Doctrine\Orm\PropertyHelperTrait as OrmPropertyHelperTrait;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
Expand All @@ -23,7 +24,7 @@
use Psr\Log\NullLogger;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;

abstract class AbstractFilter implements FilterInterface
abstract class AbstractFilter implements FilterInterface, PropertyAwareFilterInterface
{
use OrmPropertyHelperTrait;
use PropertyHelperTrait;
Expand Down Expand Up @@ -64,6 +65,14 @@ protected function getLogger(): LoggerInterface
return $this->logger;
}

/**
* @param string[] $properties
*/
public function setProperties(array $properties): void
{
$this->properties = $properties;
}

/**
* Determines whether the given property is enabled.
*/
Expand Down
112 changes: 107 additions & 5 deletions src/GraphQl/Type/FieldsBuilder.php
Expand Up @@ -290,9 +290,111 @@
$args[$id]['type'] = $this->typeConverter->resolveType($arg['type']);
}

/*
* This is @experimental, read the comment on the parameterToObjectType function as additional information.
*/
foreach ($operation->getParameters() ?? [] as $parameter) {
$key = $parameter->getKey();

Check warning on line 297 in src/GraphQl/Type/FieldsBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Type/FieldsBuilder.php#L296-L297

Added lines #L296 - L297 were not covered by tests

if (str_contains($key, ':property')) {
if (!($filterId = $parameter->getFilter()) || !$this->filterLocator->has($filterId)) {
continue;

Check warning on line 301 in src/GraphQl/Type/FieldsBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Type/FieldsBuilder.php#L299-L301

Added lines #L299 - L301 were not covered by tests
}

$parsedKey = explode('[:property]', $key);
$flattenFields = [];
foreach ($this->filterLocator->get($filterId)->getDescription($operation->getClass()) as $key => $value) {
$values = [];
parse_str($key, $values);
if (isset($values[$parsedKey[0]])) {
$values = $values[$parsedKey[0]];

Check warning on line 310 in src/GraphQl/Type/FieldsBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Type/FieldsBuilder.php#L304-L310

Added lines #L304 - L310 were not covered by tests
}

$name = key($values);
$flattenFields[] = ['name' => $name, 'required' => $value['required'] ?? null, 'description' => $value['description'] ?? null, 'leafs' => $values[$name], 'type' => $value['type'] ?? 'string'];

Check warning on line 314 in src/GraphQl/Type/FieldsBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Type/FieldsBuilder.php#L313-L314

Added lines #L313 - L314 were not covered by tests
}

$args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0]);
continue;

Check warning on line 318 in src/GraphQl/Type/FieldsBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Type/FieldsBuilder.php#L317-L318

Added lines #L317 - L318 were not covered by tests
}

$args[$key] = ['type' => GraphQLType::string()];

Check warning on line 321 in src/GraphQl/Type/FieldsBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Type/FieldsBuilder.php#L321

Added line #L321 was not covered by tests

if ($parameter->getRequired()) {
$args[$key]['type'] = GraphQLType::nonNull($args[$key]['type']);

Check warning on line 324 in src/GraphQl/Type/FieldsBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Type/FieldsBuilder.php#L323-L324

Added lines #L323 - L324 were not covered by tests
}
}

return $args;
}

/**
* Transform the result of a parse_str to a GraphQL object type.
* We should consider merging getFilterArgs and this, `getFilterArgs` uses `convertType` whereas we assume that parameters have only scalar types.
* Note that this method has a lower complexity then the `getFilterArgs` one.
* TODO: Is there a use case with an argument being a complex type (eg: a Resource, Enum etc.)?
*
* @param array<array{name: string, required: bool|null, description: string|null, leafs: string|array, type: string}> $flattenFields
*/
private function parameterToObjectType(array $flattenFields, string $name): InputObjectType

Check warning on line 339 in src/GraphQl/Type/FieldsBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Type/FieldsBuilder.php#L339

Added line #L339 was not covered by tests
{
$fields = [];
foreach ($flattenFields as $field) {
$key = $field['name'];
$type = $this->getParameterType(\in_array($field['type'], Type::$builtinTypes, true) ? new Type($field['type'], !$field['required']) : new Type('object', !$field['required'], $field['type']));

Check warning on line 344 in src/GraphQl/Type/FieldsBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Type/FieldsBuilder.php#L341-L344

Added lines #L341 - L344 were not covered by tests

if (\is_array($l = $field['leafs'])) {
if (0 === key($l)) {
$key = $key;
$type = GraphQLType::listOf($type);

Check warning on line 349 in src/GraphQl/Type/FieldsBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Type/FieldsBuilder.php#L346-L349

Added lines #L346 - L349 were not covered by tests
} else {
$n = [];
foreach ($field['leafs'] as $l => $value) {
$n[] = ['required' => null, 'name' => $l, 'leafs' => $value, 'type' => 'string', 'description' => null];

Check warning on line 353 in src/GraphQl/Type/FieldsBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Type/FieldsBuilder.php#L351-L353

Added lines #L351 - L353 were not covered by tests
}

$type = $this->parameterToObjectType($n, $key);
if (isset($fields[$key]) && ($t = $fields[$key]['type']) instanceof InputObjectType) {
$t = $fields[$key]['type'];
$t->config['fields'] = array_merge($t->config['fields'], $type->config['fields']);
$type = $t;

Check warning on line 360 in src/GraphQl/Type/FieldsBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Type/FieldsBuilder.php#L356-L360

Added lines #L356 - L360 were not covered by tests
}
}
}

if ($field['required']) {
$type = GraphQLType::nonNull($type);

Check warning on line 366 in src/GraphQl/Type/FieldsBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Type/FieldsBuilder.php#L365-L366

Added lines #L365 - L366 were not covered by tests
}

if (isset($fields[$key])) {
if ($type instanceof ListOfType) {
$key .= '_list';

Check warning on line 371 in src/GraphQl/Type/FieldsBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Type/FieldsBuilder.php#L369-L371

Added lines #L369 - L371 were not covered by tests
}
}

$fields[$key] = ['type' => $type, 'name' => $key];

Check warning on line 375 in src/GraphQl/Type/FieldsBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Type/FieldsBuilder.php#L375

Added line #L375 was not covered by tests
}

return new InputObjectType(['name' => $name, 'fields' => $fields]);

Check warning on line 378 in src/GraphQl/Type/FieldsBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Type/FieldsBuilder.php#L378

Added line #L378 was not covered by tests
}

/**
* A simplified version of convert type that does not support resources.
*/
private function getParameterType(Type $type): GraphQLType

Check warning on line 384 in src/GraphQl/Type/FieldsBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Type/FieldsBuilder.php#L384

Added line #L384 was not covered by tests
{
return match ($type->getBuiltinType()) {
Type::BUILTIN_TYPE_BOOL => GraphQLType::boolean(),
Type::BUILTIN_TYPE_INT => GraphQLType::int(),
Type::BUILTIN_TYPE_FLOAT => GraphQLType::float(),
Type::BUILTIN_TYPE_STRING => GraphQLType::string(),
Type::BUILTIN_TYPE_ARRAY => GraphQLType::listOf($this->getParameterType($type->getCollectionValueTypes()[0])),
Type::BUILTIN_TYPE_ITERABLE => GraphQLType::listOf($this->getParameterType($type->getCollectionValueTypes()[0])),
Type::BUILTIN_TYPE_OBJECT => GraphQLType::string(),
default => GraphQLType::string(),
};

Check warning on line 395 in src/GraphQl/Type/FieldsBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Type/FieldsBuilder.php#L386-L395

Added lines #L386 - L395 were not covered by tests
}

/**
* Get the field configuration of a resource.
*
Expand Down Expand Up @@ -450,9 +552,9 @@
}
}

foreach ($this->filterLocator->get($filterId)->getDescription($entityClass) as $key => $value) {
$nullable = isset($value['required']) ? !$value['required'] : true;
$filterType = \in_array($value['type'], Type::$builtinTypes, true) ? new Type($value['type'], $nullable) : new Type('object', $nullable, $value['type']);
foreach ($this->filterLocator->get($filterId)->getDescription($entityClass) as $key => $description) {
$nullable = isset($description['required']) ? !$description['required'] : true;
$filterType = \in_array($description['type'], Type::$builtinTypes, true) ? new Type($description['type'], $nullable) : new Type('object', $nullable, $description['type']);

Check warning on line 557 in src/GraphQl/Type/FieldsBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Type/FieldsBuilder.php#L555-L557

Added lines #L555 - L557 were not covered by tests
$graphqlFilterType = $this->convertType($filterType, false, $resourceOperation, $rootOperation, $resourceClass, $rootResource, $property, $depth);

if (str_ends_with($key, '[]')) {
Expand All @@ -467,8 +569,8 @@
if (\array_key_exists($key, $parsed) && \is_array($parsed[$key])) {
$parsed = [$key => ''];
}
array_walk_recursive($parsed, static function (&$value) use ($graphqlFilterType): void {
$value = $graphqlFilterType;
array_walk_recursive($parsed, static function (&$v) use ($graphqlFilterType): void {
$v = $graphqlFilterType;

Check warning on line 573 in src/GraphQl/Type/FieldsBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Type/FieldsBuilder.php#L572-L573

Added lines #L572 - L573 were not covered by tests
});
$args = $this->mergeFilterArgs($args, $parsed, $resourceOperation, $key);
}
Expand Down
Expand Up @@ -52,7 +52,7 @@ public function process(ContainerBuilder $container): void
*/
private function createFilterDefinitions(\ReflectionClass $resourceReflectionClass, ContainerBuilder $container): void
{
foreach ($this->readFilterAttributes($resourceReflectionClass) as $id => [$arguments, $filterClass]) {
foreach ($this->readFilterAttributes($resourceReflectionClass) as $id => [$arguments, $filterClass, $filterAttribute]) {
if ($container->has($id)) {
continue;
}
Expand All @@ -69,6 +69,10 @@ private function createFilterDefinitions(\ReflectionClass $resourceReflectionCla
}

$definition->addTag(self::TAG_FILTER_NAME);
if ($filterAttribute->alias) {
$definition->addTag(self::TAG_FILTER_NAME, ['id' => $filterAttribute->alias]);
}

$definition->setAutowired(true);

$parameterNames = [];
Expand Down