/** * @param Schema $schema * @param Document $ast * @param $rootValue * @param array|\ArrayAccess $variableValues * @param null $operationName * @return ExecutionResult */ public static function execute(Schema $schema, Document $ast, $rootValue = null, $variableValues = null, $operationName = null) { if (!self::$UNDEFINED) { self::$UNDEFINED = new \stdClass(); } if (null !== $variableValues) { Utils::invariant(is_array($variableValues) || $variableValues instanceof \ArrayAccess, "Variable values are expected to be array or instance of ArrayAccess, got " . Utils::getVariableType($variableValues)); } if (null !== $operationName) { Utils::invariant(is_string($operationName), "Operation name is supposed to be string, got " . Utils::getVariableType($operationName)); } $exeContext = self::buildExecutionContext($schema, $ast, $rootValue, $variableValues, $operationName); try { $data = self::executeOperation($exeContext, $exeContext->operation, $rootValue); } catch (Error $e) { $exeContext->addError($e); $data = null; } return new ExecutionResult($data, $exeContext->errors); }
public static function resolve($type) { if (is_callable($type)) { $type = $type(); } Utils::invariant($type instanceof Type, 'Expecting instance of ' . __CLASS__ . ' (or callable returning instance of that type), got "%s"', Utils::getVariableType($type)); return $type; }
/** * @param $typeName * @param $key * @param $value * @param $def * @param $pathStr * @throws \Exception */ private static function validateEntry($typeName, $key, $value, $def, $pathStr) { $type = Utils::getVariableType($value); $err = 'Error in "' . $typeName . '" type definition: expecting %s at "' . $pathStr . '", but got "' . $type . '"'; if ($def instanceof \stdClass) { if ($def->flags & self::REQUIRED === 0 && $value === null) { return; } if (($def->flags & self::MAYBE_THUNK) > 0) { Utils::invariant(is_array($value) || is_callable($value), $err, 'array or callable'); } else { Utils::invariant(is_array($value), $err, 'array'); } if (!empty($def->isMap)) { if ($def->flags & self::KEY_AS_NAME) { $value += ['name' => $key]; } self::validateMap($typeName, $value, $def->definition, $pathStr); } else { if (!empty($def->isArray)) { if ($def->flags & self::REQUIRED) { Utils::invariant(!empty($value), 'Error in "' . $typeName . '" type definition: ' . "Value at '{$pathStr}' cannot be empty array"); } $err = 'Error in "' . $typeName . '" type definition: ' . "Each entry at '{$pathStr}' must be an array, but '%s' is '%s'"; foreach ($value as $arrKey => $arrValue) { if (is_array($def->definition)) { if ($def->flags & self::MAYBE_TYPE && $arrValue instanceof Type) { $arrValue = ['type' => $arrValue]; } if ($def->flags & self::MAYBE_NAME && is_string($arrValue)) { $arrValue = ['name' => $arrValue]; } Utils::invariant(is_array($arrValue), $err, $arrKey, Utils::getVariableType($arrValue)); if ($def->flags & self::KEY_AS_NAME) { $arrValue += ['name' => $arrKey]; } self::validateMap($typeName, $arrValue, $def->definition, "{$pathStr}:{$arrKey}"); } else { self::validateEntry($typeName, $arrKey, $arrValue, $def->definition, "{$pathStr}:{$arrKey}"); } } } else { throw new InvariantViolation('Error in "' . $typeName . '" type definition: ' . "unexpected definition: " . print_r($def, true)); } } } else { Utils::invariant(is_int($def), 'Error in "' . $typeName . '" type definition: ' . "Definition for '{$pathStr}' is expected to be single integer value"); if ($def & self::REQUIRED) { Utils::invariant($value !== null, 'Value at "%s" can not be null', $pathStr); } if (null === $value) { return; // Allow nulls for non-required fields } switch (true) { case $def & self::ANY: break; case $def & self::BOOLEAN: Utils::invariant(is_bool($value), $err, 'boolean'); break; case $def & self::STRING: Utils::invariant(is_string($value), $err, 'string'); break; case $def & self::NUMERIC: Utils::invariant(is_numeric($value), $err, 'numeric'); break; case $def & self::FLOAT: Utils::invariant(is_float($value), $err, 'float'); break; case $def & self::INT: Utils::invariant(is_int($value), $err, 'int'); break; case $def & self::CALLBACK: Utils::invariant(is_callable($value), $err, 'callable'); break; case $def & self::SCALAR: Utils::invariant(is_scalar($value), $err, 'scalar'); break; case $def & self::NAME: Utils::invariant(preg_match('~^[_a-zA-Z][_a-zA-Z0-9]*$~', $value), 'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "%s" does not.', $value); break; case $def & self::INPUT_TYPE: Utils::invariant(is_callable($value) || $value instanceof InputType, $err, 'callable or InputType definition'); break; case $def & self::OUTPUT_TYPE: Utils::invariant(is_callable($value) || $value instanceof OutputType, $err, 'callable or OutputType definition'); break; case $def & self::INTERFACE_TYPE: Utils::invariant(is_callable($value) || $value instanceof InterfaceType, $err, 'callable or InterfaceType definition'); break; case $def & self::OBJECT_TYPE: Utils::invariant(is_callable($value) || $value instanceof ObjectType, $err, 'callable or ObjectType definition'); break; default: throw new InvariantViolation("Unexpected validation rule: " . $def); } } }
/** * Complete an Object value by executing all sub-selections. * * @param ExecutionContext $exeContext * @param ObjectType $returnType * @param $fieldNodes * @param ResolveInfo $info * @param array $path * @param $result * @return array|Promise|\stdClass * @throws Error */ private static function completeObjectValue(ExecutionContext $exeContext, ObjectType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result) { // 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 === $returnType->isTypeOf($result, $exeContext->contextValue, $info)) { throw new Error("Expected value of type {$returnType} but got: " . Utils::getVariableType($result), $fieldNodes); } // Collect sub-fields to execute to complete this value. $subFieldNodes = new \ArrayObject(); $visitedFragmentNames = new \ArrayObject(); foreach ($fieldNodes as $fieldNode) { if (isset($fieldNode->selectionSet)) { $subFieldNodes = self::collectFields($exeContext, $returnType, $fieldNode->selectionSet, $subFieldNodes, $visitedFragmentNames); } } return self::executeFields($exeContext, $returnType, $result, $path, $subFieldNodes); }
/** * @param array $config */ protected function init(array $config) { $config += ['query' => null, 'mutation' => null, 'subscription' => null, 'types' => [], 'directives' => [], 'validate' => true]; if ($config['query'] instanceof DefinitionContainer) { $config['query'] = $config['query']->getDefinition(); } if ($config['mutation'] instanceof DefinitionContainer) { $config['mutation'] = $config['mutation']->getDefinition(); } if ($config['subscription'] instanceof DefinitionContainer) { $config['subscription'] = $config['subscription']->getDefinition(); } Utils::invariant($config['query'] instanceof ObjectType, "Schema query must be Object Type but got: " . Utils::getVariableType($config['query'])); $this->queryType = $config['query']; Utils::invariant(!$config['mutation'] || $config['mutation'] instanceof ObjectType, "Schema mutation must be Object Type if provided but got: " . Utils::getVariableType($config['mutation'])); $this->mutationType = $config['mutation']; Utils::invariant(!$config['subscription'] || $config['subscription'] instanceof ObjectType, "Schema subscription must be Object Type if provided but got: " . Utils::getVariableType($config['subscription'])); $this->subscriptionType = $config['subscription']; Utils::invariant(!$config['types'] || is_array($config['types']), "Schema types must be Array if provided but got: " . Utils::getVariableType($config['types'])); Utils::invariant(!$config['directives'] || is_array($config['directives']) && Utils::every($config['directives'], function ($d) { return $d instanceof Directive; }), "Schema directives must be Directive[] if provided but got " . Utils::getVariableType($config['directives'])); $this->directives = $config['directives'] ?: GraphQL::getInternalDirectives(); // Build type map now to detect any errors within this schema. $initialTypes = [$config['query'], $config['mutation'], $config['subscription'], Introspection::_schema()]; if (!empty($config['types'])) { $initialTypes = array_merge($initialTypes, $config['types']); } foreach ($initialTypes as $type) { $this->extractTypes($type); } $this->typeMap += Type::getInternalTypes(); // Keep track of all implementations by interface name. $this->implementations = []; foreach ($this->typeMap as $typeName => $type) { if ($type instanceof ObjectType) { foreach ($type->getInterfaces() as $iface) { $this->implementations[$iface->name][] = $type; } } } }
/** * 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); }
private static function _validateEntry($key, $value, $def, $pathStr) { $type = Utils::getVariableType($value); $err = 'Expecting %s at "' . $pathStr . '", but got "' . $type . '"'; if ($def instanceof \stdClass) { if ($def->flags & self::REQUIRED === 0 && $value === null) { return; } Utils::invariant(is_array($value), $err, 'array'); if (!empty($def->isMap)) { if ($def->flags & self::KEY_AS_NAME) { $value += ['name' => $key]; } self::_validateMap($value, $def->definition, $pathStr); } else { if (!empty($def->isArray)) { if ($def->flags & self::REQUIRED) { Utils::invariant(!empty($value), "Value at '{$pathStr}' cannot be empty array"); } $err = "Each entry at '{$pathStr}' must be an array, but '%s' is '%s'"; foreach ($value as $arrKey => $arrValue) { if (is_array($def->definition)) { Utils::invariant(is_array($arrValue), $err, $arrKey, Utils::getVariableType($arrValue)); if ($def->flags & self::KEY_AS_NAME) { $arrValue += ['name' => $arrKey]; } self::_validateMap($arrValue, $def->definition, "{$pathStr}:{$arrKey}"); } else { self::_validateEntry($arrKey, $arrValue, $def->definition, "{$pathStr}:{$arrKey}"); } } } else { throw new \Exception("Unexpected definition: " . print_r($def, true)); } } } else { Utils::invariant(is_int($def), "Definition for '{$pathStr}' is expected to be single integer value"); if ($def & self::REQUIRED) { Utils::invariant($value !== null, 'Value at "%s" can not be null', $pathStr); } switch (true) { case $def & self::ANY: break; case $def & self::BOOLEAN: Utils::invariant(is_bool($value), $err, 'boolean'); break; case $def & self::STRING: Utils::invariant(is_string($value), $err, 'string'); break; case $def & self::NUMERIC: Utils::invariant(is_numeric($value), $err, 'numeric'); break; case $def & self::FLOAT: Utils::invariant(is_float($value), $err, 'float'); break; case $def & self::INT: Utils::invariant(is_int($value), $err, 'int'); break; case $def & self::CALLBACK: Utils::invariant(is_callable($value), $err, 'callable'); break; case $def & self::SCALAR: Utils::invariant(is_scalar($value), $err, 'scalar'); break; case $def & self::INPUT_TYPE: Utils::invariant(is_callable($value) || $value instanceof InputType, $err, 'callable or instance of GraphQL\\Type\\Definition\\InputType'); break; case $def & self::OUTPUT_TYPE: Utils::invariant(is_callable($value) || $value instanceof OutputType, $err, 'callable or instance of GraphQL\\Type\\Definition\\OutputType'); break; case $def & self::INTERFACE_TYPE: Utils::invariant(is_callable($value) || $value instanceof InterfaceType, $err, 'callable or instance of GraphQL\\Type\\Definition\\InterfaceType'); break; case $def & self::OBJECT_TYPE: Utils::invariant(is_callable($value) || $value instanceof ObjectType, $err, 'callable or instance of GraphQL\\Type\\Definition\\ObjectType'); break; default: throw new \Exception("Unexpected validation rule: " . $def); } } }
/** * @param $type * @return mixed */ public static function resolve($type) { if (is_callable($type)) { trigger_error('Passing type as closure is deprecated (see https://github.com/webonyx/graphql-php/issues/35 for alternatives)', E_USER_DEPRECATED); $type = $type(); } if ($type instanceof DefinitionContainer) { $type = $type->getDefinition(); } if (!$type instanceof Type) { throw new InvariantViolation(sprintf('Expecting instance of ' . __CLASS__ . ', got "%s"', Utils::getVariableType($type))); } return $type; }
/** * @it prohibits putting non-Object types in unions */ public function testProhibitsPuttingNonObjectTypesInUnions() { $int = Type::int(); $badUnionTypes = [$int, new NonNull($int), new ListOfType($int), $this->interfaceType, $this->unionType, $this->enumType, $this->inputObjectType]; // TODO: extract config validation to separate test Config::enableValidation(); foreach ($badUnionTypes as $type) { try { new UnionType(['name' => 'BadUnion', 'types' => [$type]]); $this->fail('Expected exception not thrown'); } catch (\Exception $e) { $this->assertSame('Error in "BadUnion" type definition: expecting callable or ObjectType definition at "types:0", but got "' . Utils::getVariableType($type) . '"', $e->getMessage()); } } Config::disableValidation(); }