Skip to content

Commit cc021e4

Browse files
authored
fix(graphql): honor custom mutation output class in payload type (#8300)
Fixes #2754
1 parent 6b1fe1e commit cc021e4

6 files changed

Lines changed: 169 additions & 1 deletion

File tree

src/GraphQl/Type/TypeBuilder.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,8 @@ private function getResourceObjectTypeConfiguration(string $shortName, ResourceM
329329
'resolveField' => $this->defaultFieldResolver,
330330
'fields' => function () use ($resourceClass, $operation, $operationName, $resourceMetadataCollection, $input, $wrapData, $depth, $ioMetadata) {
331331
if ($wrapData) {
332-
$queryNormalizationContext = $this->getQueryOperation($resourceMetadataCollection)?->getNormalizationContext() ?? [];
332+
$queryOperation = $this->getQueryOperation($resourceMetadataCollection);
333+
$queryNormalizationContext = $queryOperation?->getNormalizationContext() ?? [];
333334

334335
try {
335336
$mutationNormalizationContext = $operation instanceof Mutation || $operation instanceof Subscription ? ($resourceMetadataCollection->getOperation($operationName)->getNormalizationContext() ?? []) : [];
@@ -340,6 +341,12 @@ private function getResourceObjectTypeConfiguration(string $shortName, ResourceM
340341
// If not, use the query type in order to ensure the client cache could be used.
341342
$useWrappedType = $queryNormalizationContext !== $mutationNormalizationContext;
342343

344+
// The query type can only be reused when both operations produce the same output class.
345+
// A mutation declaring its own output class must expose that class on its payload.
346+
if (!$useWrappedType && ($operation->getOutput()['class'] ?? null) !== ($queryOperation?->getOutput()['class'] ?? null)) {
347+
$useWrappedType = true;
348+
}
349+
343350
$wrappedOperationName = $operationName;
344351

345352
if (!$useWrappedType) {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
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\Tests\Fixtures\TestBundle\ApiResource\Issue2754;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
use ApiPlatform\Metadata\GraphQl\Mutation;
18+
use ApiPlatform\Metadata\GraphQl\Query;
19+
use ApiPlatform\Metadata\Operation;
20+
21+
#[ApiResource(
22+
graphQlOperations: [
23+
new Query(
24+
name: 'item_query',
25+
provider: [self::class, 'provide'],
26+
),
27+
new Mutation(
28+
name: 'sum',
29+
resolver: 'app.graphql.mutation_resolver.issue2754_sum',
30+
output: SumResult::class,
31+
args: ['operandA' => ['type' => 'Int!'], 'operandB' => ['type' => 'Int!']],
32+
),
33+
]
34+
)]
35+
class Sum
36+
{
37+
public function __construct(public ?int $id = null, public ?int $operandA = null, public ?int $operandB = null)
38+
{
39+
}
40+
41+
public static function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
42+
{
43+
return new self(1);
44+
}
45+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
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\Tests\Fixtures\TestBundle\ApiResource\Issue2754;
15+
16+
use ApiPlatform\GraphQl\Resolver\MutationResolverInterface;
17+
18+
final class SumResolver implements MutationResolverInterface
19+
{
20+
/**
21+
* @param object|null $item
22+
* @param mixed[] $context
23+
*/
24+
public function __invoke($item, array $context): SumResult
25+
{
26+
$input = $context['args']['input'] ?? [];
27+
28+
return new SumResult(1, ($input['operandA'] ?? 0) + ($input['operandB'] ?? 0));
29+
}
30+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
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\Tests\Fixtures\TestBundle\ApiResource\Issue2754;
15+
16+
// Fields deliberately differ from Sum: the mutation payload exposes them only if it honors output.
17+
class SumResult
18+
{
19+
public function __construct(public ?int $id = null, public ?int $sum = null)
20+
{
21+
}
22+
}

tests/Fixtures/app/config/config_common.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,11 @@ services:
506506
tags:
507507
- name: 'api_platform.graphql.resolver'
508508

509+
app.graphql.mutation_resolver.issue2754_sum:
510+
class: 'ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue2754\SumResolver'
511+
tags:
512+
- name: 'api_platform.graphql.resolver'
513+
509514
app.graphql.query_resolver.security_after_resolver:
510515
class: 'ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6427\SecurityAfterResolverResolver'
511516
tags:
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
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\Tests\Functional\GraphQl;
15+
16+
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
17+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue2754\Sum;
18+
use ApiPlatform\Tests\SetupClassResourcesTrait;
19+
20+
/**
21+
* A custom GraphQL mutation declaring an explicit output DTO must expose the
22+
* output DTO's fields on its payload type, not the resource's fields.
23+
*
24+
* @see https://github.com/api-platform/core/issues/2754
25+
*/
26+
final class Issue2754Test extends ApiTestCase
27+
{
28+
use SetupClassResourcesTrait;
29+
30+
protected static ?bool $alwaysBootKernel = false;
31+
32+
/**
33+
* @return class-string[]
34+
*/
35+
public static function getResources(): array
36+
{
37+
return [Sum::class];
38+
}
39+
40+
public function testCustomMutationHonorsOutputClassFields(): void
41+
{
42+
$response = self::createClient()->request('POST', '/graphql', ['json' => [
43+
'query' => <<<'GRAPHQL'
44+
mutation {
45+
sumSum(input: {operandA: 2, operandB: 3}) {
46+
sum {
47+
sum
48+
}
49+
}
50+
}
51+
GRAPHQL,
52+
]]);
53+
54+
$this->assertResponseIsSuccessful();
55+
$json = $response->toArray(false);
56+
$this->assertArrayNotHasKey('errors', $json, json_encode($json['errors'] ?? null));
57+
$this->assertSame(5, $json['data']['sumSum']['sum']['sum']);
58+
}
59+
}

0 commit comments

Comments
 (0)