Skip to content

Commit 3edf624

Browse files
committed
Mapping Executor
1 parent aeb56d1 commit 3edf624

File tree

7 files changed

+499
-141
lines changed

7 files changed

+499
-141
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,11 +278,12 @@ Option | Type | Notes
278278
name | `string` | Required. Name of the field. If not set - GraphQL will look use `key` of fields array on type definition.
279279
type | `Type` or `callback() => Type` | Required. One of internal or custom types. Alternatively - callback that returns `type`.
280280
args | `array` | Array of possible type arguments. Each entry is expected to be an array with following keys: **name** (`string`), **type** (`Type` or `callback() => Type`), **defaultValue** (`any`)
281-
resolve | `callback($value, $args, ResolveInfo $info) => $data` | Function that receives `$value` describing parent type and returns `$data` for this field.
281+
resolve | `callback($value, $args, ResolveInfo $info) => $fieldValue` | Function that receives `$value` of parent type and returns value for this field. Mutually exclusive with `map`
282+
map | `callback($listOfValues, $args, ResolveInfo $info) => $fieldValues[]` | Function that receives list of parent type values and maps them to list of field values. Mutually exclusive with `resolve`
282283
description | `string` | Field description for clients
283284
deprecationReason | `string` | Text describing why this field is deprecated. When not empty - field will not be returned by introspection queries (unless forced)
284285

285-
The `resolve` option is exactly the place where your custom fetching logic lives.
286+
Use `map` or `resolve` for custom data fetching logic. `resolve` is easier to use, but `map` allows batching of queries to backend storage (for example you can use Redis MGET or IN(?) for SQL queries).
286287

287288

288289
### Schema

src/Executor/Executor.php

Lines changed: 205 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,12 @@ private static function executeOperation(ExecutionContext $exeContext, Operation
147147
$fields = self::collectFields($exeContext, $type, $operation->selectionSet, new \ArrayObject(), new \ArrayObject());
148148

149149
if ($operation->operation === 'mutation') {
150-
return self::executeFieldsSerially($exeContext, $type, $rootValue, $fields->getArrayCopy());
150+
$result = self::executeFieldsSerially($exeContext, $type, [$rootValue], $fields);
151+
} else {
152+
$result = self::executeFields($exeContext, $type, [$rootValue], $fields);
151153
}
152154

153-
return self::executeFields($exeContext, $type, $rootValue, $fields);
155+
return null === $result || $result === [] ? [] : $result[0];
154156
}
155157

156158

@@ -187,30 +189,40 @@ private static function getOperationRootType(Schema $schema, OperationDefinition
187189
/**
188190
* Implements the "Evaluating selection sets" section of the spec
189191
* for "write" mode.
192+
*
193+
* @param ExecutionContext $exeContext
194+
* @param ObjectType $parentType
195+
* @param $sourceList
196+
* @param $sourceIsList
197+
* @param $fields
198+
* @return array
199+
* @throws Error
200+
* @throws \Exception
190201
*/
191-
private static function executeFieldsSerially(ExecutionContext $exeContext, ObjectType $parentType, $sourceValue, $fields)
202+
private static function executeFieldsSerially(ExecutionContext $exeContext, ObjectType $parentType, $sourceList, $fields)
192203
{
193204
$results = [];
194205
foreach ($fields as $responseName => $fieldASTs) {
195-
$result = self::resolveField($exeContext, $parentType, $sourceValue, $fieldASTs);
196-
197-
if ($result !== self::$UNDEFINED) {
198-
// Undefined means that field is not defined in schema
199-
$results[$responseName] = $result;
200-
}
206+
self::resolveField($exeContext, $parentType, $sourceList, $fieldASTs, $responseName, $results);
201207
}
208+
202209
return $results;
203210
}
204211

205212
/**
206213
* Implements the "Evaluating selection sets" section of the spec
207214
* for "read" mode.
215+
* @param ExecutionContext $exeContext
216+
* @param ObjectType $parentType
217+
* @param $sourceList
218+
* @param $fields
219+
* @return array
208220
*/
209-
private static function executeFields(ExecutionContext $exeContext, ObjectType $parentType, $source, $fields)
221+
private static function executeFields(ExecutionContext $exeContext, ObjectType $parentType, $sourceList, $fields)
210222
{
211223
// Native PHP doesn't support promises.
212224
// Custom executor should be built for platforms like ReactPHP
213-
return self::executeFieldsSerially($exeContext, $parentType, $source, $fields);
225+
return self::executeFieldsSerially($exeContext, $parentType, $sourceList, $fields);
214226
}
215227

216228

@@ -345,32 +357,33 @@ private static function getFieldEntryKey(Field $node)
345357
}
346358

347359
/**
348-
* Resolves the field on the given source object. In particular, this
349-
* figures out the value that the field returns by calling its resolve function,
350-
* then calls completeValue to complete promises, serialize scalars, or execute
351-
* the sub-selection-set for objects.
360+
* Given list of parent type values returns corresponding list of field values
361+
*
362+
* In particular, this
363+
* figures out the value that the field returns by calling its `resolve` or `map` function,
364+
* then calls `completeValue` on each value to serialize scalars, or execute the sub-selection-set
365+
* for objects.
366+
*
367+
* @param ExecutionContext $exeContext
368+
* @param ObjectType $parentType
369+
* @param $sourceValueList
370+
* @param $fieldASTs
371+
* @return array
372+
* @throws Error
352373
*/
353-
private static function resolveField(ExecutionContext $exeContext, ObjectType $parentType, $source, $fieldASTs)
374+
private static function resolveField(ExecutionContext $exeContext, ObjectType $parentType, $sourceValueList, $fieldASTs, $responseName, &$resolveResult)
354375
{
355376
$fieldAST = $fieldASTs[0];
356377
$fieldName = $fieldAST->name->value;
357378

358379
$fieldDef = self::getFieldDef($exeContext->schema, $parentType, $fieldName);
359380

360381
if (!$fieldDef) {
361-
return self::$UNDEFINED;
382+
return ;
362383
}
363384

364385
$returnType = $fieldDef->getType();
365386

366-
if (isset($fieldDef->resolve)) {
367-
$resolveFn = $fieldDef->resolve;
368-
} else if (isset($parentType->resolveField)) {
369-
$resolveFn = $parentType->resolveField;
370-
} else {
371-
$resolveFn = self::$defaultResolveFn;
372-
}
373-
374387
// Build hash of arguments from the field.arguments AST, using the
375388
// variables scope to fulfill any variable references.
376389
// TODO: find a way to memoize, in case this field is within a List type.
@@ -394,30 +407,74 @@ private static function resolveField(ExecutionContext $exeContext, ObjectType $p
394407
'variableValues' => $exeContext->variableValues,
395408
]);
396409

397-
// If an error occurs while calling the field `resolve` function, ensure that
410+
$mapFn = $fieldDef->mapFn;
411+
412+
// If an error occurs while calling the field `map` or `resolve` function, ensure that
398413
// it is wrapped as a GraphQLError with locations. Log this error and return
399414
// null if allowed, otherwise throw the error so the parent field can handle
400415
// it.
401-
try {
402-
$result = call_user_func($resolveFn, $source, $args, $info);
403-
} catch (\Exception $error) {
404-
$reportedError = Error::createLocatedError($error, $fieldASTs);
416+
if ($mapFn) {
417+
try {
418+
$mapped = call_user_func($mapFn, $sourceValueList, $args, $info);
419+
420+
Utils::invariant(
421+
is_array($mapped) && count($mapped) === count($sourceValueList),
422+
"Function `map` of $parentType.$fieldName is expected to return array " .
423+
"with exact same number of items as list being mapped (first argument of `map`)"
424+
);
425+
426+
} catch (\Exception $error) {
427+
$reportedError = Error::createLocatedError($error, $fieldASTs);
428+
429+
if ($returnType instanceof NonNull) {
430+
throw $reportedError;
431+
}
405432

406-
if ($returnType instanceof NonNull) {
407-
throw $reportedError;
433+
$exeContext->addError($reportedError);
434+
return null;
408435
}
409436

410-
$exeContext->addError($reportedError);
411-
return null;
412-
}
437+
foreach ($mapped as $index => $value) {
438+
$resolveResult[$index][$responseName] = self::completeValueCatchingError(
439+
$exeContext,
440+
$returnType,
441+
$fieldASTs,
442+
$info,
443+
$value
444+
);
445+
}
446+
} else {
447+
if (isset($fieldDef->resolveFn)) {
448+
$resolveFn = $fieldDef->resolveFn;
449+
} else if (isset($parentType->resolveFieldFn)) {
450+
$resolveFn = $parentType->resolveFieldFn;
451+
} else {
452+
$resolveFn = self::$defaultResolveFn;
453+
}
413454

414-
return self::completeValueCatchingError(
415-
$exeContext,
416-
$returnType,
417-
$fieldASTs,
418-
$info,
419-
$result
420-
);
455+
foreach ($sourceValueList as $index => $value) {
456+
try {
457+
$resolved = call_user_func($resolveFn, $value, $args, $info);
458+
} catch (\Exception $error) {
459+
$reportedError = Error::createLocatedError($error, $fieldASTs);
460+
461+
if ($returnType instanceof NonNull) {
462+
throw $reportedError;
463+
}
464+
465+
$exeContext->addError($reportedError);
466+
$resolved = null;
467+
}
468+
469+
$resolveResult[$index][$responseName] = self::completeValueCatchingError(
470+
$exeContext,
471+
$returnType,
472+
$fieldASTs,
473+
$info,
474+
$resolved
475+
);
476+
}
477+
}
421478
}
422479

423480

@@ -489,32 +546,112 @@ private static function completeValue(ExecutionContext $exeContext, Type $return
489546
return null;
490547
}
491548

492-
// If field type is List, complete each item in the list with the inner type
549+
// If field type is Scalar or Enum, serialize to a valid value, returning
550+
// null if serialization is not possible.
551+
if ($returnType instanceof ScalarType ||
552+
$returnType instanceof EnumType) {
553+
return $returnType->serialize($result);
554+
}
555+
556+
// If field type is List, and return type is Composite - complete by executing these fields with list value as parameter
493557
if ($returnType instanceof ListOfType) {
494558
$itemType = $returnType->getWrappedType();
559+
495560
Utils::invariant(
496561
is_array($result) || $result instanceof \Traversable,
497562
'User Error: expected iterable, but did not find one.'
498563
);
499564

500-
$tmp = [];
501-
foreach ($result as $item) {
502-
$tmp[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
565+
// For Object[]:
566+
// Allow all object fields to process list value in it's `map` callback:
567+
if ($itemType instanceof ObjectType) {
568+
// Filter out nulls (as `map` doesn't expect it):
569+
$list = [];
570+
foreach ($result as $index => $item) {
571+
if (null !== $item) {
572+
$list[] = $item;
573+
}
574+
}
575+
576+
$subFieldASTs = self::collectSubFields($exeContext, $itemType, $fieldASTs);
577+
$mapped = self::executeFields($exeContext, $itemType, $list, $subFieldASTs);
578+
579+
$i = 0;
580+
$completed = [];
581+
foreach ($result as $index => $item) {
582+
if (null === $item) {
583+
// Complete nulls separately
584+
$completed[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
585+
} else {
586+
// Assuming same order of mapped values
587+
$completed[] = $mapped[$i++];
588+
}
589+
}
590+
return $completed;
591+
592+
} else if ($itemType instanceof AbstractType) {
593+
594+
// Values sharded by ObjectType
595+
$listPerObjectType = [];
596+
597+
// Helper structures to restore ordering after resolve calls
598+
$resultTypeMap = [];
599+
$typeNameMap = [];
600+
$cursors = [];
601+
602+
foreach ($result as $index => $item) {
603+
if (null !== $item) {
604+
$objectType = $itemType->getObjectType($item, $info);
605+
606+
if ($objectType && !$itemType->isPossibleType($objectType)) {
607+
$exeContext->addError(new Error(
608+
"Runtime Object type \"$objectType\" is not a possible type for \"$itemType\"."
609+
));
610+
$result[$index] = null;
611+
} else {
612+
$listPerObjectType[$objectType->name][] = $item;
613+
$resultTypeMap[$index] = $objectType->name;
614+
$typeNameMap[$objectType->name] = $objectType;
615+
}
616+
}
617+
}
618+
619+
$mapped = [];
620+
foreach ($listPerObjectType as $typeName => $list) {
621+
$objectType = $typeNameMap[$typeName];
622+
$subFieldASTs = self::collectSubFields($exeContext, $objectType, $fieldASTs);
623+
$mapped[$typeName] = self::executeFields($exeContext, $objectType, $list, $subFieldASTs);
624+
$cursors[$typeName] = 0;
625+
}
626+
627+
// Restore order:
628+
$completed = [];
629+
foreach ($result as $index => $item) {
630+
if (null === $item) {
631+
// Complete nulls separately
632+
$completed[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
633+
} else {
634+
$typeName = $resultTypeMap[$index];
635+
$completed[] = $mapped[$typeName][$cursors[$typeName]++];
636+
}
637+
}
638+
639+
return $completed;
640+
} else {
641+
642+
// For simple lists:
643+
$tmp = [];
644+
foreach ($result as $item) {
645+
$tmp[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
646+
}
647+
return $tmp;
503648
}
504-
return $tmp;
505-
}
506649

507-
// If field type is Scalar or Enum, serialize to a valid value, returning
508-
// null if serialization is not possible.
509-
if ($returnType instanceof ScalarType ||
510-
$returnType instanceof EnumType) {
511-
Utils::invariant(method_exists($returnType, 'serialize'), 'Missing serialize method on type');
512-
return $returnType->serialize($result);
513650
}
514651

515-
// Field type must be Object, Interface or Union and expect sub-selections.
516652
if ($returnType instanceof ObjectType) {
517653
$objectType = $returnType;
654+
518655
} else if ($returnType instanceof AbstractType) {
519656
$objectType = $returnType->getObjectType($result, $info);
520657

@@ -542,6 +679,18 @@ private static function completeValue(ExecutionContext $exeContext, Type $return
542679
}
543680

544681
// Collect sub-fields to execute to complete this value.
682+
$subFieldASTs = self::collectSubFields($exeContext, $objectType, $fieldASTs);
683+
return self::executeFields($exeContext, $objectType, [$result], $subFieldASTs)[0];
684+
}
685+
686+
/**
687+
* @param ExecutionContext $exeContext
688+
* @param ObjectType $objectType
689+
* @param $fieldASTs
690+
* @return \ArrayObject
691+
*/
692+
private static function collectSubFields(ExecutionContext $exeContext, ObjectType $objectType, $fieldASTs)
693+
{
545694
$subFieldASTs = new \ArrayObject();
546695
$visitedFragmentNames = new \ArrayObject();
547696
for ($i = 0; $i < count($fieldASTs); $i++) {
@@ -556,11 +705,9 @@ private static function completeValue(ExecutionContext $exeContext, Type $return
556705
);
557706
}
558707
}
559-
560-
return self::executeFields($exeContext, $objectType, $result, $subFieldASTs);
708+
return $subFieldASTs;
561709
}
562710

563-
564711
/**
565712
* If a resolve function is not given, then a default resolve behavior is used
566713
* which takes the property of the source object of the same name as the field

0 commit comments

Comments
 (0)