Skip to content

Commit 80ba591

Browse files
committed
Introduce resolveValue method to Interface and Union types
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 d91f412 commit 80ba591

File tree

6 files changed

+244
-0
lines changed

6 files changed

+244
-0
lines changed

src/Executor/ReferenceExecutor.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,6 +1092,7 @@ protected function completeAbstractValue(
10921092
$contextValue
10931093
) {
10941094
$typeCandidate = $returnType->resolveType($result, $contextValue, $info);
1095+
$result = $returnType->resolveValue($result, $contextValue, $info);
10951096

10961097
if ($typeCandidate === null) {
10971098
$runtimeType = static::defaultTypeResolver($result, $contextValue, $info, $returnType);

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+
* Resolves/transforms the value after type resolution.
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 transformed value (or original if no transformation needed)
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);

tests/Executor/AbstractTest.php

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use GraphQL\Tests\Executor\TestClasses\Cat;
1414
use GraphQL\Tests\Executor\TestClasses\Dog;
1515
use GraphQL\Tests\Executor\TestClasses\Human;
16+
use GraphQL\Tests\Executor\TestClasses\PetEntity;
1617
use GraphQL\Type\Definition\InterfaceType;
1718
use GraphQL\Type\Definition\ObjectType;
1819
use GraphQL\Type\Definition\Type;
@@ -773,4 +774,192 @@ public function testHintsOnConflictingTypeInstancesInResolveType(): void
773774
$error->getMessage()
774775
);
775776
}
777+
778+
public function testResolveValueAllowsModifyingObjectValueForInterfaceType(): void
779+
{
780+
$PetType = new InterfaceType([
781+
'name' => 'Pet',
782+
'resolveType' => static function (PetEntity $objectValue): string {
783+
if ($objectValue->type === 'dog') {
784+
return 'Dog';
785+
}
786+
787+
return 'Cat';
788+
},
789+
'resolveValue' => static function (PetEntity $objectValue) {
790+
if ($objectValue->type === 'dog') {
791+
return new Dog($objectValue->name, $objectValue->woofs);
792+
}
793+
794+
return new Cat($objectValue->name, $objectValue->woofs);
795+
},
796+
'fields' => [
797+
'name' => ['type' => Type::string()],
798+
],
799+
]);
800+
801+
$DogType = new ObjectType([
802+
'name' => 'Dog',
803+
'interfaces' => [$PetType],
804+
'fields' => [
805+
'name' => [
806+
'type' => Type::string(),
807+
'resolve' => fn (Dog $dog) => $dog->name,
808+
],
809+
'woofs' => [
810+
'type' => Type::boolean(),
811+
'resolve' => fn (Dog $dog) => $dog->woofs,
812+
],
813+
],
814+
]);
815+
816+
$CatType = new ObjectType([
817+
'name' => 'Cat',
818+
'interfaces' => [$PetType],
819+
'fields' => [
820+
'name' => [
821+
'type' => Type::string(),
822+
'resolve' => fn (Cat $cat) => $cat->name,
823+
],
824+
'meows' => [
825+
'type' => Type::boolean(),
826+
'resolve' => fn (Cat $cat) => $cat->meows,
827+
],
828+
],
829+
]);
830+
831+
$schema = new Schema([
832+
'query' => new ObjectType([
833+
'name' => 'Query',
834+
'fields' => [
835+
'pets' => [
836+
'type' => Type::listOf($PetType),
837+
'resolve' => static fn (): array => [
838+
new PetEntity('dog', 'Odie', true),
839+
new PetEntity('cat', 'Garfield', false),
840+
],
841+
],
842+
],
843+
]),
844+
'types' => [$CatType, $DogType],
845+
]);
846+
847+
$query = '{
848+
pets {
849+
name
850+
... on Dog {
851+
woofs
852+
}
853+
... on Cat {
854+
meows
855+
}
856+
}
857+
}';
858+
859+
$result = GraphQL::executeQuery($schema, $query)->toArray();
860+
861+
self::assertEquals(
862+
[
863+
'data' => [
864+
'pets' => [
865+
['name' => 'Odie', 'woofs' => true],
866+
['name' => 'Garfield', 'meows' => false],
867+
],
868+
],
869+
],
870+
$result
871+
);
872+
}
873+
874+
public function testResolveValueAllowsModifyingObjectValueForUnionType(): void
875+
{
876+
$DogType = new ObjectType([
877+
'name' => 'Dog',
878+
'fields' => [
879+
'name' => [
880+
'type' => Type::string(),
881+
'resolve' => fn (Dog $dog) => $dog->name,
882+
],
883+
'woofs' => [
884+
'type' => Type::boolean(),
885+
'resolve' => fn (Dog $dog) => $dog->woofs,
886+
],
887+
],
888+
]);
889+
890+
$CatType = new ObjectType([
891+
'name' => 'Cat',
892+
'fields' => [
893+
'name' => [
894+
'type' => Type::string(),
895+
'resolve' => fn (Cat $cat) => $cat->name,
896+
],
897+
'meows' => [
898+
'type' => Type::boolean(),
899+
'resolve' => fn (Cat $cat) => $cat->meows,
900+
],
901+
],
902+
]);
903+
904+
$PetType = new UnionType([
905+
'name' => 'Pet',
906+
'types' => [$DogType, $CatType],
907+
'resolveType' => static function (PetEntity $objectValue): string {
908+
if ($objectValue->type === 'dog') {
909+
return 'Dog';
910+
}
911+
912+
return 'Cat';
913+
},
914+
'resolveValue' => static function (PetEntity $objectValue) {
915+
if ($objectValue->type === 'dog') {
916+
return new Dog($objectValue->name, $objectValue->woofs);
917+
}
918+
919+
return new Cat($objectValue->name, $objectValue->woofs);
920+
},
921+
]);
922+
923+
$schema = new Schema([
924+
'query' => new ObjectType([
925+
'name' => 'Query',
926+
'fields' => [
927+
'pets' => [
928+
'type' => Type::listOf($PetType),
929+
'resolve' => static fn (): array => [
930+
new PetEntity('dog', 'Odie', true),
931+
new PetEntity('cat', 'Garfield', false),
932+
],
933+
],
934+
],
935+
]),
936+
]);
937+
938+
$query = '{
939+
pets {
940+
... on Dog {
941+
name
942+
woofs
943+
}
944+
... on Cat {
945+
name
946+
meows
947+
}
948+
}
949+
}';
950+
951+
$result = GraphQL::executeQuery($schema, $query)->toArray();
952+
953+
self::assertEquals(
954+
[
955+
'data' => [
956+
'pets' => [
957+
['name' => 'Odie', 'woofs' => true],
958+
['name' => 'Garfield', 'meows' => false],
959+
],
960+
],
961+
],
962+
$result
963+
);
964+
}
776965
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace GraphQL\Tests\Executor\TestClasses;
4+
5+
final class PetEntity
6+
{
7+
/** @var 'dog'|'cat' */
8+
public string $type;
9+
10+
public string $name;
11+
12+
public bool $woofs;
13+
14+
/** @param 'dog'|'cat' $type */
15+
public function __construct(string $type, string $name, bool $woofs)
16+
{
17+
$this->type = $type;
18+
$this->name = $name;
19+
$this->woofs = $woofs;
20+
}
21+
}

0 commit comments

Comments
 (0)