Skip to content

Commit 93b8210

Browse files
authored
Introduce resolveValue method to Interface and Union types (#1776)
This allows transforming the object value after type resolution. This is useful when you have a single entity that needs different representations. For example, when your data layer returns a generic `PetEntity` with a type discriminator, but your GraphQL schema has separate `Dog` and `Cat` types. Example: ```php $PetType = new InterfaceType([ 'name' => 'Pet', 'resolveType' => static function (PetEntity $objectValue): string { if ($objectValue->type === 'dog') { return 'Dog'; } return 'Cat'; }, 'resolveValue' => static function (PetEntity $objectValue) { if ($objectValue->type === 'dog') { return new Dog($objectValue->name, $objectValue->woofs); } return new Cat($objectValue->name, $objectValue->meows); }, 'fields' => ['name' => ['type' => Type::string()]], ]); Now field resolvers receive the properly typed Dog or Cat object instead of the generic PetEntity, allowing for type-safe resolution without needing to transform objects before they reach the type resolver. Common use cases: - Database polymorphism (single table with type column) - External APIs returning generic objects with type discriminators
1 parent bdb0c0f commit 93b8210

File tree

7 files changed

+363
-97
lines changed

7 files changed

+363
-97
lines changed

src/Executor/PromiseExecutor.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace GraphQL\Executor;
4+
5+
use GraphQL\Executor\Promise\Promise;
6+
7+
class PromiseExecutor implements ExecutorImplementation
8+
{
9+
private Promise $result;
10+
11+
public function __construct(Promise $result)
12+
{
13+
$this->result = $result;
14+
}
15+
16+
public function doExecute(): Promise
17+
{
18+
return $this->result;
19+
}
20+
}

src/Executor/ReferenceExecutor.php

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -120,19 +120,10 @@ public static function create(
120120
);
121121

122122
if (is_array($exeContext)) {
123-
return new class($promiseAdapter->createFulfilled(new ExecutionResult(null, $exeContext))) implements ExecutorImplementation {
124-
private Promise $result;
123+
$executionResult = new ExecutionResult(null, $exeContext);
124+
$fulfilledPromise = $promiseAdapter->createFulfilled($executionResult);
125125

126-
public function __construct(Promise $result)
127-
{
128-
$this->result = $result;
129-
}
130-
131-
public function doExecute(): Promise
132-
{
133-
return $this->result;
134-
}
135-
};
126+
return new PromiseExecutor($fulfilledPromise);
136127
}
137128

138129
return new static($exeContext);
@@ -1073,7 +1064,7 @@ protected function completeLeafValue(LeafType $returnType, &$result)
10731064
* @param \ArrayObject<int, FieldNode> $fieldNodes
10741065
* @param list<string|int> $path
10751066
* @param list<string|int> $unaliasedPath
1076-
* @param array<mixed> $result
1067+
* @param mixed $result
10771068
* @param mixed $contextValue
10781069
*
10791070
* @throws \Exception
@@ -1092,6 +1083,7 @@ protected function completeAbstractValue(
10921083
$contextValue
10931084
) {
10941085
$typeCandidate = $returnType->resolveType($result, $contextValue, $info);
1086+
$result = $returnType->resolveValue($result, $contextValue, $info);
10951087

10961088
if ($typeCandidate === null) {
10971089
$runtimeType = static::defaultTypeResolver($result, $contextValue, $info, $returnType);
@@ -1272,7 +1264,7 @@ protected function completeObjectValue(
12721264

12731265
/**
12741266
* @param \ArrayObject<int, FieldNode> $fieldNodes
1275-
* @param array<mixed> $result
1267+
* @param mixed $result
12761268
*/
12771269
protected function invalidReturnTypeError(
12781270
ObjectType $returnType,
@@ -1408,7 +1400,7 @@ protected function executeFields(ObjectType $parentType, $rootValue, array $path
14081400
*
14091401
* @param array<mixed>|mixed $results
14101402
*
1411-
* @return array<mixed>|\stdClass|mixed
1403+
* @return non-empty-array<mixed>|\stdClass|mixed
14121404
*/
14131405
protected static function fixResultsIfEmptyArray($results)
14141406
{

src/Type/Definition/AbstractType.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
/**
88
* @phpstan-type ResolveTypeReturn ObjectType|string|callable(): (ObjectType|string|null)|Deferred|null
99
* @phpstan-type ResolveType callable(mixed $objectValue, mixed $context, ResolveInfo $resolveInfo): ResolveTypeReturn
10+
* @phpstan-type ResolveValue callable(mixed $objectValue, mixed $context, ResolveInfo $resolveInfo): mixed
1011
*/
1112
interface AbstractType
1213
{
@@ -21,4 +22,14 @@ interface AbstractType
2122
* @phpstan-return ResolveTypeReturn
2223
*/
2324
public function resolveType($objectValue, $context, ResolveInfo $info);
25+
26+
/**
27+
* Receives the original resolved value and transforms it if necessary.
28+
*
29+
* @param mixed $objectValue The resolved value for the object type
30+
* @param mixed $context The context that was passed to GraphQL::execute()
31+
*
32+
* @return mixed The possibly transformed value
33+
*/
34+
public function resolveValue($objectValue, $context, ResolveInfo $info);
2435
}

src/Type/Definition/InterfaceType.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
/**
1212
* @phpstan-import-type ResolveType from AbstractType
13+
* @phpstan-import-type ResolveValue from AbstractType
1314
* @phpstan-import-type FieldsConfig from FieldDefinition
1415
*
1516
* @phpstan-type InterfaceTypeReference InterfaceType|callable(): InterfaceType
@@ -19,6 +20,7 @@
1920
* fields: FieldsConfig,
2021
* interfaces?: iterable<InterfaceTypeReference>|callable(): iterable<InterfaceTypeReference>,
2122
* resolveType?: ResolveType|null,
23+
* resolveValue?: ResolveValue|null,
2224
* astNode?: InterfaceTypeDefinitionNode|null,
2325
* extensionASTNodes?: array<InterfaceTypeExtensionNode>|null
2426
* }
@@ -76,6 +78,15 @@ public function resolveType($objectValue, $context, ResolveInfo $info)
7678
return null;
7779
}
7880

81+
public function resolveValue($objectValue, $context, ResolveInfo $info)
82+
{
83+
if (isset($this->config['resolveValue'])) {
84+
return ($this->config['resolveValue'])($objectValue, $context, $info);
85+
}
86+
87+
return $objectValue;
88+
}
89+
7990
/**
8091
* @throws Error
8192
* @throws InvariantViolation

src/Type/Definition/UnionType.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010

1111
/**
1212
* @phpstan-import-type ResolveType from AbstractType
13+
* @phpstan-import-type ResolveValue from AbstractType
1314
*
1415
* @phpstan-type ObjectTypeReference ObjectType|callable(): ObjectType
1516
* @phpstan-type UnionConfig array{
1617
* name?: string|null,
1718
* description?: string|null,
1819
* types: iterable<ObjectTypeReference>|callable(): iterable<ObjectTypeReference>,
1920
* resolveType?: ResolveType|null,
21+
* resolveValue?: ResolveValue|null,
2022
* astNode?: UnionTypeDefinitionNode|null,
2123
* extensionASTNodes?: array<UnionTypeExtensionNode>|null
2224
* }
@@ -115,6 +117,15 @@ public function resolveType($objectValue, $context, ResolveInfo $info)
115117
return null;
116118
}
117119

120+
public function resolveValue($objectValue, $context, ResolveInfo $info)
121+
{
122+
if (isset($this->config['resolveValue'])) {
123+
return ($this->config['resolveValue'])($objectValue, $context, $info);
124+
}
125+
126+
return $objectValue;
127+
}
128+
118129
public function assertValid(): void
119130
{
120131
Utils::assertValidName($this->name);

0 commit comments

Comments
 (0)