Skip to content

Commit 092818a

Browse files
lukaslueckealanpoulain
authored andcommitted
Use correct resource configuration for filter args of nested collection (#2970)
* Use correct resource configuration for filter args of nested collection #2969 * Revamp the getResourceFieldConfiguration implementation
1 parent cd1bfbc commit 092818a

File tree

9 files changed

+107
-64
lines changed

9 files changed

+107
-64
lines changed

features/graphql/filters.feature

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,33 @@ Feature: Collections filtering
107107
}
108108
}
109109
"""
110-
And the JSON node "data.dummies.edges[0].node.relatedDummies.edges" should have 0 elements
111-
And the JSON node "data.dummies.edges[1].node.relatedDummies.edges" should have 0 elements
110+
Then the JSON node "data.dummies.edges[0].node.relatedDummies.edges" should have 0 element
111+
And the JSON node "data.dummies.edges[1].node.relatedDummies.edges" should have 0 element
112112
And the JSON node "data.dummies.edges[2].node.relatedDummies.edges" should have 1 element
113113
And the JSON node "data.dummies.edges[2].node.relatedDummies.edges[0].node.name" should be equal to "RelatedDummy13"
114114

115+
@createSchema
116+
Scenario: Use a filter of a nested collection
117+
Given there is a DummyCar entity with related colors
118+
When I send the following GraphQL request:
119+
"""
120+
{
121+
dummyCar(id: "/dummy_cars/1") {
122+
id
123+
colors(prop: "blue") {
124+
edges {
125+
node {
126+
id
127+
prop
128+
}
129+
}
130+
}
131+
}
132+
}
133+
"""
134+
Then the JSON node "data.dummyCar.colors.edges" should have 1 element
135+
And the JSON node "data.dummyCar.colors.edges[0].node.prop" should be equal to "blue"
136+
115137
@createSchema
116138
Scenario: Retrieve a collection filtered using the related search filter
117139
Given there are 1 dummy objects having each 2 relatedDummies

src/GraphQl/Type/FieldsBuilder.php

Lines changed: 59 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Core\GraphQl\Type;
1515

16+
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
1617
use ApiPlatform\Core\GraphQl\Resolver\Factory\ResolverFactoryInterface;
1718
use ApiPlatform\Core\GraphQl\Type\Definition\TypeInterface;
1819
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
@@ -89,14 +90,14 @@ public function getQueryFields(string $resourceClass, ResourceMetadata $resource
8990

9091
$deprecationReason = $resourceMetadata->getGraphqlAttribute($queryName, 'deprecation_reason', '', true);
9192

92-
if (false !== $itemConfiguration && $fieldConfiguration = $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, null, null, $deprecationReason, new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass), $resourceClass, false, $queryName, null)) {
93+
if (false !== $itemConfiguration && $fieldConfiguration = $this->getResourceFieldConfiguration(null, null, $deprecationReason, new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass), $resourceClass, false, $queryName, null)) {
9394
$args = $this->resolveResourceArgs($itemConfiguration['args'] ?? [], $queryName, $shortName);
9495
$itemConfiguration['args'] = $args ?: $itemConfiguration['args'] ?? ['id' => ['type' => GraphQLType::nonNull(GraphQLType::id())]];
9596

9697
$queryFields[$fieldName] = array_merge($fieldConfiguration, $itemConfiguration);
9798
}
9899

99-
if (false !== $collectionConfiguration && $fieldConfiguration = $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, null, null, $deprecationReason, new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, $resourceClass)), $resourceClass, false, $queryName, null)) {
100+
if (false !== $collectionConfiguration && $fieldConfiguration = $this->getResourceFieldConfiguration(null, null, $deprecationReason, new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, $resourceClass)), $resourceClass, false, $queryName, null)) {
100101
$args = $this->resolveResourceArgs($collectionConfiguration['args'] ?? [], $queryName, $shortName);
101102
$collectionConfiguration['args'] = $args ?: $collectionConfiguration['args'] ?? $fieldConfiguration['args'];
102103

@@ -116,8 +117,8 @@ public function getMutationFields(string $resourceClass, ResourceMetadata $resou
116117
$resourceType = new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass);
117118
$deprecationReason = $resourceMetadata->getGraphqlAttribute($mutationName, 'deprecation_reason', '', true);
118119

119-
if ($fieldConfiguration = $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, null, ucfirst("{$mutationName}s a $shortName."), $deprecationReason, $resourceType, $resourceClass, false, null, $mutationName)) {
120-
$fieldConfiguration['args'] += ['input' => $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, null, null, $deprecationReason, $resourceType, $resourceClass, true, null, $mutationName)];
120+
if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, ucfirst("{$mutationName}s a $shortName."), $deprecationReason, $resourceType, $resourceClass, false, null, $mutationName)) {
121+
$fieldConfiguration['args'] += ['input' => $this->getResourceFieldConfiguration(null, null, $deprecationReason, $resourceType, $resourceClass, true, null, $mutationName)];
121122

122123
if (!$this->typeBuilder->isCollection($resourceType)) {
123124
$fieldConfiguration['resolve'] = ($this->itemMutationResolverFactory)($resourceClass, null, $mutationName);
@@ -175,15 +176,9 @@ public function getResourceObjectTypeFields(?string $resourceClass, ResourceMeta
175176
continue;
176177
}
177178

178-
$rootResource = $resourceClass;
179-
if (null !== $propertyMetadata->getSubresource()) {
180-
$resourceClass = $propertyMetadata->getSubresource()->getResourceClass();
181-
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
182-
}
183-
if ($fieldConfiguration = $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, $property, $propertyMetadata->getDescription(), $propertyMetadata->getAttribute('deprecation_reason', ''), $propertyType, $rootResource, $input, $queryName, $mutationName, $depth)) {
179+
if ($fieldConfiguration = $this->getResourceFieldConfiguration($property, $propertyMetadata->getDescription(), $propertyMetadata->getAttribute('deprecation_reason', ''), $propertyType, $resourceClass, $input, $queryName, $mutationName, $depth)) {
184180
$fields['id' === $property ? '_id' : $property] = $fieldConfiguration;
185181
}
186-
$resourceClass = $rootResource;
187182
}
188183
}
189184

@@ -215,19 +210,27 @@ public function resolveResourceArgs(array $args, string $operationName, string $
215210
*
216211
* @see http://webonyx.github.io/graphql-php/type-system/object-types/
217212
*/
218-
private function getResourceFieldConfiguration(string $resourceClass, ResourceMetadata $resourceMetadata, ?string $property, ?string $fieldDescription, string $deprecationReason, Type $type, string $rootResource, bool $input, ?string $queryName, ?string $mutationName, int $depth = 0): ?array
213+
private function getResourceFieldConfiguration(?string $property, ?string $fieldDescription, string $deprecationReason, Type $type, string $rootResource, bool $input, ?string $queryName, ?string $mutationName, int $depth = 0): ?array
219214
{
220215
try {
221-
if (null === $graphqlType = $this->convertType($type, $input, $queryName, $mutationName, $resourceClass, $property, $depth)) {
216+
$resourceClass = $this->typeBuilder->isCollection($type) && ($collectionValueType = $type->getCollectionValueType()) ? $collectionValueType->getClassName() : $type->getClassName();
217+
218+
if (null === $graphqlType = $this->convertType($type, $input, $queryName, $mutationName, $resourceClass ?? '', $rootResource, $property, $depth)) {
222219
return null;
223220
}
224221

225222
$graphqlWrappedType = $graphqlType instanceof WrappingType ? $graphqlType->getWrappedType() : $graphqlType;
226223
$isStandardGraphqlType = \in_array($graphqlWrappedType, GraphQLType::getStandardTypes(), true);
227224
if ($isStandardGraphqlType) {
228-
$className = '';
229-
} else {
230-
$className = $this->typeBuilder->isCollection($type) && ($collectionValueType = $type->getCollectionValueType()) ? $collectionValueType->getClassName() : $type->getClassName();
225+
$resourceClass = '';
226+
}
227+
228+
$resourceMetadata = null;
229+
if (!empty($resourceClass)) {
230+
try {
231+
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
232+
} catch (ResourceClassNotFoundException $e) {
233+
}
231234
}
232235

233236
$args = [];
@@ -253,40 +256,15 @@ private function getResourceFieldConfiguration(string $resourceClass, ResourceMe
253256
];
254257
}
255258

256-
foreach ($resourceMetadata->getGraphqlAttribute($queryName ?? 'query', 'filters', [], true) as $filterId) {
257-
if (null === $this->filterLocator || !$this->filterLocator->has($filterId)) {
258-
continue;
259-
}
260-
261-
foreach ($this->filterLocator->get($filterId)->getDescription($resourceClass) as $key => $value) {
262-
$nullable = isset($value['required']) ? !$value['required'] : true;
263-
$filterType = \in_array($value['type'], Type::$builtinTypes, true) ? new Type($value['type'], $nullable) : new Type('object', $nullable, $value['type']);
264-
$graphqlFilterType = $this->convertType($filterType, false, $queryName, $mutationName, $resourceClass, $property, $depth);
265-
266-
if ('[]' === substr($key, -2)) {
267-
$graphqlFilterType = GraphQLType::listOf($graphqlFilterType);
268-
$key = substr($key, 0, -2).'_list';
269-
}
270-
271-
parse_str($key, $parsed);
272-
if (\array_key_exists($key, $parsed) && \is_array($parsed[$key])) {
273-
$parsed = [$key => ''];
274-
}
275-
array_walk_recursive($parsed, function (&$value) use ($graphqlFilterType) {
276-
$value = $graphqlFilterType;
277-
});
278-
$args = $this->mergeFilterArgs($args, $parsed, $resourceMetadata, $key);
279-
}
280-
}
281-
$args = $this->convertFilterArgsToTypes($args);
259+
$args = $this->getFilterArgs($args, $resourceClass, $resourceMetadata, $rootResource, $property, $queryName, $mutationName, $depth);
282260
}
283261

284262
if ($isStandardGraphqlType || $input) {
285263
$resolve = null;
286264
} elseif ($this->typeBuilder->isCollection($type)) {
287-
$resolve = ($this->collectionResolverFactory)($className, $rootResource, $queryName);
265+
$resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $queryName);
288266
} else {
289-
$resolve = ($this->itemResolverFactory)($className, $rootResource, $queryName);
267+
$resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $queryName);
290268
}
291269

292270
return [
@@ -303,6 +281,41 @@ private function getResourceFieldConfiguration(string $resourceClass, ResourceMe
303281
return null;
304282
}
305283

284+
private function getFilterArgs(array $args, ?string $resourceClass, ?ResourceMetadata $resourceMetadata, string $rootResource, ?string $property, ?string $queryName, ?string $mutationName, int $depth): array
285+
{
286+
if (null === $resourceMetadata || null === $resourceClass) {
287+
return $args;
288+
}
289+
290+
foreach ($resourceMetadata->getGraphqlAttribute($queryName ?? 'query', 'filters', [], true) as $filterId) {
291+
if (null === $this->filterLocator || !$this->filterLocator->has($filterId)) {
292+
continue;
293+
}
294+
295+
foreach ($this->filterLocator->get($filterId)->getDescription($resourceClass) as $key => $value) {
296+
$nullable = isset($value['required']) ? !$value['required'] : true;
297+
$filterType = \in_array($value['type'], Type::$builtinTypes, true) ? new Type($value['type'], $nullable) : new Type('object', $nullable, $value['type']);
298+
$graphqlFilterType = $this->convertType($filterType, false, $queryName, $mutationName, $resourceClass, $rootResource, $property, $depth);
299+
300+
if ('[]' === substr($key, -2)) {
301+
$graphqlFilterType = GraphQLType::listOf($graphqlFilterType);
302+
$key = substr($key, 0, -2).'_list';
303+
}
304+
305+
parse_str($key, $parsed);
306+
if (\array_key_exists($key, $parsed) && \is_array($parsed[$key])) {
307+
$parsed = [$key => ''];
308+
}
309+
array_walk_recursive($parsed, function (&$value) use ($graphqlFilterType) {
310+
$value = $graphqlFilterType;
311+
});
312+
$args = $this->mergeFilterArgs($args, $parsed, $resourceMetadata, $key);
313+
}
314+
}
315+
316+
return $this->convertFilterArgsToTypes($args);
317+
}
318+
306319
private function mergeFilterArgs(array $args, array $parsed, ResourceMetadata $resourceMetadata = null, $original = ''): array
307320
{
308321
foreach ($parsed as $key => $value) {
@@ -367,9 +380,9 @@ private function convertFilterArgsToTypes(array $args): array
367380
*
368381
* @throws InvalidTypeException
369382
*/
370-
private function convertType(Type $type, bool $input, ?string $queryName, ?string $mutationName, string $resourceClass, ?string $property, int $depth)
383+
private function convertType(Type $type, bool $input, ?string $queryName, ?string $mutationName, string $resourceClass, string $rootResource, ?string $property, int $depth)
371384
{
372-
$graphqlType = $this->typeConverter->convertType($type, $input, $queryName, $mutationName, $resourceClass, $property, $depth);
385+
$graphqlType = $this->typeConverter->convertType($type, $input, $queryName, $mutationName, $resourceClass, $rootResource, $property, $depth);
373386

374387
if (null === $graphqlType) {
375388
throw new InvalidTypeException(sprintf('The type "%s" is not supported.', $type->getBuiltinType()));

src/GraphQl/Type/TypeConverter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public function __construct(TypeBuilderInterface $typeBuilder, TypesContainerInt
4949
/**
5050
* {@inheritdoc}
5151
*/
52-
public function convertType(Type $type, bool $input, ?string $queryName, ?string $mutationName, string $resourceClass, ?string $property, int $depth)
52+
public function convertType(Type $type, bool $input, ?string $queryName, ?string $mutationName, string $resourceClass, string $rootResource, ?string $property, int $depth)
5353
{
5454
switch ($type->getBuiltinType()) {
5555
case Type::BUILTIN_TYPE_BOOL:

src/GraphQl/Type/TypeConverterInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ interface TypeConverterInterface
3131
*
3232
* @return string|GraphQLType|null
3333
*/
34-
public function convertType(Type $type, bool $input, ?string $queryName, ?string $mutationName, string $resourceClass, ?string $property, int $depth);
34+
public function convertType(Type $type, bool $input, ?string $queryName, ?string $mutationName, string $resourceClass, string $rootResource, ?string $property, int $depth);
3535

3636
/**
3737
* Resolves a type written with the GraphQL type system to its object representation.

tests/Fixtures/TestBundle/Document/DummyCarColor.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313

1414
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Document;
1515

16+
use ApiPlatform\Core\Annotation\ApiFilter;
1617
use ApiPlatform\Core\Annotation\ApiResource;
18+
use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Filter\SearchFilter;
1719
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
1820
use Symfony\Component\Serializer\Annotation as Serializer;
1921
use Symfony\Component\Validator\Constraints as Assert;
@@ -43,6 +45,7 @@ class DummyCarColor
4345
* @var string
4446
*
4547
* @ODM\Field(nullable=false)
48+
* @ApiFilter(SearchFilter::class)
4649
* @Assert\NotBlank
4750
*
4851
* @Serializer\Groups({"colors"})

tests/Fixtures/TestBundle/Entity/DummyCarColor.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313

1414
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity;
1515

16+
use ApiPlatform\Core\Annotation\ApiFilter;
1617
use ApiPlatform\Core\Annotation\ApiResource;
18+
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
1719
use Doctrine\ORM\Mapping as ORM;
1820
use Symfony\Component\Serializer\Annotation as Serializer;
1921
use Symfony\Component\Validator\Constraints as Assert;
@@ -46,6 +48,7 @@ class DummyCarColor
4648
* @var string
4749
*
4850
* @ORM\Column(nullable=false)
51+
* @ApiFilter(SearchFilter::class)
4952
* @Assert\NotBlank
5053
*
5154
* @Serializer\Groups({"colors"})

tests/Fixtures/TestBundle/GraphQl/Type/TypeConverter.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,17 @@ public function __construct(TypeConverterInterface $defaultTypeConverter)
3636
/**
3737
* {@inheritdoc}
3838
*/
39-
public function convertType(Type $type, bool $input, ?string $queryName, ?string $mutationName, string $resourceClass, ?string $property, int $depth)
39+
public function convertType(Type $type, bool $input, ?string $queryName, ?string $mutationName, string $resourceClass, string $rootResource, ?string $property, int $depth)
4040
{
4141
if ('dummyDate' === $property
42-
&& \in_array($resourceClass, [Dummy::class, DummyDocument::class], true)
42+
&& \in_array($rootResource, [Dummy::class, DummyDocument::class], true)
4343
&& Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType()
4444
&& is_a($type->getClassName(), \DateTimeInterface::class, true)
4545
) {
4646
return 'DateTime';
4747
}
4848

49-
return $this->defaultTypeConverter->convertType($type, $input, $queryName, $mutationName, $resourceClass, $property, $depth);
49+
return $this->defaultTypeConverter->convertType($type, $input, $queryName, $mutationName, $resourceClass, $rootResource, $property, $depth);
5050
}
5151

5252
/**

0 commit comments

Comments
 (0)