Updated executor and it's tests for april2016 specs

This commit is contained in:
vladar 2016-05-02 03:42:05 +06:00
parent 00f12b3197
commit c3d7a49a08
34 changed files with 1877 additions and 1888 deletions

View File

@ -1,773 +0,0 @@
<?php
namespace GraphQL\Executor;
use GraphQL\Error;
use GraphQL\Language\AST\Document;
use GraphQL\Language\AST\Field;
use GraphQL\Language\AST\FragmentDefinition;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\OperationDefinition;
use GraphQL\Language\AST\SelectionSet;
use GraphQL\Schema;
use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Introspection;
use GraphQL\Utils;
/**
* @deprecated Use regular executor instead
*
* Terminology
*
* "Definitions" are the generic name for top-level statements in the document.
* Examples of this include:
* 1) Operations (such as a query)
* 2) Fragments
*
* "Operations" are a generic name for requests in the document.
* Examples of this include:
* 1) query,
* 2) mutation
*
* "Selections" are the statements that can appear legally and at
* single level of the query. These include:
* 1) field references e.g "a"
* 2) fragment "spreads" e.g. "...c"
* 3) inline fragment "spreads" e.g. "...on Type { a }"
*/
class MappingExecutor
{
private static $UNDEFINED;
private static $defaultResolveFn = [__CLASS__, 'defaultResolveFn'];
/**
* Custom default resolve function
*
* @param $fn
* @throws \Exception
*/
public function setDefaultResolveFn($fn)
{
Utils::invariant(is_callable($fn), 'Expecting callable, but got ' . Utils::getVariableType($fn));
self::$defaultResolveFn = $fn;
}
/**
* @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);
}
/**
* Constructs a ExecutionContext object from the arguments passed to
* execute, which we will pass throughout the other execution methods.
*/
private static function buildExecutionContext(Schema $schema, Document $documentAst, $rootValue, $rawVariableValues, $operationName = null)
{
$errors = [];
$operations = [];
$fragments = [];
foreach ($documentAst->definitions as $statement) {
switch ($statement->kind) {
case Node::OPERATION_DEFINITION:
$operations[$statement->name ? $statement->name->value : ''] = $statement;
break;
case Node::FRAGMENT_DEFINITION:
$fragments[$statement->name->value] = $statement;
break;
}
}
if (!$operationName && count($operations) !== 1) {
throw new Error(
'Must provide operation name if query contains multiple operations.'
);
}
$opName = $operationName ?: key($operations);
if (empty($operations[$opName])) {
throw new Error('Unknown operation named ' . $opName);
}
$operation = $operations[$opName];
$variableValues = Values::getVariableValues($schema, $operation->variableDefinitions ?: [], $rawVariableValues ?: []);
$exeContext = new ExecutionContext($schema, $fragments, $rootValue, $operation, $variableValues, $errors);
return $exeContext;
}
/**
* Implements the "Evaluating operations" section of the spec.
*/
private static function executeOperation(ExecutionContext $exeContext, OperationDefinition $operation, $rootValue)
{
$type = self::getOperationRootType($exeContext->schema, $operation);
$fields = self::collectFields($exeContext, $type, $operation->selectionSet, new \ArrayObject(), new \ArrayObject());
if ($operation->operation === 'mutation') {
$result = self::executeFieldsSerially($exeContext, $type, [$rootValue], $fields);
} else {
$result = self::executeFields($exeContext, $type, [$rootValue], $fields);
}
return null === $result || $result === [] ? [] : $result[0];
}
/**
* Extracts the root type of the operation from the schema.
*
* @param Schema $schema
* @param OperationDefinition $operation
* @return ObjectType
* @throws Error
*/
private static function getOperationRootType(Schema $schema, OperationDefinition $operation)
{
switch ($operation->operation) {
case 'query':
return $schema->getQueryType();
case 'mutation':
$mutationType = $schema->getMutationType();
if (!$mutationType) {
throw new Error(
'Schema is not configured for mutations',
[$operation]
);
}
return $mutationType;
default:
throw new Error(
'Can only execute queries and mutations',
[$operation]
);
}
}
/**
* Implements the "Evaluating selection sets" section of the spec
* for "write" mode.
*
* @param ExecutionContext $exeContext
* @param ObjectType $parentType
* @param $sourceList
* @param $fields
* @return array
* @throws Error
* @throws \Exception
*/
private static function executeFieldsSerially(ExecutionContext $exeContext, ObjectType $parentType, $sourceList, $fields)
{
$results = [];
foreach ($fields as $responseName => $fieldASTs) {
self::resolveField($exeContext, $parentType, $sourceList, $fieldASTs, $responseName, $results);
}
return $results;
}
/**
* Implements the "Evaluating selection sets" section of the spec
* for "read" mode.
* @param ExecutionContext $exeContext
* @param ObjectType $parentType
* @param $sourceList
* @param $fields
* @return array
*/
private static function executeFields(ExecutionContext $exeContext, ObjectType $parentType, $sourceList, $fields)
{
// Native PHP doesn't support promises.
// Custom executor should be built for platforms like ReactPHP
return self::executeFieldsSerially($exeContext, $parentType, $sourceList, $fields);
}
/**
* Given a selectionSet, adds all of the fields in that selection to
* the passed in map of fields, and returns it at the end.
*
* @return \ArrayObject
*/
private static function collectFields(
ExecutionContext $exeContext,
ObjectType $type,
SelectionSet $selectionSet,
$fields,
$visitedFragmentNames
)
{
for ($i = 0; $i < count($selectionSet->selections); $i++) {
$selection = $selectionSet->selections[$i];
switch ($selection->kind) {
case Node::FIELD:
if (!self::shouldIncludeNode($exeContext, $selection->directives)) {
continue;
}
$name = self::getFieldEntryKey($selection);
if (!isset($fields[$name])) {
$fields[$name] = new \ArrayObject();
}
$fields[$name][] = $selection;
break;
case Node::INLINE_FRAGMENT:
if (!self::shouldIncludeNode($exeContext, $selection->directives) ||
!self::doesFragmentConditionMatch($exeContext, $selection, $type)
) {
continue;
}
self::collectFields(
$exeContext,
$type,
$selection->selectionSet,
$fields,
$visitedFragmentNames
);
break;
case Node::FRAGMENT_SPREAD:
$fragName = $selection->name->value;
if (!empty($visitedFragmentNames[$fragName]) || !self::shouldIncludeNode($exeContext, $selection->directives)) {
continue;
}
$visitedFragmentNames[$fragName] = true;
/** @var FragmentDefinition|null $fragment */
$fragment = isset($exeContext->fragments[$fragName]) ? $exeContext->fragments[$fragName] : null;
if (!$fragment ||
!self::shouldIncludeNode($exeContext, $fragment->directives) ||
!self::doesFragmentConditionMatch($exeContext, $fragment, $type)
) {
continue;
}
self::collectFields(
$exeContext,
$type,
$fragment->selectionSet,
$fields,
$visitedFragmentNames
);
break;
}
}
return $fields;
}
/**
* Determines if a field should be included based on the @include and @skip
* directives, where @skip has higher precedence than @include.
*/
private static function shouldIncludeNode(ExecutionContext $exeContext, $directives)
{
$skipDirective = Directive::skipDirective();
$includeDirective = Directive::includeDirective();
/** @var \GraphQL\Language\AST\Directive $skipAST */
$skipAST = $directives
? Utils::find($directives, function(\GraphQL\Language\AST\Directive $directive) use ($skipDirective) {
return $directive->name->value === $skipDirective->name;
})
: null;
if ($skipAST) {
$argValues = Values::getArgumentValues($skipDirective->args, $skipAST->arguments, $exeContext->variableValues);
return empty($argValues['if']);
}
/** @var \GraphQL\Language\AST\Directive $includeAST */
$includeAST = $directives
? Utils::find($directives, function(\GraphQL\Language\AST\Directive $directive) use ($includeDirective) {
return $directive->name->value === $includeDirective->name;
})
: null;
if ($includeAST) {
$argValues = Values::getArgumentValues($includeDirective->args, $includeAST->arguments, $exeContext->variableValues);
return !empty($argValues['if']);
}
return true;
}
/**
* Determines if a fragment is applicable to the given type.
*/
private static function doesFragmentConditionMatch(ExecutionContext $exeContext,/* FragmentDefinition | InlineFragment*/ $fragment, ObjectType $type)
{
$conditionalType = Utils\TypeInfo::typeFromAST($exeContext->schema, $fragment->typeCondition);
if ($conditionalType === $type) {
return true;
}
if ($conditionalType instanceof InterfaceType ||
$conditionalType instanceof UnionType
) {
return $conditionalType->isPossibleType($type);
}
return false;
}
/**
* Implements the logic to compute the key of a given fields entry
*/
private static function getFieldEntryKey(Field $node)
{
return $node->alias ? $node->alias->value : $node->name->value;
}
/**
* 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
);
}
}
}
public static function completeValueCatchingError(
ExecutionContext $exeContext,
Type $returnType,
$fieldASTs,
ResolveInfo $info,
$result
)
{
// If the field type is non-nullable, then it is resolved without any
// protection from errors.
if ($returnType instanceof NonNull) {
return self::completeValue($exeContext, $returnType, $fieldASTs, $info, $result);
}
// Otherwise, error protection is applied, logging the error and resolving
// a null value for this field if one is encountered.
try {
return self::completeValue($exeContext, $returnType, $fieldASTs, $info, $result);
} catch (Error $err) {
$exeContext->addError($err);
return null;
}
}
/**
* 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,/* Array<Field> */ $fieldASTs, ResolveInfo $info, &$result)
{
// 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 Scalar or Enum, serialize to a valid value, returning
// null if serialization is not possible.
if ($returnType instanceof ScalarType ||
$returnType instanceof EnumType) {
return $returnType->serialize($result);
}
// If field type is List, and return type is Composite - complete by executing these fields with list value as parameter
if ($returnType instanceof ListOfType) {
$itemType = $returnType->getWrappedType();
Utils::invariant(
is_array($result) || $result instanceof \Traversable,
'User Error: expected iterable, but did not find one.'
);
// For Object[]:
// Allow all object fields to process list value in it's `map` callback:
if ($itemType instanceof ObjectType) {
// Filter out nulls (as `map` doesn't expect it):
$list = [];
foreach ($result as $index => $item) {
if (null !== $item) {
$list[] = $item;
}
}
$subFieldASTs = self::collectSubFields($exeContext, $itemType, $fieldASTs);
$mapped = self::executeFields($exeContext, $itemType, $list, $subFieldASTs);
$i = 0;
$completed = [];
foreach ($result as $index => $item) {
if (null === $item) {
// Complete nulls separately
$completed[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
} else {
// Assuming same order of mapped values
$completed[] = $mapped[$i++];
}
}
return $completed;
} else if ($itemType instanceof AbstractType) {
// Values sharded by ObjectType
$listPerObjectType = [];
// Helper structures to restore ordering after resolve calls
$resultTypeMap = [];
$typeNameMap = [];
$cursors = [];
$copied = [];
foreach ($result as $index => $item) {
$copied[$index] = $item;
if (null !== $item) {
$objectType = $itemType->getObjectType($item, $info);
if ($objectType && !$itemType->isPossibleType($objectType)) {
$exeContext->addError(new Error(
"Runtime Object type \"$objectType\" is not a possible type for \"$itemType\"."
));
$copied[$index] = null;
} else {
$listPerObjectType[$objectType->name][] = $item;
$resultTypeMap[$index] = $objectType->name;
$typeNameMap[$objectType->name] = $objectType;
}
}
}
$mapped = [];
foreach ($listPerObjectType as $typeName => $list) {
$objectType = $typeNameMap[$typeName];
$subFieldASTs = self::collectSubFields($exeContext, $objectType, $fieldASTs);
$mapped[$typeName] = self::executeFields($exeContext, $objectType, $list, $subFieldASTs);
$cursors[$typeName] = 0;
}
// Restore order:
$completed = [];
foreach ($copied as $index => $item) {
if (null === $item) {
// Complete nulls separately
$completed[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
} else {
$typeName = $resultTypeMap[$index];
$completed[] = $mapped[$typeName][$cursors[$typeName]++];
}
}
return $completed;
} else {
// For simple lists:
$tmp = [];
foreach ($result as $item) {
$tmp[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
}
return $tmp;
}
}
if ($returnType instanceof ObjectType) {
$objectType = $returnType;
} else if ($returnType instanceof AbstractType) {
$objectType = $returnType->getObjectType($result, $info);
if ($objectType && !$returnType->isPossibleType($objectType)) {
throw new Error(
"Runtime Object type \"$objectType\" is not a possible type for \"$returnType\"."
);
}
} else {
$objectType = null;
}
if (!$objectType) {
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 === $objectType->isTypeOf($result, $info)) {
throw new Error(
"Expected value of type $objectType but got: $result.",
$fieldASTs
);
}
// Collect sub-fields to execute to complete this value.
$subFieldASTs = self::collectSubFields($exeContext, $objectType, $fieldASTs);
$executed = self::executeFields($exeContext, $objectType, [$result], $subFieldASTs);
return isset($executed[0]) ? $executed[0] : null;
}
/**
* @param ExecutionContext $exeContext
* @param ObjectType $objectType
* @param $fieldASTs
* @return \ArrayObject
*/
private static function collectSubFields(ExecutionContext $exeContext, ObjectType $objectType, $fieldASTs)
{
$subFieldASTs = new \ArrayObject();
$visitedFragmentNames = new \ArrayObject();
for ($i = 0; $i < count($fieldASTs); $i++) {
$selectionSet = $fieldASTs[$i]->selectionSet;
if ($selectionSet) {
$subFieldASTs = self::collectFields(
$exeContext,
$objectType,
$selectionSet,
$subFieldASTs,
$visitedFragmentNames
);
}
}
return $subFieldASTs;
}
/**
* If a resolve function is not given, then a default resolve behavior is used
* which takes the property of the source object of the same name as the field
* and returns it as the result, or if it's a function, returns the result
* of calling that function.
*/
public static function defaultResolveFn($source, $args, ResolveInfo $info)
{
$fieldName = $info->fieldName;
$property = null;
if (is_array($source) || $source instanceof \ArrayAccess) {
if (isset($source[$fieldName])) {
$property = $source[$fieldName];
}
} else if (is_object($source)) {
if (isset($source->{$fieldName})) {
$property = $source->{$fieldName};
}
}
return $property instanceof \Closure ? $property($source) : $property;
}
/**
* This method looks up the field on the given type defintion.
* It has special casing for the two introspection fields, __schema
* and __typename. __typename is special because it can always be
* queried as a field, even in situations where no other fields
* are allowed, like on a Union. __schema could get automatically
* added to the query type, but that would require mutating type
* definitions, which would cause issues.
*
* @return FieldDefinition
*/
private static function getFieldDef(Schema $schema, ObjectType $parentType, $fieldName)
{
$schemaMetaFieldDef = Introspection::schemaMetaFieldDef();
$typeMetaFieldDef = Introspection::typeMetaFieldDef();
$typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef();
if ($fieldName === $schemaMetaFieldDef->name && $schema->getQueryType() === $parentType) {
return $schemaMetaFieldDef;
} else if ($fieldName === $typeMetaFieldDef->name && $schema->getQueryType() === $parentType) {
return $typeMetaFieldDef;
} else if ($fieldName === $typeNameMetaFieldDef->name) {
return $typeNameMetaFieldDef;
}
$tmp = $parentType->getFields();
return isset($tmp[$fieldName]) ? $tmp[$fieldName] : null;
}
}

View File

@ -24,10 +24,15 @@ class ExecutionContext
public $fragments;
/**
* @var
* @var mixed
*/
public $rootValue;
/**
* @var mixed
*/
public $contextValue;
/**
* @var OperationDefinition
*/
@ -48,11 +53,12 @@ class ExecutionContext
*/
public $memoized = [];
public function __construct($schema, $fragments, $root, $operation, $variables, $errors)
public function __construct($schema, $fragments, $root, $contextValue, $operation, $variables, $errors)
{
$this->schema = $schema;
$this->fragments = $fragments;
$this->rootValue = $root;
$this->contextValue = $contextValue;
$this->operation = $operation;
$this->variableValues = $variables;
$this->errors = $errors ?: [];

View File

@ -13,14 +13,12 @@ use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Introspection;
use GraphQL\Utils;
@ -65,11 +63,12 @@ class Executor
* @param Schema $schema
* @param Document $ast
* @param $rootValue
* @param $contextValue
* @param array|\ArrayAccess $variableValues
* @param null $operationName
* @return ExecutionResult
*/
public static function execute(Schema $schema, Document $ast, $rootValue = null, $variableValues = null, $operationName = null)
public static function execute(Schema $schema, Document $ast, $rootValue = null, $contextValue = null, $variableValues = null, $operationName = null)
{
if (!self::$UNDEFINED) {
self::$UNDEFINED = new \stdClass();
@ -88,7 +87,7 @@ class Executor
);
}
$exeContext = self::buildExecutionContext($schema, $ast, $rootValue, $variableValues, $operationName);
$exeContext = self::buildExecutionContext($schema, $ast, $rootValue, $contextValue, $variableValues, $operationName);
try {
$data = self::executeOperation($exeContext, $exeContext->operation, $rootValue);
@ -104,37 +103,58 @@ class Executor
* Constructs a ExecutionContext object from the arguments passed to
* execute, which we will pass throughout the other execution methods.
*/
private static function buildExecutionContext(Schema $schema, Document $documentAst, $rootValue, $rawVariableValues, $operationName = null)
private static function buildExecutionContext(
Schema $schema,
Document $documentAst,
$rootValue,
$contextValue,
$rawVariableValues,
$operationName = null
)
{
$errors = [];
$operations = [];
$fragments = [];
$operation = null;
foreach ($documentAst->definitions as $statement) {
switch ($statement->kind) {
foreach ($documentAst->definitions as $definition) {
switch ($definition->kind) {
case Node::OPERATION_DEFINITION:
$operations[$statement->name ? $statement->name->value : ''] = $statement;
if (!$operationName && $operation) {
throw new Error(
'Must provide operation name if query contains multiple operations.'
);
}
if (!$operationName ||
(isset($definition->name) && $definition->name->value === $operationName)) {
$operation = $definition;
}
break;
case Node::FRAGMENT_DEFINITION:
$fragments[$statement->name->value] = $statement;
$fragments[$definition->name->value] = $definition;
break;
default:
throw new Error(
"GraphQL cannot execute a request containing a {$definition->kind}.",
[$definition]
);
}
}
if (!$operationName && count($operations) !== 1) {
throw new Error(
'Must provide operation name if query contains multiple operations.'
);
if (!$operation) {
if ($operationName) {
throw new Error("Unknown operation named \"$operationName\".");
} else {
throw new Error('Must provide an operation.');
}
}
$opName = $operationName ?: key($operations);
if (empty($operations[$opName])) {
throw new Error('Unknown operation named ' . $opName);
}
$operation = $operations[$opName];
$variableValues = Values::getVariableValues(
$schema,
$operation->variableDefinitions ?: [],
$rawVariableValues ?: []
);
$variableValues = Values::getVariableValues($schema, $operation->variableDefinitions ?: [], $rawVariableValues ?: []);
$exeContext = new ExecutionContext($schema, $fragments, $rootValue, $operation, $variableValues, $errors);
$exeContext = new ExecutionContext($schema, $fragments, $rootValue, $contextValue, $operation, $variableValues, $errors);
return $exeContext;
}
@ -218,18 +238,21 @@ class Executor
* Given a selectionSet, adds all of the fields in that selection to
* the passed in map of fields, and returns it at the end.
*
* CollectFields requires the "runtime type" of an object. For a field which
* returns and Interface or Union type, the "runtime type" will be the actual
* Object type returned by that field.
*
* @return \ArrayObject
*/
private static function collectFields(
ExecutionContext $exeContext,
ObjectType $type,
ObjectType $runtimeType,
SelectionSet $selectionSet,
$fields,
$visitedFragmentNames
)
{
for ($i = 0; $i < count($selectionSet->selections); $i++) {
$selection = $selectionSet->selections[$i];
foreach ($selectionSet->selections as $selection) {
switch ($selection->kind) {
case Node::FIELD:
if (!self::shouldIncludeNode($exeContext, $selection->directives)) {
@ -243,13 +266,13 @@ class Executor
break;
case Node::INLINE_FRAGMENT:
if (!self::shouldIncludeNode($exeContext, $selection->directives) ||
!self::doesFragmentConditionMatch($exeContext, $selection, $type)
!self::doesFragmentConditionMatch($exeContext, $selection, $runtimeType)
) {
continue;
}
self::collectFields(
$exeContext,
$type,
$runtimeType,
$selection->selectionSet,
$fields,
$visitedFragmentNames
@ -264,15 +287,12 @@ class Executor
/** @var FragmentDefinition|null $fragment */
$fragment = isset($exeContext->fragments[$fragName]) ? $exeContext->fragments[$fragName] : null;
if (!$fragment ||
!self::shouldIncludeNode($exeContext, $fragment->directives) ||
!self::doesFragmentConditionMatch($exeContext, $fragment, $type)
) {
if (!$fragment || !self::doesFragmentConditionMatch($exeContext, $fragment, $runtimeType)) {
continue;
}
self::collectFields(
$exeContext,
$type,
$runtimeType,
$fragment->selectionSet,
$fields,
$visitedFragmentNames
@ -301,7 +321,9 @@ class Executor
if ($skipAST) {
$argValues = Values::getArgumentValues($skipDirective->args, $skipAST->arguments, $exeContext->variableValues);
return empty($argValues['if']);
if (isset($argValues['if']) && $argValues['if'] === true) {
return false;
}
}
/** @var \GraphQL\Language\AST\Directive $includeAST */
@ -313,7 +335,9 @@ class Executor
if ($includeAST) {
$argValues = Values::getArgumentValues($includeDirective->args, $includeAST->arguments, $exeContext->variableValues);
return !empty($argValues['if']);
if (isset($argValues['if']) && $argValues['if'] === false) {
return false;
}
}
return true;
@ -334,10 +358,8 @@ class Executor
if ($conditionalType === $type) {
return true;
}
if ($conditionalType instanceof InterfaceType ||
$conditionalType instanceof UnionType
) {
return $conditionalType->isPossibleType($type);
if ($conditionalType instanceof AbstractType) {
return $exeContext->schema->isPossibleType($conditionalType, $type);
}
return false;
}
@ -401,9 +423,14 @@ class Executor
$resolveFn = self::$defaultResolveFn;
}
// The resolve function's optional third argument is a context value that
// is provided to every resolve function within an execution. It is commonly
// used to represent an authenticated user, or request-specific caches.
$context = $exeContext->contextValue;
// Get the resolve function, regardless of if its result is normal
// or abrupt (error).
$result = self::resolveOrError($resolveFn, $source, $args, $info);
$result = self::resolveOrError($resolveFn, $source, $args, $context, $info);
$result = self::completeValueCatchingError(
$exeContext,
@ -418,15 +445,17 @@ class Executor
// Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField`
// function. Returns the result of resolveFn or the abrupt-return Error object.
private static function resolveOrError($resolveFn, $source, $args, $info)
private static function resolveOrError($resolveFn, $source, $args, $context, $info)
{
try {
return call_user_func($resolveFn, $source, $args, $info);
return call_user_func($resolveFn, $source, $args, $context, $info);
} catch (\Exception $error) {
return $error;
}
}
// This is a small wrapper around completeValue which detects and logs errors
// in the execution context.
public static function completeValueCatchingError(
ExecutionContext $exeContext,
Type $returnType,
@ -468,10 +497,22 @@ class Executor
* value of the type by calling the `serialize` method of GraphQL type
* definition.
*
* If the field is an abstract type, determine the runtime type of the value
* and then complete based on that type
*
* Otherwise, the field type expects a sub-selection set, and will complete the
* value by evaluating all sub-selections.
*
* @param ExecutionContext $exeContext
* @param Type $returnType
* @param Field[] $fieldASTs
* @param ResolveInfo $info
* @param $result
* @return array|null
* @throws Error
* @throws \Exception
*/
private static function completeValue(ExecutionContext $exeContext, Type $returnType,/* Array<Field> */ $fieldASTs, ResolveInfo $info, &$result)
private static function completeValue(ExecutionContext $exeContext, Type $returnType, $fieldASTs, ResolveInfo $info, &$result)
{
if ($result instanceof \Exception) {
throw Error::createLocatedError($result, $fieldASTs);
@ -489,7 +530,7 @@ class Executor
);
if ($completed === null) {
throw new Error(
'Cannot return null for non-nullable type.',
'Cannot return null for non-nullable field ' . $info->parentType . '.' . $info->fieldName . '.',
$fieldASTs instanceof \ArrayObject ? $fieldASTs->getArrayCopy() : $fieldASTs
);
}
@ -503,81 +544,26 @@ class Executor
// 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;
return self::completeListValue($exeContext, $returnType, $fieldASTs, $info, $result);
}
// 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);
return self::completeLeafValue($returnType, $result);
}
if ($returnType instanceof AbstractType) {
return self::completeAbstractValue($exeContext, $returnType, $fieldASTs, $info, $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;
return self::completeObjectValue($exeContext, $returnType, $fieldASTs, $info, $result);
}
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);
throw new Error("Cannot complete value of unexpected type \"{$returnType}\".");
}
@ -587,7 +573,7 @@ class Executor
* and returns it as the result, or if it's a function, returns the result
* of calling that function.
*/
public static function defaultResolveFn($source, $args, ResolveInfo $info)
public static function defaultResolveFn($source, $args, $context, ResolveInfo $info)
{
$fieldName = $info->fieldName;
$property = null;
@ -644,4 +630,135 @@ class Executor
{
return $fieldAST->loc->start . '-' . $fieldAST->loc->end . '-' . $fieldType->name;
}
/**
* Complete a value of an abstract type by determining the runtime object type
* of that value, then complete the value for that type.
*
* @param ExecutionContext $exeContext
* @param AbstractType $returnType
* @param $fieldASTs
* @param ResolveInfo $info
* @param $result
* @return mixed
* @throws Error
*/
private static function completeAbstractValue(ExecutionContext $exeContext, AbstractType $returnType, $fieldASTs, ResolveInfo $info, &$result)
{
$resolveType = $returnType->getResolveTypeFn();
$runtimeType = $resolveType ?
call_user_func($resolveType, $result, $exeContext->contextValue, $info) :
Type::getTypeOf($result, $exeContext->contextValue, $info, $returnType);
if (!($runtimeType instanceof ObjectType)) {
throw new Error(
"Abstract type {$returnType} must resolve to an Object type at runtime " .
"for field {$info->parentType}.{$info->fieldName} with value \"" . print_r($result, true) . "\"," .
"received \"$runtimeType\".",
$fieldASTs
);
}
if (!$exeContext->schema->isPossibleType($returnType, $runtimeType)) {
throw new Error(
"Runtime Object type \"$runtimeType\" is not a possible type for \"$returnType\".",
$fieldASTs
);
}
return self::completeObjectValue($exeContext, $runtimeType, $fieldASTs, $info, $result);
}
/**
* Complete a list value by completing each item in the list with the
* inner type
*
* @param ExecutionContext $exeContext
* @param ListOfType $returnType
* @param $fieldASTs
* @param ResolveInfo $info
* @param $result
* @return array
* @throws \Exception
*/
private static function completeListValue(ExecutionContext $exeContext, ListOfType $returnType, $fieldASTs, ResolveInfo $info, &$result)
{
$itemType = $returnType->getWrappedType();
Utils::invariant(
is_array($result) || $result instanceof \Traversable,
'User Error: expected iterable, but did not find one for field ' . $info->parentType . '.' . $info->fieldName . '.'
);
$tmp = [];
foreach ($result as $item) {
$tmp[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
}
return $tmp;
}
/**
* Complete a Scalar or Enum by serializing to a valid value, returning
* null if serialization is not possible.
*
* @param Type $returnType
* @param $result
* @return mixed
* @throws \Exception
*/
private static function completeLeafValue(Type $returnType, &$result)
{
Utils::invariant(method_exists($returnType, 'serialize'), 'Missing serialize method on type');
return $returnType->serialize($result);
}
/**
* Complete an Object value by executing all sub-selections.
*
* @param ExecutionContext $exeContext
* @param ObjectType $returnType
* @param $fieldASTs
* @param ResolveInfo $info
* @param $result
* @return array
* @throws Error
*/
private static function completeObjectValue(ExecutionContext $exeContext, ObjectType $returnType, $fieldASTs, ResolveInfo $info, &$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),
$fieldASTs
);
}
// Collect sub-fields to execute to complete this value.
$subFieldASTs = new \ArrayObject();
$visitedFragmentNames = new \ArrayObject();
$fieldsCount = count($fieldASTs);
for ($i = 0; $i < $fieldsCount; $i++) {
// Get memoized value if it exists
$uid = self::getFieldUid($fieldASTs[$i], $returnType);
if (isset($exeContext->memoized['collectSubFields'][$uid])) {
$subFieldASTs = $exeContext->memoized['collectSubFields'][$uid];
} else {
$selectionSet = $fieldASTs[$i]->selectionSet;
if ($selectionSet) {
$subFieldASTs = self::collectFields(
$exeContext,
$returnType,
$selectionSet,
$subFieldASTs,
$visitedFragmentNames
);
$exeContext->memoized['collectSubFields'][$uid] = $subFieldASTs;
}
}
}
return self::executeFields($exeContext, $returnType, $result, $subFieldASTs);
}
}

View File

@ -158,16 +158,14 @@ class Values
if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType();
if (is_array($value)) {
return array_reduce(
$value,
function ($acc, $item, $index) use ($itemType) {
$errors = self::isValidPHPValue($item, $itemType);
return array_merge($acc, Utils::map($errors, function ($error) use ($index) {
return "In element #$index: $error";
}));
},
[]
);
$tmp = [];
foreach ($value as $index => $item) {
$errors = self::isValidPHPValue($item, $itemType);
$tmp = array_merge($tmp, Utils::map($errors, function ($error) use ($index) {
return "In element #$index: $error";
}));
}
return $tmp;
}
return self::isValidPHPValue($value, $itemType);
}
@ -190,7 +188,7 @@ class Values
// Ensure every defined field is valid.
foreach ($fields as $fieldName => $tmp) {
$newErrors = self::isValidPHPValue($value[$fieldName], $fields[$fieldName]->getType());
$newErrors = self::isValidPHPValue(isset($value[$fieldName]) ? $value[$fieldName] : null, $fields[$fieldName]->getType());
$errors = array_merge(
$errors,
Utils::map($newErrors, function ($error) use ($fieldName) {

View File

@ -3,6 +3,7 @@ namespace GraphQL;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Executor\Executor;
use GraphQL\Language\AST\Document;
use GraphQL\Language\Parser;
use GraphQL\Language\Source;
use GraphQL\Validator\DocumentValidator;
@ -18,9 +19,9 @@ class GraphQL
* @param string|null $operationName
* @return array
*/
public static function execute(Schema $schema, $requestString, $rootValue = null, $variableValues = null, $operationName = null)
public static function execute(Schema $schema, $requestString, $rootValue = null, $contextValue = null, $variableValues = null, $operationName = null)
{
return self::executeAndReturnResult($schema, $requestString, $rootValue, $variableValues, $operationName)->toArray();
return self::executeAndReturnResult($schema, $requestString, $rootValue, $contextValue, $variableValues, $operationName)->toArray();
}
/**
@ -31,11 +32,15 @@ class GraphQL
* @param null $operationName
* @return array|ExecutionResult
*/
public static function executeAndReturnResult(Schema $schema, $requestString, $rootValue = null, $variableValues = null, $operationName = null)
public static function executeAndReturnResult(Schema $schema, $requestString, $rootValue = null, $contextValue = null, $variableValues = null, $operationName = null)
{
try {
$source = new Source($requestString ?: '', 'GraphQL request');
$documentAST = Parser::parse($source);
if ($requestString instanceof Document) {
$documentAST = $requestString;
} else {
$source = new Source($requestString ?: '', 'GraphQL request');
$documentAST = Parser::parse($source);
}
/** @var QueryComplexity $queryComplexity */
$queryComplexity = DocumentValidator::getRule('QueryComplexity');
@ -46,7 +51,7 @@ class GraphQL
if (!empty($validationErrors)) {
return new ExecutionResult(null, $validationErrors);
} else {
return Executor::execute($schema, $documentAST, $rootValue, $variableValues, $operationName);
return Executor::execute($schema, $documentAST, $rootValue, $contextValue, $variableValues, $operationName);
}
} catch (Error $e) {
return new ExecutionResult(null, [$e]);

View File

@ -3,18 +3,13 @@ namespace GraphQL;
use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Definition\WrappingType;
use GraphQL\Type\Introspection;
use GraphQL\Utils\TypeInfo;
class Schema
{
@ -79,20 +74,44 @@ class Schema
protected function _init(array $config)
{
Utils::invariant(isset($config['query']) || isset($config['mutation']), "Either query or mutation type must be set");
$config += [
'query' => null,
'mutation' => null,
'subscription' => null,
'types' => [],
'directives' => [],
'validate' => true
];
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 = array_merge($config['directives'], [
Directive::includeDirective(),
Directive::skipDirective()
@ -109,11 +128,10 @@ class Schema
$initialTypes = array_merge($initialTypes, $config['types']);
}
$map = [];
foreach ($initialTypes as $type) {
$this->_extractTypes($type, $map);
$this->_extractTypes($type);
}
$this->_typeMap = $map + Type::getInternalTypes();
$this->_typeMap += Type::getInternalTypes();
// Keep track of all implementations by interface name.
$this->_implementations = [];
@ -124,86 +142,6 @@ class Schema
}
}
}
if ($config['validate']) {
$this->validate();
}
}
/**
* Additionaly validate schema for integrity
*/
public function validate()
{
// Enforce correct interface implementations
foreach ($this->_typeMap as $typeName => $type) {
if ($type instanceof ObjectType) {
foreach ($type->getInterfaces() as $iface) {
$this->_assertObjectImplementsInterface($type, $iface);
}
}
}
}
/**
* @param ObjectType $object
* @param InterfaceType $iface
* @throws \Exception
*/
protected function _assertObjectImplementsInterface(ObjectType $object, InterfaceType $iface)
{
$objectFieldMap = $object->getFields();
$ifaceFieldMap = $iface->getFields();
foreach ($ifaceFieldMap as $fieldName => $ifaceField) {
Utils::invariant(
isset($objectFieldMap[$fieldName]),
"\"$iface\" expects field \"$fieldName\" but \"$object\" does not provide it"
);
/** @var $ifaceField FieldDefinition */
/** @var $objectField FieldDefinition */
$objectField = $objectFieldMap[$fieldName];
Utils::invariant(
TypeInfo::isTypeSubTypeOf($this, $objectField->getType(), $ifaceField->getType()),
"$iface.$fieldName expects type \"{$ifaceField->getType()}\" but " .
"$object.$fieldName provides type \"{$objectField->getType()}\"."
);
foreach ($ifaceField->args as $ifaceArg) {
/** @var $ifaceArg FieldArgument */
/** @var $objectArg FieldArgument */
$argName = $ifaceArg->name;
$objectArg = $objectField->getArg($argName);
// Assert interface field arg exists on object field.
Utils::invariant(
$objectArg,
"$iface.$fieldName expects argument \"$argName\" but $object.$fieldName does not provide it."
);
// Assert interface field arg type matches object field arg type.
// (invariant)
Utils::invariant(
TypeInfo::isEqualType($ifaceArg->getType(), $objectArg->getType()),
"$iface.$fieldName($argName:) expects type \"{$ifaceArg->getType()}\" " .
"but $object.$fieldName($argName:) provides " .
"type \"{$objectArg->getType()}\""
);
// Assert argument set invariance.
foreach ($objectField->args as $objectArg) {
$argName = $objectArg->name;
$ifaceArg = $ifaceField->getArg($argName);
Utils::invariant(
$ifaceArg,
"$iface.$fieldName does not define argument \"$argName\" but " .
"$object.$fieldName provides it."
);
}
}
}
}
/**
@ -258,7 +196,7 @@ class Schema
return $abstractType->getTypes();
}
Utils::invariant($abstractType instanceof InterfaceType);
return $this->_implementations[$abstractType->name];
return isset($this->_implementations[$abstractType->name]) ? $this->_implementations[$abstractType->name] : [];
}
/**
@ -305,24 +243,24 @@ class Schema
return null;
}
protected function _extractTypes($type, &$map)
protected function _extractTypes($type)
{
if (!$type) {
return $map;
return $this->_typeMap;
}
if ($type instanceof WrappingType) {
return $this->_extractTypes($type->getWrappedType(), $map);
return $this->_extractTypes($type->getWrappedType(true));
}
if (!empty($map[$type->name])) {
if (!empty($this->_typeMap[$type->name])) {
Utils::invariant(
$map[$type->name] === $type,
$this->_typeMap[$type->name] === $type,
"Schema must contain unique named types but contains multiple types named \"$type\"."
);
return $map;
return $this->_typeMap;
}
$map[$type->name] = $type;
$this->_typeMap[$type->name] = $type;
$nestedTypes = [];
@ -342,8 +280,8 @@ class Schema
}
}
foreach ($nestedTypes as $type) {
$this->_extractTypes($type, $map);
$this->_extractTypes($type);
}
return $map;
return $this->_typeMap;
}
}

View File

@ -10,18 +10,7 @@ GraphQLInterfaceType |
GraphQLUnionType;
*/
/**
* @return array<ObjectType>
* @return callable|null
*/
// public function getPossibleTypes();
/**
* @return ObjectType
*/
// public function getObjectType($value, ResolveInfo $info);
/**
* @param Type $type
* @return bool
*/
// public function isPossibleType(Type $type);
public function getResolveTypeFn();
}

View File

@ -17,6 +17,9 @@ class FieldDefinition
*/
private $type;
/**
* @var OutputType
*/
private $resolvedType;
/**

View File

@ -13,21 +13,6 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
public $description;
/**
* @var array<GraphQLObjectType>
*/
private $_implementations = [];
/**
* @var \Closure[]
*/
private static $_lazyLoadImplementations = [];
/**
* @var {[typeName: string]: boolean}
*/
private $_possibleTypeNames;
/**
* @var callback
*/
@ -38,45 +23,6 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
*/
public $config;
/**
* Queue the update of the interfaces to know about this implementation.
* This is an rare and unfortunate use of mutation in the type definition
* implementations, but avoids an expensive "getPossibleTypes"
* implementation for Interface types.
*
* @param ObjectType $impl
*/
public static function addImplementationToInterfaces(ObjectType $impl)
{
self::$_lazyLoadImplementations[] = function() use ($impl) {
/** @var self $interface */
foreach ($impl->getInterfaces() as $interface) {
$interface->addImplementation($impl);
}
};
}
/**
* Process ImplementationToInterfaces Queue
*/
public static function loadImplementationToInterfaces()
{
foreach (self::$_lazyLoadImplementations as $lazyLoadImplementation) {
$lazyLoadImplementation();
}
self::$_lazyLoadImplementations = [];
}
/**
* Add a implemented object type to interface
*
* @param ObjectType $impl
*/
protected function addImplementation(ObjectType $impl)
{
$this->_implementations[] = $impl;
}
/**
* InterfaceType constructor.
* @param array $config
@ -89,7 +35,7 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
FieldDefinition::getDefinition(),
Config::KEY_AS_NAME | Config::MAYBE_THUNK
),
'resolveType' => Config::CALLBACK, // function($value, ResolveInfo $info) => ObjectType
'resolveType' => Config::CALLBACK, // function($value, $context, ResolveInfo $info) => ObjectType
'description' => Config::STRING
]);
@ -128,38 +74,10 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
}
/**
* @return array<GraphQLObjectType>
* @return callable|null
*/
public function getPossibleTypes()
public function getResolveTypeFn()
{
return $this->_implementations;
}
/**
* @param Type $type
* @return bool
*/
public function isPossibleType(Type $type)
{
$possibleTypeNames = $this->_possibleTypeNames;
if (null === $possibleTypeNames) {
$this->_possibleTypeNames = $possibleTypeNames = array_reduce($this->getPossibleTypes(), function(&$map, Type $possibleType) {
$map[$possibleType->name] = true;
return $map;
}, []);
}
return !empty($possibleTypeNames[$type->name]);
}
/**
* @param $value
* @param ResolveInfo $info
* @return Type|null
* @throws \Exception
*/
public function getObjectType($value, ResolveInfo $info)
{
$resolver = $this->_resolveTypeFn;
return $resolver ? call_user_func($resolver, $value, $info) : Type::getTypeOf($value, $info, $this);
return $this->_resolveTypeFn;
}
}

View File

@ -95,10 +95,6 @@ class ObjectType extends Type implements OutputType, CompositeType
$this->resolveFieldFn = isset($config['resolveField']) ? $config['resolveField'] : null;
$this->_isTypeOf = isset($config['isTypeOf']) ? $config['isTypeOf'] : null;
$this->config = $config;
if (isset($config['interfaces'])) {
InterfaceType::addImplementationToInterfaces($this);
}
}
/**
@ -152,10 +148,11 @@ class ObjectType extends Type implements OutputType, CompositeType
/**
* @param $value
* @param $context
* @return bool|null
*/
public function isTypeOf($value, ResolveInfo $info)
public function isTypeOf($value, $context, ResolveInfo $info)
{
return isset($this->_isTypeOf) ? call_user_func($this->_isTypeOf, $value, $info) : null;
return isset($this->_isTypeOf) ? call_user_func($this->_isTypeOf, $value, $context, $info) : null;
}
}

View File

@ -185,30 +185,18 @@ GraphQLNonNull;
/**
* @param $value
* @param mixed $context
* @param AbstractType $abstractType
* @return Type
* @throws \Exception
*/
public static function getTypeOf($value, ResolveInfo $info, AbstractType $abstractType)
public static function getTypeOf($value, $context, ResolveInfo $info, AbstractType $abstractType)
{
$possibleTypes = $abstractType->getPossibleTypes();
$possibleTypes = $info->schema->getPossibleTypes($abstractType);
for ($i = 0; $i < count($possibleTypes); $i++) {
foreach ($possibleTypes as $type) {
/** @var ObjectType $type */
$type = $possibleTypes[$i];
$isTypeOf = $type->isTypeOf($value, $info);
if ($isTypeOf === null) {
// TODO: move this to a JS impl specific type system validation step
// so the error can be found before execution.
throw new \Exception(
'Non-Object Type ' . $abstractType->name . ' does not implement ' .
'getObjectType and Object Type ' . $type->name . ' does not implement ' .
'isTypeOf. There is no way to determine if a value is of this type.'
);
}
if ($isTypeOf) {
if ($type->isTypeOf($value, $context, $info)) {
return $type;
}
}

View File

@ -18,7 +18,7 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
/**
* @var callback
*/
private $_resolveType;
private $_resolveTypeFn;
/**
* @var array
@ -44,7 +44,7 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
$this->name = $config['name'];
$this->description = isset($config['description']) ? $config['description'] : null;
$this->_types = $config['types'];
$this->_resolveType = isset($config['resolveType']) ? $config['resolveType'] : null;
$this->_resolveTypeFn = isset($config['resolveType']) ? $config['resolveType'] : null;
$this->_config = $config;
}
@ -85,15 +85,10 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
}
/**
* @param ObjectType $value
* @param ResolveInfo $info
*
* @return Type
* @throws \Exception
* @return callable|null
*/
public function getObjectType($value, ResolveInfo $info)
public function getResolveTypeFn()
{
$resolver = $this->_resolveType;
return $resolver ? call_user_func($resolver, $value, $info) : Type::getTypeOf($value, $info, $this);
return $this->_resolveTypeFn;
}
}

View File

@ -426,7 +426,7 @@ EOD;
],
'possibleTypes' => [
'type' => Type::listOf(Type::nonNull([__CLASS__, '_type'])),
'resolve' => function ($type, $args, ResolveInfo $info) {
'resolve' => function ($type, $args, $context, ResolveInfo $info) {
if ($type instanceof InterfaceType || $type instanceof UnionType) {
return $info->schema->getPossibleTypes($type);
}
@ -635,6 +635,7 @@ EOD;
'resolve' => function (
$source,
$args,
$context,
ResolveInfo $info
) {
return $info->schema;
@ -654,7 +655,7 @@ EOD;
'args' => [
['name' => 'name', 'type' => Type::nonNull(Type::string())]
],
'resolve' => function ($source, $args, ResolveInfo $info) {
'resolve' => function ($source, $args, $context, ResolveInfo $info) {
return $info->schema->getType($args['name']);
}
]);
@ -673,6 +674,7 @@ EOD;
'resolve' => function (
$source,
$args,
$context,
ResolveInfo $info
) {
return $info->parentType->name;

View File

@ -3,10 +3,13 @@ namespace GraphQL\Type;
use GraphQL\Error;
use GraphQL\Schema;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils;
class SchemaValidator
{
@ -19,7 +22,8 @@ class SchemaValidator
self::noInputTypesAsOutputFieldsRule(),
self::noOutputTypesAsInputArgsRule(),
self::typesInterfacesMustShowThemAsPossibleRule(),
self::interfacePossibleTypesMustImplementTheInterfaceRule()
self::interfacePossibleTypesMustImplementTheInterfaceRule(),
self::interfacesAreCorrectlyImplemented()
];
}
return self::$rules;
@ -30,7 +34,7 @@ class SchemaValidator
return function ($context) {
$operationMayNotBeInputType = function (Type $type, $operation) {
if (!Type::isOutputType($type)) {
return new Error("Schema $operation type $type must be an object type!");
return new Error("Schema $operation must be Object Type but got: $type.");
}
return null;
};
@ -56,6 +60,14 @@ class SchemaValidator
}
}
$subscriptionType = $schema->getSubscriptionType();
if ($subscriptionType) {
$subscriptionError = $operationMayNotBeInputType($subscriptionType, 'subscription');
if ($subscriptionError !== null) {
$errors[] = $subscriptionError;
}
}
foreach ($typeMap as $typeName => $type) {
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
$fields = $type->getFields();
@ -152,6 +164,91 @@ class SchemaValidator
};
}
// Enforce correct interface implementations
public static function interfacesAreCorrectlyImplemented()
{
return function($context) {
/** @var Schema $schema */
$schema = $context['schema'];
$errors = [];
foreach ($schema->getTypeMap() as $typeName => $type) {
if ($type instanceof ObjectType) {
foreach ($type->getInterfaces() as $iface) {
try {
// FIXME: rework to return errors instead
self::assertObjectImplementsInterface($schema, $type, $iface);
} catch (\Exception $e) {
$errors[] = $e;
}
}
}
}
return $errors;
};
}
/**
* @param ObjectType $object
* @param InterfaceType $iface
* @throws \Exception
*/
protected static function assertObjectImplementsInterface(Schema $schema, ObjectType $object, InterfaceType $iface)
{
$objectFieldMap = $object->getFields();
$ifaceFieldMap = $iface->getFields();
foreach ($ifaceFieldMap as $fieldName => $ifaceField) {
Utils::invariant(
isset($objectFieldMap[$fieldName]),
"\"$iface\" expects field \"$fieldName\" but \"$object\" does not provide it"
);
/** @var $ifaceField FieldDefinition */
/** @var $objectField FieldDefinition */
$objectField = $objectFieldMap[$fieldName];
Utils::invariant(
Utils\TypeInfo::isTypeSubTypeOf($schema, $objectField->getType(), $ifaceField->getType()),
"$iface.$fieldName expects type \"{$ifaceField->getType()}\" but " .
"$object.$fieldName provides type \"{$objectField->getType()}\"."
);
foreach ($ifaceField->args as $ifaceArg) {
/** @var $ifaceArg FieldArgument */
/** @var $objectArg FieldArgument */
$argName = $ifaceArg->name;
$objectArg = $objectField->getArg($argName);
// Assert interface field arg exists on object field.
Utils::invariant(
$objectArg,
"$iface.$fieldName expects argument \"$argName\" but $object.$fieldName does not provide it."
);
// Assert interface field arg type matches object field arg type.
// (invariant)
Utils::invariant(
Utils\TypeInfo::isEqualType($ifaceArg->getType(), $objectArg->getType()),
"$iface.$fieldName($argName:) expects type \"{$ifaceArg->getType()}\" " .
"but $object.$fieldName($argName:) provides " .
"type \"{$objectArg->getType()}\""
);
// Assert argument set invariance.
foreach ($objectField->args as $objectArg) {
$argName = $objectArg->name;
$ifaceArg = $ifaceField->getArg($argName);
Utils::invariant(
$ifaceArg,
"$iface.$fieldName does not define argument \"$argName\" but " .
"$object.$fieldName provides it."
);
}
}
}
}
/**
* @param Schema $schema
* @param array <callable>|null $argRules
@ -171,4 +268,4 @@ class SchemaValidator
}
return $errors;
}
}
}

View File

@ -1,6 +1,9 @@
<?php
namespace GraphQL;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\WrappingType;
use GraphQL\Utils\SchemaUtils;
use \Traversable, \InvalidArgumentException;
class Utils
@ -168,6 +171,21 @@ class Utils
return $grouped;
}
/**
* @param $traversable
* @param callable $predicate
* @return bool
*/
public static function every($traversable, callable $predicate)
{
foreach ($traversable as $key => $value) {
if (!$predicate($value, $key)) {
return false;
}
}
return true;
}
/**
* @param $test
* @param string $message
@ -193,6 +211,13 @@ class Utils
*/
public static function getVariableType($var)
{
if ($var instanceof Type) {
// FIXME: Replace with schema printer call
if ($var instanceof WrappingType) {
$var = $var->getWrappedType(true);
}
return $var->name;
}
return is_object($var) ? get_class($var) : gettype($var);
}

View File

@ -3,17 +3,25 @@ namespace GraphQL\Tests\Executor;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Executor\Executor;
use GraphQL\FormattedError;
use GraphQL\GraphQL;
use GraphQL\Language\Parser;
use GraphQL\Language\SourceLocation;
use GraphQL\Schema;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
spl_autoload_call('GraphQL\Tests\Executor\TestClasses');
class AbstractTest extends \PHPUnit_Framework_TestCase
{
// Execute: Handles execution of abstract types
/**
* @it isTypeOf used to resolve runtime type for Interface
*/
public function testIsTypeOfUsedToResolveRuntimeTypeForInterface()
{
// isTypeOf used to resolve runtime type for Interface
@ -58,7 +66,8 @@ class AbstractTest extends \PHPUnit_Framework_TestCase
}
]
]
])
]),
'types' => [$catType, $dogType]
]);
$query = '{
@ -83,10 +92,11 @@ class AbstractTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, Executor::execute($schema, Parser::parse($query)));
}
/**
* @it isTypeOf used to resolve runtime type for Union
*/
public function testIsTypeOfUsedToResolveRuntimeTypeForUnion()
{
// isTypeOf used to resolve runtime type for Union
$dogType = new ObjectType([
'name' => 'Dog',
'isTypeOf' => function($obj) { return $obj instanceof Dog; },
@ -148,6 +158,9 @@ class AbstractTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, Executor::execute($schema, Parser::parse($query)));
}
/**
* @it resolveType on Interface yields useful error
*/
function testResolveTypeOnInterfaceYieldsUsefulError()
{
$DogType = null;
@ -198,21 +211,24 @@ class AbstractTest extends \PHPUnit_Framework_TestCase
]
]);
$schema = new Schema(new ObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => Type::listOf($PetType),
'resolve' => function () {
return [
new Dog('Odie', true),
new Cat('Garfield', false),
new Human('Jon')
];
}
]
]
]));
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => Type::listOf($PetType),
'resolve' => function () {
return [
new Dog('Odie', true),
new Cat('Garfield', false),
new Human('Jon')
];
}
]
],
]),
'types' => [$DogType, $CatType]
]);
$query = '{
@ -236,11 +252,110 @@ class AbstractTest extends \PHPUnit_Framework_TestCase
]
],
'errors' => [
[ 'message' => 'Runtime Object type "Human" is not a possible type for "Pet".' ]
FormattedError::create(
'Runtime Object type "Human" is not a possible type for "Pet".',
[new SourceLocation(2, 11)]
)
]
];
$actual = GraphQL::execute($schema, $query);
$this->assertEquals($expected, Executor::execute($schema, Parser::parse($query))->toArray());
$this->assertEquals($expected, $actual);
}
/**
* @it resolveType on Union yields useful error
*/
public function testResolveTypeOnUnionYieldsUsefulError()
{
$HumanType = new ObjectType([
'name' => 'Human',
'fields' => [
'name' => ['type' => Type::string()],
]
]);
$DogType = new ObjectType([
'name' => 'Dog',
'fields' => [
'name' => ['type' => Type::string()],
'woofs' => ['type' => Type::boolean()],
]
]);
$CatType = new ObjectType([
'name' => 'Cat',
'fields' => [
'name' => ['type' => Type::string()],
'meows' => ['type' => Type::boolean()],
]
]);
$PetType = new UnionType([
'name' => 'Pet',
'resolveType' => function ($obj) use ($DogType, $CatType, $HumanType) {
if ($obj instanceof Dog) {
return $DogType;
}
if ($obj instanceof Cat) {
return $CatType;
}
if ($obj instanceof Human) {
return $HumanType;
}
},
'types' => [$DogType, $CatType]
]);
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => Type::listOf($PetType),
'resolve' => function () {
return [
new Dog('Odie', true),
new Cat('Garfield', false),
new Human('Jon')
];
}
]
]
])
]);
$query = '{
pets {
... on Dog {
name
woofs
}
... on Cat {
name
meows
}
}
}';
$result = GraphQL::execute($schema, $query);
$expected = [
'data' => [
'pets' => [
['name' => 'Odie',
'woofs' => true],
['name' => 'Garfield',
'meows' => false],
null
]
],
'errors' => [
FormattedError::create(
'Runtime Object type "Human" is not a possible type for "Pet".',
[new SourceLocation(2, 11)]
)
]
];
$this->assertEquals($expected, $result);
}
}

View File

@ -9,14 +9,20 @@ use GraphQL\Type\Definition\Type;
class DirectivesTest extends \PHPUnit_Framework_TestCase
{
// Execute: handles directives
// works without directives
// Describe: Execute: handles directives
/**
* @describe works without directives
* @it basic query works
*/
public function testWorksWithoutDirectives()
{
// basic query works
$this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery('{ a, b }'));
}
/**
* @describe works on scalars
*/
public function testWorksOnScalars()
{
// if true includes scalar
@ -32,6 +38,9 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery('{ a, b @skip(if: true) }'));
}
/**
* @describe works on fragment spreads
*/
public function testWorksOnFragmentSpreads()
{
// if false omits fragment spread
@ -83,6 +92,9 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($q));
}
/**
* @describe works on inline fragment
*/
public function testWorksOnInlineFragment()
{
// if false omits inline fragment
@ -142,57 +154,80 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($q));
}
public function testWorksOnFragment()
/**
* @describe works on anonymous inline fragment
*/
public function testWorksOnAnonymousInlineFragment()
{
// if false omits fragment
// if false omits anonymous inline fragment
$q = '
query Q {
a
...Frag
}
fragment Frag on TestType @include(if: false) {
b
... @include(if: false) {
b
}
}
';
$this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($q));
// if true includes fragment
// if true includes anonymous inline fragment
$q = '
query Q {
a
...Frag
}
fragment Frag on TestType @include(if: true) {
b
... @include(if: true) {
b
}
}
';
$this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery($q));
// unless false includes fragment
// unless false includes anonymous inline fragment
$q = '
query Q {
a
...Frag
}
fragment Frag on TestType @skip(if: false) {
b
... @skip(if: false) {
b
}
}
';
$this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery($q));
// unless true omits fragment
// unless true includes anonymous inline fragment
$q = '
query Q {
a
...Frag
}
fragment Frag on TestType @skip(if: true) {
b
... @skip(if: true) {
b
}
}
';
$this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($q));
}
/**
* @describe works with skip and include directives
*/
public function testWorksWithSkipAndIncludeDirectives()
{
// include and no skip
$this->assertEquals(
['data' => ['a' => 'a', 'b' => 'b']],
$this->executeTestQuery('{ a, b @include(if: true) @skip(if: false) }')
);
// include and skip
$this->assertEquals(
['data' => ['a' => 'a']],
$this->executeTestQuery('{ a, b @include(if: true) @skip(if: true) }')
);
// no include or skip
$this->assertEquals(
['data' => ['a' => 'a']],
$this->executeTestQuery('{ a, b @include(if: false) @skip(if: false) }')
);
}
@ -202,13 +237,18 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase
private static function getSchema()
{
return self::$schema ?: (self::$schema = new Schema(new ObjectType([
'name' => 'TestType',
'fields' => [
'a' => ['type' => Type::string()],
'b' => ['type' => Type::string()]
]
])));
if (!self::$schema) {
self::$schema = new Schema([
'query' => new ObjectType([
'name' => 'TestType',
'fields' => [
'a' => ['type' => Type::string()],
'b' => ['type' => Type::string()]
]
])
]);
}
return self::$schema;
}
private static function getData()

View File

@ -11,6 +11,10 @@ use GraphQL\Type\Definition\Type;
class ExecutorSchemaTest extends \PHPUnit_Framework_TestCase
{
// Execute: Handles execution with a complex schema
/**
* @it executes using a schema
*/
public function testExecutesUsingASchema()
{
$BlogArticle = null;
@ -83,7 +87,7 @@ class ExecutorSchemaTest extends \PHPUnit_Framework_TestCase
]
]);
$BlogSchema = new Schema($BlogQuery);
$BlogSchema = new Schema(['query' => $BlogQuery]);
$request = '

View File

@ -137,9 +137,9 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
'deeper' => [ 'type' => Type::listOf($dataType) ]
]
]);
$schema = new Schema($dataType);
$schema = new Schema(['query' => $dataType]);
$this->assertEquals($expected, Executor::execute($schema, $ast, $data, ['size' => 100], 'Example')->toArray());
$this->assertEquals($expected, Executor::execute($schema, $ast, $data, null, ['size' => 100], 'Example')->toArray());
}
public function testMergesParallelFragments()
@ -180,7 +180,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
]
]
]);
$schema = new Schema($Type);
$schema = new Schema(['query' => $Type]);
$expected = [
'data' => [
'a' => 'Apple',
@ -212,20 +212,22 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
];
$ast = Parser::parse($doc);
$schema = new Schema(new ObjectType([
'name' => 'Type',
'fields' => [
'a' => [
'type' => Type::string(),
'resolve' => function ($context) use ($doc, &$gotHere) {
$this->assertEquals('thing', $context['contextThing']);
$gotHere = true;
}
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Type',
'fields' => [
'a' => [
'type' => Type::string(),
'resolve' => function ($context) use ($doc, &$gotHere) {
$this->assertEquals('thing', $context['contextThing']);
$gotHere = true;
}
]
]
]
]));
])
]);
Executor::execute($schema, $ast, $data, [], 'Example');
Executor::execute($schema, $ast, $data, null, [], 'Example');
$this->assertEquals(true, $gotHere);
}
@ -240,24 +242,26 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
$gotHere = false;
$docAst = Parser::parse($doc);
$schema = new Schema(new ObjectType([
'name' => 'Type',
'fields' => [
'b' => [
'args' => [
'numArg' => ['type' => Type::int()],
'stringArg' => ['type' => Type::string()]
],
'type' => Type::string(),
'resolve' => function ($_, $args) use (&$gotHere) {
$this->assertEquals(123, $args['numArg']);
$this->assertEquals('foo', $args['stringArg']);
$gotHere = true;
}
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Type',
'fields' => [
'b' => [
'args' => [
'numArg' => ['type' => Type::int()],
'stringArg' => ['type' => Type::string()]
],
'type' => Type::string(),
'resolve' => function ($_, $args) use (&$gotHere) {
$this->assertEquals(123, $args['numArg']);
$this->assertEquals('foo', $args['stringArg']);
$gotHere = true;
}
]
]
]
]));
Executor::execute($schema, $docAst, null, [], 'Example');
])
]);
Executor::execute($schema, $docAst, null, null, [], 'Example');
$this->assertSame($gotHere, true);
}
@ -296,17 +300,19 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
];
$docAst = Parser::parse($doc);
$schema = new Schema(new ObjectType([
'name' => 'Type',
'fields' => [
'sync' => ['type' => Type::string()],
'syncError' => ['type' => Type::string()],
'syncRawError' => [ 'type' => Type::string() ],
'async' => ['type' => Type::string()],
'asyncReject' => ['type' => Type::string() ],
'asyncError' => ['type' => Type::string()],
]
]));
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Type',
'fields' => [
'sync' => ['type' => Type::string()],
'syncError' => ['type' => Type::string()],
'syncRawError' => [ 'type' => Type::string() ],
'async' => ['type' => Type::string()],
'asyncReject' => ['type' => Type::string() ],
'asyncError' => ['type' => Type::string()],
]
])
]);
$expected = [
'data' => [
@ -336,12 +342,14 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
$doc = '{ a }';
$data = ['a' => 'b'];
$ast = Parser::parse($doc);
$schema = new Schema(new ObjectType([
'name' => 'Type',
'fields' => [
'a' => ['type' => Type::string()],
]
]));
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Type',
'fields' => [
'a' => ['type' => Type::string()],
]
])
]);
$ex = Executor::execute($schema, $ast, $data);
@ -353,12 +361,14 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
$doc = 'query Example { a }';
$data = [ 'a' => 'b' ];
$ast = Parser::parse($doc);
$schema = new Schema(new ObjectType([
'name' => 'Type',
'fields' => [
'a' => [ 'type' => Type::string() ],
]
]));
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Type',
'fields' => [
'a' => [ 'type' => Type::string() ],
]
])
]);
$ex = Executor::execute($schema, $ast, $data);
$this->assertEquals(['data' => ['a' => 'b']], $ex->toArray());
@ -369,12 +379,14 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
$doc = 'query Example { a } query OtherExample { a }';
$data = [ 'a' => 'b' ];
$ast = Parser::parse($doc);
$schema = new Schema(new ObjectType([
'name' => 'Type',
'fields' => [
'a' => [ 'type' => Type::string() ],
]
]));
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Type',
'fields' => [
'a' => [ 'type' => Type::string() ],
]
])
]);
try {
Executor::execute($schema, $ast, $data);
@ -389,22 +401,22 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
$doc = 'query Q { a } mutation M { c }';
$data = ['a' => 'b', 'c' => 'd'];
$ast = Parser::parse($doc);
$schema = new Schema(
new ObjectType([
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Q',
'fields' => [
'a' => ['type' => Type::string()],
]
]),
new ObjectType([
'mutation' => new ObjectType([
'name' => 'M',
'fields' => [
'c' => ['type' => Type::string()],
]
])
);
]);
$queryResult = Executor::execute($schema, $ast, $data, [], 'Q');
$queryResult = Executor::execute($schema, $ast, $data, null, [], 'Q');
$this->assertEquals(['data' => ['a' => 'b']], $queryResult->toArray());
}
@ -413,21 +425,21 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
$doc = 'query Q { a } mutation M { c }';
$data = [ 'a' => 'b', 'c' => 'd' ];
$ast = Parser::parse($doc);
$schema = new Schema(
new ObjectType([
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Q',
'fields' => [
'a' => ['type' => Type::string()],
]
]),
new ObjectType([
'mutation' => new ObjectType([
'name' => 'M',
'fields' => [
'c' => [ 'type' => Type::string() ],
]
])
);
$mutationResult = Executor::execute($schema, $ast, $data, [], 'M');
]);
$mutationResult = Executor::execute($schema, $ast, $data, null, [], 'M');
$this->assertEquals(['data' => ['c' => 'd']], $mutationResult->toArray());
}
@ -447,14 +459,16 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
';
$data = ['a' => 'b'];
$ast = Parser::parse($doc);
$schema = new Schema(new ObjectType([
'name' => 'Type',
'fields' => [
'a' => ['type' => Type::string()],
]
]));
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Type',
'fields' => [
'a' => ['type' => Type::string()],
]
])
]);
$queryResult = Executor::execute($schema, $ast, $data, [], 'Q');
$queryResult = Executor::execute($schema, $ast, $data, null, [], 'Q');
$this->assertEquals(['data' => ['a' => 'b']], $queryResult->toArray());
}
@ -464,28 +478,28 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
thisIsIllegalDontIncludeMe
}';
$ast = Parser::parse($doc);
$schema = new Schema(
new ObjectType([
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Q',
'fields' => [
'a' => ['type' => Type::string()],
]
]),
new ObjectType([
'mutation' => new ObjectType([
'name' => 'M',
'fields' => [
'c' => ['type' => Type::string()],
]
])
);
]);
$mutationResult = Executor::execute($schema, $ast);
$this->assertEquals(['data' => []], $mutationResult->toArray());
}
public function testDoesNotIncludeArgumentsThatWereNotSet()
{
$schema = new Schema(
new ObjectType([
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Type',
'fields' => [
'field' => [
@ -501,7 +515,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
]
]
])
);
]);
$query = Parser::parse('{ field(a: true, c: false, e: 0) }');
$result = Executor::execute($schema, $query);
@ -516,8 +530,8 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
public function testSubstitutesArgumentWithDefaultValue()
{
$schema = new Schema(
new ObjectType([
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Type',
'fields' => [
'field' => [
@ -535,7 +549,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
]
]
])
);
]);
$query = Parser::parse('{ field }');
$result = Executor::execute($schema, $query);

View File

@ -12,376 +12,186 @@ use GraphQL\Type\Definition\Type;
class ListsTest extends \PHPUnit_Framework_TestCase
{
// Execute: Handles list nullability
public function testHandlesListsWhenTheyReturnNonNullValues()
private function check($testType, $testData, $expected)
{
$doc = '
query Q {
nest {
list,
}
}
';
$data = ['test' => $testData];
$dataType = null;
$ast = Parser::parse($doc);
$expected = ['data' => ['nest' => ['list' => [1,2]]]];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
}
public function testHandlesListsOfNonNullsWhenTheyReturnNonNullValues()
{
$doc = '
query Q {
nest {
listOfNonNull,
}
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => [
'nest' => [
'listOfNonNull' => [1, 2],
]
]
];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
}
public function testHandlesNonNullListsOfWhenTheyReturnNonNullValues()
{
$doc = '
query Q {
nest {
nonNullList,
}
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => [
'nest' => [
'nonNullList' => [1, 2],
]
]
];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
}
public function testHandlesNonNullListsOfNonNullsWhenTheyReturnNonNullValues()
{
$doc = '
query Q {
nest {
nonNullListOfNonNull,
}
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => [
'nest' => [
'nonNullListOfNonNull' => [1, 2],
]
]
];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
}
public function testHandlesListsWhenTheyReturnNullAsAValue()
{
$doc = '
query Q {
nest {
listContainsNull,
}
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => [
'nest' => [
'listContainsNull' => [1, null, 2],
]
]
];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
}
public function testHandlesListsOfNonNullsWhenTheyReturnNullAsAValue()
{
$doc = '
query Q {
nest {
listOfNonNullContainsNull,
}
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => [
'nest' => [
'listOfNonNullContainsNull' => null
]
],
'errors' => [
FormattedError::create(
'Cannot return null for non-nullable type.',
[new SourceLocation(4, 11)]
)
]
];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
}
public function testHandlesNonNullListsOfWhenTheyReturnNullAsAValue()
{
$doc = '
query Q {
nest {
nonNullListContainsNull,
}
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => [
'nest' => ['nonNullListContainsNull' => [1, null, 2]]
]
];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
}
public function testHandlesNonNullListsOfNonNullsWhenTheyReturnNullAsAValue()
{
$doc = '
query Q {
nest {
nonNullListOfNonNullContainsNull,
}
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => [
'nest' => null
],
'errors' => [
FormattedError::create(
'Cannot return null for non-nullable type.',
[new SourceLocation(4, 11)]
)
]
];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
}
public function testHandlesListsWhenTheyReturnNull()
{
$doc = '
query Q {
nest {
listReturnsNull,
}
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => [
'nest' => [
'listReturnsNull' => null
]
]
];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
}
public function testHandlesListsOfNonNullsWhenTheyReturnNull()
{
$doc = '
query Q {
nest {
listOfNonNullReturnsNull,
}
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => [
'nest' => [
'listOfNonNullReturnsNull' => null
]
]
];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
}
public function testHandlesNonNullListsOfWhenTheyReturnNull()
{
$doc = '
query Q {
nest {
nonNullListReturnsNull,
}
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => [
'nest' => null,
],
'errors' => [
FormattedError::create(
'Cannot return null for non-nullable type.',
[new SourceLocation(4, 11)]
)
]
];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
}
public function testHandlesNonNullListsOfNonNullsWhenTheyReturnNull()
{
$doc = '
query Q {
nest {
nonNullListOfNonNullReturnsNull,
}
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => [
'nest' => null
],
'errors' => [
FormattedError::create(
'Cannot return null for non-nullable type.',
[new SourceLocation(4, 11)]
)
]
];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
}
private function schema()
{
$dataType = new ObjectType([
'name' => 'DataType',
'fields' => [
'list' => [
'type' => Type::listOf(Type::int())
],
'listOfNonNull' => [
'type' => Type::listOf(Type::nonNull(Type::int()))
],
'nonNullList' => [
'type' => Type::nonNull(Type::listOf(Type::int()))
],
'nonNullListOfNonNull' => [
'type' => Type::nonNull(Type::listOf(Type::nonNull(Type::int())))
],
'listContainsNull' => [
'type' => Type::listOf(Type::int())
],
'listOfNonNullContainsNull' => [
'type' => Type::listOf(Type::nonNull(Type::int())),
],
'nonNullListContainsNull' => [
'type' => Type::nonNull(Type::listOf(Type::int()))
],
'nonNullListOfNonNullContainsNull' => [
'type' => Type::nonNull(Type::listOf(Type::nonNull(Type::int())))
],
'listReturnsNull' => [
'type' => Type::listOf(Type::int())
],
'listOfNonNullReturnsNull' => [
'type' => Type::listOf(Type::nonNull(Type::int()))
],
'nonNullListReturnsNull' => [
'type' => Type::nonNull(Type::listOf(Type::int()))
],
'nonNullListOfNonNullReturnsNull' => [
'type' => Type::nonNull(Type::listOf(Type::nonNull(Type::int())))
],
'nest' => ['type' => function () use (&$dataType) {
return $dataType;
}]
]
'fields' => function () use (&$testType, &$dataType, $data) {
return [
'test' => [
'type' => $testType
],
'nest' => [
'type' => $dataType,
'resolve' => function () use ($data) {
return $data;
}
]
];
}
]);
$schema = new Schema($dataType);
return $schema;
$schema = new Schema([
'query' => $dataType
]);
$ast = Parser::parse('{ nest { test } }');
$result = Executor::execute($schema, $ast, $data);
$this->assertEquals($expected, $result->toArray());
}
private function data()
// Describe: Execute: Handles list nullability
/**
* @describe [T]
*/
public function testHandlesNullableLists()
{
return [
'list' => function () {
return [1, 2];
},
'listOfNonNull' => function () {
return [1, 2];
},
'nonNullList' => function () {
return [1, 2];
},
'nonNullListOfNonNull' => function () {
return [1, 2];
},
'listContainsNull' => function () {
return [1, null, 2];
},
'listOfNonNullContainsNull' => function () {
return [1, null, 2];
},
'nonNullListContainsNull' => function () {
return [1, null, 2];
},
'nonNullListOfNonNullContainsNull' => function () {
return [1, null, 2];
},
'listReturnsNull' => function () {
return null;
},
'listOfNonNullReturnsNull' => function () {
return null;
},
'nonNullListReturnsNull' => function () {
return null;
},
'nonNullListOfNonNullReturnsNull' => function () {
return null;
},
'nest' => function () {
return self::data();
}
];
$type = Type::listOf(Type::int());
// Contains values
$this->check(
$type,
[ 1, 2 ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
);
// Contains null
$this->check(
$type,
[ 1, null, 2 ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, null, 2 ] ] ] ]
);
// Returns null
$this->check(
$type,
null,
[ 'data' => [ 'nest' => [ 'test' => null ] ] ]
);
}
/**
* @describe [T]!
*/
public function testHandlesNonNullableLists()
{
$type = Type::nonNull(Type::listOf(Type::int()));
// Contains values
$this->check(
$type,
[ 1, 2 ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
);
// Contains null
$this->check(
$type,
[ 1, null, 2 ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, null, 2 ] ] ] ]
);
// Returns null
$this->check(
$type,
null,
[
'data' => [ 'nest' => null ],
'errors' => [
FormattedError::create(
'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ]
)
]
]
);
}
/**
* @describe [T!]
*/
public function testHandlesListOfNonNulls()
{
$type = Type::listOf(Type::nonNull(Type::int()));
// Contains values
$this->check(
$type,
[ 1, 2 ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
);
// Contains null
$this->check(
$type,
[ 1, null, 2 ],
[
'data' => [ 'nest' => [ 'test' => null ] ],
'errors' => [
FormattedError::create(
'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ]
)
]
]
);
// Returns null
$this->check(
$type,
null,
[ 'data' => [ 'nest' => [ 'test' => null ] ] ]
);
}
/**
* @describe [T!]!
*/
public function testHandlesNonNullListOfNonNulls()
{
$type = Type::nonNull(Type::listOf(Type::nonNull(Type::int())));
// Contains values
$this->check(
$type,
[ 1, 2 ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
);
// Contains null
$this->check(
$type,
[ 1, null, 2 ],
[
'data' => [ 'nest' => null ],
'errors' => [
FormattedError::create(
'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ]
)
]
]
);
// Returns null
$this->check(
$type,
null,
[
'data' => [ 'nest' => null ],
'errors' => [
FormattedError::create(
'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ]
)
]
]
);
}
}

View File

@ -12,6 +12,10 @@ use GraphQL\Type\Definition\Type;
class MutationsTest extends \PHPUnit_Framework_TestCase
{
// Execute: Handles mutation execution ordering
/**
* @it evaluates mutations serially
*/
public function testEvaluatesMutationsSerially()
{
$doc = 'mutation M {
@ -32,7 +36,7 @@ class MutationsTest extends \PHPUnit_Framework_TestCase
}
}';
$ast = Parser::parse($doc);
$mutationResult = Executor::execute($this->schema(), $ast, new Root(6), null, 'M');
$mutationResult = Executor::execute($this->schema(), $ast, new Root(6));
$expected = [
'data' => [
'first' => [
@ -55,6 +59,9 @@ class MutationsTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, $mutationResult->toArray());
}
/**
* @it evaluates mutations correctly in the presense of a failed mutation
*/
public function testEvaluatesMutationsCorrectlyInThePresenseOfAFailedMutation()
{
$doc = 'mutation M {
@ -78,7 +85,7 @@ class MutationsTest extends \PHPUnit_Framework_TestCase
}
}';
$ast = Parser::parse($doc);
$mutationResult = Executor::execute($this->schema(), $ast, new Root(6), null, 'M');
$mutationResult = Executor::execute($this->schema(), $ast, new Root(6));
$expected = [
'data' => [
'first' => [
@ -118,14 +125,14 @@ class MutationsTest extends \PHPUnit_Framework_TestCase
],
'name' => 'NumberHolder',
]);
$schema = new Schema(
new ObjectType([
$schema = new Schema([
'query' => new ObjectType([
'fields' => [
'numberHolder' => ['type' => $numberHolderType],
],
'name' => 'Query',
]),
new ObjectType([
'mutation' => new ObjectType([
'fields' => [
'immediatelyChangeTheNumber' => [
'type' => $numberHolderType,
@ -158,7 +165,7 @@ class MutationsTest extends \PHPUnit_Framework_TestCase
],
'name' => 'Mutation',
])
);
]);
return $schema;
}
}

View File

@ -68,10 +68,14 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
]
]);
$this->schema = new Schema($dataType);
$this->schema = new Schema(['query' => $dataType]);
}
// Execute: handles non-nullable types
/**
* @it nulls a nullable field that throws synchronously
*/
public function testNullsANullableFieldThatThrowsSynchronously()
{
$doc = '
@ -93,7 +97,7 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
)
]
];
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, [], 'Q')->toArray());
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q')->toArray());
}
public function testNullsASynchronouslyReturnedObjectThatContainsANonNullableFieldThatThrowsSynchronously()
@ -117,7 +121,7 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
FormattedError::create($this->nonNullSyncError->message, [new SourceLocation(4, 11)])
]
];
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, [], 'Q')->toArray());
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q')->toArray());
}
public function testNullsAComplexTreeOfNullableFieldsThatThrow()
@ -149,7 +153,7 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
FormattedError::create($this->syncError->message, [new SourceLocation(6, 13)]),
]
];
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, [], 'Q')->toArray());
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q')->toArray());
}
public function testNullsANullableFieldThatSynchronouslyReturnsNull()
@ -167,7 +171,7 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
'sync' => null,
]
];
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, [], 'Q')->toArray());
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray());
}
public function test4()
@ -188,10 +192,10 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
'nest' => null
],
'errors' => [
FormattedError::create('Cannot return null for non-nullable type.', [new SourceLocation(4, 11)])
FormattedError::create('Cannot return null for non-nullable field DataType.nonNullSync.', [new SourceLocation(4, 11)])
]
];
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, [], 'Q')->toArray());
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray());
}
public function test5()
@ -227,7 +231,7 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
],
]
];
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, [], 'Q')->toArray());
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray());
}
public function testNullsTheTopLevelIfSyncNonNullableFieldThrows()
@ -255,7 +259,7 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
$expected = [
'data' => null,
'errors' => [
FormattedError::create('Cannot return null for non-nullable type.', [new SourceLocation(2, 17)]),
FormattedError::create('Cannot return null for non-nullable field DataType.nonNullSync.', [new SourceLocation(2, 17)]),
]
];
$this->assertEquals($expected, Executor::execute($this->schema, Parser::parse($doc), $this->nullingData)->toArray());

View File

@ -0,0 +1,97 @@
<?php
namespace GraphQL\Tests\Executor;
use GraphQL\GraphQL;
use GraphQL\Schema;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
class ResolveTest extends \PHPUnit_Framework_TestCase
{
// Execute: resolve function
private function buildSchema($testField)
{
return new Schema([
'query' => new ObjectType([
'name' => 'Query',
'fields' => [
'test' => $testField
]
])
]);
}
/**
* @it default function accesses properties
*/
public function testDefaultFunctionAccessesProperties()
{
$schema = $this->buildSchema(['type' => Type::string()]);
$source = [
'test' => 'testValue'
];
$this->assertEquals(
['data' => ['test' => 'testValue']],
GraphQL::execute($schema, '{ test }', $source)
);
}
/**
* @it default function calls methods
*/
public function testDefaultFunctionCallsMethods()
{
$schema = $this->buildSchema(['type' => Type::string()]);
$_secret = 'secretValue' . uniqid();
$source = [
'test' => function () use ($_secret) {
return $_secret;
}
];
$this->assertEquals(
['data' => ['test' => $_secret]],
GraphQL::execute($schema, '{ test }', $source)
);
}
/**
* @it uses provided resolve function
*/
public function testUsesProvidedResolveFunction()
{
$schema = $this->buildSchema([
'type' => Type::string(),
'args' => [
'aStr' => ['type' => Type::string()],
'aInt' => ['type' => Type::int()],
],
'resolve' => function ($source, $args) {
return json_encode([$source, $args]);
}
]);
$this->assertEquals(
['data' => ['test' => '[null,[]]']],
GraphQL::execute($schema, '{ test }')
);
$this->assertEquals(
['data' => ['test' => '["Source!",[]]']],
GraphQL::execute($schema, '{ test }', 'Source!')
);
$this->assertEquals(
['data' => ['test' => '["Source!",{"aStr":"String!"}]']],
GraphQL::execute($schema, '{ test(aStr: "String!") }', 'Source!')
);
$this->assertEquals(
['data' => ['test' => '["Source!",{"aStr":"String!","aInt":-123}]']],
GraphQL::execute($schema, '{ test(aInt: -123, aStr: "String!") }', 'Source!')
);
}
}

View File

@ -4,11 +4,13 @@ namespace GraphQL\Tests\Executor;
require_once __DIR__ . '/TestClasses.php';
use GraphQL\Executor\Executor;
use GraphQL\GraphQL;
use GraphQL\Language\Parser;
use GraphQL\Schema;
use GraphQL\Type\Definition\Config;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
@ -79,7 +81,10 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
}
]);
$this->schema = new Schema($PersonType);
$this->schema = new Schema([
'query' => $PersonType,
'types' => [ $PetType ]
]);
$this->garfield = new Cat('Garfield', false);
$this->odie = new Dog('Odie', true);
@ -89,6 +94,10 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
}
// Execute: Union and intersection types
/**
* @it can introspect on union and intersection types
*/
public function testCanIntrospectOnUnionAndIntersectionTypes()
{
@ -125,9 +134,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
],
'interfaces' => null,
'possibleTypes' => [
['name' => 'Person'],
['name' => 'Dog'],
['name' => 'Cat'],
['name' => 'Person']
['name' => 'Cat']
],
'enumValues' => null,
'inputFields' => null
@ -149,6 +158,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, Executor::execute($this->schema, $ast)->toArray());
}
/**
* @it executes using union types
*/
public function testExecutesUsingUnionTypes()
{
// NOTE: This is an *invalid* query, but it should be an *executable* query.
@ -178,6 +190,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray());
}
/**
* @it executes union types with inline fragments
*/
public function testExecutesUnionTypesWithInlineFragments()
{
// This is the valid version of the query in the above test.
@ -212,6 +227,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray());
}
/**
* @it executes using interface types
*/
public function testExecutesUsingInterfaceTypes()
{
// NOTE: This is an *invalid* query, but it should be an *executable* query.
@ -241,6 +259,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray());
}
/**
* @it executes interface types with inline fragments
*/
public function testExecutesInterfaceTypesWithInlineFragments()
{
// This is the valid version of the query in the above test.
@ -274,6 +295,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray());
}
/**
* @it allows fragment conditions to be abstract types
*/
public function testAllowsFragmentConditionsToBeAbstractTypes()
{
$ast = Parser::parse('
@ -325,4 +349,55 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray());
}
/**
* @it gets execution info in resolver
*/
public function testGetsExecutionInfoInResolver()
{
$encounteredContext = null;
$encounteredSchema = null;
$encounteredRootValue = null;
$PersonType2 = null;
$NamedType2 = new InterfaceType([
'name' => 'Named',
'fields' => [
'name' => ['type' => Type::string()]
],
'resolveType' => function ($obj, $context, ResolveInfo $info) use (&$encounteredContext, &$encounteredSchema, &$encounteredRootValue, &$PersonType2) {
$encounteredContext = $context;
$encounteredSchema = $info->schema;
$encounteredRootValue = $info->rootValue;
return $PersonType2;
}
]);
$PersonType2 = new ObjectType([
'name' => 'Person',
'interfaces' => [$NamedType2],
'fields' => [
'name' => ['type' => Type::string()],
'friends' => ['type' => Type::listOf($NamedType2)],
],
]);
$schema2 = new Schema([
'query' => $PersonType2
]);
$john2 = new Person('John', [], [$this->liz]);
$context = ['authToken' => '123abc'];
$ast = Parser::parse('{ name, friends { name } }');
$this->assertEquals(
['data' => ['name' => 'John', 'friends' => [['name' => 'Liz']]]],
GraphQL::execute($schema2, $ast, $john2, $context)
);
$this->assertSame($context, $encounteredContext);
$this->assertSame($schema2, $encounteredSchema);
$this->assertSame($john2, $encounteredRootValue);
}
}

View File

@ -11,7 +11,6 @@ use GraphQL\Language\SourceLocation;
use GraphQL\Schema;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type;
class VariablesTest extends \PHPUnit_Framework_TestCase
@ -19,6 +18,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
// Execute: Handles inputs
// Handles objects and nullability
/**
* @describe using inline structs
*/
public function testUsingInlineStructs()
{
// executes with complex input:
@ -46,10 +48,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
$expected = ['data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}']];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
}
public function testDoesNotUseIncorrectValue()
{
// does not use incorrect value
$doc = '
{
fieldWithObjectInput(input: ["foo", "bar", "baz"])
@ -62,8 +62,23 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
'data' => ['fieldWithObjectInput' => null]
];
$this->assertEquals($expected, $result);
// properly runs parseLiteral on complex scalar types
$doc = '
{
fieldWithObjectInput(input: {a: "foo", d: "SerializedValue"})
}
';
$ast = Parser::parse($doc);
$this->assertEquals(
['data' => ['fieldWithObjectInput' => '{"a":"foo","d":"DeserializedValue"}']],
Executor::execute($this->schema(), $ast)->toArray()
);
}
/**
* @describe using variables
*/
public function testUsingVariables()
{
// executes with complex input:
@ -78,7 +93,7 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(
['data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}']],
Executor::execute($schema, $ast, null, $params)->toArray()
Executor::execute($schema, $ast, null, null, $params)->toArray()
);
// uses default value when not provided:
@ -94,17 +109,16 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
];
$this->assertEquals($expected, $result);
// properly parses single value to array:
$params = ['input' => ['a' => 'foo', 'b' => 'bar', 'c' => 'baz']];
$this->assertEquals(
['data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}']],
Executor::execute($schema, $ast, null, $params)->toArray()
Executor::execute($schema, $ast, null, null, $params)->toArray()
);
// executes with complex scalar input:
$params = [ 'input' => [ 'c' => 'foo', 'd' => 'SerializedValue' ] ];
$result = Executor::execute($schema, $ast, null, $params)->toArray();
$result = Executor::execute($schema, $ast, null, null, $params)->toArray();
$expected = [
'data' => [
'fieldWithObjectInput' => '{"c":"foo","d":"DeserializedValue"}'
@ -115,14 +129,13 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
// errors on null for nested non-null:
$params = ['input' => ['a' => 'foo', 'b' => 'bar', 'c' => null]];
$expected = FormattedError::create(
'Variable $input expected value of type ' .
'TestInputObject but got: ' .
'{"a":"foo","b":"bar","c":null}.',
'Variable "$input" got invalid value {"a":"foo","b":"bar","c":null}.'. "\n".
'In field "c": Expected "String!", found null.',
[new SourceLocation(2, 17)]
);
try {
Executor::execute($schema, $ast, null, $params);
Executor::execute($schema, $ast, null, null, $params);
$this->fail('Expected exception not thrown');
} catch (Error $err) {
$this->assertEquals($expected, Error::formatError($err));
@ -132,11 +145,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
$params = [ 'input' => 'foo bar' ];
try {
Executor::execute($schema, $ast, null, $params);
Executor::execute($schema, $ast, null, null, $params);
$this->fail('Expected exception not thrown');
} catch (Error $error) {
$expected = FormattedError::create(
'Variable $input expected value of type TestInputObject but got: "foo bar".',
'Variable "$input" got invalid value "foo bar".'."\n".
'Expected "TestInputObject", found not an object.',
[new SourceLocation(2, 17)]
);
$this->assertEquals($expected, Error::formatError($error));
@ -146,36 +160,61 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
$params = ['input' => ['a' => 'foo', 'b' => 'bar']];
try {
Executor::execute($schema, $ast, null, $params);
Executor::execute($schema, $ast, null, null, $params);
$this->fail('Expected exception not thrown');
} catch (Error $e) {
$expected = FormattedError::create(
'Variable $input expected value of type ' .
'TestInputObject but got: {"a":"foo","b":"bar"}.',
'Variable "$input" got invalid value {"a":"foo","b":"bar"}.'. "\n".
'In field "c": Expected "String!", found null.',
[new SourceLocation(2, 17)]
);
$this->assertEquals($expected, Error::formatError($e));
}
// errors on deep nested errors and with many errors
$nestedDoc = '
query q($input: TestNestedInputObject) {
fieldWithNestedObjectInput(input: $input)
}
';
$nestedAst = Parser::parse($nestedDoc);
$params = [ 'input' => [ 'na' => [ 'a' => 'foo' ] ] ];
try {
Executor::execute($schema, $nestedAst, null, null, $params);
$this->fail('Expected exception not thrown');
} catch (Error $error) {
$expected = FormattedError::create(
'Variable "$input" got invalid value {"na":{"a":"foo"}}.' . "\n" .
'In field "na": In field "c": Expected "String!", found null.' . "\n" .
'In field "nb": Expected "String!", found null.',
[new SourceLocation(2, 19)]
);
$this->assertEquals($expected, Error::formatError($error));
}
// errors on addition of unknown input field
$params = ['input' => [ 'a' => 'foo', 'b' => 'bar', 'c' => 'baz', 'd' => 'dog' ]];
try {
Executor::execute($schema, $ast, null, $params);
Executor::execute($schema, $ast, null, null, $params);
$this->fail('Expected exception not thrown');
} catch (Error $e) {
$expected = FormattedError::create(
'Variable $input expected value of type TestInputObject but ' .
'got: {"a":"foo","b":"bar","c":"baz","d":"dog"}.',
'Variable "$input" got invalid value {"a":"foo","b":"bar","c":"baz","d":"dog"}.'."\n".
'In field "d": Expected type "ComplexScalar", found "dog".',
[new SourceLocation(2, 17)]
);
$this->assertEquals($expected, Error::formatError($e));
}
}
// Describe: Handles nullable scalars
// Handles nullable scalars
/**
* @it allows nullable inputs to be omitted
*/
public function testAllowsNullableInputsToBeOmitted()
{
$doc = '
@ -191,6 +230,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
}
/**
* @it allows nullable inputs to be omitted in a variable
*/
public function testAllowsNullableInputsToBeOmittedInAVariable()
{
$doc = '
@ -204,6 +246,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
}
/**
* @it allows nullable inputs to be omitted in an unlisted variable
*/
public function testAllowsNullableInputsToBeOmittedInAnUnlistedVariable()
{
$doc = '
@ -216,6 +261,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
}
/**
* @it allows nullable inputs to be set to null in a variable
*/
public function testAllowsNullableInputsToBeSetToNullInAVariable()
{
$doc = '
@ -229,6 +277,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['value' => null])->toArray());
}
/**
* @it allows nullable inputs to be set to a value in a variable
*/
public function testAllowsNullableInputsToBeSetToAValueInAVariable()
{
$doc = '
@ -238,9 +289,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
';
$ast = Parser::parse($doc);
$expected = ['data' => ['fieldWithNullableStringInput' => '"a"']];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['value' => 'a'])->toArray());
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['value' => 'a'])->toArray());
}
/**
* @it allows nullable inputs to be set to a value directly
*/
public function testAllowsNullableInputsToBeSetToAValueDirectly()
{
$doc = '
@ -254,10 +308,13 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
}
// Handles non-nullable scalars
// Describe: Handles non-nullable scalars
/**
* @it does not allow non-nullable inputs to be omitted in a variable
*/
public function testDoesntAllowNonNullableInputsToBeOmittedInAVariable()
{
// does not allow non-nullable inputs to be omitted in a variable
$doc = '
query SetsNonNullable($value: String!) {
fieldWithNonNullableStringInput(input: $value)
@ -269,16 +326,18 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
$this->fail('Expected exception not thrown');
} catch (Error $e) {
$expected = FormattedError::create(
'Variable $value expected value of type String! but got: null.',
'Variable "$value" of required type "String!" was not provided.',
[new SourceLocation(2, 31)]
);
$this->assertEquals($expected, Error::formatError($e));
}
}
/**
* @it does not allow non-nullable inputs to be set to null in a variable
*/
public function testDoesNotAllowNonNullableInputsToBeSetToNullInAVariable()
{
// does not allow non-nullable inputs to be set to null in a variable
$doc = '
query SetsNonNullable($value: String!) {
fieldWithNonNullableStringInput(input: $value)
@ -291,13 +350,16 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
$this->fail('Expected exception not thrown');
} catch (Error $e) {
$expected = FormattedError::create(
'Variable $value expected value of type String! but got: null.',
'Variable "$value" of required type "String!" was not provided.',
[new SourceLocation(2, 31)]
);
$this->assertEquals($expected, Error::formatError($e));
}
}
/**
* @it allows non-nullable inputs to be set to a value in a variable
*/
public function testAllowsNonNullableInputsToBeSetToAValueInAVariable()
{
$doc = '
@ -307,9 +369,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
';
$ast = Parser::parse($doc);
$expected = ['data' => ['fieldWithNonNullableStringInput' => '"a"']];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['value' => 'a'])->toArray());
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['value' => 'a'])->toArray());
}
/**
* @it allows non-nullable inputs to be set to a value directly
*/
public function testAllowsNonNullableInputsToBeSetToAValueDirectly()
{
$doc = '
@ -323,6 +388,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
}
/**
* @it passes along null for non-nullable inputs if explcitly set in the query
*/
public function testPassesAlongNullForNonNullableInputsIfExplcitlySetInTheQuery()
{
$doc = '
@ -335,7 +403,11 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
}
// Handles lists and nullability
// Describe: Handles lists and nullability
/**
* @it allows lists to be null
*/
public function testAllowsListsToBeNull()
{
$doc = '
@ -349,6 +421,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => null])->toArray());
}
/**
* @it allows lists to contain values
*/
public function testAllowsListsToContainValues()
{
$doc = '
@ -358,9 +433,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
';
$ast = Parser::parse($doc);
$expected = ['data' => ['list' => '["A"]']];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => ['A']])->toArray());
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => ['A']])->toArray());
}
/**
* @it allows lists to contain null
*/
public function testAllowsListsToContainNull()
{
$doc = '
@ -370,9 +448,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
';
$ast = Parser::parse($doc);
$expected = ['data' => ['list' => '["A",null,"B"]']];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => ['A',null,'B']])->toArray());
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => ['A',null,'B']])->toArray());
}
/**
* @it does not allow non-null lists to be null
*/
public function testDoesNotAllowNonNullListsToBeNull()
{
$doc = '
@ -382,18 +463,21 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
';
$ast = Parser::parse($doc);
$expected = FormattedError::create(
'Variable $input expected value of type [String]! but got: null.',
'Variable "$input" of required type "[String]!" was not provided.',
[new SourceLocation(2, 17)]
);
try {
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => null])->toArray());
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => null])->toArray());
$this->fail('Expected exception not thrown');
} catch (Error $e) {
$this->assertEquals($expected, Error::formatError($e));
}
}
/**
* @it allows non-null lists to contain values
*/
public function testAllowsNonNullListsToContainValues()
{
$doc = '
@ -403,9 +487,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
';
$ast = Parser::parse($doc);
$expected = ['data' => ['nnList' => '["A"]']];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => 'A'])->toArray());
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => 'A'])->toArray());
}
/**
* @it allows non-null lists to contain null
*/
public function testAllowsNonNullListsToContainNull()
{
$doc = '
@ -416,9 +503,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
$ast = Parser::parse($doc);
$expected = ['data' => ['nnList' => '["A",null,"B"]']];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => ['A',null,'B']])->toArray());
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => ['A',null,'B']])->toArray());
}
/**
* @it allows lists of non-nulls to be null
*/
public function testAllowsListsOfNonNullsToBeNull()
{
$doc = '
@ -431,6 +521,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => null])->toArray());
}
/**
* @it allows lists of non-nulls to contain values
*/
public function testAllowsListsOfNonNullsToContainValues()
{
$doc = '
@ -441,9 +534,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
$ast = Parser::parse($doc);
$expected = ['data' => ['listNN' => '["A"]']];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => 'A'])->toArray());
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => 'A'])->toArray());
}
/**
* @it does not allow lists of non-nulls to contain null
*/
public function testDoesNotAllowListsOfNonNullsToContainNull()
{
$doc = '
@ -453,18 +549,22 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
';
$ast = Parser::parse($doc);
$expected = FormattedError::create(
'Variable $input expected value of type [String!] but got: ["A",null,"B"].',
'Variable "$input" got invalid value ["A",null,"B"].' . "\n" .
'In element #1: Expected "String!", found null.',
[new SourceLocation(2, 17)]
);
try {
Executor::execute($this->schema(), $ast, null, ['input' => ['A', null, 'B']]);
Executor::execute($this->schema(), $ast, null, null, ['input' => ['A', null, 'B']]);
$this->fail('Expected exception not thrown');
} catch (Error $e) {
$this->assertEquals($expected, Error::formatError($e));
}
}
/**
* @it does not allow non-null lists of non-nulls to be null
*/
public function testDoesNotAllowNonNullListsOfNonNullsToBeNull()
{
$doc = '
@ -474,16 +574,20 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
';
$ast = Parser::parse($doc);
$expected = FormattedError::create(
'Variable $input expected value of type [String!]! but got: null.',
'Variable "$input" of required type "[String!]!" was not provided.',
[new SourceLocation(2, 17)]
);
try {
Executor::execute($this->schema(), $ast, null, ['input' => null]);
Executor::execute($this->schema(), $ast, null, null, ['input' => null]);
$this->fail('Expected exception not thrown');
} catch (Error $e) {
$this->assertEquals($expected, Error::formatError($e));
}
}
/**
* @it allows non-null lists of non-nulls to contain values
*/
public function testAllowsNonNullListsOfNonNullsToContainValues()
{
$doc = '
@ -493,9 +597,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
';
$ast = Parser::parse($doc);
$expected = ['data' => ['nnListNN' => '["A"]']];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => ['A']])->toArray());
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => ['A']])->toArray());
}
/**
* @it does not allow non-null lists of non-nulls to contain null
*/
public function testDoesNotAllowNonNullListsOfNonNullsToContainNull()
{
$doc = '
@ -505,16 +612,116 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
';
$ast = Parser::parse($doc);
$expected = FormattedError::create(
'Variable $input expected value of type [String!]! but got: ["A",null,"B"].',
'Variable "$input" got invalid value ["A",null,"B"].'."\n".
'In element #1: Expected "String!", found null.',
[new SourceLocation(2, 17)]
);
try {
Executor::execute($this->schema(), $ast, null, ['input' => ['A', null, 'B']]);
Executor::execute($this->schema(), $ast, null, null, ['input' => ['A', null, 'B']]);
$this->fail('Expected exception not thrown');
} catch (Error $e) {
$this->assertEquals($expected, Error::formatError($e));
}
}
/**
* @it does not allow invalid types to be used as values
*/
public function testDoesNotAllowInvalidTypesToBeUsedAsValues()
{
$doc = '
query q($input: TestType!) {
fieldWithObjectInput(input: $input)
}
';
$ast = Parser::parse($doc);
$vars = [ 'input' => [ 'list' => [ 'A', 'B' ] ] ];
try {
Executor::execute($this->schema(), $ast, null, null, $vars);
$this->fail('Expected exception not thrown');
} catch (Error $error) {
$expected = FormattedError::create(
'Variable "$input" expected value of type "TestType!" which cannot ' .
'be used as an input type.',
[new SourceLocation(2, 17)]
);
$this->assertEquals($expected, Error::formatError($error));
}
}
/**
* @it does not allow unknown types to be used as values
*/
public function testDoesNotAllowUnknownTypesToBeUsedAsValues()
{
$doc = '
query q($input: UnknownType!) {
fieldWithObjectInput(input: $input)
}
';
$ast = Parser::parse($doc);
$vars = ['input' => 'whoknows'];
try {
Executor::execute($this->schema(), $ast, null, null, $vars);
$this->fail('Expected exception not thrown');
} catch (Error $error) {
$expected = FormattedError::create(
'Variable "$input" expected value of type "UnknownType!" which ' .
'cannot be used as an input type.',
[new SourceLocation(2, 17)]
);
$this->assertEquals($expected, Error::formatError($error));
}
}
// Describe: Execute: Uses argument default values
/**
* @it when no argument provided
*/
public function testWhenNoArgumentProvided()
{
$ast = Parser::parse('{
fieldWithDefaultArgumentValue
}');
$this->assertEquals(
['data' => ['fieldWithDefaultArgumentValue' => '"Hello World"']],
Executor::execute($this->schema(), $ast)->toArray()
);
}
/**
* @it when nullable variable provided
*/
public function testWhenNullableVariableProvided()
{
$ast = Parser::parse('query optionalVariable($optional: String) {
fieldWithDefaultArgumentValue(input: $optional)
}');
$this->assertEquals(
['data' => ['fieldWithDefaultArgumentValue' => '"Hello World"']],
Executor::execute($this->schema(), $ast)->toArray()
);
}
/**
* @it when argument provided cannot be parsed
*/
public function testWhenArgumentProvidedCannotBeParsed()
{
$ast = Parser::parse('{
fieldWithDefaultArgumentValue(input: WRONG_TYPE)
}');
$this->assertEquals(
['data' => ['fieldWithDefaultArgumentValue' => '"Hello World"']],
Executor::execute($this->schema(), $ast)->toArray()
);
}
public function schema()
{
@ -530,6 +737,14 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
]
]);
$TestNestedInputObject = new InputObjectType([
'name' => 'TestNestedInputObject',
'fields' => [
'na' => [ 'type' => Type::nonNull($TestInputObject) ],
'nb' => [ 'type' => Type::nonNull(Type::string()) ],
],
]);
$TestType = new ObjectType([
'name' => 'TestType',
'fields' => [
@ -561,6 +776,18 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
return isset($args['input']) ? json_encode($args['input']) : null;
}
],
'fieldWithNestedInputObject' => [
'type' => Type::string(),
'args' => [
'input' => [
'type' => $TestNestedInputObject,
'defaultValue' => 'Hello World'
]
],
'resolve' => function($_, $args) {
return isset($args['input']) ? json_encode($args['input']) : null;
}
],
'list' => [
'type' => Type::string(),
'args' => ['input' => ['type' => Type::listOf(Type::string())]],
@ -592,7 +819,7 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
]
]);
$schema = new Schema($TestType);
$schema = new Schema(['query' => $TestType]);
return $schema;
}
}

View File

@ -8,7 +8,10 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
{
// Star Wars Introspection Tests
// Basic Introspection
// it('Allows querying the schema for types')
/**
* @it Allows querying the schema for types
*/
public function testAllowsQueryingTheSchemaForTypes()
{
$query = '
@ -26,8 +29,8 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
['name' => 'Query'],
['name' => 'Episode'],
['name' => 'Character'],
['name' => 'Human'],
['name' => 'String'],
['name' => 'Human'],
['name' => 'Droid'],
['name' => '__Schema'],
['name' => '__Type'],
@ -37,6 +40,7 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
['name' => '__InputValue'],
['name' => '__EnumValue'],
['name' => '__Directive'],
['name' => '__DirectiveLocation'],
['name' => 'ID'],
['name' => 'Float'],
['name' => 'Int']
@ -46,7 +50,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
$this->assertValidQuery($query, $expected);
}
// it('Allows querying the schema for query type')
/**
* @it Allows querying the schema for query type
*/
public function testAllowsQueryingTheSchemaForQueryType()
{
$query = '
@ -68,7 +74,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
$this->assertValidQuery($query, $expected);
}
// it('Allows querying the schema for a specific type')
/**
* @it Allows querying the schema for a specific type
*/
public function testAllowsQueryingTheSchemaForASpecificType()
{
$query = '
@ -86,7 +94,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
$this->assertValidQuery($query, $expected);
}
// it('Allows querying the schema for an object kind')
/**
* @it Allows querying the schema for an object kind
*/
public function testAllowsQueryingForAnObjectKind()
{
$query = '
@ -106,7 +116,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
$this->assertValidQuery($query, $expected);
}
// it('Allows querying the schema for an interface kind')
/**
* @it Allows querying the schema for an interface kind
*/
public function testAllowsQueryingForInterfaceKind()
{
$query = '
@ -126,7 +138,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
$this->assertValidQuery($query, $expected);
}
// it('Allows querying the schema for object fields')
/**
* @it Allows querying the schema for object fields
*/
public function testAllowsQueryingForObjectFields()
{
$query = '
@ -188,7 +202,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
$this->assertValidQuery($query, $expected);
}
// it('Allows querying the schema for nested object fields')
/**
* @it Allows querying the schema for nested object fields
*/
public function testAllowsQueryingTheSchemaForNestedObjectFields()
{
$query = '
@ -268,6 +284,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
$this->assertValidQuery($query, $expected);
}
/**
* @it Allows querying the schema for field args
*/
public function testAllowsQueryingTheSchemaForFieldArgs()
{
$query = '
@ -359,7 +378,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
$this->assertValidQuery($query, $expected);
}
// it('Allows querying the schema for documentation')
/**
* @it Allows querying the schema for documentation
*/
public function testAllowsQueryingTheSchemaForDocumentation()
{
$query = '

View File

@ -9,9 +9,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
// Star Wars Query Tests
// Basic Queries
/**
* @it Correctly identifies R2-D2 as the hero of the Star Wars Saga
*/
public function testCorrectlyIdentifiesR2D2AsTheHeroOfTheStarWarsSaga()
{
// Correctly identifies R2-D2 as the hero of the Star Wars Saga
$query = '
query HeroNameQuery {
hero {
@ -27,6 +29,9 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
$this->assertValidQuery($query, $expected);
}
/**
* @it Allows us to query for the ID and friends of R2-D2
*/
public function testAllowsUsToQueryForTheIDAndFriendsOfR2D2()
{
$query = '
@ -60,7 +65,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
$this->assertValidQuery($query, $expected);
}
// Nested Queries
// Describe: Nested Queries
/**
* @it Allows us to query for the friends of friends of R2-D2
*/
public function testAllowsUsToQueryForTheFriendsOfFriendsOfR2D2()
{
$query = '
@ -117,7 +126,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
$this->assertValidQuery($query, $expected);
}
// Using IDs and query parameters to refetch objects
// Describe: Using IDs and query parameters to refetch objects
/**
* @it Using IDs and query parameters to refetch objects
*/
public function testAllowsUsToQueryForLukeSkywalkerDirectlyUsingHisID()
{
$query = '
@ -136,9 +149,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
$this->assertValidQuery($query, $expected);
}
/**
* @it Allows us to create a generic query, then use it to fetch Luke Skywalker using his ID
*/
public function testGenericQueryToGetLukeSkywalkerById()
{
// Allows us to create a generic query, then use it to fetch Luke Skywalker using his ID
$query = '
query FetchSomeIDQuery($someId: String!) {
human(id: $someId) {
@ -158,9 +173,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
$this->assertValidQueryWithParams($query, $params, $expected);
}
/**
* @it Allows us to create a generic query, then use it to fetch Han Solo using his ID
*/
public function testGenericQueryToGetHanSoloById()
{
// Allows us to create a generic query, then use it to fetch Han Solo using his ID
$query = '
query FetchSomeIDQuery($someId: String!) {
human(id: $someId) {
@ -179,9 +196,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
$this->assertValidQueryWithParams($query, $params, $expected);
}
/**
* @it Allows us to create a generic query, then pass an invalid ID to get null back
*/
public function testGenericQueryWithInvalidId()
{
// Allows us to create a generic query, then pass an invalid ID to get null back
$query = '
query humanQuery($id: String!) {
human(id: $id) {
@ -199,9 +218,12 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
}
// Using aliases to change the key in the response
/**
* @it Allows us to query for Luke, changing his key with an alias
*/
function testLukeKeyAlias()
{
// Allows us to query for Luke, changing his key with an alias
$query = '
query FetchLukeAliased {
luke: human(id: "1000") {
@ -217,9 +239,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
$this->assertValidQuery($query, $expected);
}
/**
* @it Allows us to query for both Luke and Leia, using two root fields and an alias
*/
function testTwoRootKeysAsAnAlias()
{
// Allows us to query for both Luke and Leia, using two root fields and an alias
$query = '
query FetchLukeAndLeiaAliased {
luke: human(id: "1000") {
@ -242,9 +266,12 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
}
// Uses fragments to express more complex queries
/**
* @it Allows us to query using duplicated content
*/
function testQueryUsingDuplicatedContent()
{
// Allows us to query using duplicated content
$query = '
query DuplicateFields {
luke: human(id: "1000") {
@ -270,9 +297,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
$this->assertValidQuery($query, $expected);
}
/**
* @it Allows us to use a fragment to avoid duplicating content
*/
function testUsingFragment()
{
// Allows us to use a fragment to avoid duplicating content
$query = '
query UseFragment {
luke: human(id: "1000") {
@ -302,58 +331,9 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
$this->assertValidQuery($query, $expected);
}
function testFragmentWithProjection()
{
$query = '
query UseFragment {
human(id: "1003") {
name
...fa
...fb
}
}
fragment fa on Character {
friends {
id
}
}
fragment fb on Character {
friends {
name
}
}
';
$expected = [
'human' => [
'name' => 'Leia Organa',
'friends' => [
[
'name' => 'Luke Skywalker',
'id' => '1000'
],
[
'name' => 'Han Solo',
'id' => '1002'
],
[
'name' => 'C-3PO',
'id' => '2000'
],
[
'name' => 'R2-D2',
'id' => '2001'
]
]
]
];
$this->assertValidQuery($query, $expected);
}
// Using __typename to find the type of an object
/**
* @it Using __typename to find the type of an object
*/
public function testVerifyThatR2D2IsADroid()
{
$query = '
@ -373,6 +353,9 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
$this->assertValidQuery($query, $expected);
}
/**
* @it Allows us to verify that Luke is a human
*/
public function testVerifyThatLukeIsHuman()
{
$query = '
@ -407,6 +390,6 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
*/
private function assertValidQueryWithParams($query, $params, $expected)
{
$this->assertEquals(['data' => $expected], GraphQL::execute(StarWarsSchema::build(), $query, null, $params));
$this->assertEquals(['data' => $expected], GraphQL::execute(StarWarsSchema::build(), $query, null, null, $params));
}
}

View File

@ -291,6 +291,6 @@ class StarWarsSchema
]
]);
return new Schema($queryType);
return new Schema(['query' => $queryType]);
}
}

View File

@ -8,6 +8,10 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase
{
// Star Wars Validation Tests
// Basic Queries
/**
* @it Validates a complex but valid query
*/
public function testValidatesAComplexButValidQuery()
{
$query = '
@ -32,9 +36,11 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(true, empty($errors));
}
/**
* @it Notes that non-existent fields are invalid
*/
public function testThatNonExistentFieldsAreInvalid()
{
// Notes that non-existent fields are invalid
$query = '
query HeroSpaceshipQuery {
hero {
@ -46,6 +52,9 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(false, empty($errors));
}
/**
* @it Requires fields on objects
*/
public function testRequiresFieldsOnObjects()
{
$query = '
@ -58,9 +67,11 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(false, empty($errors));
}
/**
* @it Disallows fields on scalars
*/
public function testDisallowsFieldsOnScalars()
{
$query = '
query HeroFieldsOnScalarQuery {
hero {
@ -74,6 +85,9 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(false, empty($errors));
}
/**
* @it Disallows object fields on interfaces
*/
public function testDisallowsObjectFieldsOnInterfaces()
{
$query = '
@ -88,6 +102,9 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(false, empty($errors));
}
/**
* @it Allows object fields in fragments
*/
public function testAllowsObjectFieldsInFragments()
{
$query = '
@ -106,6 +123,9 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(true, empty($errors));
}
/**
* @it Allows object fields in inline fragments
*/
public function testAllowsObjectFieldsInInlineFragments()
{
$query = '

View File

@ -11,6 +11,7 @@ use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Utils;
class DefinitionTest extends \PHPUnit_Framework_TestCase
{
@ -415,7 +416,7 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase
$this->fail('Expected exception not thrown');
} catch (\Exception $e) {
$this->assertSame(
'Error in "BadUnion" type definition: expecting callable or instance of GraphQL\Type\Definition\ObjectType at "types:0", but got "' . get_class($type) . '"',
'Error in "BadUnion" type definition: expecting callable or instance of GraphQL\Type\Definition\ObjectType at "types:0", but got "' . Utils::getVariableType($type) . '"',
$e->getMessage()
);
}

View File

@ -189,6 +189,7 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase
$this->schema,
'query test($color: Color!) { colorEnum(fromEnum: $color) }',
null,
null,
['color' => 'BLUE']
)
);
@ -205,6 +206,7 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase
$this->schema,
'mutation x($color: Color!) { favoriteEnum(color: $color) }',
null,
null,
['color' => 'GREEN']
)
);
@ -224,6 +226,7 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase
$this->schema,
'subscription x($color: Color!) { subscribeToEnum(color: $color) }',
null,
null,
['color' => 'GREEN']
)
);
@ -295,7 +298,7 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase
private function expectFailure($query, $vars, $err)
{
$result = GraphQL::executeAndReturnResult($this->schema, $query, null, $vars);
$result = GraphQL::executeAndReturnResult($this->schema, $query, null, null, $vars);
$this->assertEquals(1, count($result->errors));
$this->assertEquals(

View File

@ -1099,9 +1099,6 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
$actual = GraphQL::execute($emptySchema, $request);
// print_r($actual);
// exit;
$this->assertEquals($expected, $actual);
}

View File

@ -135,7 +135,7 @@ class ResolveInfoTest extends \PHPUnit_Framework_TestCase
'fields' => [
'article' => [
'type' => $article,
'resolve' => function($value, $args, ResolveInfo $info) use (&$hasCalled, &$actualDefaultSelection, &$actualDeepSelection) {
'resolve' => function($value, $args, $context, ResolveInfo $info) use (&$hasCalled, &$actualDefaultSelection, &$actualDeepSelection) {
$hasCalled = true;
$actualDefaultSelection = $info->getFieldSelection();
$actualDeepSelection = $info->getFieldSelection(5);
@ -145,7 +145,7 @@ class ResolveInfoTest extends \PHPUnit_Framework_TestCase
]
]);
$schema = new Schema($blogQuery);
$schema = new Schema(['query' => $blogQuery]);
$result = GraphQL::execute($schema, $doc);
$this->assertTrue($hasCalled);

View File

@ -2,6 +2,8 @@
namespace GraphQL\Tests\Type;
use GraphQL\Schema;
use GraphQL\Type\Definition\CustomScalarType;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ListOfType;
@ -11,63 +13,402 @@ use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Introspection;
use GraphQL\Type\SchemaValidator;
use GraphQL\Utils;
class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
{
public $someInputType;
private $someInputObjectType;
private $someScalarType;
private $someObjectType;
private $objectWithIsTypeOf;
private $someEnumType;
private $someUnionType;
private $someInterfaceType;
private $outputTypes;
private $noOutputTypes;
private $inputTypes;
private $noInputTypes;
public function setUp()
{
$this->someInputType = new InputObjectType([
'name' => 'SomeInputType',
$this->someScalarType = new CustomScalarType([
'name' => 'SomeScalar',
'serialize' => function() {},
'parseValue' => function() {},
'parseLiteral' => function() {}
]);
$this->someObjectType = new ObjectType([
'name' => 'SomeObject',
'fields' => ['f' => ['type' => Type::string()]]
]);
$this->objectWithIsTypeOf = new ObjectType([
'name' => 'ObjectWithIsTypeOf',
'isTypeOf' => function() {return true;},
'fields' => ['f' => ['type' => Type::string()]]
]);
$this->someUnionType = new UnionType([
'name' => 'SomeUnion',
'resolveType' => function() {return null;},
'types' => [$this->someObjectType]
]);
$this->someInterfaceType = new InterfaceType([
'name' => 'SomeInterface',
'resolveType' => function() {return null;},
'fields' => ['f' => ['type' => Type::string()]]
]);
$this->someEnumType = new EnumType([
'name' => 'SomeEnum',
'resolveType' => function() {return null;},
'fields' => ['f' => ['type' => Type::string()]]
]);
$this->someInputObjectType = new InputObjectType([
'name' => 'SomeInputObject',
'fields' => [
'val' => [ 'type' => Type::float(), 'defaultValue' => 42 ]
'val' => ['type' => Type::float(), 'defaultValue' => 42]
]
]);
$this->outputTypes = $this->withModifiers([
Type::string(),
$this->someScalarType,
$this->someEnumType,
$this->someObjectType,
$this->someUnionType,
$this->someInterfaceType
]);
$this->noOutputTypes = $this->withModifiers([
$this->someInputObjectType
]);
$this->noOutputTypes[] = 'SomeString';
$this->inputTypes = $this->withModifiers([
Type::string(),
$this->someScalarType,
$this->someEnumType,
$this->someInputObjectType
]);
$this->noInputTypes = $this->withModifiers([
$this->someObjectType,
$this->someUnionType,
$this->someInterfaceType
]);
$this->noInputTypes[] = 'SomeString';
}
private function withModifiers($types)
{
return array_merge(
Utils::map($types, function($type) {return Type::listOf($type);}),
Utils::map($types, function($type) {return Type::nonNull($type);}),
Utils::map($types, function($type) {return Type::nonNull(Type::listOf($type));})
);
}
private function schemaWithFieldType($type)
{
return [
'query' => new ObjectType([
'name' => 'Query',
'fields' => ['f' => ['type' => $type]]
]),
'types' => [$type],
];
}
private function expectPasses($schemaConfig)
{
$schema = new Schema(['validate' => true] + $schemaConfig);
$errors = SchemaValidator::validate($schema);
$this->assertEquals([], $errors);
}
private function expectFails($schemaConfig, $error)
{
try {
$schema = new Schema($schemaConfig);
$errors = SchemaValidator::validate($schema);
if ($errors) {
throw $errors[0];
}
$this->fail('Expected exception not thrown');
} catch (\Exception $e) {
$this->assertEquals($e->getMessage(), $error);
}
}
// Type System: A Schema must have Object root types
/**
* @it accepts a Schema whose query type is an object type
*/
public function testAcceptsSchemaWithQueryTypeOfObjectType()
{
$this->expectPasses([
'query' => $this->someObjectType
]);
}
/**
* @it accepts a Schema whose query and mutation types are object types
*/
public function testAcceptsSchemaWithQueryAndMutationTypesOfObjectType()
{
$MutationType = new ObjectType([
'name' => 'Mutation',
'fields' => ['edit' => ['type' => Type::string()]]
]);
$this->expectPasses([
'query' => $this->someObjectType,
'mutation' => $MutationType
]);
}
/**
* @it accepts a Schema whose query and subscription types are object types
*/
public function testAcceptsSchemaWhoseQueryAndSubscriptionTypesAreObjectTypes()
{
$SubscriptionType = new ObjectType([
'name' => 'Subscription',
'fields' => ['subscribe' => ['type' => Type::string()]]
]);
$this->expectPasses([
'query' => $this->someObjectType,
'subscription' => $SubscriptionType
]);
}
/**
* @it rejects a Schema without a query type
*/
public function testRejectsSchemaWithoutQueryType()
{
$this->expectFails([], 'Schema query must be Object Type but got: NULL');
}
/**
* @it rejects a Schema whose query type is an input type
*/
public function testRejectsSchemaWhoseQueryTypeIsAnInputType()
{
$this->expectFails(
['query' => $this->someInputObjectType],
'Schema query must be Object Type but got: SomeInputObject'
);
}
/**
* @it rejects a Schema whose mutation type is an input type
*/
public function testRejectsSchemaWhoseMutationTypeIsInputType()
{
$this->expectFails(
['query' => $this->someObjectType, 'mutation' => $this->someInputObjectType],
'Schema mutation must be Object Type if provided but got: SomeInputObject'
);
}
/**
* @it rejects a Schema whose subscription type is an input type
*/
public function testRejectsSchemaWhoseSubscriptionTypeIsInputType()
{
$this->expectFails(
[
'query' => $this->someObjectType,
'subscription' => $this->someInputObjectType
],
'Schema subscription must be Object Type if provided but got: SomeInputObject'
);
}
/**
* @it rejects a Schema whose directives are incorrectly typed
*/
public function testRejectsSchemaWhoseDirectivesAreIncorrectlyTyped()
{
$this->expectFails(
[
'query' => $this->someObjectType,
'directives' => [ 'somedirective' ]
],
'Schema directives must be Directive[] if provided but got array'
);
}
// Type System: A Schema must contain uniquely named types
/**
* @it rejects a Schema which redefines a built-in type
*/
public function testRejectsSchemaWhichRedefinesBuiltInType()
{
$FakeString = new CustomScalarType([
'name' => 'String',
'serialize' => function() {return null;},
]);
$QueryType = new ObjectType([
'name' => 'Query',
'fields' => [
'normal' => [ 'type' => Type::string() ],
'fake' => [ 'type' => $FakeString ],
]
]);
$this->expectFails(
[ 'query' => $QueryType ],
'Schema must contain unique named types but contains multiple types named "String".'
);
}
/**
* @it rejects a Schema which defines an object type twice
*/
public function testRejectsSchemaWhichDefinesObjectTypeTwice()
{
$A = new ObjectType([
'name' => 'SameName',
'fields' => ['f' => ['type' => Type::string()]],
]);
$B = new ObjectType([
'name' => 'SameName',
'fields' => ['f' => ['type' => Type::string()]],
]);
$QueryType = new ObjectType([
'name' => 'Query',
'fields' => [
'a' => ['type' => $A],
'b' => ['type' => $B]
]
]);
$this->expectFails(
['query' => $QueryType],
'Schema must contain unique named types but contains multiple types named "SameName".'
);
}
/**
* @it rejects a Schema which have same named objects implementing an interface
*/
public function testRejectsSchemaWhichHaveSameNamedObjectsImplementingInterface()
{
$AnotherInterface = new InterfaceType([
'name' => 'AnotherInterface',
'resolveType' => function () {
return null;
},
'fields' => ['f' => ['type' => Type::string()]],
]);
$FirstBadObject = new ObjectType([
'name' => 'BadObject',
'interfaces' => [$AnotherInterface],
'fields' => ['f' => ['type' => Type::string()]],
]);
$SecondBadObject = new ObjectType([
'name' => 'BadObject',
'interfaces' => [$AnotherInterface],
'fields' => ['f' => ['type' => Type::string()]],
]);
$QueryType = new ObjectType([
'name' => 'Query',
'fields' => [
'iface' => ['type' => $AnotherInterface],
]
]);
$this->expectFails(
[
'query' => $QueryType,
'types' => [$FirstBadObject, $SecondBadObject]
],
'Schema must contain unique named types but contains multiple types named "BadObject".'
);
}
// Type System: Objects must have fields
/**
* @it accepts an Object type with fields object
*/
public function testAcceptsAnObjectTypeWithFieldsObject()
{
$schemaConfig = $this->schemaWithFieldType(new ObjectType([
'name' => 'SomeObject',
'fields' => [
'f' => [ 'type' => Type::string() ]
]
]));
$this->expectPasses($schemaConfig);
}
/**
* @it accepts an Object type with a field function
*/
public function testAcceptsAnObjectTypeWithFieldFunction()
{
$schemaConfig = $this->schemaWithFieldType(new ObjectType([
'name' => 'SomeObject',
'fields' => function() {
return [
'f' => ['type' => Type::string()]
];
}
]));
$this->expectPasses($schemaConfig);
}
// Type System Config
public function testPassesOnTheIntrospectionSchema()
{
$schema = new Schema(Introspection::_schema());
$errors = SchemaValidator::validate($schema);
$this->assertEmpty($errors);
}
// Rule: NoInputTypesAsOutputFields
public function testRejectsSchemaWhoseQueryOrMutationTypeIsAnInputType()
{
$schema = new Schema($this->someInputType);
$validationResult = SchemaValidator::validate($schema, [SchemaValidator::noInputTypesAsOutputFieldsRule()]);
$this->checkValidationResult($validationResult, 'query');
$schema = new Schema(null, $this->someInputType);
$validationResult = SchemaValidator::validate($schema, [SchemaValidator::noInputTypesAsOutputFieldsRule()]);
$this->checkValidationResult($validationResult, 'mutation');
$this->expectPasses(['query' => Introspection::_schema()]);
}
public function testRejectsASchemaThatUsesAnInputTypeAsAField()
{
$kinds = [
'GraphQL\Type\Definition\ObjectType',
'GraphQL\Type\Definition\InterfaceType',
];
foreach ($kinds as $kind) {
$someOutputType = new $kind([
'name' => 'SomeOutputType',
'fields' => [
'sneaky' => ['type' => function() {return $this->someInputType;}]
'sneaky' => ['type' => function() {return $this->someInputObjectType;}]
]
]);
$schema = new Schema($someOutputType);
$schema = new Schema(['query' => $someOutputType]);
$validationResult = SchemaValidator::validate($schema, [SchemaValidator::noInputTypesAsOutputFieldsRule()]);
$this->assertSame(1, count($validationResult));
$this->assertSame(
'Field SomeOutputType.sneaky is of type SomeInputType, which is an ' .
'Field SomeOutputType.sneaky is of type SomeInputObject, which is an ' .
'input type, but field types must be output types!',
$validationResult[0]->message
);
@ -85,13 +426,13 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
'name' => 'SomeOutputType',
'fields' => [
'fieldWithArg' => [
'args' => ['someArg' => ['type' => $this->someInputType]],
'args' => ['someArg' => ['type' => $this->someInputObjectType]],
'type' => Type::float()
]
]
]);
$schema = new Schema($someOutputType);
$schema = new Schema(['query' => $someOutputType]);
$errors = SchemaValidator::validate($schema, [$rule]);
$this->assertEmpty($errors);
}
@ -101,7 +442,7 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
$this->assertNotEmpty($validationErrors, "Should not validate");
$this->assertEquals(1, count($validationErrors));
$this->assertEquals(
"Schema $operationType type SomeInputType must be an object type!",
"Schema $operationType must be Object Type but got: SomeInputObject.",
$validationErrors[0]->message
);
}
@ -153,7 +494,7 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
{
// rejects a schema with a list of objects as an input field arg
$listObjects = new ListOfType(new ObjectType([
'name' => 'SomeInputType',
'name' => 'SomeInputObject',
'fields' => ['f' => ['type' => Type::float()]]
]));
$this->assertRejectingFieldArgOfType($listObjects);
@ -174,7 +515,7 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
{
// accepts a schema with a list of input type as an input field arg
$this->assertAcceptingFieldArgOfType(new ListOfType(new InputObjectType([
'name' => 'SomeInputType'
'name' => 'SomeInputObject'
])));
}
@ -182,7 +523,7 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
{
// accepts a schema with a nonnull input type as an input field arg
$this->assertAcceptingFieldArgOfType(new NonNull(new InputObjectType([
'name' => 'SomeInputType'
'name' => 'SomeInputObject'
])));
}
@ -219,7 +560,7 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
]
]);
return new Schema($queryType);
return new Schema(['query' => $queryType]);
}
private function expectRejectionBecauseFieldIsNotInputType($errors, $fieldTypeName)
@ -233,83 +574,8 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
}
// Rule: InterfacePossibleTypesMustImplementTheInterface
public function testAcceptsInterfaceWithSubtypeDeclaredUsingOurInfra()
{
// accepts an interface with a subtype declared using our infra
$this->assertAcceptingAnInterfaceWithANormalSubtype(SchemaValidator::interfacePossibleTypesMustImplementTheInterfaceRule());
}
public function testRejectsWhenAPossibleTypeDoesNotImplementTheInterface()
{
// TODO: Validation for interfaces / implementors
}
private function assertAcceptingAnInterfaceWithANormalSubtype($rule)
{
$interfaceType = new InterfaceType([
'name' => 'InterfaceType',
'fields' => []
]);
$subType = new ObjectType([
'name' => 'SubType',
'fields' => [],
'interfaces' => [$interfaceType]
]);
$schema = new Schema($interfaceType, $subType);
$errors = SchemaValidator::validate($schema, [$rule]);
$this->assertEmpty($errors);
}
// Rule: TypesInterfacesMustShowThemAsPossible
public function testAcceptsInterfaceWithASubtypeDeclaredUsingOurInfra()
{
// accepts an interface with a subtype declared using our infra
$this->assertAcceptingAnInterfaceWithANormalSubtype(SchemaValidator::typesInterfacesMustShowThemAsPossibleRule());
}
public function testRejectsWhenAnImplementationIsNotAPossibleType()
{
// rejects when an implementation is not a possible type
$interfaceType = new InterfaceType([
'name' => 'InterfaceType',
'fields' => []
]);
$subType = new ObjectType([
'name' => 'SubType',
'fields' => [],
'interfaces' => []
]);
$tmp = new \ReflectionObject($subType);
$prop = $tmp->getProperty('_interfaces');
$prop->setAccessible(true);
$prop->setValue($subType, [$interfaceType]);
// Sanity check the test.
$this->assertEquals([$interfaceType], $subType->getInterfaces());
$this->assertSame(false, $interfaceType->isPossibleType($subType));
// Need to make sure SubType is in the schema! We rely on
// possibleTypes to be able to see it unless it's explicitly used.
$schema = new Schema($interfaceType, $subType);
// Another sanity check.
$this->assertSame($subType, $schema->getType('SubType'));
$errors = SchemaValidator::validate($schema, [SchemaValidator::typesInterfacesMustShowThemAsPossibleRule()]);
$this->assertSame(1, count($errors));
$this->assertSame(
'SubType implements interface InterfaceType, but InterfaceType does ' .
'not list it as possible!',
$errors[0]->message
);
}
}