Skip to content

Commit 9563fc5

Browse files
authored
Create field args mapper and cache args resolution
1 parent c4df6e5 commit 9563fc5

File tree

10 files changed

+177
-18
lines changed

10 files changed

+177
-18
lines changed

docs/class-reference.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1341,8 +1341,9 @@ const CLASS_MAP = [
13411341

13421342
Implements the "Evaluating requests" section of the GraphQL specification.
13431343

1344+
@phpstan-type ArgsMapper callable(array<string, mixed>, FieldDefinition, FieldNode, mixed): mixed
13441345
@phpstan-type FieldResolver callable(mixed, array<string, mixed>, mixed, ResolveInfo): mixed
1345-
@phpstan-type ImplementationFactory callable(PromiseAdapter, Schema, DocumentNode, mixed, mixed, array<mixed>, ?string, callable): ExecutorImplementation
1346+
@phpstan-type ImplementationFactory callable(PromiseAdapter, Schema, DocumentNode, mixed, mixed, array<mixed>, ?string, callable, callable): ExecutorImplementation
13461347

13471348
@see \GraphQL\Tests\Executor\ExecutorTest
13481349

@@ -1388,6 +1389,7 @@ static function execute(
13881389
* @param array<string, mixed>|null $variableValues
13891390
*
13901391
* @phpstan-param FieldResolver|null $fieldResolver
1392+
* @phpstan-param ArgsMapper|null $argsMapper
13911393
*
13921394
* @api
13931395
*/
@@ -1399,7 +1401,8 @@ static function promiseToExecute(
13991401
$contextValue = null,
14001402
?array $variableValues = null,
14011403
?string $operationName = null,
1402-
?callable $fieldResolver = null
1404+
?callable $fieldResolver = null,
1405+
?callable $argsMapper = null
14031406
): GraphQL\Executor\Promise\Promise
14041407
```
14051408

docs/type-definitions/object-types.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ This example uses **inline** style for Object Type definitions, but you can also
6666
| interfaces | `array` or `callable` | List of interfaces implemented by this type or callable returning such a list. See [Interface Types](interfaces.md) for details. See also the section on [Circular types](#recurring-and-circular-types) for an explanation of when to use callable for this option. |
6767
| isTypeOf | `callable` | **function ($value, $context, [ResolveInfo](../class-reference.md#graphqltypedefinitionresolveinfo) $info): bool**<br> Expected to return **true** if **$value** qualifies for this type (see section about [Abstract Type Resolution](interfaces.md#interface-role-in-data-fetching) for explanation). |
6868
| resolveField | `callable` | **function ($value, array $args, $context, [ResolveInfo](../class-reference.md#graphqltypedefinitionresolveinfo) $info): mixed**<br> Given the **$value** of this type, it is expected to return value for a field defined in **$info->fieldName**. A good place to define a type-specific strategy for field resolution. See section on [Data Fetching](../data-fetching.md) for details. |
69+
| argsMapper | `callable` | **function (array $args, FieldDefinition, FieldNode): mixed**<br> Called once, when Executor resolves arguments for given field. Could be used to validate args and/or to map them to DTO/Object. |
6970
| visible | `bool` or `callable` | Defaults to `true`. The given callable receives no arguments and is expected to return a `bool`, it is called once when the field may be accessed. The field is treated as if it were not defined at all when this is `false`. |
7071

7172
### Field configuration options

src/Executor/ExecutionContext.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* and the fragments defined in the query document.
1616
*
1717
* @phpstan-import-type FieldResolver from Executor
18+
* @phpstan-import-type ArgsMapper from Executor
1819
*/
1920
class ExecutionContext
2021
{
@@ -41,6 +42,13 @@ class ExecutionContext
4142
*/
4243
public $fieldResolver;
4344

45+
/**
46+
* @var callable
47+
*
48+
* @phpstan-var ArgsMapper
49+
*/
50+
public $argsMapper;
51+
4452
/** @var array<int, Error> */
4553
public array $errors;
4654

@@ -64,6 +72,7 @@ public function __construct(
6472
array $variableValues,
6573
array $errors,
6674
callable $fieldResolver,
75+
callable $argsMapper,
6776
PromiseAdapter $promiseAdapter
6877
) {
6978
$this->schema = $schema;
@@ -74,6 +83,7 @@ public function __construct(
7483
$this->variableValues = $variableValues;
7584
$this->errors = $errors;
7685
$this->fieldResolver = $fieldResolver;
86+
$this->argsMapper = $argsMapper;
7787
$this->promiseAdapter = $promiseAdapter;
7888
}
7989

src/Executor/Executor.php

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,18 @@
77
use GraphQL\Executor\Promise\Promise;
88
use GraphQL\Executor\Promise\PromiseAdapter;
99
use GraphQL\Language\AST\DocumentNode;
10+
use GraphQL\Language\AST\FieldNode;
11+
use GraphQL\Type\Definition\FieldDefinition;
1012
use GraphQL\Type\Definition\ResolveInfo;
1113
use GraphQL\Type\Schema;
1214
use GraphQL\Utils\Utils;
1315

1416
/**
1517
* Implements the "Evaluating requests" section of the GraphQL specification.
1618
*
19+
* @phpstan-type ArgsMapper callable(array<string, mixed>, FieldDefinition, FieldNode, mixed): mixed
1720
* @phpstan-type FieldResolver callable(mixed, array<string, mixed>, mixed, ResolveInfo): mixed
18-
* @phpstan-type ImplementationFactory callable(PromiseAdapter, Schema, DocumentNode, mixed, mixed, array<mixed>, ?string, callable): ExecutorImplementation
21+
* @phpstan-type ImplementationFactory callable(PromiseAdapter, Schema, DocumentNode, mixed, mixed, array<mixed>, ?string, callable, callable): ExecutorImplementation
1922
*
2023
* @see \GraphQL\Tests\Executor\ExecutorTest
2124
*/
@@ -28,6 +31,13 @@ class Executor
2831
*/
2932
private static $defaultFieldResolver = [self::class, 'defaultFieldResolver'];
3033

34+
/**
35+
* @var callable
36+
*
37+
* @phpstan-var ArgsMapper
38+
*/
39+
private static $defaultArgsMapper = [self::class, 'defaultArgsMapper'];
40+
3141
private static ?PromiseAdapter $defaultPromiseAdapter;
3242

3343
/**
@@ -53,6 +63,12 @@ public static function setDefaultFieldResolver(callable $fieldResolver): void
5363
self::$defaultFieldResolver = $fieldResolver;
5464
}
5565

66+
/** @phpstan-param ArgsMapper $argsMapper */
67+
public static function setDefaultArgsMapper(callable $argsMapper): void
68+
{
69+
self::$defaultArgsMapper = $argsMapper;
70+
}
71+
5672
public static function getPromiseAdapter(): PromiseAdapter
5773
{
5874
return self::$defaultPromiseAdapter ??= new SyncPromiseAdapter();
@@ -132,6 +148,7 @@ public static function execute(
132148
* @param array<string, mixed>|null $variableValues
133149
*
134150
* @phpstan-param FieldResolver|null $fieldResolver
151+
* @phpstan-param ArgsMapper|null $argsMapper
135152
*
136153
* @api
137154
*/
@@ -143,7 +160,8 @@ public static function promiseToExecute(
143160
$contextValue = null,
144161
?array $variableValues = null,
145162
?string $operationName = null,
146-
?callable $fieldResolver = null
163+
?callable $fieldResolver = null,
164+
?callable $argsMapper = null
147165
): Promise {
148166
$executor = (self::$implementationFactory)(
149167
$promiseAdapter,
@@ -153,7 +171,8 @@ public static function promiseToExecute(
153171
$contextValue,
154172
$variableValues ?? [],
155173
$operationName,
156-
$fieldResolver ?? self::$defaultFieldResolver
174+
$fieldResolver ?? self::$defaultFieldResolver,
175+
$argsMapper ?? self::$defaultArgsMapper,
157176
);
158177

159178
return $executor->doExecute();
@@ -179,4 +198,16 @@ public static function defaultFieldResolver($objectLikeValue, array $args, $cont
179198
? $property($objectLikeValue, $args, $contextValue, $info)
180199
: $property;
181200
}
201+
202+
/**
203+
* @template T of array<string, mixed>
204+
*
205+
* @param T $args
206+
*
207+
* @return T
208+
*/
209+
public static function defaultArgsMapper(array $args): array
210+
{
211+
return $args;
212+
}
182213
}

src/Executor/ReferenceExecutor.php

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
/**
3838
* @phpstan-import-type FieldResolver from Executor
3939
* @phpstan-import-type Path from ResolveInfo
40+
* @phpstan-import-type ArgsMapper from Executor
4041
*
4142
* @phpstan-type Fields \ArrayObject<string, \ArrayObject<int, FieldNode>>
4243
*/
@@ -60,6 +61,9 @@ class ReferenceExecutor implements ExecutorImplementation
6061
*/
6162
protected \SplObjectStorage $subFieldCache;
6263

64+
/** @var \SplObjectStorage<FieldDefinition, \SplObjectStorage<FieldNode, mixed>> */
65+
protected \SplObjectStorage $fieldArgsCache;
66+
6367
protected function __construct(ExecutionContext $context)
6468
{
6569
if (! isset(static::$UNDEFINED)) {
@@ -68,6 +72,7 @@ protected function __construct(ExecutionContext $context)
6872

6973
$this->exeContext = $context;
7074
$this->subFieldCache = new \SplObjectStorage();
75+
$this->fieldArgsCache = new \SplObjectStorage();
7176
}
7277

7378
/**
@@ -76,6 +81,7 @@ protected function __construct(ExecutionContext $context)
7681
* @param array<string, mixed> $variableValues
7782
*
7883
* @phpstan-param FieldResolver $fieldResolver
84+
* @phpstan-param ArgsMapper $argsMapper
7985
*
8086
* @throws \Exception
8187
*/
@@ -87,7 +93,8 @@ public static function create(
8793
$contextValue,
8894
array $variableValues,
8995
?string $operationName,
90-
callable $fieldResolver
96+
callable $fieldResolver,
97+
callable $argsMapper
9198
): ExecutorImplementation {
9299
$exeContext = static::buildExecutionContext(
93100
$schema,
@@ -97,7 +104,8 @@ public static function create(
97104
$variableValues,
98105
$operationName,
99106
$fieldResolver,
100-
$promiseAdapter
107+
$argsMapper,
108+
$promiseAdapter,
101109
);
102110

103111
if (\is_array($exeContext)) {
@@ -141,6 +149,7 @@ protected static function buildExecutionContext(
141149
array $rawVariableValues,
142150
?string $operationName,
143151
callable $fieldResolver,
152+
callable $argsMapper,
144153
PromiseAdapter $promiseAdapter
145154
) {
146155
/** @var array<int, Error> $errors */
@@ -217,6 +226,7 @@ protected static function buildExecutionContext(
217226
$variableValues,
218227
$errors,
219228
$fieldResolver,
229+
$argsMapper,
220230
$promiseAdapter
221231
);
222232
}
@@ -640,20 +650,22 @@ protected function resolveField(
640650
$exeContext->variableValues,
641651
$unaliasedPath
642652
);
643-
if ($fieldDef->resolveFn !== null) {
644-
$resolveFn = $fieldDef->resolveFn;
645-
} elseif ($parentType->resolveFieldFn !== null) {
646-
$resolveFn = $parentType->resolveFieldFn;
647-
} else {
648-
$resolveFn = $this->exeContext->fieldResolver;
649-
}
653+
654+
$resolveFn = $fieldDef->resolveFn
655+
?? $parentType->resolveFieldFn
656+
?? $this->exeContext->fieldResolver;
657+
658+
$argsMapper = $fieldDef->argsMapper
659+
?? $parentType->argsMapper
660+
?? $this->exeContext->argsMapper;
650661

651662
// Get the resolve function, regardless of if its result is normal
652663
// or abrupt (error).
653664
$result = $this->resolveFieldValueOrError(
654665
$fieldDef,
655666
$fieldNode,
656667
$resolveFn,
668+
$argsMapper,
657669
$rootValue,
658670
$info,
659671
$contextValue
@@ -721,18 +733,22 @@ protected function resolveFieldValueOrError(
721733
FieldDefinition $fieldDef,
722734
FieldNode $fieldNode,
723735
callable $resolveFn,
736+
callable $argsMapper,
724737
$rootValue,
725738
ResolveInfo $info,
726739
$contextValue
727740
) {
728741
try {
729742
// Build a map of arguments from the field.arguments AST, using the
730743
// variables scope to fulfill any variable references.
731-
$args = Values::getArgumentValues(
744+
/** @phpstan-ignore-next-line ignored because no way to tell phpstan what are generics of SplObjectStorage without assign it to var first */
745+
$this->fieldArgsCache[$fieldDef] ??= new \SplObjectStorage();
746+
747+
$args = $this->fieldArgsCache[$fieldDef][$fieldNode] ??= $argsMapper(Values::getArgumentValues(
732748
$fieldDef,
733749
$fieldNode,
734750
$this->exeContext->variableValues
735-
);
751+
), $fieldDef, $fieldNode, $contextValue);
736752

737753
return $resolveFn($rootValue, $args, $contextValue, $info);
738754
} catch (\Throwable $error) {

src/Type/Definition/FieldDefinition.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* @see Executor
1313
*
1414
* @phpstan-import-type FieldResolver from Executor
15+
* @phpstan-import-type ArgsMapper from Executor
1516
* @phpstan-import-type ArgumentListConfig from Argument
1617
*
1718
* @phpstan-type FieldType (Type&OutputType)|callable(): (Type&OutputType)
@@ -22,6 +23,7 @@
2223
* type: FieldType,
2324
* resolve?: FieldResolver|null,
2425
* args?: ArgumentListConfig|null,
26+
* argsMapper?: ArgsMapper|null,
2527
* description?: string|null,
2628
* visible?: VisibilityFn|bool,
2729
* deprecationReason?: string|null,
@@ -32,6 +34,7 @@
3234
* type: FieldType,
3335
* resolve?: FieldResolver|null,
3436
* args?: ArgumentListConfig|null,
37+
* argsMapper?: ArgsMapper|null,
3538
* description?: string|null,
3639
* visible?: VisibilityFn|bool,
3740
* deprecationReason?: string|null,
@@ -56,6 +59,15 @@ class FieldDefinition
5659
/** @var array<int, Argument> */
5760
public array $args;
5861

62+
/**
63+
* Callback to transform args to value object.
64+
*
65+
* @var callable|null
66+
*
67+
* @phpstan-var ArgsMapper|null
68+
*/
69+
public $argsMapper;
70+
5971
/**
6072
* Callback for resolving field value given parent value.
6173
*
@@ -103,6 +115,7 @@ public function __construct(array $config)
103115
$this->args = isset($config['args'])
104116
? Argument::listFromConfig($config['args'])
105117
: [];
118+
$this->argsMapper = $config['argsMapper'] ?? null;
106119
$this->description = $config['description'] ?? null;
107120
$this->visible = $config['visible'] ?? true;
108121
$this->deprecationReason = $config['deprecationReason'] ?? null;

src/Type/Definition/ObjectType.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,14 @@
5050
* ]);
5151
*
5252
* @phpstan-import-type FieldResolver from Executor
53+
* @phpstan-import-type ArgsMapper from Executor
5354
*
5455
* @phpstan-type InterfaceTypeReference InterfaceType|callable(): InterfaceType
5556
* @phpstan-type ObjectConfig array{
5657
* name?: string|null,
5758
* description?: string|null,
5859
* resolveField?: FieldResolver|null,
60+
* argsMapper?: ArgsMapper|null,
5961
* fields: (callable(): iterable<mixed>)|iterable<mixed>,
6062
* interfaces?: iterable<InterfaceTypeReference>|callable(): iterable<InterfaceTypeReference>,
6163
* isTypeOf?: (callable(mixed $objectValue, mixed $context, ResolveInfo $resolveInfo): (bool|Deferred|null))|null,
@@ -81,6 +83,13 @@ class ObjectType extends Type implements OutputType, CompositeType, NullableType
8183
*/
8284
public $resolveFieldFn;
8385

86+
/**
87+
* @var callable|null
88+
*
89+
* @phpstan-var ArgsMapper|null
90+
*/
91+
public $argsMapper;
92+
8493
/** @phpstan-var ObjectConfig */
8594
public array $config;
8695

@@ -94,6 +103,7 @@ public function __construct(array $config)
94103
$this->name = $config['name'] ?? $this->inferName();
95104
$this->description = $config['description'] ?? null;
96105
$this->resolveFieldFn = $config['resolveField'] ?? null;
106+
$this->argsMapper = $config['argsMapper'] ?? null;
97107
$this->astNode = $config['astNode'] ?? null;
98108
$this->extensionASTNodes = $config['extensionASTNodes'] ?? [];
99109

src/Utils/SchemaExtender.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,7 @@ protected function extendFieldMap(Type $type): array
505505
'type' => $this->extendType($field->getType()),
506506
'args' => $this->extendArgs($field->args),
507507
'resolve' => $field->resolveFn,
508+
'argsMapper' => $field->argsMapper,
508509
'astNode' => $field->astNode,
509510
];
510511
}
@@ -537,7 +538,8 @@ protected function extendObjectType(ObjectType $type): ObjectType
537538
'interfaces' => fn (): array => $this->extendImplementedInterfaces($type),
538539
'fields' => fn (): array => $this->extendFieldMap($type),
539540
'isTypeOf' => [$type, 'isTypeOf'],
540-
'resolveField' => $type->resolveFieldFn ?? null,
541+
'resolveField' => $type->resolveFieldFn,
542+
'argsMapper' => $type->argsMapper,
541543
'astNode' => $type->astNode,
542544
'extensionASTNodes' => $extensionASTNodes,
543545
]);

0 commit comments

Comments
 (0)