/** * Format error for output. * * @param Error $e * @return array */ public function formatError(Error $e) { $error = ['message' => $e->getMessage()]; $locations = $e->getLocations(); if (!empty($locations)) { $error['locations'] = array_map(function ($location) { return $location->toArray(); }, $locations); } $previous = $e->getPrevious(); if ($previous && $previous instanceof ValidationError) { $error['validation'] = $previous->getValidatorMessages(); } return $error; }
/** * @param Schema $schema * @param $requestString * @param mixed $rootValue * @param array <string, string>|null $variableValues * @param string|null $operationName * @return array */ public static function execute(Schema $schema, $requestString, $rootValue = null, $variableValues = null, $operationName = null) { try { $source = new Source($requestString ?: '', 'GraphQL request'); $documentAST = Parser::parse($source); $validationErrors = DocumentValidator::validate($schema, $documentAST); if (!empty($validationErrors)) { return ['errors' => array_map(['GraphQL\\Error', 'formatError'], $validationErrors)]; } else { return Executor::execute($schema, $documentAST, $rootValue, $variableValues, $operationName)->toArray(); } } catch (Error $e) { return ['errors' => [Error::formatError($e)]]; } }
/** * @param Schema $schema * @param $requestString * @param mixed $rootObject * @param array <string, string>|null $variableValues * @param string|null $operationName * @return array */ public static function execute(Schema $schema, $requestString, $rootObject = null, $variableValues = null, $operationName = null) { try { $source = new Source($requestString ?: '', 'GraphQL request'); $ast = Parser::parse($source); $validationResult = DocumentValidator::validate($schema, $ast); if (empty($validationResult['isValid'])) { return ['errors' => $validationResult['errors']]; } else { return Executor::execute($schema, $rootObject, $ast, $operationName, $variableValues); } } catch (\Exception $e) { return ['errors' => Error::formatError($e)]; } }
/** * 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); }
/** * @param Error $error * @return array */ public static function formatError(Error $error) { return FormattedError::create($error->getMessage(), $error->getLocations()); }
/** * 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); } } }
/** * 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; } } }
/** * 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); }
public function testDoesNotAllowNonNullListsOfNonNullsToContainNull() { $doc = ' query q($input:[String!]!) { nnListNN(input: $input) } '; $ast = Parser::parse($doc); $expected = FormattedError::create('Variable $input expected value of type [String!]! but got: ["A",null,"B"].', [new SourceLocation(2, 17)]); try { Executor::execute($this->schema(), $ast, null, ['input' => ['A', null, 'B']]); } catch (Error $e) { $this->assertEquals($expected, Error::formatError($e)); } }
/** * @param Source $source * @param int $position * @param string $description */ public function __construct(Source $source, $position, $description) { $location = $source->getLocation($position); $syntaxError = "Syntax Error {$source->name} ({$location->line}:{$location->column}) {$description}\n\n" . self::highlightSourceAtLocation($source, $location); parent::__construct($syntaxError, null, null, $source, [$position]); }