Skip to content

Commit 5bc84ce

Browse files
authored
fix(graphql): use default nested query operations (#5174)
Instead of relying on a resource metadata factory to retrieve a "dynamic" nested query operation, this PR adds default nested query operations if the resource does not have one. It simplifies a lot of things and catching the OperationNotFound exception is not needed anymore. Since operations are "nested", they do not appear as top-level queries. This PR also improves the XML/YAML compatibility.
1 parent dbf4447 commit 5bc84ce

25 files changed

+216
-324
lines changed

src/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactory.php

-58
This file was deleted.

src/GraphQl/Type/FieldsBuilder.php

+10-13
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
namespace ApiPlatform\GraphQl\Type;
1515

1616
use ApiPlatform\Api\ResourceClassResolverInterface;
17-
use ApiPlatform\Exception\OperationNotFoundException;
1817
use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface;
1918
use ApiPlatform\GraphQl\Type\Definition\TypeInterface;
2019
use ApiPlatform\Metadata\GraphQl\Mutation;
@@ -45,7 +44,7 @@
4544
*/
4645
final class FieldsBuilder implements FieldsBuilderInterface
4746
{
48-
public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, private readonly TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator, private readonly ?ResourceMetadataCollectionFactoryInterface $graphQlNestedOperationResourceMetadataFactory = null)
47+
public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, private readonly TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator)
4948
{
5049
}
5150

@@ -68,6 +67,10 @@ public function getNodeQueryFields(): array
6867
*/
6968
public function getItemQueryFields(string $resourceClass, Operation $operation, array $configuration): array
7069
{
70+
if ($operation instanceof Query && $operation->getNested()) {
71+
return [];
72+
}
73+
7174
$fieldName = lcfirst('item_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName());
7275

7376
if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $operation->getDescription(), $operation->getDeprecationReason(), new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass), $resourceClass, false, $operation)) {
@@ -85,6 +88,10 @@ public function getItemQueryFields(string $resourceClass, Operation $operation,
8588
*/
8689
public function getCollectionQueryFields(string $resourceClass, Operation $operation, array $configuration): array
8790
{
91+
if ($operation instanceof Query && $operation->getNested()) {
92+
return [];
93+
}
94+
8895
$fieldName = lcfirst('collection_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName());
8996

9097
if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $operation->getDescription(), $operation->getDeprecationReason(), new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, $resourceClass)), $resourceClass, false, $operation)) {
@@ -257,17 +264,7 @@ private function getResourceFieldConfiguration(?string $property, ?string $field
257264
$resourceOperation = $rootOperation;
258265
if ($resourceClass && $rootOperation->getClass() && $this->resourceClassResolver->isResourceClass($resourceClass) && $rootOperation->getClass() !== $resourceClass) {
259266
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
260-
try {
261-
$resourceOperation = $resourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query');
262-
} catch (OperationNotFoundException) {
263-
// If there is no query operation for a nested resource we force one to exist
264-
$nestedResourceMetadataCollection = $this->graphQlNestedOperationResourceMetadataFactory->create($resourceClass);
265-
$resourceOperation = $nestedResourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query');
266-
// Add filters from the metadata defined on the resource itself.
267-
if ($filters = $resourceMetadataCollection[0]?->getFilters()) {
268-
$resourceOperation = $resourceOperation->withFilters($filters);
269-
}
270-
}
267+
$resourceOperation = $resourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query');
271268
}
272269

273270
if (!$resourceOperation instanceof Operation) {

src/GraphQl/Type/SchemaBuilder.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public function getSchema(): Schema
5050
foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
5151
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
5252
foreach ($resourceMetadataCollection as $resourceMetadata) {
53-
foreach ($resourceMetadata->getGraphQlOperations() ?? [] as $operationName => $operation) {
53+
foreach ($resourceMetadata->getGraphQlOperations() ?? [] as $operation) {
5454
$configuration = null !== $operation->getArgs() ? ['args' => $operation->getArgs()] : [];
5555

5656
if ($operation instanceof Query && $operation instanceof CollectionOperationInterface) {

src/GraphQl/Type/TypeBuilder.php

+4-14
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
use ApiPlatform\Metadata\GraphQl\Mutation;
2020
use ApiPlatform\Metadata\GraphQl\Operation;
2121
use ApiPlatform\Metadata\GraphQl\Query;
22-
use ApiPlatform\Metadata\GraphQl\QueryCollection;
2322
use ApiPlatform\Metadata\GraphQl\Subscription;
2423
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
2524
use ApiPlatform\State\Pagination\Pagination;
@@ -74,12 +73,9 @@ public function getResourceObjectType(?string $resourceClass, ResourceMetadataCo
7473
}
7574

7675
if ('item_query' === $operationName || 'collection_query' === $operationName) {
77-
// Test if the collection/item operation exists and it has different groups
78-
try {
79-
if ($resourceMetadataCollection->getOperation($operation instanceof CollectionOperationInterface ? 'item_query' : 'collection_query')->getNormalizationContext() !== $operation->getNormalizationContext()) {
80-
$shortName .= $operation instanceof CollectionOperationInterface ? 'Collection' : 'Item';
81-
}
82-
} catch (OperationNotFoundException) {
76+
// Test if the collection/item has different groups
77+
if ($resourceMetadataCollection->getOperation($operation instanceof CollectionOperationInterface ? 'item_query' : 'collection_query')->getNormalizationContext() !== $operation->getNormalizationContext()) {
78+
$shortName .= $operation instanceof CollectionOperationInterface ? 'Collection' : 'Item';
8379
}
8480
}
8581

@@ -126,13 +122,7 @@ public function getResourceObjectType(?string $resourceClass, ResourceMetadataCo
126122
$wrappedOperationName = $operation instanceof Query ? $operationName : 'item_query';
127123
}
128124

129-
try {
130-
$wrappedOperation = $resourceMetadataCollection->getOperation($wrappedOperationName);
131-
} catch (OperationNotFoundException) {
132-
$wrappedOperation = ('collection_query' === $wrappedOperationName ? new QueryCollection() : new Query())
133-
->withResource($resourceMetadataCollection[0])
134-
->withName($wrappedOperationName);
135-
}
125+
$wrappedOperation = $resourceMetadataCollection->getOperation($wrappedOperationName);
136126

137127
$fields = [
138128
lcfirst($wrappedOperation->getShortName()) => $this->getResourceObjectType($resourceClass, $resourceMetadataCollection, $wrappedOperation instanceof Operation ? $wrappedOperation : null, $input, true, $depth),

src/GraphQl/Type/TypeConverter.php

+7-14
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,8 @@
1616
use ApiPlatform\Exception\InvalidArgumentException;
1717
use ApiPlatform\Exception\OperationNotFoundException;
1818
use ApiPlatform\Exception\ResourceClassNotFoundException;
19-
use ApiPlatform\Metadata\CollectionOperationInterface;
2019
use ApiPlatform\Metadata\GraphQl\Operation;
2120
use ApiPlatform\Metadata\GraphQl\Query;
22-
use ApiPlatform\Metadata\GraphQl\QueryCollection;
2321
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2422
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2523
use GraphQL\Error\SyntaxError;
@@ -144,25 +142,20 @@ private function getResourceType(Type $type, bool $input, Operation $rootOperati
144142
}
145143

146144
$operationName = $rootOperation->getName();
147-
$isCollection = $rootOperation instanceof CollectionOperationInterface || 'collection_query' === $operationName;
145+
$isCollection = $this->typeBuilder->isCollection($type);
148146

149-
// We're retrieving the type of a property which is a relation to the rootResource
150-
if ($resourceClass !== $rootResource && $property && $rootOperation instanceof Query) {
151-
$isCollection = $this->typeBuilder->isCollection($type);
147+
// We're retrieving the type of a property which is a relation to the root resource.
148+
if ($resourceClass !== $rootResource && $rootOperation instanceof Query) {
152149
$operationName = $isCollection ? 'collection_query' : 'item_query';
153150
}
154151

155152
try {
156153
$operation = $resourceMetadataCollection->getOperation($operationName);
157-
158-
if (!$operation instanceof Operation) {
159-
throw new OperationNotFoundException();
160-
}
161154
} catch (OperationNotFoundException) {
162-
/** @var Operation $operation */
163-
$operation = ($isCollection ? new QueryCollection() : new Query())
164-
->withResource($resourceMetadataCollection[0])
165-
->withName($operationName);
155+
$operation = $resourceMetadataCollection->getOperation($isCollection ? 'collection_query' : 'item_query');
156+
}
157+
if (!$operation instanceof Operation) {
158+
throw new OperationNotFoundException();
166159
}
167160

168161
return $this->typeBuilder->getResourceObjectType($resourceClass, $resourceMetadataCollection, $operation, $input, false, $depth);

src/Metadata/Extractor/XmlResourceExtractor.php

+12-1
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515

1616
use ApiPlatform\Exception\InvalidArgumentException;
1717
use ApiPlatform\Metadata\GetCollection;
18+
use ApiPlatform\Metadata\GraphQl\DeleteMutation;
1819
use ApiPlatform\Metadata\GraphQl\Mutation;
1920
use ApiPlatform\Metadata\GraphQl\Query;
21+
use ApiPlatform\Metadata\GraphQl\QueryCollection;
2022
use ApiPlatform\Metadata\GraphQl\Subscription;
2123
use ApiPlatform\Metadata\Post;
2224
use Symfony\Component\Config\Util\XmlUtils;
@@ -332,9 +334,18 @@ private function buildGraphQlOperations(\SimpleXMLElement $resource, array $root
332334
}
333335
}
334336

337+
$collection = $this->phpize($operation, 'collection', 'bool', false);
338+
if (Query::class === $class && $collection) {
339+
$class = QueryCollection::class;
340+
}
341+
342+
$delete = $this->phpize($operation, 'delete', 'bool', false);
343+
if (Mutation::class === $class && $delete) {
344+
$class = DeleteMutation::class;
345+
}
346+
335347
$data[] = array_merge($datum, [
336348
'graphql_operation_class' => $class,
337-
'collection' => $this->phpize($operation, 'collection', 'bool'),
338349
'resolver' => $this->phpize($operation, 'resolver', 'string'),
339350
'args' => $this->buildArgs($operation),
340351
'class' => $this->phpize($operation, 'class', 'string'),

src/Metadata/Extractor/schema/resources.xsd

+2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@
6565
<xsd:attributeGroup ref="base"/>
6666
<xsd:attribute type="xsd:string" name="resolver"/>
6767
<xsd:attribute type="xsd:string" name="class"/>
68+
<xsd:attribute type="xsd:boolean" name="collection"/>
69+
<xsd:attribute type="xsd:boolean" name="delete"/>
6870
<xsd:attribute type="xsd:boolean" name="read"/>
6971
<xsd:attribute type="xsd:boolean" name="deserialize"/>
7072
<xsd:attribute type="xsd:boolean" name="validate"/>

src/Metadata/GraphQl/Operation.php

-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
class Operation extends AbstractOperation
2020
{
2121
/**
22-
* @param string $resolver
2322
* @param mixed|null $input
2423
* @param mixed|null $output
2524
* @param mixed|null $mercure

src/Metadata/GraphQl/Query.php

+16-1
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,24 @@ public function __construct(
6868
?string $name = null,
6969
$provider = null,
7070
$processor = null,
71-
array $extraProperties = []
71+
array $extraProperties = [],
72+
73+
protected ?bool $nested = null,
7274
) {
7375
parent::__construct(...\func_get_args());
7476
$this->name = $name ?: 'item_query';
7577
}
78+
79+
public function getNested(): ?bool
80+
{
81+
return $this->nested;
82+
}
83+
84+
public function withNested(?bool $nested = null): self
85+
{
86+
$self = clone $this;
87+
$self->nested = $nested;
88+
89+
return $self;
90+
}
7691
}

src/Metadata/GraphQl/QueryCollection.php

+16-1
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,24 @@ public function __construct(
7070
?string $name = null,
7171
$provider = null,
7272
$processor = null,
73-
array $extraProperties = []
73+
array $extraProperties = [],
74+
75+
protected ?bool $nested = null,
7476
) {
7577
parent::__construct(...\func_get_args());
7678
$this->name = $name ?: 'collection_query';
7779
}
80+
81+
public function getNested(): ?bool
82+
{
83+
return $this->nested;
84+
}
85+
86+
public function withNested(?bool $nested = null): self
87+
{
88+
$self = clone $this;
89+
$self->nested = $nested;
90+
91+
return $self;
92+
}
7893
}

0 commit comments

Comments
 (0)