Skip to content

Commit 67524b8

Browse files
committed
Merge 3.2
2 parents 1f6d756 + 76af4ef commit 67524b8

27 files changed

+334
-38
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@ api_platform:
108108
form: ['multipart/form-data']
109109
```
110110

111+
## v3.2.20
112+
113+
### Bug fixes
114+
115+
* [90c9fb31a](https://github.com/api-platform/core/commit/90c9fb31a322a2c7891fbbafb75d60b09fd67772) fix(symfony): register api_error route (#6281)
116+
* [d061c3811](https://github.com/api-platform/core/commit/d061c381120b858c471157dbdccbb5084a39fb04) fix(graphql): increment graphql normalizer priority (#6283)
117+
111118
## v3.2.19
112119

113120
### Bug fixes

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
"doctrine/orm": "<2.14.0",
109109
"doctrine/mongodb-odm": "<2.4",
110110
"doctrine/persistence": "<1.3",
111+
"symfony/framework-bundle": "6.4.6 || 7.0.6",
111112
"symfony/var-exporter": "<6.1.1",
112113
"phpunit/phpunit": "<9.5",
113114
"phpspec/prophecy": "<1.15",

features/graphql/query.feature

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Feature: GraphQL query support
2222

2323
@createSchema
2424
Scenario: Retrieve an item with different relations to the same resource
25-
Given there are 2 multiRelationsDummy objects having each a manyToOneRelation, 2 manyToManyRelations, 3 oneToManyRelations and 4 embeddedRelations
25+
Given there are 2 multiRelationsDummy objects having each 1 manyToOneRelation, 2 manyToManyRelations, 3 oneToManyRelations and 4 embeddedRelations
2626
When I send the following GraphQL request:
2727
"""
2828
{
@@ -33,6 +33,10 @@ Feature: GraphQL query support
3333
id
3434
name
3535
}
36+
manyToOneResolveRelation {
37+
id
38+
name
39+
}
3640
manyToManyRelations {
3741
edges{
3842
node {
@@ -70,7 +74,7 @@ Feature: GraphQL query support
7074

7175
@createSchema
7276
Scenario: Retrieve embedded collections
73-
Given there are 2 multiRelationsDummy objects having each a manyToOneRelation, 2 manyToManyRelations, 3 oneToManyRelations and 4 embeddedRelations
77+
Given there are 2 multiRelationsDummy objects having each 1 manyToOneRelation, 2 manyToManyRelations, 3 oneToManyRelations and 4 embeddedRelations
7478
When I send the following GraphQL request:
7579
"""
7680
{
@@ -81,6 +85,10 @@ Feature: GraphQL query support
8185
id
8286
name
8387
}
88+
manyToOneResolveRelation {
89+
id
90+
name
91+
}
8492
manyToManyRelations {
8593
edges{
8694
node {
@@ -113,10 +121,13 @@ Feature: GraphQL query support
113121
Then the response status code should be 200
114122
And the response should be in JSON
115123
And the header "Content-Type" should be equal to "application/json"
124+
And the JSON node "errors" should not exist
116125
And the JSON node "data.multiRelationsDummy.id" should be equal to "/multi_relations_dummies/2"
117126
And the JSON node "data.multiRelationsDummy.name" should be equal to "Dummy #2"
118127
And the JSON node "data.multiRelationsDummy.manyToOneRelation.id" should not be null
119128
And the JSON node "data.multiRelationsDummy.manyToOneRelation.name" should be equal to "RelatedManyToOneDummy #2"
129+
And the JSON node "data.multiRelationsDummy.manyToOneResolveRelation.id" should not be null
130+
And the JSON node "data.multiRelationsDummy.manyToOneResolveRelation.name" should be equal to "RelatedManyToOneResolveDummy #2"
120131
And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges" should have 2 element
121132
And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges[1].node.id" should not be null
122133
And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges[0].node.name" should match "#RelatedManyToManyDummy(1|2)2#"
@@ -135,6 +146,65 @@ Feature: GraphQL query support
135146
And the JSON node "data.multiRelationsDummy.nestedPaginatedCollection.edges[2].node.name" should be equal to "NestedPaginatedDummy3"
136147
And the JSON node "data.multiRelationsDummy.nestedPaginatedCollection.edges[3].node.name" should be equal to "NestedPaginatedDummy4"
137148

149+
@createSchema
150+
Scenario: Retrieve an item with different relations (all unset)
151+
Given there are 2 multiRelationsDummy objects having each 0 manyToOneRelation, 0 manyToManyRelations, 0 oneToManyRelations and 0 embeddedRelations
152+
When I send the following GraphQL request:
153+
"""
154+
{
155+
multiRelationsDummy(id: "/multi_relations_dummies/2") {
156+
id
157+
name
158+
manyToOneRelation {
159+
id
160+
name
161+
}
162+
manyToOneResolveRelation {
163+
id
164+
name
165+
}
166+
manyToManyRelations {
167+
edges{
168+
node {
169+
id
170+
name
171+
}
172+
}
173+
}
174+
oneToManyRelations {
175+
edges{
176+
node {
177+
id
178+
name
179+
}
180+
}
181+
}
182+
nestedCollection {
183+
name
184+
}
185+
nestedPaginatedCollection {
186+
edges{
187+
node {
188+
name
189+
}
190+
}
191+
}
192+
}
193+
}
194+
"""
195+
Then the response status code should be 200
196+
And the response should be in JSON
197+
And the header "Content-Type" should be equal to "application/json"
198+
And the JSON node "errors" should not exist
199+
And the JSON node "data.multiRelationsDummy.id" should be equal to "/multi_relations_dummies/2"
200+
And the JSON node "data.multiRelationsDummy.name" should be equal to "Dummy #2"
201+
And the JSON node "data.multiRelationsDummy.manyToOneRelation" should be null
202+
And the JSON node "data.multiRelationsDummy.manyToOneResolveRelation" should be null
203+
And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges" should have 0 element
204+
And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges" should have 0 element
205+
And the JSON node "data.multiRelationsDummy.nestedCollection" should have 0 element
206+
And the JSON node "data.multiRelationsDummy.nestedPaginatedCollection.edges" should have 0 element
207+
138208
@createSchema @!mongodb
139209
Scenario: Retrieve an item with child relation to the same resource
140210
Given there are tree dummies

src/GraphQl/Resolver/Factory/CollectionResolverFactory.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface;
2121
use ApiPlatform\Metadata\GraphQl\Operation;
2222
use ApiPlatform\Metadata\GraphQl\Query;
23+
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2324
use ApiPlatform\Metadata\Util\CloneTrait;
2425
use ApiPlatform\State\Pagination\ArrayPaginator;
2526
use GraphQL\Type\Definition\ResolveInfo;
@@ -40,7 +41,7 @@ public function __construct(private readonly ReadStageInterface $readStage, priv
4041
{
4142
}
4243

43-
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable
44+
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable
4445
{
4546
return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation): ?array {
4647
// If authorization has failed for a relation field (e.g. via ApiProperty security), the field is not present in the source: null can be returned directly to ensure the collection isn't in the response.

src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use ApiPlatform\GraphQl\Resolver\Stage\WriteStageInterface;
2525
use ApiPlatform\Metadata\DeleteOperationInterface;
2626
use ApiPlatform\Metadata\GraphQl\Operation;
27+
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2728
use ApiPlatform\Metadata\Util\ClassInfoTrait;
2829
use ApiPlatform\Metadata\Util\CloneTrait;
2930
use GraphQL\Type\Definition\ResolveInfo;
@@ -44,7 +45,7 @@ public function __construct(private readonly ReadStageInterface $readStage, priv
4445
{
4546
}
4647

47-
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable
48+
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable
4849
{
4950
return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation): ?array {
5051
if (null === $resourceClass || null === $operation) {

src/GraphQl/Resolver/Factory/ItemResolverFactory.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface;
2121
use ApiPlatform\Metadata\GraphQl\Operation;
2222
use ApiPlatform\Metadata\GraphQl\Query;
23+
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2324
use ApiPlatform\Metadata\Util\ClassInfoTrait;
2425
use ApiPlatform\Metadata\Util\CloneTrait;
2526
use GraphQL\Type\Definition\ResolveInfo;
@@ -41,7 +42,7 @@ public function __construct(private readonly ReadStageInterface $readStage, priv
4142
{
4243
}
4344

44-
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable
45+
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable
4546
{
4647
return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation) {
4748
// Data already fetched and normalized (field or nested resource)

src/GraphQl/Resolver/Factory/ItemSubscriptionResolverFactory.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use ApiPlatform\GraphQl\Subscription\MercureSubscriptionIriGeneratorInterface;
2020
use ApiPlatform\GraphQl\Subscription\SubscriptionManagerInterface;
2121
use ApiPlatform\Metadata\GraphQl\Operation;
22+
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2223
use ApiPlatform\Metadata\Util\ClassInfoTrait;
2324
use ApiPlatform\Metadata\Util\CloneTrait;
2425
use GraphQL\Type\Definition\ResolveInfo;
@@ -37,7 +38,7 @@ public function __construct(private readonly ReadStageInterface $readStage, priv
3738
{
3839
}
3940

40-
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable
41+
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable
4142
{
4243
return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation): ?array {
4344
if (null === $resourceClass || null === $operation) {

src/GraphQl/Resolver/Factory/ResolverFactory.php

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use ApiPlatform\Metadata\GraphQl\Operation;
1919
use ApiPlatform\Metadata\GraphQl\Query;
2020
use ApiPlatform\State\Pagination\ArrayPaginator;
21+
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2122
use ApiPlatform\State\ProcessorInterface;
2223
use ApiPlatform\State\ProviderInterface;
2324
use GraphQL\Type\Definition\ResolveInfo;
@@ -30,21 +31,28 @@ public function __construct(
3031
) {
3132
}
3233

33-
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable
34+
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable
3435
{
35-
return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation) {
36-
// Data already fetched and normalized (field or nested resource)
36+
return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation, $propertyMetadataFactory) {
3737
if ($body = $source[$info->fieldName] ?? null) {
3838
// special treatment for nested resources without a resolver/provider
39-
if ($operation instanceof Query && $operation->getNested() && !$operation->getResolver() && (!$operation->getProvider() || NoopProvider::class === $operation->getProvider())) {
40-
return $this->resolve($source, $args, $info, $rootClass, $operation, new ArrayPaginator($body, 0, \count($body)));
41-
}
4239

4340
return $body;
4441
}
4542

46-
if (null === $resourceClass && \array_key_exists($info->fieldName, $source ?? [])) {
47-
return $body;
43+
if (\array_key_exists($info->fieldName, $source ?? [])) {
44+
$body = $source[$info->fieldName];
45+
46+
if ($operation instanceof Query && $operation->getNested() && !$operation->getResolver() && (!$operation->getProvider() || NoopProvider::class === $operation->getProvider())) {
47+
return $this->resolve($source, $args, $info, $rootClass, $operation, new ArrayPaginator($body, 0, \count($body)));
48+
}
49+
50+
$propertyMetadata = $rootClass ? $propertyMetadataFactory?->create($rootClass, $info->fieldName) : null;
51+
$type = $propertyMetadata?->getBuiltinTypes()[0] ?? null;
52+
// Data already fetched and normalized (field or nested resource)
53+
if ($body || null === $resourceClass || ($type && !$type->isCollection())) {
54+
return $body;
55+
}
4856
}
4957

5058
// If authorization has failed for a relation field (e.g. via ApiProperty security), the field is not present in the source: null can be returned directly to ensure the collection isn't in the response.

src/GraphQl/Resolver/Factory/ResolverFactoryInterface.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace ApiPlatform\GraphQl\Resolver\Factory;
1515

1616
use ApiPlatform\Metadata\GraphQl\Operation;
17+
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
1718

1819
/**
1920
* Builds a GraphQL resolver.
@@ -22,5 +23,5 @@
2223
*/
2324
interface ResolverFactoryInterface
2425
{
25-
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable;
26+
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable;
2627
}

src/GraphQl/Tests/Resolver/Factory/ResolverFactoryTest.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
namespace ApiPlatform\GraphQl\Tests\Resolver\Factory;
1515

1616
use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactory;
17+
use ApiPlatform\Metadata\ApiProperty;
1718
use ApiPlatform\Metadata\GraphQl\Mutation;
1819
use ApiPlatform\Metadata\GraphQl\Operation;
1920
use ApiPlatform\Metadata\GraphQl\Query;
21+
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2022
use ApiPlatform\State\ProcessorInterface;
2123
use ApiPlatform\State\ProviderInterface;
2224
use GraphQL\Type\Definition\ResolveInfo;
@@ -38,11 +40,13 @@ public function testGraphQlResolver(?string $resourceClass = null, ?string $root
3840
$provider->expects($this->once())->method('provide')->with($providedOperation ?: $operation, [], $context)->willReturn($body);
3941
$processor = $this->createMock(ProcessorInterface::class);
4042
$processor->expects($this->once())->method('process')->with($body, $processedOperation ?: $operation, [], $context)->willReturn($returnValue);
43+
$propertyMetadataFactory = $this->createMock(PropertyMetadataFactoryInterface::class);
44+
$propertyMetadataFactory->expects($this->once())->method('create')->with($rootClass, 'test')->willReturn(new ApiProperty(schema: ['type' => 'array']));
4145
$resolveInfo = $this->createMock(ResolveInfo::class);
4246
$resolveInfo->fieldName = 'test';
4347

4448
$resolverFactory = new ResolverFactory($provider, $processor);
45-
$this->assertEquals($resolverFactory->__invoke($resourceClass, $rootClass, $operation)(['test' => null], [], [], $resolveInfo), $returnValue);
49+
$this->assertEquals($resolverFactory->__invoke($resourceClass, $rootClass, $operation, $propertyMetadataFactory)(['test' => null], [], [], $resolveInfo), $returnValue);
4650
}
4751

4852
public static function graphQlQueries(): array

0 commit comments

Comments
 (0)