/** * Resolves the field on the given source object. In particular, this * figures out the value that the field returns by calling its resolve function, * then calls completeValue to complete promises, serialize scalars, or execute * the sub-selection-set for objects. */ private static function resolveField(ExecutionContext $exeContext, ObjectType $parentType, $source, $fieldASTs) { $fieldAST = $fieldASTs[0]; $fieldName = $fieldAST->name->value; $fieldDef = self::getFieldDef($exeContext->schema, $parentType, $fieldName); if (!$fieldDef) { return self::$UNDEFINED; } $returnType = $fieldDef->getType(); if (isset($fieldDef->resolve)) { $resolveFn = $fieldDef->resolve; } else { if (isset(self::$defaultResolveFn)) { $resolveFn = self::$defaultResolveFn; } else { $resolveFn = [__CLASS__, 'defaultResolveFn']; } } // Build hash of arguments from the field.arguments AST, using the // variables scope to fulfill any variable references. // TODO: find a way to memoize, in case this field is within a List type. $args = Values::getArgumentValues($fieldDef->args, $fieldAST->arguments, $exeContext->variableValues); // The resolve function's optional third argument is a collection of // information about the current execution state. $info = new ResolveInfo(['fieldName' => $fieldName, 'fieldASTs' => $fieldASTs, 'returnType' => $returnType, 'parentType' => $parentType, 'schema' => $exeContext->schema, 'fragments' => $exeContext->fragments, 'rootValue' => $exeContext->rootValue, 'operation' => $exeContext->operation, 'variableValues' => $exeContext->variableValues]); // If an error occurs while calling the field `resolve` function, ensure that // it is wrapped as a GraphQLError with locations. Log this error and return // null if allowed, otherwise throw the error so the parent field can handle // it. try { $result = call_user_func($resolveFn, $source, $args, $info); } catch (\Exception $error) { $reportedError = Error::createLocatedError($error, $fieldASTs); if ($returnType instanceof NonNull) { throw $reportedError; } $exeContext->addError($reportedError); return null; } return self::completeValueCatchingError($exeContext, $returnType, $fieldASTs, $info, $result); }
/** * Given list of parent type values returns corresponding list of field values * * In particular, this * figures out the value that the field returns by calling its `resolve` or `map` function, * then calls `completeValue` on each value to serialize scalars, or execute the sub-selection-set * for objects. * * @param ExecutionContext $exeContext * @param ObjectType $parentType * @param $sourceValueList * @param $fieldASTs * @return array * @throws Error */ private static function resolveField(ExecutionContext $exeContext, ObjectType $parentType, $sourceValueList, $fieldASTs, $responseName, &$resolveResult) { $fieldAST = $fieldASTs[0]; $uid = self::getUid($fieldAST); // Get memoized variables if they exist if (isset(self::$memoized['resolveField'][$uid])) { $memoized = self::$memoized['resolveField'][$uid]; $fieldDef = $memoized['fieldDef']; $returnType = $fieldDef->getType(); $args = $memoized['args']; $info = $memoized['info']; } else { $fieldName = $fieldAST->name->value; $fieldDef = self::getFieldDef($exeContext->schema, $parentType, $fieldName); if (!$fieldDef) { return; } $returnType = $fieldDef->getType(); // Build hash of arguments from the field.arguments AST, using the // variables scope to fulfill any variable references. $args = Values::getArgumentValues($fieldDef->args, $fieldAST->arguments, $exeContext->variableValues); // The resolve function's optional third argument is a collection of // information about the current execution state. $info = new ResolveInfo(['fieldName' => $fieldName, 'fieldASTs' => $fieldASTs, 'returnType' => $returnType, 'parentType' => $parentType, 'schema' => $exeContext->schema, 'fragments' => $exeContext->fragments, 'rootValue' => $exeContext->rootValue, 'operation' => $exeContext->operation, 'variableValues' => $exeContext->variableValues]); self::$memoized['resolveField'][$uid] = ['fieldDef' => $fieldDef, 'args' => $args, 'info' => $info, 'results' => new \SplObjectStorage()]; } $mapFn = $fieldDef->mapFn; // If an error occurs while calling the field `map` or `resolve` function, ensure that // it is wrapped as a GraphQLError with locations. Log this error and return // null if allowed, otherwise throw the error so the parent field can handle // it. if ($mapFn) { try { $mapped = call_user_func($mapFn, $sourceValueList, $args, $info); Utils::invariant(is_array($mapped) && count($mapped) === count($sourceValueList), "Function `map` of {$parentType}.{$fieldName} is expected to return array " . "with exact same number of items as list being mapped (first argument of `map`)"); } catch (\Exception $error) { $reportedError = Error::createLocatedError($error, $fieldASTs); if ($returnType instanceof NonNull) { throw $reportedError; } $exeContext->addError($reportedError); return null; } foreach ($mapped as $index => $value) { $resolveResult[$index][$responseName] = self::completeValueCatchingError($exeContext, $returnType, $fieldASTs, $info, $value); } } else { if (isset($fieldDef->resolveFn)) { $resolveFn = $fieldDef->resolveFn; } else { if (isset($parentType->resolveFieldFn)) { $resolveFn = $parentType->resolveFieldFn; } else { $resolveFn = self::$defaultResolveFn; } } foreach ($sourceValueList as $index => $value) { // If FieldAST uid and parent object are both the same as in a previous run, // use its result instead to prevent unnecessary work. Works for objects only. $isObject = is_object($value); if ($isObject && isset(self::$memoized['resolveField'][$uid]['results'][$value])) { $result = self::$memoized['resolveField'][$uid]['results'][$value]; } else { try { $resolved = call_user_func($resolveFn, $value, $args, $info); } catch (\Exception $error) { $reportedError = Error::createLocatedError($error, $fieldASTs); if ($returnType instanceof NonNull) { throw $reportedError; } $exeContext->addError($reportedError); $resolved = null; } $result = self::completeValueCatchingError($exeContext, $returnType, $fieldASTs, $info, $resolved); if ($isObject) { self::$memoized['resolveField'][$uid]['results'][$value] = $result; } } $resolveResult[$index][$responseName] = $result; } } }
/** * Given list of parent type values returns corresponding list of field values * * In particular, this * figures out the value that the field returns by calling its `resolve` or `map` function, * then calls `completeValue` on each value to serialize scalars, or execute the sub-selection-set * for objects. * * @param ExecutionContext $exeContext * @param ObjectType $parentType * @param $sourceValueList * @param $fieldASTs * @return array * @throws Error */ private static function resolveField(ExecutionContext $exeContext, ObjectType $parentType, $sourceValueList, $fieldASTs, $responseName, &$resolveResult) { $fieldAST = $fieldASTs[0]; $fieldName = $fieldAST->name->value; $fieldDef = self::getFieldDef($exeContext->schema, $parentType, $fieldName); if (!$fieldDef) { return; } $returnType = $fieldDef->getType(); // Build hash of arguments from the field.arguments AST, using the // variables scope to fulfill any variable references. // TODO: find a way to memoize, in case this field is within a List type. $args = Values::getArgumentValues($fieldDef->args, $fieldAST->arguments, $exeContext->variableValues); // The resolve function's optional third argument is a collection of // information about the current execution state. $info = new ResolveInfo(['fieldName' => $fieldName, 'fieldASTs' => $fieldASTs, 'returnType' => $returnType, 'parentType' => $parentType, 'schema' => $exeContext->schema, 'fragments' => $exeContext->fragments, 'rootValue' => $exeContext->rootValue, 'operation' => $exeContext->operation, 'variableValues' => $exeContext->variableValues]); $mapFn = $fieldDef->mapFn; // If an error occurs while calling the field `map` or `resolve` function, ensure that // it is wrapped as a GraphQLError with locations. Log this error and return // null if allowed, otherwise throw the error so the parent field can handle // it. if ($mapFn) { try { $mapped = call_user_func($mapFn, $sourceValueList, $args, $info); $validType = is_array($mapped) || $mapped instanceof \Traversable && $mapped instanceof \Countable; $mappedCount = count($mapped); $sourceCount = count($sourceValueList); Utils::invariant($validType && count($mapped) === count($sourceValueList), "Function `map` of {$parentType}.{$fieldName} is expected to return array or " . "countable traversable with exact same number of items as list being mapped. " . "Got '%s' with count '{$mappedCount}' against '{$sourceCount}' expected.", Utils::getVariableType($mapped)); } catch (\Exception $error) { $reportedError = Error::createLocatedError($error, $fieldASTs); if ($returnType instanceof NonNull) { throw $reportedError; } $exeContext->addError($reportedError); return null; } foreach ($mapped as $index => $value) { $resolveResult[$index][$responseName] = self::completeValueCatchingError($exeContext, $returnType, $fieldASTs, $info, $value); } } else { if (isset($fieldDef->resolveFn)) { $resolveFn = $fieldDef->resolveFn; } else { if (isset($parentType->resolveFieldFn)) { $resolveFn = $parentType->resolveFieldFn; } else { $resolveFn = self::$defaultResolveFn; } } foreach ($sourceValueList as $index => $value) { try { $resolved = call_user_func($resolveFn, $value, $args, $info); } catch (\Exception $error) { $reportedError = Error::createLocatedError($error, $fieldASTs); if ($returnType instanceof NonNull) { throw $reportedError; } $exeContext->addError($reportedError); $resolved = null; } $resolveResult[$index][$responseName] = self::completeValueCatchingError($exeContext, $returnType, $fieldASTs, $info, $resolved); } } }
/** * Implements the instructions for completeValue as defined in the * "Field entries" section of the spec. * * If the field type is Non-Null, then this recursively completes the value * for the inner type. It throws a field error if that completion returns null, * as per the "Nullability" section of the spec. * * If the field type is a List, then this recursively completes the value * for the inner type on each item in the list. * * If the field type is a Scalar or Enum, ensures the completed value is a legal * value of the type by calling the `serialize` method of GraphQL type * definition. * * Otherwise, the field type expects a sub-selection set, and will complete the * value by evaluating all sub-selections. */ private static function completeValue(ExecutionContext $exeContext, Type $returnType, $fieldASTs, ResolveInfo $info, &$result) { if ($result instanceof \Exception) { throw Error::createLocatedError($result, $fieldASTs); } // If field type is NonNull, complete for inner type, and throw field error // if result is null. if ($returnType instanceof NonNull) { $completed = self::completeValue($exeContext, $returnType->getWrappedType(), $fieldASTs, $info, $result); if ($completed === null) { throw new Error('Cannot return null for non-nullable type.', $fieldASTs instanceof \ArrayObject ? $fieldASTs->getArrayCopy() : $fieldASTs); } return $completed; } // If result is null-like, return null. if (null === $result) { return null; } // If field type is List, complete each item in the list with the inner type if ($returnType instanceof ListOfType) { $itemType = $returnType->getWrappedType(); Utils::invariant(is_array($result) || $result instanceof \Traversable, 'User Error: expected iterable, but did not find one.'); $tmp = []; foreach ($result as $item) { $tmp[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item); } return $tmp; } // If field type is Scalar or Enum, serialize to a valid value, returning // null if serialization is not possible. if ($returnType instanceof ScalarType || $returnType instanceof EnumType) { Utils::invariant(method_exists($returnType, 'serialize'), 'Missing serialize method on type'); return $returnType->serialize($result); } // Field type must be Object, Interface or Union and expect sub-selections. if ($returnType instanceof ObjectType) { $runtimeType = $returnType; } else { if ($returnType instanceof AbstractType) { $runtimeType = $returnType->getObjectType($result, $info); if ($runtimeType && !$returnType->isPossibleType($runtimeType)) { throw new Error("Runtime Object type \"{$runtimeType}\" is not a possible type for \"{$returnType}\"."); } } else { $runtimeType = null; } } if (!$runtimeType) { return null; } // If there is an isTypeOf predicate function, call it with the // current result. If isTypeOf returns false, then raise an error rather // than continuing execution. if (false === $runtimeType->isTypeOf($result, $info)) { throw new Error("Expected value of type {$runtimeType} but got: " . Utils::getVariableType($result), $fieldASTs); } // Collect sub-fields to execute to complete this value. $subFieldASTs = new \ArrayObject(); $visitedFragmentNames = new \ArrayObject(); for ($i = 0; $i < count($fieldASTs); $i++) { // Get memoized value if it exists $uid = self::getFieldUid($fieldASTs[$i], $runtimeType); if (isset($exeContext->memoized['collectSubFields'][$uid])) { $subFieldASTs = $exeContext->memoized['collectSubFields'][$uid]; } else { $selectionSet = $fieldASTs[$i]->selectionSet; if ($selectionSet) { $subFieldASTs = self::collectFields($exeContext, $runtimeType, $selectionSet, $subFieldASTs, $visitedFragmentNames); $exeContext->memoized['collectSubFields'][$uid] = $subFieldASTs; } } } return self::executeFields($exeContext, $runtimeType, $result, $subFieldASTs); }