Skip to content

Commit eb32fd9

Browse files
committed
Manage custom queries by using the resolvers
1 parent 7f28ff9 commit eb32fd9

26 files changed

+687
-310
lines changed

features/bootstrap/DoctrineContext.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyAggregateOffer as DummyAggregateOfferDocument;
2121
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyCar as DummyCarDocument;
2222
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyCarColor as DummyCarColorDocument;
23+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyCustomQuery as DummyCustomQueryDocument;
2324
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDate as DummyDateDocument;
2425
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoCustom as DummyDtoCustomDocument;
2526
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoNoInput as DummyDtoNoInputDocument;
@@ -60,6 +61,7 @@
6061
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyAggregateOffer;
6162
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCar;
6263
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCarColor;
64+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCustomQuery;
6365
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDate;
6466
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoCustom;
6567
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoNoInput;
@@ -413,6 +415,20 @@ public function thereAreDummyDtoNoOutputObjects(int $nb)
413415
$this->manager->flush();
414416
}
415417

418+
/**
419+
* @Given there are :nb dummyCustomQuery objects
420+
*/
421+
public function thereAreDummyCustomQueryObjects(int $nb)
422+
{
423+
for ($i = 1; $i <= $nb; ++$i) {
424+
$dummyCustomQuery = $this->buildDummyCustomQuery();
425+
426+
$this->manager->persist($dummyCustomQuery);
427+
}
428+
429+
$this->manager->flush();
430+
}
431+
416432
/**
417433
* @Given there are :nb dummy objects with JSON and array data
418434
*/
@@ -1331,6 +1347,14 @@ private function buildDummyDtoNoOutput()
13311347
return $this->isOrm() ? new DummyDtoNoOutput() : new DummyDtoNoOutputDocument();
13321348
}
13331349

1350+
/**
1351+
* @return DummyCustomQuery|DummyCustomQueryDocument
1352+
*/
1353+
private function buildDummyCustomQuery()
1354+
{
1355+
return $this->isOrm() ? new DummyCustomQuery() : new DummyCustomQueryDocument();
1356+
}
1357+
13341358
/**
13351359
* @return DummyFriend|DummyFriendDocument
13361360
*/

features/graphql/query.feature

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,11 +284,35 @@ Feature: GraphQL query support
284284
}
285285
"""
286286

287+
Scenario: Custom not retrieved item query
288+
When I send the following GraphQL request:
289+
"""
290+
{
291+
testNotRetrievedItemDummyCustomQuery {
292+
message
293+
}
294+
}
295+
"""
296+
Then the response status code should be 200
297+
And the response should be in JSON
298+
And the header "Content-Type" should be equal to "application/json"
299+
And the JSON should be equal to:
300+
"""
301+
{
302+
"data": {
303+
"testNotRetrievedItemDummyCustomQuery": {
304+
"message": "Success (not retrieved)!"
305+
}
306+
}
307+
}
308+
"""
309+
287310
Scenario: Custom item query
311+
Given there are 2 dummyCustomQuery objects
288312
When I send the following GraphQL request:
289313
"""
290314
{
291-
testItemDummyCustomQuery {
315+
testItemDummyCustomQuery(id: "/dummy_custom_queries/1") {
292316
message
293317
}
294318
}
@@ -329,6 +353,9 @@ Feature: GraphQL query support
329353
"data": {
330354
"testCollectionDummyCustomQueries": {
331355
"edges": [
356+
{
357+
"node": {"message": "Success!"}
358+
},
332359
{
333360
"node": {"message": "Success!"}
334361
}

src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface;
2828
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
2929
use ApiPlatform\Core\Exception\RuntimeException;
30-
use ApiPlatform\Core\GraphQl\Resolver\QueryResolverInterface;
30+
use ApiPlatform\Core\GraphQl\Resolver\QueryCollectionResolverInterface;
31+
use ApiPlatform\Core\GraphQl\Resolver\QueryItemResolverInterface;
3132
use ApiPlatform\Core\GraphQl\Type\Definition\TypeInterface as GraphQlTypeInterface;
3233
use Doctrine\Common\Annotations\Annotation;
3334
use Doctrine\ORM\Version;
@@ -110,7 +111,9 @@ public function load(array $configs, ContainerBuilder $container)
110111
->addTag('api_platform.subresource_data_provider');
111112
$container->registerForAutoconfiguration(FilterInterface::class)
112113
->addTag('api_platform.filter');
113-
$container->registerForAutoconfiguration(QueryResolverInterface::class)
114+
$container->registerForAutoconfiguration(QueryItemResolverInterface::class)
115+
->addTag('api_platform.graphql.query_resolver');
116+
$container->registerForAutoconfiguration(QueryCollectionResolverInterface::class)
114117
->addTag('api_platform.graphql.query_resolver');
115118
$container->registerForAutoconfiguration(GraphQlTypeInterface::class)
116119
->addTag('api_platform.graphql.type');

src/Bridge/Symfony/Bundle/Resources/config/graphql.xml

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,18 @@
99

1010
<!-- Resolvers -->
1111

12+
<service id="api_platform.graphql.resolver.factory.item" class="ApiPlatform\Core\GraphQl\Resolver\Factory\ItemResolverFactory" public="false">
13+
<argument type="service" id="api_platform.iri_converter" />
14+
<argument type="service" id="api_platform.graphql.query_resolver_locator" />
15+
<argument type="service" id="serializer" />
16+
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
17+
<argument type="service" id="api_platform.security.resource_access_checker" on-invalid="ignore" />
18+
</service>
19+
1220
<service id="api_platform.graphql.resolver.factory.collection" class="ApiPlatform\Core\GraphQl\Resolver\Factory\CollectionResolverFactory" public="false">
1321
<argument type="service" id="api_platform.collection_data_provider" />
1422
<argument type="service" id="api_platform.subresource_data_provider" />
23+
<argument type="service" id="api_platform.graphql.query_resolver_locator" />
1524
<argument type="service" id="serializer" />
1625
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
1726
<argument type="service" id="api_platform.security.resource_access_checker" on-invalid="null" />
@@ -28,13 +37,6 @@
2837
<argument type="service" id="api_platform.validator" on-invalid="null" />
2938
</service>
3039

31-
<service id="api_platform.graphql.resolver.item" class="ApiPlatform\Core\GraphQl\Resolver\ItemResolver" public="false">
32-
<argument type="service" id="api_platform.iri_converter" />
33-
<argument type="service" id="serializer" />
34-
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
35-
<argument type="service" id="api_platform.security.resource_access_checker" on-invalid="ignore" />
36-
</service>
37-
3840
<service id="api_platform.graphql.resolver.resource_field" class="ApiPlatform\Core\GraphQl\Resolver\ResourceFieldResolver" public="false">
3941
<argument type="service" id="api_platform.iri_converter" />
4042
</service>
@@ -62,11 +64,10 @@
6264
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
6365
<argument type="service" id="api_platform.metadata.resource.name_collection_factory" />
6466
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
67+
<argument type="service" id="api_platform.graphql.resolver.factory.item" />
6568
<argument type="service" id="api_platform.graphql.resolver.factory.collection" />
6669
<argument type="service" id="api_platform.graphql.resolver.factory.item_mutation" />
67-
<argument type="service" id="api_platform.graphql.resolver.item" />
6870
<argument type="service" id="api_platform.graphql.resolver.resource_field" />
69-
<argument type="service" id="api_platform.graphql.query_resolver_locator" />
7071
<argument type="service" id="api_platform.graphql.types_factory" />
7172
<argument type="service" id="api_platform.filter_locator" />
7273
<argument>%api_platform.collection.pagination.enabled%</argument>

src/GraphQl/Resolver/Factory/CollectionResolverFactory.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@
1818
use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface;
1919
use ApiPlatform\Core\Exception\ResourceClassNotSupportedException;
2020
use ApiPlatform\Core\GraphQl\Resolver\FieldsToAttributesTrait;
21+
use ApiPlatform\Core\GraphQl\Resolver\QueryCollectionResolverInterface;
2122
use ApiPlatform\Core\GraphQl\Resolver\ResourceAccessCheckerTrait;
2223
use ApiPlatform\Core\GraphQl\Serializer\ItemNormalizer;
2324
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
2425
use ApiPlatform\Core\Security\ResourceAccessCheckerInterface;
2526
use GraphQL\Error\Error;
2627
use GraphQL\Type\Definition\ResolveInfo;
28+
use Psr\Container\ContainerInterface;
2729
use Symfony\Component\HttpFoundation\RequestStack;
2830
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
2931

@@ -42,16 +44,18 @@ final class CollectionResolverFactory implements ResolverFactoryInterface
4244

4345
private $collectionDataProvider;
4446
private $subresourceDataProvider;
47+
private $queryResolverLocator;
4548
private $normalizer;
4649
private $resourceAccessChecker;
4750
private $requestStack;
4851
private $paginationEnabled;
4952
private $resourceMetadataFactory;
5053

51-
public function __construct(CollectionDataProviderInterface $collectionDataProvider, SubresourceDataProviderInterface $subresourceDataProvider, NormalizerInterface $normalizer, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourceAccessCheckerInterface $resourceAccessChecker = null, RequestStack $requestStack = null, bool $paginationEnabled = false)
54+
public function __construct(CollectionDataProviderInterface $collectionDataProvider, SubresourceDataProviderInterface $subresourceDataProvider, ContainerInterface $queryResolverLocator, NormalizerInterface $normalizer, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourceAccessCheckerInterface $resourceAccessChecker = null, RequestStack $requestStack = null, bool $paginationEnabled = false)
5255
{
53-
$this->subresourceDataProvider = $subresourceDataProvider;
5456
$this->collectionDataProvider = $collectionDataProvider;
57+
$this->subresourceDataProvider = $subresourceDataProvider;
58+
$this->queryResolverLocator = $queryResolverLocator;
5559
$this->normalizer = $normalizer;
5660
$this->resourceAccessChecker = $resourceAccessChecker;
5761
$this->requestStack = $requestStack;
@@ -87,6 +91,13 @@ public function __invoke(string $resourceClass = null, string $rootClass = null,
8791
$collection = $this->collectionDataProvider->getCollection($resourceClass, null, $dataProviderContext);
8892
}
8993

94+
$queryResolverId = $resourceMetadata->getGraphqlAttribute($operationName ?? 'query', 'collection_query');
95+
if (null !== $queryResolverId) {
96+
/** @var QueryCollectionResolverInterface $queryResolver */
97+
$queryResolver = $this->queryResolverLocator->get($queryResolverId);
98+
$collection = $queryResolver($collection, ['source' => $source, 'args' => $args, 'info' => $info]);
99+
}
100+
90101
$this->canAccess($this->resourceAccessChecker, $resourceMetadata, $resourceClass, $info, $collection, $operationName ?? 'query');
91102

92103
if (!$this->paginationEnabled) {
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\GraphQl\Resolver\Factory;
15+
16+
use ApiPlatform\Core\Api\IriConverterInterface;
17+
use ApiPlatform\Core\Exception\ItemNotFoundException;
18+
use ApiPlatform\Core\Exception\RuntimeException;
19+
use ApiPlatform\Core\GraphQl\Resolver\FieldsToAttributesTrait;
20+
use ApiPlatform\Core\GraphQl\Resolver\QueryItemResolverInterface;
21+
use ApiPlatform\Core\GraphQl\Resolver\ResourceAccessCheckerTrait;
22+
use ApiPlatform\Core\GraphQl\Serializer\ItemNormalizer;
23+
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
24+
use ApiPlatform\Core\Security\ResourceAccessCheckerInterface;
25+
use ApiPlatform\Core\Util\ClassInfoTrait;
26+
use GraphQL\Type\Definition\ResolveInfo;
27+
use Psr\Container\ContainerInterface;
28+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
29+
30+
/**
31+
* Creates a function retrieving an item to resolve a GraphQL query.
32+
*
33+
* @experimental
34+
*
35+
* @author Alan Poulain <[email protected]>
36+
* @author Kévin Dunglas <[email protected]>
37+
*/
38+
final class ItemResolverFactory implements ResolverFactoryInterface
39+
{
40+
use ClassInfoTrait;
41+
use FieldsToAttributesTrait;
42+
use ResourceAccessCheckerTrait;
43+
44+
private $iriConverter;
45+
private $queryResolverLocator;
46+
private $resourceAccessChecker;
47+
private $normalizer;
48+
private $resourceMetadataFactory;
49+
50+
public function __construct(IriConverterInterface $iriConverter, ContainerInterface $queryResolverLocator, NormalizerInterface $normalizer, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourceAccessCheckerInterface $resourceAccessChecker = null)
51+
{
52+
$this->iriConverter = $iriConverter;
53+
$this->queryResolverLocator = $queryResolverLocator;
54+
$this->normalizer = $normalizer;
55+
$this->resourceMetadataFactory = $resourceMetadataFactory;
56+
$this->resourceAccessChecker = $resourceAccessChecker;
57+
}
58+
59+
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?string $operationName = null): callable
60+
{
61+
return function ($source, $args, $context, ResolveInfo $info) use ($resourceClass, $operationName) {
62+
// Data already fetched and normalized (field or nested resource)
63+
if (isset($source[$info->fieldName])) {
64+
return $source[$info->fieldName];
65+
}
66+
67+
$baseNormalizationContext = ['attributes' => $this->fieldsToAttributes($info)];
68+
$item = $this->getItem($args, $baseNormalizationContext);
69+
$resourceClass = $this->getResourceClass($item, $resourceClass);
70+
71+
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
72+
73+
$queryResolverId = $resourceMetadata->getGraphqlAttribute($operationName ?? 'query', 'item_query');
74+
if (null !== $queryResolverId) {
75+
/** @var QueryItemResolverInterface $queryResolver */
76+
$queryResolver = $this->queryResolverLocator->get($queryResolverId);
77+
$item = $queryResolver($item, ['source' => $source, 'args' => $args, 'info' => $info]);
78+
$resourceClass = $this->getResourceClass($item, $resourceClass, sprintf('Custom query resolver "%s"', $queryResolverId).' has to return an item of class %s but returned an item of class %s');
79+
}
80+
81+
$this->canAccess($this->resourceAccessChecker, $resourceMetadata, $resourceClass, $info, $item, $operationName ?? 'query');
82+
83+
$normalizationContext = $resourceMetadata->getGraphqlAttribute($operationName ?? 'query', 'normalization_context', [], true);
84+
85+
return $this->normalizer->normalize($item, ItemNormalizer::FORMAT, $normalizationContext + $baseNormalizationContext);
86+
};
87+
}
88+
89+
/**
90+
* @return object|null
91+
*/
92+
private function getItem($args, array $baseNormalizationContext)
93+
{
94+
if (!isset($args['id'])) {
95+
return null;
96+
}
97+
98+
try {
99+
$item = $this->iriConverter->getItemFromIri($args['id'], $baseNormalizationContext);
100+
} catch (ItemNotFoundException $e) {
101+
return null;
102+
}
103+
104+
return $item;
105+
}
106+
107+
/**
108+
* @param object|null $item
109+
*
110+
* @throws RuntimeException
111+
*/
112+
private function getResourceClass($item, ?string $resourceClass, string $errorMessage = 'Resolver only handles items of class %s but retrieved item is of class %s'): ?string
113+
{
114+
if (null === $item) {
115+
return $resourceClass;
116+
}
117+
118+
$itemClass = $this->getObjectClass($item);
119+
120+
if (null === $resourceClass) {
121+
return $itemClass;
122+
}
123+
124+
if ($resourceClass !== $itemClass) {
125+
throw new RuntimeException(sprintf($errorMessage, $resourceClass, $itemClass));
126+
}
127+
128+
return $resourceClass;
129+
}
130+
}

0 commit comments

Comments
 (0)