Skip to content

Commit 89c9229

Browse files
authored
feat(graphql): support nullable embedded relations in GraphQL types (#6100)
1 parent 6b00cea commit 89c9229

10 files changed

+376
-116
lines changed

features/graphql/schema.feature

+13
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,16 @@ Feature: GraphQL schema-related features
9898
clientMutationId: String
9999
}
100100
"""
101+
And the command output should contain:
102+
"""
103+
"Updates a OptionalRequiredDummy."
104+
input updateOptionalRequiredDummyInput {
105+
id: ID!
106+
thirdLevel: updateThirdLevelNestedInput
107+
thirdLevelRequired: updateThirdLevelNestedInput!
108+
109+
"Get relatedToDummyFriend."
110+
relatedToDummyFriend: [updateRelatedToDummyFriendNestedInput]
111+
clientMutationId: String
112+
}
113+
"""

src/GraphQl/Tests/Type/FieldsBuilderTest.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface;
1717
use ApiPlatform\GraphQl\Tests\Fixtures\Enum\GenderTypeEnum;
1818
use ApiPlatform\GraphQl\Tests\Fixtures\Serializer\NameConverter\CustomConverter;
19+
use ApiPlatform\GraphQl\Type\ContextAwareTypeBuilderInterface;
1920
use ApiPlatform\GraphQl\Type\FieldsBuilder;
20-
use ApiPlatform\GraphQl\Type\TypeBuilderEnumInterface;
2121
use ApiPlatform\GraphQl\Type\TypeConverterInterface;
2222
use ApiPlatform\GraphQl\Type\TypesContainerInterface;
2323
use ApiPlatform\Metadata\ApiProperty;
@@ -79,7 +79,7 @@ protected function setUp(): void
7979
$this->propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
8080
$this->resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
8181
$this->typesContainerProphecy = $this->prophesize(TypesContainerInterface::class);
82-
$this->typeBuilderProphecy = $this->prophesize(TypeBuilderEnumInterface::class);
82+
$this->typeBuilderProphecy = $this->prophesize(ContextAwareTypeBuilderInterface::class);
8383
$this->typeConverterProphecy = $this->prophesize(TypeConverterInterface::class);
8484
$this->itemResolverFactoryProphecy = $this->prophesize(ResolverFactoryInterface::class);
8585
$this->collectionResolverFactoryProphecy = $this->prophesize(ResolverFactoryInterface::class);

src/GraphQl/Tests/Type/TypeBuilderTest.php

+60-29
Large diffs are not rendered by default.

src/GraphQl/Tests/Type/TypeConverterTest.php

+10-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
use ApiPlatform\GraphQl\Tests\Fixtures\Enum\GenderTypeEnum;
1717
use ApiPlatform\GraphQl\Tests\Fixtures\Type\Definition\DateTimeType;
18-
use ApiPlatform\GraphQl\Type\TypeBuilderEnumInterface;
18+
use ApiPlatform\GraphQl\Type\ContextAwareTypeBuilderInterface;
1919
use ApiPlatform\GraphQl\Type\TypeConverter;
2020
use ApiPlatform\GraphQl\Type\TypesContainerInterface;
2121
use ApiPlatform\Metadata\ApiProperty;
@@ -54,7 +54,7 @@ class TypeConverterTest extends TestCase
5454
*/
5555
protected function setUp(): void
5656
{
57-
$this->typeBuilderProphecy = $this->prophesize(TypeBuilderEnumInterface::class);
57+
$this->typeBuilderProphecy = $this->prophesize(ContextAwareTypeBuilderInterface::class);
5858
$this->typesContainerProphecy = $this->prophesize(TypesContainerInterface::class);
5959
$this->resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
6060
$this->propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
@@ -155,13 +155,15 @@ public function testConvertTypeInputResource(): void
155155
$type = new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummy');
156156
/** @var Operation $operation */
157157
$operation = new Query();
158+
/** @var ApiProperty $propertyMetadata */
159+
$propertyMetadata = (new ApiProperty())->withWritableLink(true);
158160
$graphqlResourceMetadata = new ResourceMetadataCollection('dummy', [(new ApiResource())->withGraphQlOperations(['item_query' => $operation])]);
159161
$expectedGraphqlType = new ObjectType(['name' => 'resourceObjectType', 'fields' => []]);
160162

161163
$this->resourceMetadataCollectionFactoryProphecy->create('dummy')->willReturn($graphqlResourceMetadata);
162164
$this->typeBuilderProphecy->isCollection($type)->willReturn(false);
163165
$this->propertyMetadataFactoryProphecy->create('rootClass', 'dummyProperty', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withWritableLink(true));
164-
$this->typeBuilderProphecy->getResourceObjectType('dummy', $graphqlResourceMetadata, $operation, true, false, 1)->shouldBeCalled()->willReturn($expectedGraphqlType);
166+
$this->typeBuilderProphecy->getResourceObjectType($graphqlResourceMetadata, $operation, $propertyMetadata, ['input' => true, 'wrapped' => false, 'depth' => 1])->shouldBeCalled()->willReturn($expectedGraphqlType);
165167

166168
$graphqlType = $this->typeConverter->convertType($type, true, $operation, 'dummy', 'rootClass', 'dummyProperty', 1);
167169
$this->assertSame($expectedGraphqlType, $graphqlType);
@@ -179,7 +181,11 @@ public function testConvertTypeCollectionResource(Type $type, ObjectType $expect
179181

180182
$this->typeBuilderProphecy->isCollection($type)->shouldBeCalled()->willReturn(true);
181183
$this->resourceMetadataCollectionFactoryProphecy->create('dummyValue')->shouldBeCalled()->willReturn($graphqlResourceMetadata);
182-
$this->typeBuilderProphecy->getResourceObjectType('dummyValue', $graphqlResourceMetadata, $collectionOperation, false, false, 0)->shouldBeCalled()->willReturn($expectedGraphqlType);
184+
$this->typeBuilderProphecy->getResourceObjectType($graphqlResourceMetadata, $collectionOperation, null, [
185+
'input' => false,
186+
'wrapped' => false,
187+
'depth' => 0,
188+
])->shouldBeCalled()->willReturn($expectedGraphqlType);
183189

184190
/** @var Operation $rootOperation */
185191
$rootOperation = (new Query())->withName('test');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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\GraphQl\Type;
15+
16+
use ApiPlatform\Metadata\ApiProperty;
17+
use ApiPlatform\Metadata\GraphQl\Operation;
18+
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
19+
use GraphQL\Type\Definition\InterfaceType;
20+
use GraphQL\Type\Definition\Type as GraphQLType;
21+
use Symfony\Component\PropertyInfo\Type;
22+
23+
/**
24+
* Interface implemented to build a GraphQL type.
25+
*
26+
* @author Antoine Bluchet <[email protected]>
27+
*/
28+
interface ContextAwareTypeBuilderInterface
29+
{
30+
/**
31+
* Gets the object type of the given resource.
32+
*
33+
* @param array<string, mixed>&array{input?: bool, wrapped?: bool, depth?: int} $context
34+
*
35+
* @return GraphQLType the object type, possibly wrapped by NonNull
36+
*/
37+
public function getResourceObjectType(ResourceMetadataCollection $resourceMetadataCollection, Operation $operation, ?ApiProperty $propertyMetadata = null, array $context = []): GraphQLType;
38+
39+
/**
40+
* Get the interface type of a node.
41+
*/
42+
public function getNodeInterface(): InterfaceType;
43+
44+
/**
45+
* Gets the type of a paginated collection of the given resource type.
46+
*/
47+
public function getPaginatedCollectionType(GraphQLType $resourceType, Operation $operation): GraphQLType;
48+
49+
/**
50+
* Gets the type corresponding to an enum.
51+
*/
52+
public function getEnumType(Operation $operation): GraphQLType;
53+
54+
/**
55+
* Returns true if a type is a collection.
56+
*/
57+
public function isCollection(Type $type): bool;
58+
}

src/GraphQl/Type/FieldsBuilder.php

+5-2
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,16 @@
4747
*/
4848
final class FieldsBuilder implements FieldsBuilderInterface, FieldsBuilderEnumInterface
4949
{
50-
private readonly TypeBuilderEnumInterface|TypeBuilderInterface $typeBuilder;
50+
private readonly ContextAwareTypeBuilderInterface|TypeBuilderEnumInterface|TypeBuilderInterface $typeBuilder;
5151

52-
public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, TypeBuilderEnumInterface|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)
52+
public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, ContextAwareTypeBuilderInterface|TypeBuilderEnumInterface|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)
5353
{
5454
if ($typeBuilder instanceof TypeBuilderInterface) {
5555
@trigger_error(sprintf('$typeBuilder argument of FieldsBuilder implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', TypeBuilderInterface::class, TypeBuilderEnumInterface::class), \E_USER_DEPRECATED);
5656
}
57+
if ($typeBuilder instanceof TypeBuilderEnumInterface) {
58+
@trigger_error(sprintf('$typeBuilder argument of TypeConverter implementing "%s" is deprecated since API Platform 3.3. It has to implement "%s" instead.', TypeBuilderEnumInterface::class, ContextAwareTypeBuilderInterface::class), \E_USER_DEPRECATED);
59+
}
5760
$this->typeBuilder = $typeBuilder;
5861
}
5962

0 commit comments

Comments
 (0)