Skip to content

Commit c3e631e

Browse files
vincentchalamondunglas
authored andcommitted
Support DTO class on GraphQL (#2427)
* Support DTO class on GraphQL * Fix review * Add Behat tests * Fix CS * Fix left over * Handle false value as input_class/output_class * Fix review * Optimize imports * Revert optimize imports * Fix review
1 parent 009e86a commit c3e631e

File tree

10 files changed

+460
-20
lines changed

10 files changed

+460
-20
lines changed

features/bootstrap/FeatureContext.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCar;
2323
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCarColor;
2424
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDate;
25+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoNoInput;
26+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoNoOutput;
2527
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyFriend;
2628
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyGroup;
2729
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyImmutableDate;
@@ -346,6 +348,38 @@ public function thereAreDummyObjectsWithRelatedDummy(int $nb)
346348
$this->manager->flush();
347349
}
348350

351+
/**
352+
* @Given there are :nb dummyDtoNoInput objects
353+
*/
354+
public function thereAreDummyDtoNoInputObjects(int $nb)
355+
{
356+
for ($i = 1; $i <= $nb; ++$i) {
357+
$dummyDto = new DummyDtoNoInput();
358+
$dummyDto->lorem = 'DummyDtoNoInput foo #'.$i;
359+
$dummyDto->ipsum = round($i / 3, 2);
360+
361+
$this->manager->persist($dummyDto);
362+
}
363+
364+
$this->manager->flush();
365+
}
366+
367+
/**
368+
* @Given there are :nb dummyDtoNoOutput objects
369+
*/
370+
public function thereAreDummyDtoNoOutputObjects(int $nb)
371+
{
372+
for ($i = 1; $i <= $nb; ++$i) {
373+
$dummyDto = new DummyDtoNoOutput();
374+
$dummyDto->lorem = 'DummyDtoNoOutput foo #'.$i;
375+
$dummyDto->ipsum = $i / 3;
376+
377+
$this->manager->persist($dummyDto);
378+
}
379+
380+
$this->manager->flush();
381+
}
382+
349383
/**
350384
* @Given there are :nb dummy objects with JSON and array data
351385
*/

features/graphql/mutation.feature

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,3 +262,72 @@ Feature: GraphQL mutation support
262262
And the response should be in JSON
263263
And the header "Content-Type" should be equal to "application/json"
264264
And the JSON node "errors[0].message" should be equal to "name: This value should not be blank."
265+
266+
Scenario: Create an item using custom inputClass & disabled outputClass
267+
Given there are 2 dummyDtoNoOutput objects
268+
When I send the following GraphQL request:
269+
"""
270+
mutation {
271+
createDummyDtoNoOutput(input: {foo: "A new one", bar: 3, clientMutationId: "myId"}) {
272+
clientMutationId
273+
}
274+
}
275+
"""
276+
Then the response status code should be 200
277+
And the response should be in JSON
278+
And the header "Content-Type" should be equal to "application/json"
279+
And the JSON should be equal to:
280+
"""
281+
{
282+
"data": {
283+
"createDummyDtoNoOutput": {
284+
"clientMutationId": "myId"
285+
}
286+
}
287+
}
288+
"""
289+
290+
Scenario: Cannot create an item using disabled inputClass
291+
Given there are 2 dummyDtoNoInput objects
292+
When I send the following GraphQL request:
293+
"""
294+
mutation {
295+
createDummyDtoNoInput(input: {foo: "A new one", bar: 3, clientMutationId: "myId"}) {
296+
clientMutationId
297+
}
298+
}
299+
"""
300+
Then the response status code should be 200
301+
And the response should be in JSON
302+
And the header "Content-Type" should be equal to "application/json"
303+
And the JSON should be equal to:
304+
"""
305+
{
306+
"errors": [
307+
{
308+
"message": "Field \"foo\" is not defined by type createDummyDtoNoInputInput.",
309+
"extensions": {
310+
"category": "graphql"
311+
},
312+
"locations": [
313+
{
314+
"line": 2,
315+
"column": 33
316+
}
317+
]
318+
},
319+
{
320+
"message": "Field \"bar\" is not defined by type createDummyDtoNoInputInput.",
321+
"extensions": {
322+
"category": "graphql"
323+
},
324+
"locations": [
325+
{
326+
"line": 2,
327+
"column": 51
328+
}
329+
]
330+
}
331+
]
332+
}
333+
"""

features/graphql/query.feature

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,75 @@ Feature: GraphQL query support
211211
}
212212
}
213213
"""
214+
215+
Scenario: Use outputClass instead of resource class through a GraphQL query
216+
Given there are 2 dummyDtoNoInput objects
217+
When I send the following GraphQL request:
218+
"""
219+
{
220+
dummyDtoNoInputs {
221+
edges {
222+
node {
223+
baz
224+
bat
225+
}
226+
}
227+
}
228+
}
229+
"""
230+
Then the response status code should be 200
231+
And the response should be in JSON
232+
And the header "Content-Type" should be equal to "application/json"
233+
And the JSON should be equal to:
234+
"""
235+
{
236+
"data": {
237+
"dummyDtoNoInputs": {
238+
"edges": [
239+
{
240+
"node": {
241+
"baz": 0.33,
242+
"bat": "DummyDtoNoInput foo #1"
243+
}
244+
},
245+
{
246+
"node": {
247+
"baz": 0.67,
248+
"bat": "DummyDtoNoInput foo #2"
249+
}
250+
}
251+
]
252+
}
253+
}
254+
}
255+
"""
256+
257+
@createSchema
258+
Scenario: Disable outputClass leads to an empty response through a GraphQL query
259+
Given there are 2 dummyDtoNoOutput objects
260+
When I send the following GraphQL request:
261+
"""
262+
{
263+
dummyDtoNoInputs {
264+
edges {
265+
node {
266+
baz
267+
bat
268+
}
269+
}
270+
}
271+
}
272+
"""
273+
Then the response status code should be 200
274+
And the response should be in JSON
275+
And the header "Content-Type" should be equal to "application/json"
276+
And the JSON should be equal to:
277+
"""
278+
{
279+
"data": {
280+
"dummyDtoNoInputs": {
281+
"edges": []
282+
}
283+
}
284+
}
285+
"""

src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ public function __invoke(string $resourceClass = null, string $rootClass = null,
8787

8888
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
8989
$this->canAccess($this->resourceAccessChecker, $resourceMetadata, $resourceClass, $info, $item, $operationName);
90+
if (false === $resourceClass = $resourceMetadata->getAttribute('input_class', $resourceClass)) {
91+
return null;
92+
}
9093

9194
switch ($operationName) {
9295
case 'create':

src/GraphQl/Type/SchemaBuilder.php

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,10 @@ private function convertType(Type $type, bool $input = false, string $mutationNa
381381
return null;
382382
}
383383

384-
$graphqlType = $this->getResourceObjectType($resourceClass, $resourceMetadata, $input, $mutationName, $depth);
384+
if (false !== $dtoClass = $resourceMetadata->getAttribute($input ? 'input_class' : 'output_class', $resourceClass)) {
385+
$resourceClass = $dtoClass;
386+
}
387+
$graphqlType = $this->getResourceObjectType(false === $dtoClass ? null : $resourceClass, $resourceMetadata, $input, $mutationName, $depth);
385388
break;
386389
default:
387390
throw new InvalidTypeException(sprintf('The type "%s" is not supported.', $builtinType));
@@ -399,7 +402,7 @@ private function convertType(Type $type, bool $input = false, string $mutationNa
399402
*
400403
* @return ObjectType|InputObjectType
401404
*/
402-
private function getResourceObjectType(string $resourceClass, ResourceMetadata $resourceMetadata, bool $input = false, string $mutationName = null, int $depth = 0): GraphQLType
405+
private function getResourceObjectType(?string $resourceClass, ResourceMetadata $resourceMetadata, bool $input = false, string $mutationName = null, int $depth = 0): GraphQLType
403406
{
404407
$shortName = $resourceMetadata->getShortName();
405408
if (null !== $mutationName) {
@@ -431,7 +434,7 @@ private function getResourceObjectType(string $resourceClass, ResourceMetadata $
431434
/**
432435
* Gets the fields of the type of the given resource.
433436
*/
434-
private function getResourceObjectTypeFields(string $resourceClass, ResourceMetadata $resourceMetadata, bool $input = false, string $mutationName = null, int $depth = 0): array
437+
private function getResourceObjectTypeFields(?string $resourceClass, ResourceMetadata $resourceMetadata, bool $input = false, string $mutationName = null, int $depth = 0): array
435438
{
436439
$fields = [];
437440
$idField = ['type' => GraphQLType::nonNull(GraphQLType::id())];
@@ -448,25 +451,27 @@ private function getResourceObjectTypeFields(string $resourceClass, ResourceMeta
448451
$fields['id'] = $idField;
449452
}
450453

451-
foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) {
452-
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property, ['graphql_operation_name' => $mutationName ?? 'query']);
453-
if (
454-
null === ($propertyType = $propertyMetadata->getType())
455-
|| (!$input && null === $mutationName && false === $propertyMetadata->isReadable())
456-
|| (null !== $mutationName && false === $propertyMetadata->isWritable())
457-
) {
458-
continue;
459-
}
454+
if (null !== $resourceClass) {
455+
foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) {
456+
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property, ['graphql_operation_name' => $mutationName ?? 'query']);
457+
if (
458+
null === ($propertyType = $propertyMetadata->getType())
459+
|| (!$input && null === $mutationName && false === $propertyMetadata->isReadable())
460+
|| (null !== $mutationName && false === $propertyMetadata->isWritable())
461+
) {
462+
continue;
463+
}
460464

461-
$rootResource = $resourceClass;
462-
if (null !== $propertyMetadata->getSubresource()) {
463-
$resourceClass = $propertyMetadata->getSubresource()->getResourceClass();
464-
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
465-
}
466-
if ($fieldConfiguration = $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, $propertyMetadata->getDescription(), $propertyMetadata->getAttribute('deprecation_reason', ''), $propertyType, $rootResource, $input, $mutationName, ++$depth)) {
467-
$fields['id' === $property ? '_id' : $property] = $fieldConfiguration;
465+
$rootResource = $resourceClass;
466+
if (null !== $propertyMetadata->getSubresource()) {
467+
$resourceClass = $propertyMetadata->getSubresource()->getResourceClass();
468+
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
469+
}
470+
if ($fieldConfiguration = $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, $propertyMetadata->getDescription(), $propertyMetadata->getAttribute('deprecation_reason', ''), $propertyType, $rootResource, $input, $mutationName, ++$depth)) {
471+
$fields['id' === $property ? '_id' : $property] = $fieldConfiguration;
472+
}
473+
$resourceClass = $rootResource;
468474
}
469-
$resourceClass = $rootResource;
470475
}
471476

472477
if (null !== $mutationName) {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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\Tests\Fixtures\TestBundle\DataPersister;
15+
16+
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
17+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoNoOutput;
18+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\InputDto;
19+
use Doctrine\Common\Persistence\ManagerRegistry;
20+
21+
class DummyDtoNoOutputDataPersister implements DataPersisterInterface
22+
{
23+
private $registry;
24+
25+
public function __construct(ManagerRegistry $registry)
26+
{
27+
$this->registry = $registry;
28+
}
29+
30+
/**
31+
* {@inheritdoc}
32+
*/
33+
public function supports($data): bool
34+
{
35+
return $data instanceof InputDto;
36+
}
37+
38+
/**
39+
* {@inheritdoc}
40+
*/
41+
public function persist($data)
42+
{
43+
$output = new DummyDtoNoOutput();
44+
$output->lorem = $data->foo;
45+
$output->ipsum = (string) $data->bar;
46+
47+
$em = $this->registry->getManagerForClass(DummyDtoNoOutput::class);
48+
$em->persist($output);
49+
$em->flush();
50+
51+
return $output;
52+
}
53+
54+
/**
55+
* {@inheritdoc}
56+
*/
57+
public function remove($data)
58+
{
59+
return null;
60+
}
61+
}

0 commit comments

Comments
 (0)