Updated to latest version of graphql-js

This commit is contained in:
vladar 2015-08-17 20:01:55 +06:00
parent 022c962942
commit 841d6ab851
88 changed files with 3227 additions and 1669 deletions

View File

@ -1,6 +1,7 @@
<?php <?php
namespace GraphQL\Executor; namespace GraphQL\Executor;
use GraphQL\Error;
use GraphQL\Language\AST\OperationDefinition; use GraphQL\Language\AST\OperationDefinition;
use GraphQL\Schema; use GraphQL\Schema;
@ -25,7 +26,7 @@ class ExecutionContext
/** /**
* @var * @var
*/ */
public $root; public $rootValue;
/** /**
* @var OperationDefinition * @var OperationDefinition
@ -35,7 +36,7 @@ class ExecutionContext
/** /**
* @var array * @var array
*/ */
public $variables; public $variableValues;
/** /**
* @var array * @var array
@ -46,13 +47,13 @@ class ExecutionContext
{ {
$this->schema = $schema; $this->schema = $schema;
$this->fragments = $fragments; $this->fragments = $fragments;
$this->root = $root; $this->rootValue = $root;
$this->operation = $operation; $this->operation = $operation;
$this->variables = $variables; $this->variableValues = $variables;
$this->errors = $errors ?: []; $this->errors = $errors ?: [];
} }
public function addError($error) public function addError(Error $error)
{ {
$this->errors[] = $error; $this->errors[] = $error;
return $this; return $this;

View File

@ -0,0 +1,38 @@
<?php
namespace GraphQL\Executor;
use GraphQL\Error;
class ExecutionResult
{
/**
* @var array
*/
public $data;
/**
* @var Error[]
*/
public $errors;
/**
* @param array $data
* @param array $errors
*/
public function __construct(array $data = null, array $errors = [])
{
$this->data = $data;
$this->errors = $errors;
}
public function toArray()
{
$result = ['data' => $this->data];
if (!empty($this->errors)) {
$result['errors'] = array_map(['GraphQL\Error', 'formatError'], $this->errors);
}
return $result;
}
}

View File

@ -9,6 +9,7 @@ use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\OperationDefinition; use GraphQL\Language\AST\OperationDefinition;
use GraphQL\Language\AST\SelectionSet; use GraphQL\Language\AST\SelectionSet;
use GraphQL\Schema; use GraphQL\Schema;
use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\FieldDefinition;
@ -16,6 +17,7 @@ use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Definition\UnionType;
@ -45,40 +47,70 @@ class Executor
{ {
private static $UNDEFINED; private static $UNDEFINED;
public static function execute(Schema $schema, $root, Document $ast, $operationName = null, array $args = null) private static $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) { if (!self::$UNDEFINED) {
self::$UNDEFINED = new \stdClass(); 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 { try {
$errors = new \ArrayObject(); $data = self::executeOperation($exeContext, $exeContext->operation, $rootValue);
$exeContext = self::buildExecutionContext($schema, $root, $ast, $operationName, $args, $errors); } catch (Error $e) {
$data = self::executeOperation($exeContext, $root, $exeContext->operation); $exeContext->addError($e);
} catch (\Exception $e) { $data = null;
$errors[] = $e;
} }
$result = [ return new ExecutionResult($data, $exeContext->errors);
'data' => isset($data) ? $data : null
];
if (count($errors) > 0) {
$result['errors'] = array_map(['GraphQL\Error', 'formatError'], $errors->getArrayCopy());
}
return $result;
} }
/** /**
* Constructs a ExecutionContext object from the arguments passed to * Constructs a ExecutionContext object from the arguments passed to
* execute, which we will pass throughout the other execution methods. * execute, which we will pass throughout the other execution methods.
*/ */
private static function buildExecutionContext(Schema $schema, $root, Document $ast, $operationName = null, array $args = null, &$errors) private static function buildExecutionContext(Schema $schema, Document $documentAst, $rootValue, $rawVariableValues, $operationName = null)
{ {
$errors = [];
$operations = []; $operations = [];
$fragments = []; $fragments = [];
foreach ($ast->definitions as $statement) { foreach ($documentAst->definitions as $statement) {
switch ($statement->kind) { switch ($statement->kind) {
case Node::OPERATION_DEFINITION: case Node::OPERATION_DEFINITION:
$operations[$statement->name ? $statement->name->value : ''] = $statement; $operations[$statement->name ? $statement->name->value : ''] = $statement;
@ -91,32 +123,34 @@ class Executor
if (!$operationName && count($operations) !== 1) { if (!$operationName && count($operations) !== 1) {
throw new Error( throw new Error(
'Must provide operation name if query contains multiple operations' 'Must provide operation name if query contains multiple operations.'
); );
} }
$opName = $operationName ?: key($operations); $opName = $operationName ?: key($operations);
if (!isset($operations[$opName])) { if (empty($operations[$opName])) {
throw new Error('Unknown operation name: ' . $opName); throw new Error('Unknown operation named ' . $opName);
} }
$operation = $operations[$opName]; $operation = $operations[$opName];
$variables = Values::getVariableValues($schema, $operation->variableDefinitions ?: array(), $args ?: []); $variableValues = Values::getVariableValues($schema, $operation->variableDefinitions ?: [], $rawVariableValues ?: []);
$exeContext = new ExecutionContext($schema, $fragments, $root, $operation, $variables, $errors); $exeContext = new ExecutionContext($schema, $fragments, $rootValue, $operation, $variableValues, $errors);
return $exeContext; return $exeContext;
} }
/** /**
* Implements the "Evaluating operations" section of the spec. * Implements the "Evaluating operations" section of the spec.
*/ */
private static function executeOperation(ExecutionContext $exeContext, $root, OperationDefinition $operation) private static function executeOperation(ExecutionContext $exeContext, OperationDefinition $operation, $rootValue)
{ {
$type = self::getOperationRootType($exeContext->schema, $operation); $type = self::getOperationRootType($exeContext->schema, $operation);
$fields = self::collectFields($exeContext, $type, $operation->selectionSet, new \ArrayObject(), new \ArrayObject()); $fields = self::collectFields($exeContext, $type, $operation->selectionSet, new \ArrayObject(), new \ArrayObject());
if ($operation->operation === 'mutation') { if ($operation->operation === 'mutation') {
return self::executeFieldsSerially($exeContext, $type, $root, $fields->getArrayCopy()); return self::executeFieldsSerially($exeContext, $type, $rootValue, $fields->getArrayCopy());
} }
return self::executeFields($exeContext, $type, $root, $fields);
return self::executeFields($exeContext, $type, $rootValue, $fields);
} }
@ -154,11 +188,11 @@ class Executor
* Implements the "Evaluating selection sets" section of the spec * Implements the "Evaluating selection sets" section of the spec
* for "write" mode. * for "write" mode.
*/ */
private static function executeFieldsSerially(ExecutionContext $exeContext, ObjectType $parentType, $source, $fields) private static function executeFieldsSerially(ExecutionContext $exeContext, ObjectType $parentType, $sourceValue, $fields)
{ {
$results = []; $results = [];
foreach ($fields as $responseName => $fieldASTs) { foreach ($fields as $responseName => $fieldASTs) {
$result = self::resolveField($exeContext, $parentType, $source, $fieldASTs); $result = self::resolveField($exeContext, $parentType, $sourceValue, $fieldASTs);
if ($result !== self::$UNDEFINED) { if ($result !== self::$UNDEFINED) {
// Undefined means that field is not defined in schema // Undefined means that field is not defined in schema
@ -250,18 +284,36 @@ class Executor
} }
/** /**
* Determines if a field should be included based on @if and @unless directives. * 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) private static function shouldIncludeNode(ExecutionContext $exeContext, $directives)
{ {
$ifDirective = Values::getDirectiveValue(Directive::ifDirective(), $directives, $exeContext->variables); $skipDirective = Directive::skipDirective();
if ($ifDirective !== null) { $includeDirective = Directive::includeDirective();
return $ifDirective;
/** @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']);
} }
$unlessDirective = Values::getDirectiveValue(Directive::unlessDirective(), $directives, $exeContext->variables); /** @var \GraphQL\Language\AST\Directive $includeAST */
if ($unlessDirective !== null) { $includeAST = $directives
return !$unlessDirective; ? 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; return true;
@ -293,97 +345,106 @@ class Executor
} }
/** /**
* A wrapper function for resolving the field, that catches the error * Resolves the field on the given source object. In particular, this
* and adds it to the context's global if the error is not rethrowable. * figures out the value that the field returns by calling its resolve function,
* then calls completeValue to complete promises, serialize scalars, or execute
* the sub-selection-set for objects.
*/ */
private static function resolveField(ExecutionContext $exeContext, ObjectType $parentType, $source, $fieldASTs) private static function resolveField(ExecutionContext $exeContext, ObjectType $parentType, $source, $fieldASTs)
{ {
$fieldDef = self::getFieldDef($exeContext->schema, $parentType, $fieldASTs[0]); $fieldAST = $fieldASTs[0];
$fieldName = $fieldAST->name->value;
$fieldDef = self::getFieldDef($exeContext->schema, $parentType, $fieldName);
if (!$fieldDef) { if (!$fieldDef) {
return self::$UNDEFINED; return self::$UNDEFINED;
} }
$returnType = $fieldDef->getType();
if (isset($fieldDef->resolve)) {
$resolveFn = $fieldDef->resolve;
} else if (isset(self::$defaultResolveFn)) {
$resolveFn = self::$defaultResolveFn;
} else {
$resolveFn = [__CLASS__, 'defaultResolveFn'];
}
// Build hash of arguments from the field.arguments AST, using the
// variables scope to fulfill any variable references.
// TODO: find a way to memoize, in case this field is within a List type.
$args = Values::getArgumentValues(
$fieldDef->args,
$fieldAST->arguments,
$exeContext->variableValues
);
// The resolve function's optional third argument is a collection of
// information about the current execution state.
$info = new ResolveInfo([
'fieldName' => $fieldName,
'fieldASTs' => $fieldASTs,
'returnType' => $returnType,
'parentType' => $parentType,
'schema' => $exeContext->schema,
'fragments' => $exeContext->fragments,
'rootValue' => $exeContext->rootValue,
'operation' => $exeContext->operation,
'variableValues' => $exeContext->variableValues,
]);
// If an error occurs while calling the field `resolve` function, ensure that
// it is wrapped as a GraphQLError with locations. Log this error and return
// null if allowed, otherwise throw the error so the parent field can handle
// it.
try {
$result = call_user_func($resolveFn, $source, $args, $info);
} catch (\Exception $error) {
$reportedError = Error::createLocatedError($error, $fieldASTs);
if ($returnType instanceof NonNull) {
throw $reportedError;
}
$exeContext->addError($reportedError);
return null;
}
return self::completeValueCatchingError(
$exeContext,
$returnType,
$fieldASTs,
$info,
$result
);
}
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 // If the field type is non-nullable, then it is resolved without any
// protection from errors. // protection from errors.
if ($fieldDef->getType() instanceof NonNull) { if ($returnType instanceof NonNull) {
return self::resolveFieldOrError( return self::completeValue($exeContext, $returnType, $fieldASTs, $info, $result);
$exeContext,
$parentType,
$source,
$fieldASTs,
$fieldDef
);
} }
// Otherwise, error protection is applied, logging the error and resolving // Otherwise, error protection is applied, logging the error and resolving
// a null value for this field if one is encountered. // a null value for this field if one is encountered.
try { try {
$result = self::resolveFieldOrError( return self::completeValue($exeContext, $returnType, $fieldASTs, $info, $result);
$exeContext, } catch (Error $err) {
$parentType, $exeContext->addError($err);
$source,
$fieldASTs,
$fieldDef
);
return $result;
} catch (\Exception $error) {
$exeContext->addError($error);
return null; return null;
} }
} }
/**
* Resolves the field on the given source object. In particular, this
* figures out the object that the field returns using the resolve function,
* then calls completeField to coerce scalars or execute the sub
* selection set for objects.
*/
private static function resolveFieldOrError(
ExecutionContext $exeContext,
ObjectType $parentType,
$source,
/*array<Field>*/ $fieldASTs,
FieldDefinition $fieldDef
)
{
$fieldAST = $fieldASTs[0];
$fieldType = $fieldDef->getType();
$resolveFn = $fieldDef->resolve ?: [__CLASS__, 'defaultResolveFn'];
// Build a JS object 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 Array type.
$args = Values::getArgumentValues(
$fieldDef->args,
$fieldAST->arguments,
$exeContext->variables
);
try {
$result = call_user_func($resolveFn,
$source,
$args,
$exeContext->root,
// TODO: provide all fieldASTs, not just the first field
$fieldAST,
$fieldType,
$parentType,
$exeContext->schema
);
} catch (\Exception $error) {
throw Error::createLocatedError($error, [$fieldAST]);
}
return self::completeField(
$exeContext,
$fieldType,
$fieldASTs,
$result
);
}
/** /**
* Implements the instructions for completeValue as defined in the * Implements the instructions for completeValue as defined in the
* "Field entries" section of the spec. * "Field entries" section of the spec.
@ -396,20 +457,22 @@ class Executor
* for the inner type on each item in the list. * 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 * If the field type is a Scalar or Enum, ensures the completed value is a legal
* value of the type by calling the `coerce` method of GraphQL type definition. * 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 * Otherwise, the field type expects a sub-selection set, and will complete the
* value by evaluating all sub-selections. * value by evaluating all sub-selections.
*/ */
private static function completeField(ExecutionContext $exeContext, Type $fieldType,/* Array<Field> */ $fieldASTs, &$result) 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 field type is NonNull, complete for inner type, and throw field error
// if result is null. // if result is null.
if ($fieldType instanceof NonNull) { if ($returnType instanceof NonNull) {
$completed = self::completeField( $completed = self::completeValue(
$exeContext, $exeContext,
$fieldType->getWrappedType(), $returnType->getWrappedType(),
$fieldASTs, $fieldASTs,
$info,
$result $result
); );
if ($completed === null) { if ($completed === null) {
@ -427,8 +490,8 @@ class Executor
} }
// If field type is List, complete each item in the list with the inner type // If field type is List, complete each item in the list with the inner type
if ($fieldType instanceof ListOfType) { if ($returnType instanceof ListOfType) {
$itemType = $fieldType->getWrappedType(); $itemType = $returnType->getWrappedType();
Utils::invariant( Utils::invariant(
is_array($result) || $result instanceof \Traversable, is_array($result) || $result instanceof \Traversable,
'User Error: expected iterable, but did not find one.' 'User Error: expected iterable, but did not find one.'
@ -436,32 +499,48 @@ class Executor
$tmp = []; $tmp = [];
foreach ($result as $item) { foreach ($result as $item) {
$tmp[] = self::completeField($exeContext, $itemType, $fieldASTs, $item); $tmp[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
} }
return $tmp; return $tmp;
} }
// If field type is Scalar or Enum, coerce to a valid value, returning null // If field type is Scalar or Enum, serialize to a valid value, returning
// if coercion is not possible. // null if serialization is not possible.
if ($fieldType instanceof ScalarType || if ($returnType instanceof ScalarType ||
$fieldType instanceof EnumType $returnType instanceof EnumType) {
) { Utils::invariant(method_exists($returnType, 'serialize'), 'Missing serialize method on type');
Utils::invariant(method_exists($fieldType, 'coerce'), 'Missing coerce method on type'); return $returnType->serialize($result);
return $fieldType->coerce($result);
} }
// Field type must be Object, Interface or Union and expect sub-selections. // Field type must be Object, Interface or Union and expect sub-selections.
if ($returnType instanceof ObjectType) {
$objectType = $returnType;
} else if ($returnType instanceof AbstractType) {
$objectType = $returnType->getObjectType($result, $info);
$objectType = if ($objectType && !$returnType->isPossibleType($objectType)) {
$fieldType instanceof ObjectType ? $fieldType : throw new Error(
($fieldType instanceof InterfaceType || "Runtime Object type \"$objectType\" is not a possible type for \"$returnType\"."
$fieldType instanceof UnionType ? $fieldType->resolveType($result) : );
null); }
} else {
$objectType = null;
}
if (!$objectType) { if (!$objectType) {
return null; 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. // Collect sub-fields to execute to complete this value.
$subFieldASTs = new \ArrayObject(); $subFieldASTs = new \ArrayObject();
$visitedFragmentNames = new \ArrayObject(); $visitedFragmentNames = new \ArrayObject();
@ -488,17 +567,18 @@ class Executor
* and returns it as the result, or if it's a function, returns the result * and returns it as the result, or if it's a function, returns the result
* of calling that function. * of calling that function.
*/ */
public static function defaultResolveFn($source, $args, $root, $fieldAST) public static function defaultResolveFn($source, $args, ResolveInfo $info)
{ {
$fieldName = $info->fieldName;
$property = null; $property = null;
if (is_array($source) || $source instanceof \ArrayAccess) { if (is_array($source) || $source instanceof \ArrayAccess) {
if (isset($source[$fieldAST->name->value])) { if (isset($source[$fieldName])) {
$property = $source[$fieldAST->name->value]; $property = $source[$fieldName];
} }
} else if (is_object($source)) { } else if (is_object($source)) {
if (property_exists($source, $fieldAST->name->value)) { if (property_exists($source, $fieldName)) {
$e = func_get_args(); $property = $source->{$fieldName};
$property = $source->{$fieldAST->name->value};
} }
} }
@ -516,25 +596,21 @@ class Executor
* *
* @return FieldDefinition * @return FieldDefinition
*/ */
private static function getFieldDef(Schema $schema, ObjectType $parentType, Field $fieldAST) private static function getFieldDef(Schema $schema, ObjectType $parentType, $fieldName)
{ {
$name = $fieldAST->name->value;
$schemaMetaFieldDef = Introspection::schemaMetaFieldDef(); $schemaMetaFieldDef = Introspection::schemaMetaFieldDef();
$typeMetaFieldDef = Introspection::typeMetaFieldDef(); $typeMetaFieldDef = Introspection::typeMetaFieldDef();
$typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef(); $typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef();
if ($name === $schemaMetaFieldDef->name && if ($fieldName === $schemaMetaFieldDef->name && $schema->getQueryType() === $parentType) {
$schema->getQueryType() === $parentType
) {
return $schemaMetaFieldDef; return $schemaMetaFieldDef;
} else if ($name === $typeMetaFieldDef->name && } else if ($fieldName === $typeMetaFieldDef->name && $schema->getQueryType() === $parentType) {
$schema->getQueryType() === $parentType
) {
return $typeMetaFieldDef; return $typeMetaFieldDef;
} else if ($name === $typeNameMetaFieldDef->name) { } else if ($fieldName === $typeNameMetaFieldDef->name) {
return $typeNameMetaFieldDef; return $typeNameMetaFieldDef;
} }
$tmp = $parentType->getFields(); $tmp = $parentType->getFields();
return isset($tmp[$name]) ? $tmp[$name] : null; return isset($tmp[$fieldName]) ? $tmp[$fieldName] : null;
} }
} }

View File

@ -3,16 +3,25 @@ namespace GraphQL\Executor;
use GraphQL\Error; use GraphQL\Error;
use GraphQL\Language\AST\Argument;
use GraphQL\Language\AST\ListType;
use GraphQL\Language\AST\ListValue;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\ObjectValue;
use GraphQL\Language\AST\Value;
use GraphQL\Language\AST\Variable;
use GraphQL\Language\AST\VariableDefinition; use GraphQL\Language\AST\VariableDefinition;
use GraphQL\Language\Printer; use GraphQL\Language\Printer;
use GraphQL\Schema; use GraphQL\Schema;
use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Utils; use GraphQL\Utils;
@ -23,8 +32,14 @@ class Values
* Prepares an object map of variables of the correct type based on the provided * Prepares an object map of variables of the correct type based on the provided
* variable definitions and arbitrary input. If the input cannot be coerced * variable definitions and arbitrary input. If the input cannot be coerced
* to match the variable definitions, a Error will be thrown. * to match the variable definitions, a Error will be thrown.
*
* @param Schema $schema
* @param VariableDefinition[] $definitionASTs
* @param array $inputs
* @return array
* @throws Error
*/ */
public static function getVariableValues(Schema $schema, /* Array<VariableDefinition> */ $definitionASTs, array $inputs) public static function getVariableValues(Schema $schema, $definitionASTs, array $inputs)
{ {
$values = []; $values = [];
foreach ($definitionASTs as $defAST) { foreach ($definitionASTs as $defAST) {
@ -37,11 +52,16 @@ class Values
/** /**
* Prepares an object map of argument values given a list of argument * Prepares an object map of argument values given a list of argument
* definitions and list of argument AST nodes. * definitions and list of argument AST nodes.
*
* @param FieldArgument[] $argDefs
* @param Argument[] $argASTs
* @param $variableValues
* @return array
*/ */
public static function getArgumentValues(/* Array<GraphQLFieldArgument>*/ $argDefs, /*Array<Argument>*/ $argASTs, $variables) public static function getArgumentValues($argDefs, $argASTs, $variableValues)
{ {
if (!$argDefs || count($argDefs) === 0) { if (!$argDefs || !$argASTs) {
return null; return [];
} }
$argASTMap = $argASTs ? Utils::keyMap($argASTs, function ($arg) { $argASTMap = $argASTs ? Utils::keyMap($argASTs, function ($arg) {
return $arg->name->value; return $arg->name->value;
@ -50,28 +70,71 @@ class Values
foreach ($argDefs as $argDef) { foreach ($argDefs as $argDef) {
$name = $argDef->name; $name = $argDef->name;
$valueAST = isset($argASTMap[$name]) ? $argASTMap[$name]->value : null; $valueAST = isset($argASTMap[$name]) ? $argASTMap[$name]->value : null;
$result[$name] = self::coerceValueAST($argDef->getType(), $valueAST, $variables); $value = self::valueFromAST($valueAST, $argDef->getType(), $variableValues);
if (null === $value) {
$value = $argDef->defaultValue;
}
if (null !== $value) {
$result[$name] = $value;
}
} }
return $result; return $result;
} }
public static function getDirectiveValue(Directive $directiveDef, /* Array<Directive> */ $directives, $variables) public static function valueFromAST($valueAST, InputType $type, $variables = null)
{ {
$directiveAST = null; if ($type instanceof NonNull) {
if ($directives) { return self::valueFromAST($valueAST, $type->getWrappedType(), $variables);
foreach ($directives as $directive) {
if ($directive->name->value === $directiveDef->name) {
$directiveAST = $directive;
break;
} }
}
} if (!$valueAST) {
if ($directiveAST) {
if (!$directiveDef->type) {
return null; return null;
} }
return self::coerceValueAST($directiveDef->type, $directiveAST->value, $variables);
if ($valueAST instanceof Variable) {
$variableName = $valueAST->name->value;
if (!$variables || !isset($variables[$variableName])) {
return null;
} }
return $variables[$variableName];
}
if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType();
if ($valueAST instanceof ListValue) {
return array_map(function($itemAST) use ($itemType, $variables) {
return Values::valueFromAST($itemAST, $itemType, $variables);
}, $valueAST->values);
} else {
return [self::valueFromAST($valueAST, $itemType, $variables)];
}
}
if ($type instanceof InputObjectType) {
$fields = $type->getFields();
if (!$valueAST instanceof ObjectValue) {
return null;
}
$fieldASTs = Utils::keyMap($valueAST->fields, function($field) {return $field->name->value;});
$values = [];
foreach ($fields as $field) {
$fieldAST = isset($fieldASTs[$field->name]) ? $fieldASTs[$field->name] : null;
$fieldValue = self::valueFromAST($fieldAST ? $fieldAST->value : null, $field->getType(), $variables);
if (null === $fieldValue) {
$fieldValue = $field->defaultValue;
}
if (null !== $fieldValue) {
$values[$field->name] = $fieldValue;
}
}
return $values;
}
Utils::invariant($type instanceof ScalarType || $type instanceof EnumType, 'Must be input type');
return $type->parseLiteral($valueAST);
} }
/** /**
@ -81,14 +144,21 @@ class Values
private static function getVariableValue(Schema $schema, VariableDefinition $definitionAST, $input) private static function getVariableValue(Schema $schema, VariableDefinition $definitionAST, $input)
{ {
$type = Utils\TypeInfo::typeFromAST($schema, $definitionAST->type); $type = Utils\TypeInfo::typeFromAST($schema, $definitionAST->type);
if (!$type) { $variable = $definitionAST->variable;
return null;
if (!$type || !Type::isInputType($type)) {
$printed = Printer::doPrint($definitionAST->type);
throw new Error(
"Variable \"\${$variable->name->value}\" expected value of type " .
"\"$printed\" which cannot be used as an input type.",
[ $definitionAST ]
);
} }
if (self::isValidValue($type, $input)) { if (self::isValidValue($input, $type)) {
if (null === $input) { if (null === $input) {
$defaultValue = $definitionAST->defaultValue; $defaultValue = $definitionAST->defaultValue;
if ($defaultValue) { if ($defaultValue) {
return self::coerceValueAST($type, $defaultValue); return self::valueFromAST($defaultValue, $type);
} }
} }
return self::coerceValue($type, $input); return self::coerceValue($type, $input);
@ -103,15 +173,21 @@ class Values
/** /**
* Given a type and any value, return true if that value is valid. * Given a PHP value and a GraphQL type, determine if the value will be
* accepted for that type. This is primarily useful for validating the
* runtime values of query variables.
*
* @param $value
* @param Type $type
* @return bool
*/ */
private static function isValidValue(Type $type, $value) private static function isValidValue($value, Type $type)
{ {
if ($type instanceof NonNull) { if ($type instanceof NonNull) {
if (null === $value) { if (null === $value) {
return false; return false;
} }
return self::isValidValue($type->getWrappedType(), $value); return self::isValidValue($value, $type->getWrappedType());
} }
if ($value === null) { if ($value === null) {
@ -122,34 +198,44 @@ class Values
$itemType = $type->getWrappedType(); $itemType = $type->getWrappedType();
if (is_array($value)) { if (is_array($value)) {
foreach ($value as $item) { foreach ($value as $item) {
if (!self::isValidValue($itemType, $item)) { if (!self::isValidValue($item, $itemType)) {
return false; return false;
} }
} }
return true; return true;
} else { } else {
return self::isValidValue($itemType, $value); return self::isValidValue($value, $itemType);
} }
} }
if ($type instanceof InputObjectType) { if ($type instanceof InputObjectType) {
$fields = $type->getFields(); if (!is_array($value)) {
foreach ($fields as $fieldName => $field) {
/** @var FieldDefinition $field */
if (!self::isValidValue($field->getType(), isset($value[$fieldName]) ? $value[$fieldName] : null)) {
return false; return false;
} }
$fields = $type->getFields();
$fieldMap = [];
// Ensure every defined field is valid.
foreach ($fields as $fieldName => $field) {
/** @var FieldDefinition $field */
if (!self::isValidValue(isset($value[$fieldName]) ? $value[$fieldName] : null, $field->getType())) {
return false;
} }
$fieldMap[$field->name] = $field;
}
// Ensure every provided field is defined.
$diff = array_diff_key($value, $fieldMap);
if (!empty($diff)) {
return false;
}
return true; return true;
} }
if ($type instanceof ScalarType || Utils::invariant($type instanceof ScalarType || $type instanceof EnumType, 'Must be input type');
$type instanceof EnumType return null !== $type->parseValue($value);
) {
return null !== $type->coerce($value);
}
return false;
} }
/** /**
@ -183,93 +269,18 @@ class Values
$fields = $type->getFields(); $fields = $type->getFields();
$obj = []; $obj = [];
foreach ($fields as $fieldName => $field) { foreach ($fields as $fieldName => $field) {
$fieldValue = self::coerceValue($field->getType(), $value[$fieldName]); $fieldValue = self::coerceValue($field->getType(), isset($value[$fieldName]) ? $value[$fieldName] : null);
$obj[$fieldName] = $fieldValue === null ? $field->defaultValue : $fieldValue; if (null === $fieldValue) {
$fieldValue = $field->defaultValue;
}
if (null !== $fieldValue) {
$obj[$fieldName] = $fieldValue;
}
} }
return $obj; return $obj;
} }
Utils::invariant($type instanceof ScalarType || $type instanceof EnumType, 'Must be input type');
if ($type instanceof ScalarType || return $type->parseValue($value);
$type instanceof EnumType
) {
$coerced = $type->coerce($value);
if (null !== $coerced) {
return $coerced;
}
}
return null;
}
/**
* Given a type and a value AST node known to match this type, build a
* runtime value.
*/
private static function coerceValueAST(Type $type, $valueAST, $variables)
{
if ($type instanceof NonNull) {
// Note: we're not checking that the result of coerceValueAST is non-null.
// We're assuming that this query has been validated and the value used
// here is of the correct type.
return self::coerceValueAST($type->getWrappedType(), $valueAST, $variables);
}
if (!$valueAST) {
return null;
}
if ($valueAST->kind === Node::VARIABLE) {
$variableName = $valueAST->name->value;
if (!isset($variables, $variables[$variableName])) {
return null;
}
// Note: we're not doing any checking that this variable is correct. We're
// assuming that this query has been validated and the variable usage here
// is of the correct type.
return $variables[$variableName];
}
if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType();
if ($valueAST->kind === Node::ARR) {
$tmp = [];
foreach ($valueAST->values as $itemAST) {
$tmp[] = self::coerceValueAST($itemType, $itemAST, $variables);
}
return $tmp;
} else {
return [self::coerceValueAST($itemType, $valueAST, $variables)];
}
}
if ($type instanceof InputObjectType) {
$fields = $type->getFields();
if ($valueAST->kind !== Node::OBJECT) {
return null;
}
$fieldASTs = Utils::keyMap($valueAST->fields, function ($field) {
return $field->name->value;
});
$obj = [];
foreach ($fields as $fieldName => $field) {
$fieldAST = $fieldASTs[$fieldName];
$fieldValue = self::coerceValueAST($field->getType(), $fieldAST ? $fieldAST->value : null, $variables);
$obj[$fieldName] = $fieldValue === null ? $field->defaultValue : $fieldValue;
}
return $obj;
}
if ($type instanceof ScalarType || $type instanceof EnumType) {
$coerced = $type->coerceLiteral($valueAST);
if (null !== $coerced) {
return $coerced;
}
}
return null;
} }
} }

View File

@ -11,25 +11,25 @@ class GraphQL
/** /**
* @param Schema $schema * @param Schema $schema
* @param $requestString * @param $requestString
* @param mixed $rootObject * @param mixed $rootValue
* @param array <string, string>|null $variableValues * @param array <string, string>|null $variableValues
* @param string|null $operationName * @param string|null $operationName
* @return array * @return array
*/ */
public static function execute(Schema $schema, $requestString, $rootObject = null, $variableValues = null, $operationName = null) public static function execute(Schema $schema, $requestString, $rootValue = null, $variableValues = null, $operationName = null)
{ {
try { try {
$source = new Source($requestString ?: '', 'GraphQL request'); $source = new Source($requestString ?: '', 'GraphQL request');
$ast = Parser::parse($source); $documentAST = Parser::parse($source);
$validationResult = DocumentValidator::validate($schema, $ast); $validationErrors = DocumentValidator::validate($schema, $documentAST);
if (empty($validationResult['isValid'])) { if (!empty($validationErrors)) {
return ['errors' => $validationResult['errors']]; return ['errors' => array_map(['GraphQL\Error', 'formatError'], $validationErrors)];
} else { } else {
return Executor::execute($schema, $rootObject, $ast, $operationName, $variableValues); return Executor::execute($schema, $documentAST, $rootValue, $variableValues, $operationName)->toArray();
} }
} catch (\Exception $e) { } catch (Error $e) {
return ['errors' => Error::formatError($e)]; return ['errors' => [Error::formatError($e)]];
} }
} }
} }

View File

@ -1,7 +1,7 @@
<?php <?php
namespace GraphQL\Language\AST; namespace GraphQL\Language\AST;
class Argument extends NamedType class Argument extends Node
{ {
public $kind = Node::ARGUMENT; public $kind = Node::ARGUMENT;
@ -9,4 +9,9 @@ class Argument extends NamedType
* @var Value * @var Value
*/ */
public $value; public $value;
/**
* @var Name
*/
public $name;
} }

View File

@ -1,10 +1,15 @@
<?php <?php
namespace GraphQL\Language\AST; namespace GraphQL\Language\AST;
class Field extends NamedType class Field extends Node
{ {
public $kind = Node::FIELD; public $kind = Node::FIELD;
/**
* @var Name
*/
public $name;
/** /**
* @var Name|null * @var Name|null
*/ */

View File

@ -2,10 +2,15 @@
namespace GraphQL\Language\AST; namespace GraphQL\Language\AST;
class FragmentDefinition extends NamedType implements Definition class FragmentDefinition extends Node implements Definition
{ {
public $kind = Node::FRAGMENT_DEFINITION; public $kind = Node::FRAGMENT_DEFINITION;
/**
* @var Name
*/
public $name;
/** /**
* @var NamedType * @var NamedType
*/ */

View File

@ -1,10 +1,15 @@
<?php <?php
namespace GraphQL\Language\AST; namespace GraphQL\Language\AST;
class FragmentSpread extends NamedType class FragmentSpread extends Node
{ {
public $kind = Node::FRAGMENT_SPREAD; public $kind = Node::FRAGMENT_SPREAD;
/**
* @var Name
*/
public $name;
/** /**
* @var array<Directive> * @var array<Directive>
*/ */

View File

@ -1,7 +1,7 @@
<?php <?php
namespace GraphQL\Language\AST; namespace GraphQL\Language\AST;
class NamedType extends Node class NamedType extends Node implements Type
{ {
public $kind = Node::NAMED_TYPE; public $kind = Node::NAMED_TYPE;

View File

@ -2,10 +2,15 @@
namespace GraphQL\Language\AST; namespace GraphQL\Language\AST;
class ObjectField extends NamedType class ObjectField extends Node
{ {
public $kind = Node::OBJECT_FIELD; public $kind = Node::OBJECT_FIELD;
/**
* @var Name
*/
public $name;
/** /**
* @var Value * @var Value
*/ */

View File

@ -1,13 +1,18 @@
<?php <?php
namespace GraphQL\Language\AST; namespace GraphQL\Language\AST;
class OperationDefinition extends NamedType implements Definition class OperationDefinition extends Node implements Definition
{ {
/** /**
* @var string * @var string
*/ */
public $kind = Node::OPERATION_DEFINITION; public $kind = Node::OPERATION_DEFINITION;
/**
* @var Name
*/
public $name;
/** /**
* @var string (oneOf 'query', 'mutation')) * @var string (oneOf 'query', 'mutation'))
*/ */

View File

@ -1,7 +1,12 @@
<?php <?php
namespace GraphQL\Language\AST; namespace GraphQL\Language\AST;
class Variable extends NamedType class Variable extends Node
{ {
public $kind = Node::VARIABLE; public $kind = Node::VARIABLE;
/**
* @var Name
*/
public $name;
} }

View File

@ -2,7 +2,12 @@
namespace GraphQL; namespace GraphQL;
use GraphQL\Type\Definition\Directive; 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\InterfaceType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Definition\UnionType;
@ -24,6 +29,99 @@ class Schema
Utils::invariant($querySchema || $mutationSchema, "Either query or mutation type must be set"); Utils::invariant($querySchema || $mutationSchema, "Either query or mutation type must be set");
$this->querySchema = $querySchema; $this->querySchema = $querySchema;
$this->mutationSchema = $mutationSchema; $this->mutationSchema = $mutationSchema;
// Build type map now to detect any errors within this schema.
$map = [];
foreach ([$this->getQueryType(), $this->getMutationType(), Introspection::_schema()] as $type) {
$this->_extractTypes($type, $map);
}
$this->_typeMap = $map + Type::getInternalTypes();
// 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
*/
private 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(
$this->isEqualType($ifaceField->getType(), $objectField->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(
$this->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 $typeA
* @param $typeB
* @return bool
*/
private function isEqualType($typeA, $typeB)
{
if ($typeA instanceof NonNull && $typeB instanceof NonNull) {
return $this->isEqualType($typeA->getWrappedType(), $typeB->getWrappedType());
}
if ($typeA instanceof ListOfType && $typeB instanceof ListOfType) {
return $this->isEqualType($typeA->getWrappedType(), $typeB->getWrappedType());
}
return $typeA === $typeB;
} }
public function getQueryType() public function getQueryType()
@ -57,8 +155,8 @@ class Schema
{ {
if (!$this->_directives) { if (!$this->_directives) {
$this->_directives = [ $this->_directives = [
Directive::ifDirective(), Directive::includeDirective(),
Directive::unlessDirective() Directive::skipDirective()
]; ];
} }
return $this->_directives; return $this->_directives;
@ -66,27 +164,26 @@ class Schema
public function getTypeMap() public function getTypeMap()
{ {
if (null === $this->_typeMap) {
$map = [];
foreach ([$this->getQueryType(), $this->getMutationType(), Introspection::_schema()] as $type) {
$this->_extractTypes($type, $map);
}
$this->_typeMap = $map + Type::getInternalTypes();
}
return $this->_typeMap; return $this->_typeMap;
} }
private function _extractTypes($type, &$map) private function _extractTypes($type, &$map)
{ {
if (!$type) {
return $map;
}
if ($type instanceof WrappingType) { if ($type instanceof WrappingType) {
return $this->_extractTypes($type->getWrappedType(), $map); return $this->_extractTypes($type->getWrappedType(), $map);
} }
if (!$type instanceof Type || !empty($map[$type->name])) { if (!empty($map[$type->name])) {
// TODO: warning? Utils::invariant(
$map[$type->name] === $type,
"Schema must contain unique named types but contains multiple types named \"$type\"."
);
return $map; return $map;
} }
$map[$type->name] = $type; $map[$type->name] = $type;
$nestedTypes = []; $nestedTypes = [];
@ -97,13 +194,12 @@ class Schema
if ($type instanceof ObjectType) { if ($type instanceof ObjectType) {
$nestedTypes = array_merge($nestedTypes, $type->getInterfaces()); $nestedTypes = array_merge($nestedTypes, $type->getInterfaces());
} }
if ($type instanceof ObjectType || $type instanceof InterfaceType) { if ($type instanceof ObjectType || $type instanceof InterfaceType || $type instanceof InputObjectType) {
foreach ((array) $type->getFields() as $fieldName => $field) { foreach ((array) $type->getFields() as $fieldName => $field) {
if (null === $field->args) { if (isset($field->args)) {
trigger_error('WTF ' . $field->name . ' has no args?'); // gg
}
$fieldArgTypes = array_map(function($arg) { return $arg->getType(); }, $field->args); $fieldArgTypes = array_map(function($arg) { return $arg->getType(); }, $field->args);
$nestedTypes = array_merge($nestedTypes, $fieldArgTypes); $nestedTypes = array_merge($nestedTypes, $fieldArgTypes);
}
$nestedTypes[] = $field->getType(); $nestedTypes[] = $field->getType();
} }
} }

View File

@ -13,4 +13,15 @@ GraphQLUnionType;
* @return array<ObjectType> * @return array<ObjectType>
*/ */
public function getPossibleTypes(); public function getPossibleTypes();
/**
* @return ObjectType
*/
public function getObjectType($value, ResolveInfo $info);
/**
* @param Type $type
* @return bool
*/
public function isPossibleType(Type $type);
} }

View File

@ -7,12 +7,17 @@ class BooleanType extends ScalarType
{ {
public $name = Type::BOOLEAN; public $name = Type::BOOLEAN;
public function coerce($value) public function serialize($value)
{ {
return !!$value; return !!$value;
} }
public function coerceLiteral($ast) public function parseValue($value)
{
return !!$value;
}
public function parseLiteral($ast)
{ {
if ($ast instanceof BooleanValue) { if ($ast instanceof BooleanValue) {
return (bool) $ast->value; return (bool) $ast->value;

View File

@ -8,39 +8,51 @@ class Directive
/** /**
* @return Directive * @return Directive
*/ */
public static function ifDirective() public static function includeDirective()
{ {
$internal = self::getInternalDirectives(); $internal = self::getInternalDirectives();
return $internal['if']; return $internal['include'];
} }
/** /**
* @return Directive * @return Directive
*/ */
public static function unlessDirective() public static function skipDirective()
{ {
$internal = self::getInternalDirectives(); $internal = self::getInternalDirectives();
return $internal['unless']; return $internal['skip'];
} }
public static function getInternalDirectives() public static function getInternalDirectives()
{ {
if (!self::$internalDirectives) { if (!self::$internalDirectives) {
self::$internalDirectives = [ self::$internalDirectives = [
'if' => new self([ 'include' => new self([
'name' => 'include',
'description' => 'Directs the executor to include this field or fragment only when the `if` argument is true.',
'args' => [
new FieldArgument([
'name' => 'if', 'name' => 'if',
'description' => 'Directs the executor to omit this field if the argument provided is false.',
'type' => Type::nonNull(Type::boolean()), 'type' => Type::nonNull(Type::boolean()),
'description' => 'Included when true.'
])
],
'onOperation' => false, 'onOperation' => false,
'onFragment' => false, 'onFragment' => true,
'onField' => true 'onField' => true
]), ]),
'unless' => new self([ 'skip' => new self([
'name' => 'unless', 'name' => 'skip',
'description' => 'Directs the executor to omit this field if the argument provided is true.', 'description' => 'Directs the executor to skip this field or fragment when the `if` argument is true.',
'args' => [
new FieldArgument([
'name' => 'if',
'type' => Type::nonNull(Type::boolean()), 'type' => Type::nonNull(Type::boolean()),
'description' => 'Skipped when true'
])
],
'onOperation' => false, 'onOperation' => false,
'onFragment' => false, 'onFragment' => true,
'onField' => true 'onField' => true
]) ])
]; ];
@ -59,9 +71,9 @@ class Directive
public $description; public $description;
/** /**
* @var Type * @var FieldArgument[]
*/ */
public $type; public $args;
/** /**
* @var boolean * @var boolean

View File

@ -53,13 +53,13 @@ class EnumType extends Type implements InputType, OutputType
return $this->_values; return $this->_values;
} }
public function coerce($value) public function serialize($value)
{ {
$enumValue = $this->_getValueLookup()->offsetGet($value); $enumValue = $this->_getValueLookup()->offsetGet($value);
return $enumValue ? $enumValue->name : null; return $enumValue ? $enumValue->name : null;
} }
public function coerceLiteral($value) public function parseLiteral($value)
{ {
if ($value instanceof EnumValue) { if ($value instanceof EnumValue) {
$lookup = $this->_getNameLookup(); $lookup = $this->_getNameLookup();

View File

@ -4,6 +4,12 @@ namespace GraphQL\Type\Definition;
use GraphQL\Utils; use GraphQL\Utils;
/**
* Class FieldArgument
*
* @package GraphQL\Type\Definition
* @todo Rename to Argument as it is also applicable to directives, not only fields
*/
class FieldArgument class FieldArgument
{ {
/** /**

View File

@ -1,6 +1,8 @@
<?php <?php
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use GraphQL\Utils;
class FieldDefinition class FieldDefinition
{ {
/** /**
@ -98,12 +100,28 @@ class FieldDefinition
$this->deprecationReason = isset($config['deprecationReason']) ? $config['deprecationReason'] : null; $this->deprecationReason = isset($config['deprecationReason']) ? $config['deprecationReason'] : null;
} }
/**
* @param $name
* @return FieldArgument|null
*/
public function getArg($name)
{
foreach ($this->args ?: [] as $arg) {
/** @var FieldArgument $arg */
if ($arg->name === $name) {
return $arg;
}
}
return null;
}
/** /**
* @return Type * @return Type
*/ */
public function getType() public function getType()
{ {
if (null === $this->resolvedType) { if (null === $this->resolvedType) {
// TODO: deprecate types as callbacks - instead just allow field definitions to be callbacks
$this->resolvedType = Type::resolve($this->type); $this->resolvedType = Type::resolve($this->type);
} }
return $this->resolvedType; return $this->resolvedType;

View File

@ -8,12 +8,22 @@ class FloatType extends ScalarType
{ {
public $name = Type::FLOAT; public $name = Type::FLOAT;
public function coerce($value) public function serialize($value)
{
return $this->coerceFloat($value);
}
public function parseValue($value)
{
return $this->coerceFloat($value);
}
private function coerceFloat($value)
{ {
return is_numeric($value) || $value === true || $value === false ? (float) $value : null; return is_numeric($value) || $value === true || $value === false ? (float) $value : null;
} }
public function coerceLiteral($ast) public function parseLiteral($ast)
{ {
if ($ast instanceof FloatValue || $ast instanceof IntValue) { if ($ast instanceof FloatValue || $ast instanceof IntValue) {
return (float) $ast->value; return (float) $ast->value;

View File

@ -8,12 +8,17 @@ class IDType extends ScalarType
{ {
public $name = 'ID'; public $name = 'ID';
public function coerce($value) public function serialize($value)
{ {
return (string) $value; return (string) $value;
} }
public function coerceLiteral($ast) public function parseValue($value)
{
return (string) $value;
}
public function parseLiteral($ast)
{ {
if ($ast instanceof StringValue || $ast instanceof IntValue) { if ($ast instanceof StringValue || $ast instanceof IntValue) {
return $ast->value; return $ast->value;

View File

@ -32,7 +32,7 @@ class InputObjectType extends Type implements InputType
} }
/** /**
* @return array<InputObjectField> * @return InputObjectField[]
*/ */
public function getFields() public function getFields()
{ {

View File

@ -2,12 +2,23 @@
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use GraphQL\Language\AST\IntValue; use GraphQL\Language\AST\IntValue;
use GraphQL\Language\AST\Value;
class IntType extends ScalarType class IntType extends ScalarType
{ {
public $name = Type::INT; public $name = Type::INT;
public function coerce($value) public function serialize($value)
{
return $this->coerceInt($value);
}
public function parseValue($value)
{
return $this->coerceInt($value);
}
private function coerceInt($value)
{ {
if (false === $value || true === $value) { if (false === $value || true === $value) {
return (int) $value; return (int) $value;
@ -18,7 +29,7 @@ class IntType extends ScalarType
return null; return null;
} }
public function coerceLiteral($ast) public function parseLiteral($ast)
{ {
if ($ast instanceof IntValue) { if ($ast instanceof IntValue) {
$val = (int) $ast->value; $val = (int) $ast->value;

View File

@ -35,11 +35,11 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
* implementation for Interface types. * implementation for Interface types.
* *
* @param ObjectType $impl * @param ObjectType $impl
* @param array<InterfaceType> $interfaces * @param InterfaceType[] $interfaces
*/ */
public static function addImplementationToInterfaces(ObjectType $impl, array $interfaces) public static function addImplementationToInterfaces(ObjectType $impl)
{ {
foreach ($interfaces as $interface) { foreach ($impl->getInterfaces() as $interface) {
$interface->_implementations[] = $impl; $interface->_implementations[] = $impl;
} }
} }
@ -84,10 +84,10 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
return $this->_implementations; return $this->_implementations;
} }
public function isPossibleType(ObjectType $type) public function isPossibleType(Type $type)
{ {
$possibleTypeNames = $this->_possibleTypeNames; $possibleTypeNames = $this->_possibleTypeNames;
if (!$possibleTypeNames) { if (null === $possibleTypeNames) {
$this->_possibleTypeNames = $possibleTypeNames = array_reduce($this->getPossibleTypes(), function(&$map, Type $possibleType) { $this->_possibleTypeNames = $possibleTypeNames = array_reduce($this->getPossibleTypes(), function(&$map, Type $possibleType) {
$map[$possibleType->name] = true; $map[$possibleType->name] = true;
return $map; return $map;
@ -98,11 +98,13 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
/** /**
* @param $value * @param $value
* @return ObjectType|null * @param ResolveInfo $info
* @return Type|null
* @throws \Exception
*/ */
public function resolveType($value) public function getObjectType($value, ResolveInfo $info)
{ {
$resolver = $this->_resolveType; $resolver = $this->_resolveType;
return $resolver ? call_user_func($resolver, $value) : Type::getTypeOf($value, $this); return $resolver ? call_user_func($resolver, $value, $info) : Type::getTypeOf($value, $info, $this);
} }
} }

View File

@ -45,7 +45,7 @@ class ObjectType extends Type implements OutputType, CompositeType
/** /**
* @var array<Field> * @var array<Field>
*/ */
private $_fields = []; private $_fields;
/** /**
* @var array<InterfaceType> * @var array<InterfaceType>
@ -57,9 +57,46 @@ class ObjectType extends Type implements OutputType, CompositeType
*/ */
private $_isTypeOf; private $_isTypeOf;
/**
* Keeping reference of config for late bindings
*
* @var array
*/
private $_config;
private $_initialized = false;
public function __construct(array $config) public function __construct(array $config)
{ {
Config::validate($config, [ $this->name = $config['name'];
$this->description = isset($config['description']) ? $config['description'] : null;
$this->_config = $config;
if (isset($config['interfaces'])) {
InterfaceType::addImplementationToInterfaces($this);
}
}
/**
* Late instance initialization
*/
private function initialize()
{
if ($this->_initialized) {
return ;
}
$config = $this->_config;
if (isset($config['fields']) && is_callable($config['fields'])) {
$config['fields'] = call_user_func($config['fields']);
}
if (isset($config['interfaces']) && is_callable($config['interfaces'])) {
$config['interfaces'] = call_user_func($config['interfaces']);
}
// Note: this validation is disabled by default, because it is resource-consuming
// TODO: add bin/validate script to check if schema is valid during development
Config::validate($this->_config, [
'name' => Config::STRING | Config::REQUIRED, 'name' => Config::STRING | Config::REQUIRED,
'fields' => Config::arrayOf( 'fields' => Config::arrayOf(
FieldDefinition::getDefinition(), FieldDefinition::getDefinition(),
@ -69,22 +106,13 @@ class ObjectType extends Type implements OutputType, CompositeType
'interfaces' => Config::arrayOf( 'interfaces' => Config::arrayOf(
Config::INTERFACE_TYPE Config::INTERFACE_TYPE
), ),
'isTypeOf' => Config::CALLBACK, 'isTypeOf' => Config::CALLBACK, // ($value, ResolveInfo $info) => boolean
]); ]);
$this->name = $config['name'];
$this->description = isset($config['description']) ? $config['description'] : null;
if (isset($config['fields'])) {
$this->_fields = FieldDefinition::createMap($config['fields']); $this->_fields = FieldDefinition::createMap($config['fields']);
}
$this->_interfaces = isset($config['interfaces']) ? $config['interfaces'] : []; $this->_interfaces = isset($config['interfaces']) ? $config['interfaces'] : [];
$this->_isTypeOf = isset($config['isTypeOf']) ? $config['isTypeOf'] : null; $this->_isTypeOf = isset($config['isTypeOf']) ? $config['isTypeOf'] : null;
$this->_initialized = true;
if (!empty($this->_interfaces)) {
InterfaceType::addImplementationToInterfaces($this, $this->_interfaces);
}
} }
/** /**
@ -92,6 +120,9 @@ class ObjectType extends Type implements OutputType, CompositeType
*/ */
public function getFields() public function getFields()
{ {
if (false === $this->_initialized) {
$this->initialize();
}
return $this->_fields; return $this->_fields;
} }
@ -102,6 +133,9 @@ class ObjectType extends Type implements OutputType, CompositeType
*/ */
public function getField($name) public function getField($name)
{ {
if (false === $this->_initialized) {
$this->initialize();
}
Utils::invariant(isset($this->_fields[$name]), "Field '%s' is not defined for type '%s'", $name, $this->name); Utils::invariant(isset($this->_fields[$name]), "Field '%s' is not defined for type '%s'", $name, $this->name);
return $this->_fields[$name]; return $this->_fields[$name];
} }
@ -111,6 +145,9 @@ class ObjectType extends Type implements OutputType, CompositeType
*/ */
public function getInterfaces() public function getInterfaces()
{ {
if (false === $this->_initialized) {
$this->initialize();
}
return $this->_interfaces; return $this->_interfaces;
} }
@ -118,8 +155,8 @@ class ObjectType extends Type implements OutputType, CompositeType
* @param $value * @param $value
* @return bool|null * @return bool|null
*/ */
public function isTypeOf($value) public function isTypeOf($value, ResolveInfo $info)
{ {
return isset($this->_isTypeOf) ? call_user_func($this->_isTypeOf, $value) : null; return isset($this->_isTypeOf) ? call_user_func($this->_isTypeOf, $value, $info) : null;
} }
} }

View File

@ -0,0 +1,61 @@
<?php
namespace GraphQL\Type\Definition;
use GraphQL\Language\AST\Field;
use GraphQL\Language\AST\FragmentDefinition;
use GraphQL\Language\AST\OperationDefinition;
use GraphQL\Schema;
use GraphQL\Utils;
class ResolveInfo
{
/**
* @var string
*/
public $fieldName;
/**
* @var Field[]
*/
public $fieldASTs;
/**
* @var OutputType
*/
public $returnType;
/**
* @var Type|CompositeType
*/
public $parentType;
/**
* @var Schema
*/
public $schema;
/**
* @var array<fragmentName, FragmentDefinition>
*/
public $fragments;
/**
* @var mixed
*/
public $rootValue;
/**
* @var OperationDefinition
*/
public $operation;
/**
* @var array<variableName, mixed>
*/
public $variableValues;
public function __construct(array $values)
{
Utils::assign($this, $values);
}
}

View File

@ -14,7 +14,7 @@ use GraphQL\Utils;
* *
* var OddType = new GraphQLScalarType({ * var OddType = new GraphQLScalarType({
* name: 'Odd', * name: 'Odd',
* coerce(value) { * serialize(value) {
* return value % 2 === 1 ? value : null; * return value % 2 === 1 ? value : null;
* } * }
* }); * });
@ -27,7 +27,9 @@ abstract class ScalarType extends Type implements OutputType, InputType
Utils::invariant($this->name, 'Type must be named.'); Utils::invariant($this->name, 'Type must be named.');
} }
abstract public function coerce($value); abstract public function serialize($value);
abstract public function coerceLiteral($ast); abstract public function parseValue($value);
abstract public function parseLiteral(/* GraphQL\Language\AST\Value */$valueAST);
} }

View File

@ -1,26 +0,0 @@
<?php
namespace GraphQL\Type\Definition;
class ScalarTypeConfig
{
/**
* @var string
*/
public $name;
/**
* @var string|null
*/
public $description;
/**
* @var \Closure
*/
public $coerce;
/**
* @var \Closure
*/
public $coerceLiteral;
}

View File

@ -7,7 +7,12 @@ class StringType extends ScalarType
{ {
public $name = Type::STRING; public $name = Type::STRING;
public function coerce($value) public function serialize($value)
{
return $this->parseValue($value);
}
public function parseValue($value)
{ {
if ($value === true) { if ($value === true) {
return 'true'; return 'true';
@ -18,7 +23,7 @@ class StringType extends ScalarType
return (string) $value; return (string) $value;
} }
public function coerceLiteral($ast) public function parseLiteral($ast)
{ {
if ($ast instanceof StringValue) { if ($ast instanceof StringValue) {
return $ast->value; return $ast->value;

View File

@ -111,7 +111,7 @@ GraphQLNonNull;
*/ */
public static function isInputType($type) public static function isInputType($type)
{ {
$nakedType = self::getUnmodifiedType($type); $nakedType = self::getNamedType($type);
return $nakedType instanceof InputType; return $nakedType instanceof InputType;
} }
@ -121,13 +121,14 @@ GraphQLNonNull;
*/ */
public static function isOutputType($type) public static function isOutputType($type)
{ {
$nakedType = self::getUnmodifiedType($type); $nakedType = self::getNamedType($type);
return $nakedType instanceof OutputType; return $nakedType instanceof OutputType;
} }
public static function isLeafType($type) public static function isLeafType($type)
{ {
$nakedType = self::getUnmodifiedType($type); // TODO: add LeafType interface
$nakedType = self::getNamedType($type);
return ( return (
$nakedType instanceof ScalarType || $nakedType instanceof ScalarType ||
$nakedType instanceof EnumType $nakedType instanceof EnumType
@ -136,19 +137,12 @@ GraphQLNonNull;
public static function isCompositeType($type) public static function isCompositeType($type)
{ {
return ( return $type instanceof CompositeType;
$type instanceof ObjectType ||
$type instanceof InterfaceType ||
$type instanceof UnionType
);
} }
public static function isAbstractType($type) public static function isAbstractType($type)
{ {
return ( return $type instanceof AbstractType;
$type instanceof InterfaceType ||
$type instanceof UnionType
);
} }
/** /**
@ -164,7 +158,7 @@ GraphQLNonNull;
* @param $type * @param $type
* @return UnmodifiedType * @return UnmodifiedType
*/ */
public static function getUnmodifiedType($type) public static function getNamedType($type)
{ {
if (null === $type) { if (null === $type) {
return null; return null;
@ -195,21 +189,21 @@ GraphQLNonNull;
* @return Type * @return Type
* @throws \Exception * @throws \Exception
*/ */
public static function getTypeOf($value, AbstractType $abstractType) public static function getTypeOf($value, ResolveInfo $info, AbstractType $abstractType)
{ {
$possibleTypes = $abstractType->getPossibleTypes(); $possibleTypes = $abstractType->getPossibleTypes();
for ($i = 0; $i < count($possibleTypes); $i++) { for ($i = 0; $i < count($possibleTypes); $i++) {
/** @var ObjectType $type */ /** @var ObjectType $type */
$type = $possibleTypes[$i]; $type = $possibleTypes[$i];
$isTypeOf = $type->isTypeOf($value); $isTypeOf = $type->isTypeOf($value, $info);
if ($isTypeOf === null) { if ($isTypeOf === null) {
// TODO: move this to a JS impl specific type system validation step // TODO: move this to a JS impl specific type system validation step
// so the error can be found before execution. // so the error can be found before execution.
throw new \Exception( throw new \Exception(
'Non-Object Type ' . $abstractType->name . ' does not implement ' . 'Non-Object Type ' . $abstractType->name . ' does not implement ' .
'resolveType and Object Type ' . $type->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.' 'isTypeOf. There is no way to determine if a value is of this type.'
); );
} }

View File

@ -73,9 +73,9 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
* @param ObjectType $value * @param ObjectType $value
* @return Type * @return Type
*/ */
public function resolveType($value) public function getObjectType($value, ResolveInfo $info)
{ {
$resolver = $this->_resolveType; $resolver = $this->_resolveType;
return $resolver ? call_user_func($resolver, $value) : Type::getTypeOf($value, $this); return $resolver ? call_user_func($resolver, $value) : Type::getTypeOf($value, $info, $this);
} }
} }

View File

@ -3,6 +3,7 @@ namespace GraphQL\Type;
use GraphQL\Schema; use GraphQL\Schema;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputObjectType;
@ -10,6 +11,7 @@ use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Definition\UnionType;
@ -30,6 +32,165 @@ class Introspection
{ {
private static $_map = []; private static $_map = [];
/**
* @return string
*/
public static function getIntrospectionQuery($includeDescription = true)
{
$withDescription = <<<'EOD'
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
types {
...FullType
}
directives {
name
description
args {
...InputValue
}
onOperation
onFragment
onField
}
}
}
fragment FullType on __Type {
kind
name
description
fields {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
description
type { ...TypeRef }
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
EOD;
$withoutDescription = <<<'EOD'
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
types {
...FullType
}
directives {
name
args {
...InputValue
}
onOperation
onFragment
onField
}
}
}
fragment FullType on __Type {
kind
name
fields {
name
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues {
name
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
type { ...TypeRef }
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
EOD;
return $includeDescription ? $withDescription : $withoutDescription;
}
public static function _schema() public static function _schema()
{ {
if (!isset(self::$_map['__Schema'])) { if (!isset(self::$_map['__Schema'])) {
@ -83,12 +244,15 @@ class Introspection
self::$_map['__Directive'] = new ObjectType([ self::$_map['__Directive'] = new ObjectType([
'name' => '__Directive', 'name' => '__Directive',
'fields' => [ 'fields' => [
'name' => ['type' => Type::string()], 'name' => ['type' => Type::nonNull(Type::string())],
'description' => ['type' => Type::string()], 'description' => ['type' => Type::string()],
'type' => ['type' => [__CLASS__, '_type']], 'args' => [
'onOperation' => ['type' => Type::boolean()], 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))),
'onFragment' => ['type' => Type::boolean()], 'resolve' => function(Directive $directive) {return $directive->args ?: [];}
'onField' => ['type' => Type::boolean()] ],
'onOperation' => ['type' => Type::nonNull(Type::boolean())],
'onFragment' => ['type' => Type::nonNull(Type::boolean())],
'onField' => ['type' => Type::nonNull(Type::boolean())]
] ]
]); ]);
} }
@ -349,14 +513,9 @@ class Introspection
'resolve' => function ( 'resolve' => function (
$source, $source,
$args, $args,
$root, ResolveInfo $info
$fieldAST,
$fieldType,
$parentType,
$schema
) { ) {
// TODO: move 3+ args to separate object return $info->schema;
return $schema;
} }
]); ]);
} }
@ -373,8 +532,8 @@ class Introspection
'args' => [ 'args' => [
['name' => 'name', 'type' => Type::nonNull(Type::string())] ['name' => 'name', 'type' => Type::nonNull(Type::string())]
], ],
'resolve' => function ($source, $args, $root, $fieldAST, $fieldType, $parentType, $schema) { 'resolve' => function ($source, $args, ResolveInfo $info) {
return $schema->getType($args['name']); return $info->schema->getType($args['name']);
} }
]); ]);
} }
@ -392,12 +551,9 @@ class Introspection
'resolve' => function ( 'resolve' => function (
$source, $source,
$args, $args,
$root, ResolveInfo $info
$fieldAST,
$fieldType,
$parentType
) { ) {
return $parentType->name; return $info->parentType->name;
} }
]); ]);
} }

View File

@ -169,11 +169,6 @@ class SchemaValidator
$errors = array_merge($errors, $newErrors); $errors = array_merge($errors, $newErrors);
} }
} }
$isValid = empty($errors); return $errors;
$result = [
'isValid' => $isValid,
'errors' => $isValid ? null : array_map(['GraphQL\Error', 'formatError'], $errors)
];
return (object) $result;
} }
} }

View File

@ -4,9 +4,12 @@ namespace GraphQL\Utils;
use GraphQL\Language\AST\Field; use GraphQL\Language\AST\Field;
use GraphQL\Language\AST\ListType; use GraphQL\Language\AST\ListType;
use GraphQL\Language\AST\Name; use GraphQL\Language\AST\Name;
use GraphQL\Language\AST\NamedType;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NonNullType; use GraphQL\Language\AST\NonNullType;
use GraphQL\Schema; use GraphQL\Schema;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\InputType;
@ -38,8 +41,8 @@ class TypeInfo
return $innerType ? new NonNull($innerType) : null; return $innerType ? new NonNull($innerType) : null;
} }
Utils::invariant($inputTypeAst instanceof Name, 'Must be a type name'); Utils::invariant($inputTypeAst->kind === Node::NAMED_TYPE, 'Must be a named type');
return $schema->getType($inputTypeAst->value); return $schema->getType($inputTypeAst->name->value);
} }
/** /**
@ -103,6 +106,15 @@ class TypeInfo
*/ */
private $_fieldDefStack; private $_fieldDefStack;
/**
* @var Directive
*/
private $_directive;
/**
* @var FieldArgument
*/
private $_argument;
public function __construct(Schema $schema) public function __construct(Schema $schema)
{ {
@ -157,6 +169,21 @@ class TypeInfo
return null; return null;
} }
/**
* @return Directive|null
*/
function getDirective()
{
return $this->_directive;
}
/**
* @return FieldArgument|null
*/
function getArgument()
{
return $this->_argument;
}
function enter(Node $node) function enter(Node $node)
{ {
@ -164,16 +191,19 @@ class TypeInfo
switch ($node->kind) { switch ($node->kind) {
case Node::SELECTION_SET: case Node::SELECTION_SET:
// var $compositeType: ?GraphQLCompositeType; $namedType = Type::getNamedType($this->getType());
$rawType = Type::getUnmodifiedType($this->getType());
$compositeType = null; $compositeType = null;
if (Type::isCompositeType($rawType)) { if (Type::isCompositeType($namedType)) {
// isCompositeType is a type refining predicate, so this is safe. // isCompositeType is a type refining predicate, so this is safe.
$compositeType = $rawType; $compositeType = $namedType;
} }
array_push($this->_parentTypeStack, $compositeType); array_push($this->_parentTypeStack, $compositeType);
break; break;
case Node::DIRECTIVE:
$this->_directive = $schema->getDirective($node->name->value);
break;
case Node::FIELD: case Node::FIELD:
$parentType = $this->getParentType(); $parentType = $this->getParentType();
$fieldDef = null; $fieldDef = null;
@ -196,7 +226,7 @@ class TypeInfo
case Node::INLINE_FRAGMENT: case Node::INLINE_FRAGMENT:
case Node::FRAGMENT_DEFINITION: case Node::FRAGMENT_DEFINITION:
$type = $schema->getType($node->typeCondition->value); $type = self::typeFromAST($schema, $node->typeCondition);
array_push($this->_typeStack, $type); array_push($this->_typeStack, $type);
break; break;
@ -205,32 +235,28 @@ class TypeInfo
break; break;
case Node::ARGUMENT: case Node::ARGUMENT:
$field = $this->getFieldDef(); $fieldOrDirective = $this->getDirective() ?: $this->getFieldDef();
$argType = null; $argDef = $argType = null;
if ($field) { if ($fieldOrDirective) {
$argDef = Utils::find($field->args, function($arg) use ($node) {return $arg->name === $node->name->value;}); $argDef = Utils::find($fieldOrDirective->args, function($arg) use ($node) {return $arg->name === $node->name->value;});
if ($argDef) { if ($argDef) {
$argType = $argDef->getType(); $argType = $argDef->getType();
} }
} }
$this->_argument = $argDef;
array_push($this->_inputTypeStack, $argType); array_push($this->_inputTypeStack, $argType);
break; break;
case Node::DIRECTIVE:
$directive = $schema->getDirective($node->name->value);
array_push($this->_inputTypeStack, $directive ? $directive->type : null);
break;
case Node::LST: case Node::LST:
$arrayType = Type::getNullableType($this->getInputType()); $listType = Type::getNullableType($this->getInputType());
array_push( array_push(
$this->_inputTypeStack, $this->_inputTypeStack,
$arrayType instanceof ListOfType ? $arrayType->getWrappedType() : null $listType instanceof ListOfType ? $listType->getWrappedType() : null
); );
break; break;
case Node::OBJECT_FIELD: case Node::OBJECT_FIELD:
$objectType = Type::getUnmodifiedType($this->getInputType()); $objectType = Type::getNamedType($this->getInputType());
$fieldType = null; $fieldType = null;
if ($objectType instanceof InputObjectType) { if ($objectType instanceof InputObjectType) {
$tmp = $objectType->getFields(); $tmp = $objectType->getFields();
@ -248,10 +274,16 @@ class TypeInfo
case Node::SELECTION_SET: case Node::SELECTION_SET:
array_pop($this->_parentTypeStack); array_pop($this->_parentTypeStack);
break; break;
case Node::FIELD: case Node::FIELD:
array_pop($this->_fieldDefStack); array_pop($this->_fieldDefStack);
array_pop($this->_typeStack); array_pop($this->_typeStack);
break; break;
case Node::DIRECTIVE:
$this->_directive = null;
break;
case Node::OPERATION_DEFINITION: case Node::OPERATION_DEFINITION:
case Node::INLINE_FRAGMENT: case Node::INLINE_FRAGMENT:
case Node::FRAGMENT_DEFINITION: case Node::FRAGMENT_DEFINITION:
@ -261,9 +293,9 @@ class TypeInfo
array_pop($this->_inputTypeStack); array_pop($this->_inputTypeStack);
break; break;
case Node::ARGUMENT: case Node::ARGUMENT:
$this->_argument = null;
array_pop($this->_inputTypeStack); array_pop($this->_inputTypeStack);
break; break;
case Node::DIRECTIVE:
case Node::LST: case Node::LST:
case Node::OBJECT_FIELD: case Node::OBJECT_FIELD:
array_pop($this->_inputTypeStack); array_pop($this->_inputTypeStack);

View File

@ -2,7 +2,7 @@
namespace GraphQL\Validator; namespace GraphQL\Validator;
use GraphQL\Error; use GraphQL\Error;
use GraphQL\Language\AST\ArrayValue; use GraphQL\Language\AST\ListValue;
use GraphQL\Language\AST\Document; use GraphQL\Language\AST\Document;
use GraphQL\Language\AST\FragmentSpread; use GraphQL\Language\AST\FragmentSpread;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\Node;
@ -33,6 +33,7 @@ use GraphQL\Validator\Rules\NoUnusedFragments;
use GraphQL\Validator\Rules\NoUnusedVariables; use GraphQL\Validator\Rules\NoUnusedVariables;
use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged; use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged;
use GraphQL\Validator\Rules\PossibleFragmentSpreads; use GraphQL\Validator\Rules\PossibleFragmentSpreads;
use GraphQL\Validator\Rules\ProvidedNonNullArguments;
use GraphQL\Validator\Rules\ScalarLeafs; use GraphQL\Validator\Rules\ScalarLeafs;
use GraphQL\Validator\Rules\VariablesAreInputTypes; use GraphQL\Validator\Rules\VariablesAreInputTypes;
use GraphQL\Validator\Rules\VariablesInAllowedPosition; use GraphQL\Validator\Rules\VariablesInAllowedPosition;
@ -45,23 +46,28 @@ class DocumentValidator
{ {
if (null === self::$allRules) { if (null === self::$allRules) {
self::$allRules = [ self::$allRules = [
new ArgumentsOfCorrectType(), // new UniqueOperationNames,
new DefaultValuesOfCorrectType(), // new LoneAnonymousOperation,
new FieldsOnCorrectType(), new KnownTypeNames,
new FragmentsOnCompositeTypes(), new FragmentsOnCompositeTypes,
new KnownArgumentNames(), new VariablesAreInputTypes,
new KnownDirectives(), new ScalarLeafs,
new KnownFragmentNames(), new FieldsOnCorrectType,
new KnownTypeNames(), // new UniqueFragmentNames,
new NoFragmentCycles(), new KnownFragmentNames,
new NoUndefinedVariables(), new NoUnusedFragments,
new NoUnusedFragments(), new PossibleFragmentSpreads,
new NoUnusedVariables(), new NoFragmentCycles,
new OverlappingFieldsCanBeMerged(), new NoUndefinedVariables,
new PossibleFragmentSpreads(), new NoUnusedVariables,
new ScalarLeafs(), new KnownDirectives,
new VariablesAreInputTypes(), new KnownArgumentNames,
new VariablesInAllowedPosition() // new UniqueArgumentNames,
new ArgumentsOfCorrectType,
new ProvidedNonNullArguments,
new DefaultValuesOfCorrectType,
new VariablesInAllowedPosition,
new OverlappingFieldsCanBeMerged,
]; ];
} }
return self::$allRules; return self::$allRules;
@ -70,13 +76,7 @@ class DocumentValidator
public static function validate(Schema $schema, Document $ast, array $rules = null) public static function validate(Schema $schema, Document $ast, array $rules = null)
{ {
$errors = self::visitUsingRules($schema, $ast, $rules ?: self::allRules()); $errors = self::visitUsingRules($schema, $ast, $rules ?: self::allRules());
$isValid = empty($errors); return $errors;
$result = [
'isValid' => $isValid,
'errors' => $isValid ? null : array_map(['GraphQL\Error', 'formatError'], $errors)
];
return $result;
} }
static function isError($value) static function isError($value)
@ -121,7 +121,7 @@ class DocumentValidator
// Lists accept a non-list value as a list of one. // Lists accept a non-list value as a list of one.
if ($type instanceof ListOfType) { if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType(); $itemType = $type->getWrappedType();
if ($valueAST instanceof ArrayValue) { if ($valueAST instanceof ListValue) {
foreach($valueAST->values as $itemAST) { foreach($valueAST->values as $itemAST) {
if (!self::isValidLiteralValue($itemAST, $itemType)) { if (!self::isValidLiteralValue($itemAST, $itemType)) {
return false; return false;
@ -133,10 +133,10 @@ class DocumentValidator
} }
} }
// Scalar/Enum input checks to ensure the type can coerce the value to // Scalar/Enum input checks to ensure the type can serialize the value to
// a non-null value. // a non-null value.
if ($type instanceof ScalarType || $type instanceof EnumType) { if ($type instanceof ScalarType || $type instanceof EnumType) {
return $type->coerceLiteral($valueAST) !== null; return $type->parseLiteral($valueAST) !== null;
} }
// Input objects check each defined field, ensuring it is of the correct // Input objects check each defined field, ensuring it is of the correct
@ -294,7 +294,7 @@ class DocumentValidator
if ($result->doBreak) { if ($result->doBreak) {
$instances[$i] = null; $instances[$i] = null;
} }
} if (self::isError($result)) { } else if (self::isError($result)) {
self::append($errors, $result); self::append($errors, $result);
} else if ($result !== null) { } else if ($result !== null) {
throw new \Exception("Config cannot edit document."); throw new \Exception("Config cannot edit document.");

View File

@ -16,53 +16,23 @@ use GraphQL\Validator\ValidationContext;
class ArgumentsOfCorrectType class ArgumentsOfCorrectType
{ {
static function badValueMessage($argName, $type, $value)
{
return "Argument \"$argName\" expected type \"$type\" but got: $value.";
}
public function __invoke(ValidationContext $context) public function __invoke(ValidationContext $context)
{ {
return [ return [
Node::FIELD => function(Field $fieldAST) use ($context) { Node::ARGUMENT => function(Argument $argAST) use ($context) {
$fieldDef = $context->getFieldDef(); $argDef = $context->getArgument();
if (!$fieldDef) {
return Visitor::skipNode();
}
$errors = [];
$argASTs = $fieldAST->arguments ?: [];
$argASTMap = Utils::keyMap($argASTs, function (Argument $arg) {
return $arg->name->value;
});
foreach ($fieldDef->args as $argDef) {
$argAST = isset($argASTMap[$argDef->name]) ? $argASTMap[$argDef->name] : null;
if (!$argAST && $argDef->getType() instanceof NonNull) {
$errors[] = new Error(
Messages::missingArgMessage(
$fieldAST->name->value,
$argDef->name,
$argDef->getType()
),
[$fieldAST]
);
}
}
$argDefMap = Utils::keyMap($fieldDef->args, function ($def) {
return $def->name;
});
foreach ($argASTs as $argAST) {
$argDef = $argDefMap[$argAST->name->value];
if ($argDef && !DocumentValidator::isValidLiteralValue($argAST->value, $argDef->getType())) { if ($argDef && !DocumentValidator::isValidLiteralValue($argAST->value, $argDef->getType())) {
$errors[] = new Error( return new Error(
Messages::badValueMessage( self::badValueMessage($argAST->name->value, $argDef->getType(), Printer::doPrint($argAST->value)),
$argAST->name->value,
$argDef->getType(),
Printer::doPrint($argAST->value)
),
[$argAST->value] [$argAST->value]
); );
} }
} }
return !empty($errors) ? $errors : null;
}
]; ];
} }
} }

View File

@ -6,35 +6,43 @@ use GraphQL\Error;
use GraphQL\Language\AST\FragmentDefinition; use GraphQL\Language\AST\FragmentDefinition;
use GraphQL\Language\AST\InlineFragment; use GraphQL\Language\AST\InlineFragment;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\Node;
use GraphQL\Language\Printer;
use GraphQL\Type\Definition\CompositeType; use GraphQL\Type\Definition\CompositeType;
use GraphQL\Type\Definition\Type;
use GraphQL\Validator\Messages; use GraphQL\Validator\Messages;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
class FragmentsOnCompositeTypes class FragmentsOnCompositeTypes
{ {
static function inlineFragmentOnNonCompositeErrorMessage($type)
{
return "Fragment cannot condition on non composite type \"$type\".";
}
static function fragmentOnNonCompositeErrorMessage($fragName, $type)
{
return "Fragment \"$fragName\" cannot condition on non composite type \"$type\".";
}
public function __invoke(ValidationContext $context) public function __invoke(ValidationContext $context)
{ {
return [ return [
Node::INLINE_FRAGMENT => function(InlineFragment $node) use ($context) { Node::INLINE_FRAGMENT => function(InlineFragment $node) use ($context) {
$typeName = $node->typeCondition->value; $type = $context->getType();
$type = $context->getSchema()->getType($typeName);
$isCompositeType = $type instanceof CompositeType;
if (!$isCompositeType) { if ($type && !Type::isCompositeType($type)) {
return new Error( return new Error(
"Fragment cannot condition on non composite type \"$typeName\".", self::inlineFragmentOnNonCompositeErrorMessage($type),
[$node->typeCondition] [$node->typeCondition]
); );
} }
}, },
Node::FRAGMENT_DEFINITION => function(FragmentDefinition $node) use ($context) { Node::FRAGMENT_DEFINITION => function(FragmentDefinition $node) use ($context) {
$typeName = $node->typeCondition->value; $type = $context->getType();
$type = $context->getSchema()->getType($typeName);
$isCompositeType = $type instanceof CompositeType;
if (!$isCompositeType) { if ($type && !Type::isCompositeType($type)) {
return new Error( return new Error(
Messages::fragmentOnNonCompositeErrorMessage($node->name->value, $typeName), self::fragmentOnNonCompositeErrorMessage($node->name->value, Printer::doPrint($node->typeCondition)),
[$node->typeCondition] [$node->typeCondition]
); );
} }

View File

@ -11,29 +11,59 @@ use GraphQL\Validator\ValidationContext;
class KnownArgumentNames class KnownArgumentNames
{ {
public static function unknownArgMessage($argName, $fieldName, $type)
{
return "Unknown argument \"$argName\" on field \"$fieldName\" of type \"$type\".";
}
public static function unknownDirectiveArgMessage($argName, $directiveName)
{
return "Unknown argument \"$argName\" on directive \"@$directiveName\".";
}
public function __invoke(ValidationContext $context) public function __invoke(ValidationContext $context)
{ {
return [ return [
Node::ARGUMENT => function(Argument $node) use ($context) { Node::ARGUMENT => function(Argument $node, $key, $parent, $path, $ancestors) use ($context) {
$argumentOf = $ancestors[count($ancestors) - 1];
if ($argumentOf->kind === Node::FIELD) {
$fieldDef = $context->getFieldDef(); $fieldDef = $context->getFieldDef();
if ($fieldDef) { if ($fieldDef) {
$argDef = null; $fieldArgDef = null;
foreach ($fieldDef->args as $arg) { foreach ($fieldDef->args as $arg) {
if ($arg->name === $node->name->value) { if ($arg->name === $node->name->value) {
$argDef = $arg; $fieldArgDef = $arg;
break; break;
} }
} }
if (!$fieldArgDef) {
if (!$argDef) {
$parentType = $context->getParentType(); $parentType = $context->getParentType();
Utils::invariant($parentType); Utils::invariant($parentType);
return new Error( return new Error(
Messages::unknownArgMessage($node->name->value, $fieldDef->name, $parentType->name), self::unknownArgMessage($node->name->value, $fieldDef->name, $parentType->name),
[$node] [$node]
); );
} }
} }
} else if ($argumentOf->kind === Node::DIRECTIVE) {
$directive = $context->getDirective();
if ($directive) {
$directiveArgDef = null;
foreach ($directive->args as $arg) {
if ($arg->name === $node->name->value) {
$directiveArgDef = $arg;
break;
}
}
if (!$directiveArgDef) {
return new Error(
self::unknownDirectiveArgMessage($node->name->value, $directive->name),
[$node]
);
}
}
}
} }
]; ];
} }

View File

@ -15,6 +15,16 @@ use GraphQL\Validator\ValidationContext;
class KnownDirectives class KnownDirectives
{ {
static function unknownDirectiveMessage($directiveName)
{
return "Unknown directive \"$directiveName\".";
}
static function misplacedDirectiveMessage($directiveName, $placement)
{
return "Directive \"$directiveName\" may not be used on \"$placement\".";
}
public function __invoke(ValidationContext $context) public function __invoke(ValidationContext $context)
{ {
return [ return [
@ -29,7 +39,7 @@ class KnownDirectives
if (!$directiveDef) { if (!$directiveDef) {
return new Error( return new Error(
Messages::unknownDirectiveMessage($node->name->value), self::unknownDirectiveMessage($node->name->value),
[$node] [$node]
); );
} }
@ -37,13 +47,13 @@ class KnownDirectives
if ($appliedTo instanceof OperationDefinition && !$directiveDef->onOperation) { if ($appliedTo instanceof OperationDefinition && !$directiveDef->onOperation) {
return new Error( return new Error(
Messages::misplacedDirectiveMessage($node->name->value, 'operation'), self::misplacedDirectiveMessage($node->name->value, 'operation'),
[$node] [$node]
); );
} }
if ($appliedTo instanceof Field && !$directiveDef->onField) { if ($appliedTo instanceof Field && !$directiveDef->onField) {
return new Error( return new Error(
Messages::misplacedDirectiveMessage($node->name->value, 'field'), self::misplacedDirectiveMessage($node->name->value, 'field'),
[$node] [$node]
); );
} }
@ -56,7 +66,7 @@ class KnownDirectives
if ($fragmentKind && !$directiveDef->onFragment) { if ($fragmentKind && !$directiveDef->onFragment) {
return new Error( return new Error(
Messages::misplacedDirectiveMessage($node->name->value, 'fragment'), self::misplacedDirectiveMessage($node->name->value, 'fragment'),
[$node] [$node]
); );
} }

View File

@ -9,6 +9,11 @@ use GraphQL\Validator\ValidationContext;
class KnownFragmentNames class KnownFragmentNames
{ {
static function unknownFragmentMessage($fragName)
{
return "Unknown fragment \"$fragName\".";
}
public function __invoke(ValidationContext $context) public function __invoke(ValidationContext $context)
{ {
return [ return [
@ -17,7 +22,7 @@ class KnownFragmentNames
$fragment = $context->getFragment($fragmentName); $fragment = $context->getFragment($fragmentName);
if (!$fragment) { if (!$fragment) {
return new Error( return new Error(
"Undefined fragment $fragmentName.", self::unknownFragmentMessage($fragmentName),
[$node->name] [$node->name]
); );
} }

View File

@ -4,22 +4,27 @@ namespace GraphQL\Validator\Rules;
use GraphQL\Error; use GraphQL\Error;
use GraphQL\Language\AST\Name; use GraphQL\Language\AST\Name;
use GraphQL\Language\AST\NamedType;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\Node;
use GraphQL\Validator\Messages; use GraphQL\Validator\Messages;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
class KnownTypeNames class KnownTypeNames
{ {
static function unknownTypeMessage($type)
{
return "Unknown type \"$type\".";
}
public function __invoke(ValidationContext $context) public function __invoke(ValidationContext $context)
{ {
return [ return [
Node::NAME => function(Name $node, $key) use ($context) { Node::NAMED_TYPE => function(NamedType $node, $key) use ($context) {
if ($key === 'type' || $key === 'typeCondition') { if ($key === 'type' || $key === 'typeCondition') {
$typeName = $node->value; $typeName = $node->name->value;
$type = $context->getSchema()->getType($typeName); $type = $context->getSchema()->getType($typeName);
if (!$type) { if (!$type) {
return new Error(Messages::unknownTypeMessage($typeName), [$node]); return new Error(self::unknownTypeMessage($typeName), [$node]);
} }
} }
} }

View File

@ -14,11 +14,16 @@ use GraphQL\Language\AST\FragmentDefinition;
use GraphQL\Language\AST\FragmentSpread; use GraphQL\Language\AST\FragmentSpread;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\Node;
use GraphQL\Language\Visitor; use GraphQL\Language\Visitor;
use GraphQL\Validator\Messages;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
class NoFragmentCycles class NoFragmentCycles
{ {
static function cycleErrorMessage($fragName, array $spreadNames = [])
{
$via = !empty($spreadNames) ? ' via ' . implode(', ', $spreadNames) : '';
return "Cannot spread fragment \"$fragName\" within itself$via.";
}
public function __invoke(ValidationContext $context) public function __invoke(ValidationContext $context)
{ {
// Gather all the fragment spreads ASTs for each fragment definition. // Gather all the fragment spreads ASTs for each fragment definition.
@ -67,7 +72,7 @@ class NoFragmentCycles
$knownToLeadToCycle[$spread] = true; $knownToLeadToCycle[$spread] = true;
} }
$errors[] = new Error( $errors[] = new Error(
Messages::cycleErrorMessage($initialName, array_map(function ($s) { self::cycleErrorMessage($initialName, array_map(function ($s) {
return $s->name->value; return $s->name->value;
}, $spreadPath)), }, $spreadPath)),
$cyclePath $cyclePath

View File

@ -23,6 +23,16 @@ use GraphQL\Validator\ValidationContext;
*/ */
class NoUndefinedVariables class NoUndefinedVariables
{ {
static function undefinedVarMessage($varName)
{
return "Variable \"$$varName\" is not defined.";
}
static function undefinedVarByOpMessage($varName, $opName)
{
return "Variable \"$$varName\" is not defined by operation \"$opName\".";
}
public function __invoke(ValidationContext $context) public function __invoke(ValidationContext $context)
{ {
$operation = null; $operation = null;
@ -53,12 +63,12 @@ class NoUndefinedVariables
} }
if ($withinFragment && $operation && $operation->name) { if ($withinFragment && $operation && $operation->name) {
return new Error( return new Error(
Messages::undefinedVarByOpMessage($varName, $operation->name->value), self::undefinedVarByOpMessage($varName, $operation->name->value),
[$variable, $operation] [$variable, $operation]
); );
} }
return new Error( return new Error(
Messages::undefinedVarMessage($varName), self::undefinedVarMessage($varName),
[$variable] [$variable]
); );
} }

View File

@ -11,6 +11,11 @@ use GraphQL\Validator\ValidationContext;
class NoUnusedFragments class NoUnusedFragments
{ {
static function unusedFragMessage($fragName)
{
return "Fragment \"$fragName\" is never used.";
}
public function __invoke(ValidationContext $context) public function __invoke(ValidationContext $context)
{ {
$fragmentDefs = []; $fragmentDefs = [];
@ -43,7 +48,7 @@ class NoUnusedFragments
foreach ($fragmentDefs as $def) { foreach ($fragmentDefs as $def) {
if (empty($fragmentNameUsed[$def->name->value])) { if (empty($fragmentNameUsed[$def->name->value])) {
$errors[] = new Error( $errors[] = new Error(
Messages::unusedFragMessage($def->name->value), self::unusedFragMessage($def->name->value),
[$def] [$def]
); );
} }
@ -59,6 +64,8 @@ class NoUnusedFragments
foreach ($spreads as $fragName => $fragment) { foreach ($spreads as $fragName => $fragment) {
if (empty($fragmentNameUsed[$fragName])) { if (empty($fragmentNameUsed[$fragName])) {
$fragmentNameUsed[$fragName] = true; $fragmentNameUsed[$fragName] = true;
if (isset($fragAdjacencies->{$fragName})) {
$this->reduceSpreadFragments( $this->reduceSpreadFragments(
$fragAdjacencies->{$fragName}, $fragAdjacencies->{$fragName},
$fragmentNameUsed, $fragmentNameUsed,
@ -67,4 +74,5 @@ class NoUnusedFragments
} }
} }
} }
}
} }

View File

@ -10,6 +10,11 @@ use GraphQL\Validator\ValidationContext;
class NoUnusedVariables class NoUnusedVariables
{ {
static function unusedVariableMessage($varName)
{
return "Variable \"$$varName\" is never used.";
}
public function __invoke(ValidationContext $context) public function __invoke(ValidationContext $context)
{ {
$visitedFragmentNames = new \stdClass(); $visitedFragmentNames = new \stdClass();
@ -30,7 +35,7 @@ class NoUnusedVariables
foreach ($variableDefs as $def) { foreach ($variableDefs as $def) {
if (empty($variableNameUsed->{$def->variable->name->value})) { if (empty($variableNameUsed->{$def->variable->name->value})) {
$errors[] = new Error( $errors[] = new Error(
Messages::unusedVariableMessage($def->variable->name->value), self::unusedVariableMessage($def->variable->name->value),
[$def] [$def]
); );
} }

View File

@ -3,19 +3,40 @@ namespace GraphQL\Validator\Rules;
use GraphQL\Error; use GraphQL\Error;
use GraphQL\Language\AST\Directive;
use GraphQL\Language\AST\FragmentSpread; use GraphQL\Language\AST\FragmentSpread;
use GraphQL\Language\AST\InlineFragment; use GraphQL\Language\AST\InlineFragment;
use GraphQL\Language\AST\NamedType;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\SelectionSet; use GraphQL\Language\AST\SelectionSet;
use GraphQL\Language\Printer; use GraphQL\Language\Printer;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Utils;
use GraphQL\Utils\PairSet; use GraphQL\Utils\PairSet;
use GraphQL\Utils\TypeInfo; use GraphQL\Utils\TypeInfo;
use GraphQL\Validator\Messages;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
class OverlappingFieldsCanBeMerged class OverlappingFieldsCanBeMerged
{ {
static function fieldsConflictMessage($responseName, $reason)
{
$reasonMessage = self::reasonMessage($reason);
return "Fields \"$responseName\" conflict because $reasonMessage.";
}
static function reasonMessage($reason)
{
if (is_array($reason)) {
$tmp = array_map(function ($tmp) {
list($responseName, $subReason) = $tmp;
$reasonMessage = self::reasonMessage($subReason);
return "subfields \"$responseName\" conflict because $reasonMessage";
}, $reason);
return implode(' and ', $tmp);
}
return $reason;
}
public function __invoke(ValidationContext $context) public function __invoke(ValidationContext $context)
{ {
$comparedSet = new PairSet(); $comparedSet = new PairSet();
@ -27,7 +48,7 @@ class OverlappingFieldsCanBeMerged
'leave' => function(SelectionSet $selectionSet) use ($context, $comparedSet) { 'leave' => function(SelectionSet $selectionSet) use ($context, $comparedSet) {
$fieldMap = $this->collectFieldASTsAndDefs( $fieldMap = $this->collectFieldASTsAndDefs(
$context, $context,
$context->getType(), $context->getParentType(),
$selectionSet $selectionSet
); );
@ -37,11 +58,11 @@ class OverlappingFieldsCanBeMerged
return array_map(function ($conflict) { return array_map(function ($conflict) {
$responseName = $conflict[0][0]; $responseName = $conflict[0][0];
$reason = $conflict[0][1]; $reason = $conflict[0][1];
$blameNodes = $conflict[1]; $fields = $conflict[1];
return new Error( return new Error(
Messages::fieldsConflictMessage($responseName, $reason), self::fieldsConflictMessage($responseName, $reason),
$blameNodes $fields
); );
}, $conflicts); }, $conflicts);
@ -101,7 +122,7 @@ class OverlappingFieldsCanBeMerged
$type1 = isset($def1) ? $def1->getType() : null; $type1 = isset($def1) ? $def1->getType() : null;
$type2 = isset($def2) ? $def2->getType() : null; $type2 = isset($def2) ? $def2->getType() : null;
if (!$this->sameType($type1, $type2)) { if ($type1 && $type2 && !$this->sameType($type1, $type2)) {
return [ return [
[$responseName, "they return differing types $type1 and $type2"], [$responseName, "they return differing types $type1 and $type2"],
[$ast1, $ast2] [$ast1, $ast2]
@ -111,7 +132,7 @@ class OverlappingFieldsCanBeMerged
$args1 = isset($ast1->arguments) ? $ast1->arguments : []; $args1 = isset($ast1->arguments) ? $ast1->arguments : [];
$args2 = isset($ast2->arguments) ? $ast2->arguments : []; $args2 = isset($ast2->arguments) ? $ast2->arguments : [];
if (!$this->sameNameValuePairs($args1, $args2)) { if (!$this->sameArguments($args1, $args2)) {
return [ return [
[$responseName, 'they have differing arguments'], [$responseName, 'they have differing arguments'],
[$ast1, $ast2] [$ast1, $ast2]
@ -121,7 +142,7 @@ class OverlappingFieldsCanBeMerged
$directives1 = isset($ast1->directives) ? $ast1->directives : []; $directives1 = isset($ast1->directives) ? $ast1->directives : [];
$directives2 = isset($ast2->directives) ? $ast2->directives : []; $directives2 = isset($ast2->directives) ? $ast2->directives : [];
if (!$this->sameNameValuePairs($directives1, $directives2)) { if (!$this->sameDirectives($directives1, $directives2)) {
return [ return [
[$responseName, 'they have differing directives'], [$responseName, 'they have differing directives'],
[$ast1, $ast2] [$ast1, $ast2]
@ -136,13 +157,13 @@ class OverlappingFieldsCanBeMerged
$subfieldMap = $this->collectFieldASTsAndDefs( $subfieldMap = $this->collectFieldASTsAndDefs(
$context, $context,
$type1, Type::getNamedType($type1),
$selectionSet1, $selectionSet1,
$visitedFragmentNames $visitedFragmentNames
); );
$subfieldMap = $this->collectFieldASTsAndDefs( $subfieldMap = $this->collectFieldASTsAndDefs(
$context, $context,
$type2, Type::getNamedType($type2),
$selectionSet2, $selectionSet2,
$visitedFragmentNames, $visitedFragmentNames,
$subfieldMap $subfieldMap
@ -152,7 +173,7 @@ class OverlappingFieldsCanBeMerged
if (!empty($conflicts)) { if (!empty($conflicts)) {
return [ return [
[$responseName, array_map(function ($conflict) { return $conflict[0]; }, $conflicts)], [$responseName, array_map(function ($conflict) { return $conflict[0]; }, $conflicts)],
array_reduce($conflicts, function ($list, $conflict) { return array_merge($list, $conflict[1]); }, [$ast1, $ast2]) array_reduce($conflicts, function ($allFields, $conflict) { return array_merge($allFields, $conflict[1]); }, [$ast1, $ast2])
]; ];
} }
} }
@ -183,8 +204,7 @@ class OverlappingFieldsCanBeMerged
switch ($selection->kind) { switch ($selection->kind) {
case Node::FIELD: case Node::FIELD:
$fieldAST = $selection; $fieldName = $selection->name->value;
$fieldName = $fieldAST->name->value;
$fieldDef = null; $fieldDef = null;
if ($parentType && method_exists($parentType, 'getFields')) { if ($parentType && method_exists($parentType, 'getFields')) {
$tmp = $parentType->getFields(); $tmp = $parentType->getFields();
@ -192,28 +212,26 @@ class OverlappingFieldsCanBeMerged
$fieldDef = $tmp[$fieldName]; $fieldDef = $tmp[$fieldName];
} }
} }
$responseName = $fieldAST->alias ? $fieldAST->alias->value : $fieldName; $responseName = $selection->alias ? $selection->alias->value : $fieldName;
if (!isset($_astAndDefs[$responseName])) { if (!isset($_astAndDefs[$responseName])) {
$_astAndDefs[$responseName] = new \ArrayObject(); $_astAndDefs[$responseName] = new \ArrayObject();
} }
$_astAndDefs[$responseName][] = [$fieldAST, $fieldDef]; $_astAndDefs[$responseName][] = [$selection, $fieldDef];
break; break;
case Node::INLINE_FRAGMENT: case Node::INLINE_FRAGMENT:
/** @var InlineFragment $inlineFragment */ /** @var InlineFragment $inlineFragment */
$inlineFragment = $selection;
$_astAndDefs = $this->collectFieldASTsAndDefs( $_astAndDefs = $this->collectFieldASTsAndDefs(
$context, $context,
TypeInfo::typeFromAST($context->getSchema(), $inlineFragment->typeCondition), TypeInfo::typeFromAST($context->getSchema(), $selection->typeCondition),
$inlineFragment->selectionSet, $selection->selectionSet,
$_visitedFragmentNames, $_visitedFragmentNames,
$_astAndDefs $_astAndDefs
); );
break; break;
case Node::FRAGMENT_SPREAD: case Node::FRAGMENT_SPREAD:
/** @var FragmentSpread $fragmentSpread */ /** @var FragmentSpread $selection */
$fragmentSpread = $selection; $fragName = $selection->name->value;
$fragName = $fragmentSpread->name->value;
if (!empty($_visitedFragmentNames[$fragName])) { if (!empty($_visitedFragmentNames[$fragName])) {
continue; continue;
} }
@ -235,29 +253,53 @@ class OverlappingFieldsCanBeMerged
return $_astAndDefs; return $_astAndDefs;
} }
private function sameDirectives(array $directives1, array $directives2)
{
if (count($directives1) !== count($directives2)) {
return false;
}
foreach ($directives1 as $directive1) {
$directive2 = null;
foreach ($directives2 as $tmp) {
if ($tmp->name->value === $directive1->name->value) {
$directive2 = $tmp;
break;
}
}
if (!$directive2) {
return false;
}
if (!$this->sameArguments($directive1->arguments, $directive2->arguments)) {
return false;
}
}
return true;
}
/** /**
* @param Array<Argument | Directive> $pairs1 * @param Array<Argument | Directive> $pairs1
* @param Array<Argument | Directive> $pairs2 * @param Array<Argument | Directive> $pairs2
* @return bool|string * @return bool|string
*/ */
private function sameNameValuePairs(array $pairs1, array $pairs2) private function sameArguments(array $arguments1, array $arguments2)
{ {
if (count($pairs1) !== count($pairs2)) { if (count($arguments1) !== count($arguments2)) {
return false; return false;
} }
foreach ($pairs1 as $pair1) { foreach ($arguments1 as $arg1) {
$matchedPair2 = null; $arg2 = null;
foreach ($pairs2 as $pair2) { foreach ($arguments2 as $arg) {
if ($pair2->name->value === $pair1->name->value) { if ($arg->name->value === $arg1->name->value) {
$matchedPair2 = $pair2; $arg2 = $arg;
break; break;
} }
} }
if (!$matchedPair2) { if (!$arg2) {
return false; return false;
} }
if (!$this->sameValue($pair1->value, $matchedPair2->value)) { if (!$this->sameValue($arg1->value, $arg2->value)) {
return false; return false;
} }
} }
@ -271,6 +313,6 @@ class OverlappingFieldsCanBeMerged
function sameType($type1, $type2) function sameType($type1, $type2)
{ {
return (!$type1 && !$type2) || (string) $type1 === (string) $type2; return (string) $type1 === (string) $type2;
} }
} }

View File

@ -11,32 +11,41 @@ use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Definition\UnionType;
use GraphQL\Utils; use GraphQL\Utils;
use GraphQL\Validator\Messages;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
class PossibleFragmentSpreads class PossibleFragmentSpreads
{ {
static function typeIncompatibleSpreadMessage($fragName, $parentType, $fragType)
{
return "Fragment \"$fragName\" cannot be spread here as objects of type \"$parentType\" can never be of type \"$fragType\".";
}
static function typeIncompatibleAnonSpreadMessage($parentType, $fragType)
{
return "Fragment cannot be spread here as objects of type \"$parentType\" can never be of type \"$fragType\".";
}
public function __invoke(ValidationContext $context) public function __invoke(ValidationContext $context)
{ {
return [ return [
Node::INLINE_FRAGMENT => function(InlineFragment $node) use ($context) { Node::INLINE_FRAGMENT => function(InlineFragment $node) use ($context) {
$fragType = Type::getUnmodifiedType($context->getType()); $fragType = Type::getNamedType($context->getType());
$parentType = $context->getParentType(); $parentType = $context->getParentType();
if ($fragType && $parentType && !$this->doTypesOverlap($fragType, $parentType)) { if ($fragType && $parentType && !$this->doTypesOverlap($fragType, $parentType)) {
return new Error( return new Error(
Messages::typeIncompatibleAnonSpreadMessage($parentType, $fragType), self::typeIncompatibleAnonSpreadMessage($parentType, $fragType),
[$node] [$node]
); );
} }
}, },
Node::FRAGMENT_SPREAD => function(FragmentSpread $node) use ($context) { Node::FRAGMENT_SPREAD => function(FragmentSpread $node) use ($context) {
$fragName = $node->name->value; $fragName = $node->name->value;
$fragType = Type::getUnmodifiedType($this->getFragmentType($context, $fragName)); $fragType = Type::getNamedType($this->getFragmentType($context, $fragName));
$parentType = $context->getParentType(); $parentType = $context->getParentType();
if ($fragType && $parentType && !$this->doTypesOverlap($fragType, $parentType)) { if ($fragType && $parentType && !$this->doTypesOverlap($fragType, $parentType)) {
return new Error( return new Error(
Messages::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType), self::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType),
[$node] [$node]
); );
} }
@ -47,7 +56,7 @@ class PossibleFragmentSpreads
private function getFragmentType(ValidationContext $context, $name) private function getFragmentType(ValidationContext $context, $name)
{ {
$frag = $context->getFragment($name); $frag = $context->getFragment($name);
return $frag ? $context->getSchema()->getType($frag->typeCondition->value) : null; return $frag ? Utils\TypeInfo::typeFromAST($context->getSchema(), $frag->typeCondition) : null;
} }
private function doTypesOverlap($t1, $t2) private function doTypesOverlap($t1, $t2)

View File

@ -0,0 +1,87 @@
<?php
namespace GraphQL\Validator\Rules;
use GraphQL\Error;
use GraphQL\Language\AST\Directive;
use GraphQL\Language\AST\Field;
use GraphQL\Language\AST\Node;
use GraphQL\Language\Visitor;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Utils;
use GraphQL\Validator\ValidationContext;
class ProvidedNonNullArguments
{
static function missingFieldArgMessage($fieldName, $argName, $type)
{
return "Field \"$fieldName\" argument \"$argName\" of type \"$type\" is required but not provided.";
}
static function missingDirectiveArgMessage($directiveName, $argName, $type)
{
return "Directive \"@$directiveName\" argument \"$argName\" of type \"$type\" is required but not provided.";
}
public function __invoke(ValidationContext $context)
{
return [
Node::FIELD => [
'leave' => function(Field $fieldAST) use ($context) {
$fieldDef = $context->getFieldDef();
if (!$fieldDef) {
return Visitor::skipNode();
}
$errors = [];
$argASTs = $fieldAST->arguments ?: [];
$argASTMap = [];
foreach ($argASTs as $argAST) {
$argASTMap[$argAST->name->value] = $argASTs;
}
foreach ($fieldDef->args as $argDef) {
$argAST = isset($argASTMap[$argDef->name]) ? $argASTMap[$argDef->name] : null;
if (!$argAST && $argDef->getType() instanceof NonNull) {
$errors[] = new Error(
self::missingFieldArgMessage($fieldAST->name->value, $argDef->name, $argDef->getType()),
[$fieldAST]
);
}
}
if (!empty($errors)) {
return $errors;
}
}
],
Node::DIRECTIVE => [
'leave' => function(Directive $directiveAST) use ($context) {
$directiveDef = $context->getDirective();
if (!$directiveDef) {
return Visitor::skipNode();
}
$errors = [];
$argASTs = $directiveAST->arguments ?: [];
$argASTMap = [];
foreach ($argASTs as $argAST) {
$argASTMap[$argAST->name->value] = $argASTs;
}
foreach ($directiveDef->args as $argDef) {
$argAST = isset($argASTMap[$argDef->name]) ? $argASTMap[$argDef->name] : null;
if (!$argAST && $argDef->getType() instanceof NonNull) {
$errors[] = new Error(
self::missingDirectiveArgMessage($directiveAST->name->value, $argDef->name, $argDef->getType()),
[$directiveAST]
);
}
}
if (!empty($errors)) {
return $errors;
}
}
]
];
}
}

View File

@ -11,6 +11,16 @@ use GraphQL\Validator\ValidationContext;
class ScalarLeafs class ScalarLeafs
{ {
static function noSubselectionAllowedMessage($field, $type)
{
return "Field \"$field\" of type \"$type\" must not have a sub selection.";
}
static function requiredSubselectionMessage($field, $type)
{
return "Field \"$field\" of type \"$type\" must have a sub selection.";
}
public function __invoke(ValidationContext $context) public function __invoke(ValidationContext $context)
{ {
return [ return [
@ -20,13 +30,13 @@ class ScalarLeafs
if (Type::isLeafType($type)) { if (Type::isLeafType($type)) {
if ($node->selectionSet) { if ($node->selectionSet) {
return new Error( return new Error(
Messages::noSubselectionAllowedMessage($node->name->value, $type), self::noSubselectionAllowedMessage($node->name->value, $type),
[$node->selectionSet] [$node->selectionSet]
); );
} }
} else if (!$node->selectionSet) { } else if (!$node->selectionSet) {
return new Error( return new Error(
Messages::requiredSubselectionMessage($node->name->value, $type), self::requiredSubselectionMessage($node->name->value, $type),
[$node] [$node]
); );
} }

View File

@ -4,40 +4,35 @@ namespace GraphQL\Validator\Rules;
use GraphQL\Error; use GraphQL\Error;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\Type;
use GraphQL\Language\AST\VariableDefinition; use GraphQL\Language\AST\VariableDefinition;
use GraphQL\Language\Printer; use GraphQL\Language\Printer;
use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils; use GraphQL\Utils;
use GraphQL\Validator\Messages;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
class VariablesAreInputTypes class VariablesAreInputTypes
{ {
static function nonInputTypeOnVarMessage($variableName, $typeName)
{
return "Variable \"\$$variableName\" cannot be non-input type \"$typeName\".";
}
public function __invoke(ValidationContext $context) public function __invoke(ValidationContext $context)
{ {
return [ return [
Node::VARIABLE_DEFINITION => function(VariableDefinition $node) use ($context) { Node::VARIABLE_DEFINITION => function(VariableDefinition $node) use ($context) {
$typeName = $this->getTypeASTName($node->type); $type = Utils\TypeInfo::typeFromAST($context->getSchema(), $node->type);
$type = $context->getSchema()->getType($typeName);
if (!($type instanceof InputType)) { // If the variable type is not an input type, return an error.
if ($type && !Type::isInputType($type)) {
$variableName = $node->variable->name->value; $variableName = $node->variable->name->value;
return new Error( return new Error(
Messages::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)), self::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)),
[$node->type] [ $node->type ]
); );
} }
} }
]; ];
} }
private function getTypeASTName(Type $typeAST)
{
if ($typeAST->kind === Node::NAME) {
return $typeAST->value;
}
Utils::invariant($typeAST->type, 'Must be wrapping type');
return $this->getTypeASTName($typeAST->type);
}
} }

View File

@ -113,4 +113,14 @@ class ValidationContext
{ {
return $this->_typeInfo->getFieldDef(); return $this->_typeInfo->getFieldDef();
} }
function getDirective()
{
return $this->_typeInfo->getDirective();
}
function getArgument()
{
return $this->_typeInfo->getArgument();
}
} }

View File

@ -0,0 +1,244 @@
<?php
namespace GraphQL\Executor;
require_once __DIR__ . '/TestClasses.php';
use GraphQL\Language\Parser;
use GraphQL\Schema;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
class AbstractTest extends \PHPUnit_Framework_TestCase
{
// Execute: Handles execution of abstract types
public function testIsTypeOfUsedToResolveRuntimeTypeForInterface()
{
// isTypeOf used to resolve runtime type for Interface
$petType = new InterfaceType([
'name' => 'Pet',
'fields' => [
'name' => ['type' => Type::string()]
]
]);
// Added to interface type when defined
$dogType = new ObjectType([
'name' => 'Dog',
'interfaces' => [$petType],
'isTypeOf' => function($obj) { return $obj instanceof Dog; },
'fields' => [
'name' => ['type' => Type::string()],
'woofs' => ['type' => Type::boolean()]
]
]);
$catType = new ObjectType([
'name' => 'Cat',
'interfaces' => [$petType],
'isTypeOf' => function ($obj) {
return $obj instanceof Cat;
},
'fields' => [
'name' => ['type' => Type::string()],
'meows' => ['type' => Type::boolean()],
]
]);
$schema = new Schema(
new ObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => Type::listOf($petType),
'resolve' => function () {
return [new Dog('Odie', true), new Cat('Garfield', false)];
}
]
]
])
);
$query = '{
pets {
name
... on Dog {
woofs
}
... on Cat {
meows
}
}
}';
$expected = new ExecutionResult([
'pets' => [
['name' => 'Odie', 'woofs' => true],
['name' => 'Garfield', 'meows' => false]
]
]);
$this->assertEquals($expected, Executor::execute($schema, Parser::parse($query)));
}
public function testIsTypeOfUsedToResolveRuntimeTypeForUnion()
{
// isTypeOf used to resolve runtime type for Union
$dogType = new ObjectType([
'name' => 'Dog',
'isTypeOf' => function($obj) { return $obj instanceof Dog; },
'fields' => [
'name' => ['type' => Type::string()],
'woofs' => ['type' => Type::boolean()]
]
]);
$catType = new ObjectType([
'name' => 'Cat',
'isTypeOf' => function ($obj) {
return $obj instanceof Cat;
},
'fields' => [
'name' => ['type' => Type::string()],
'meows' => ['type' => Type::boolean()],
]
]);
$petType = new UnionType([
'name' => 'Pet',
'types' => [$dogType, $catType]
]);
$schema = new Schema(new ObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => Type::listOf($petType),
'resolve' => function() {
return [ new Dog('Odie', true), new Cat('Garfield', false) ];
}
]
]
]));
$query = '{
pets {
name
... on Dog {
woofs
}
... on Cat {
meows
}
}
}';
$expected = new ExecutionResult([
'pets' => [
['name' => 'Odie', 'woofs' => true],
['name' => 'Garfield', 'meows' => false]
]
]);
$this->assertEquals($expected, Executor::execute($schema, Parser::parse($query)));
}
function testResolveTypeOnInterfaceYieldsUsefulError()
{
$DogType = null;
$CatType = null;
$HumanType = null;
$PetType = new InterfaceType([
'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;
}
return null;
},
'fields' => [
'name' => ['type' => Type::string()]
]
]);
$HumanType = new ObjectType([
'name' => 'Human',
'fields' => [
'name' => ['type' => Type::string()],
]
]);
$DogType = new ObjectType([
'name' => 'Dog',
'interfaces' => [$PetType],
'fields' => [
'name' => ['type' => Type::string()],
'woofs' => ['type' => Type::boolean()],
]
]);
$CatType = new ObjectType([
'name' => 'Cat',
'interfaces' => [$PetType],
'fields' => [
'name' => ['type' => Type::string()],
'meows' => ['type' => Type::boolean()],
]
]);
$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')
];
}
]
]
]));
$query = '{
pets {
name
... on Dog {
woofs
}
... on Cat {
meows
}
}
}';
$expected = [
'data' => [
'pets' => [
['name' => 'Odie', 'woofs' => true],
['name' => 'Garfield', 'meows' => false],
null
]
],
'errors' => [
[ 'message' => 'Runtime Object type "Human" is not a possible type for "Pet".' ]
]
];
$this->assertEquals($expected, Executor::execute($schema, Parser::parse($query))->toArray());
}
}

View File

@ -19,16 +19,16 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase
public function testWorksOnScalars() public function testWorksOnScalars()
{ {
// if true includes scalar // if true includes scalar
$this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery('{ a, b @if:true }')); $this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery('{ a, b @include(if: true) }'));
// if false omits on scalar // if false omits on scalar
$this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery('{ a, b @if:false }')); $this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery('{ a, b @include(if: false) }'));
// unless false includes scalar // unless false includes scalar
$this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery('{ a, b @unless:false }')); $this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery('{ a, b @skip(if: false) }'));
// unless true omits scalar // unless true omits scalar
$this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery('{ a, b @unless:true }')); $this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery('{ a, b @skip(if: true) }'));
} }
public function testWorksOnFragmentSpreads() public function testWorksOnFragmentSpreads()
@ -37,7 +37,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase
$q = ' $q = '
query Q { query Q {
a a
...Frag @if:false ...Frag @include(if: false)
} }
fragment Frag on TestType { fragment Frag on TestType {
b b
@ -49,7 +49,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase
$q = ' $q = '
query Q { query Q {
a a
...Frag @if:true ...Frag @include(if: true)
} }
fragment Frag on TestType { fragment Frag on TestType {
b b
@ -61,7 +61,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase
$q = ' $q = '
query Q { query Q {
a a
...Frag @unless:false ...Frag @skip(if: false)
} }
fragment Frag on TestType { fragment Frag on TestType {
b b
@ -73,7 +73,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase
$q = ' $q = '
query Q { query Q {
a a
...Frag @unless:true ...Frag @skip(if: true)
} }
fragment Frag on TestType { fragment Frag on TestType {
b b
@ -88,7 +88,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase
$q = ' $q = '
query Q { query Q {
a a
... on TestType @if:false { ... on TestType @include(if: false) {
b b
} }
} }
@ -102,7 +102,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase
$q = ' $q = '
query Q { query Q {
a a
... on TestType @if:true { ... on TestType @include(if: true) {
b b
} }
} }
@ -116,7 +116,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase
$q = ' $q = '
query Q { query Q {
a a
... on TestType @unless:false { ... on TestType @skip(if: false) {
b b
} }
} }
@ -130,7 +130,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase
$q = ' $q = '
query Q { query Q {
a a
... on TestType @unless:true { ... on TestType @skip(if: true) {
b b
} }
} }
@ -149,7 +149,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase
a a
...Frag ...Frag
} }
fragment Frag on TestType @if:false { fragment Frag on TestType @include(if: false) {
b b
} }
'; ';
@ -161,7 +161,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase
a a
...Frag ...Frag
} }
fragment Frag on TestType @if:true { fragment Frag on TestType @include(if: true) {
b b
} }
'; ';
@ -173,7 +173,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase
a a
...Frag ...Frag
} }
fragment Frag on TestType @unless:false { fragment Frag on TestType @skip(if: false) {
b b
} }
'; ';
@ -185,7 +185,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase
a a
...Frag ...Frag
} }
fragment Frag on TestType @unless:true { fragment Frag on TestType @skip(if: true) {
b b
} }
'; ';
@ -213,13 +213,13 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase
private static function getData() private static function getData()
{ {
return self::$data ?: (self::$data = [ return self::$data ?: (self::$data = [
'a' => function() { return 'a'; }, 'a' => 'a',
'b' => function() { return 'b'; } 'b' => 'b'
]); ]);
} }
private function executeTestQuery($doc) private function executeTestQuery($doc)
{ {
return Executor::execute(self::getSchema(), self::getData(), Parser::parse($doc)); return Executor::execute(self::getSchema(), Parser::parse($doc), self::getData())->toArray();
} }
} }

View File

@ -168,7 +168,7 @@ class ExecutorSchemaTest extends \PHPUnit_Framework_TestCase
] ]
]; ];
$this->assertEquals($expected, Executor::execute($BlogSchema, null, Parser::parse($request), '', [])); $this->assertEquals($expected, Executor::execute($BlogSchema, Parser::parse($request))->toArray());
} }
private function article($id) private function article($id)

View File

@ -1,6 +1,7 @@
<?php <?php
namespace GraphQL\Executor; namespace GraphQL\Executor;
use GraphQL\Error;
use GraphQL\FormattedError; use GraphQL\FormattedError;
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
use GraphQL\Language\SourceLocation; use GraphQL\Language\SourceLocation;
@ -132,7 +133,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
]); ]);
$schema = new Schema($dataType); $schema = new Schema($dataType);
$this->assertEquals($expected, Executor::execute($schema, $data, $ast, 'Example', ['size' => 100])); $this->assertEquals($expected, Executor::execute($schema, $ast, $data, ['size' => 100], 'Example')->toArray());
} }
public function testMergesParallelFragments() public function testMergesParallelFragments()
@ -187,11 +188,12 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
] ]
]; ];
$this->assertEquals($expected, Executor::execute($schema, null, $ast)); $this->assertEquals($expected, Executor::execute($schema, $ast)->toArray());
} }
public function testThreadsContextCorrectly() public function testThreadsContextCorrectly()
{ {
// threads context correctly
$doc = 'query Example { a }'; $doc = 'query Example { a }';
$gotHere = false; $gotHere = false;
@ -214,7 +216,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
] ]
])); ]));
Executor::execute($schema, $data, $ast, 'Example', []); Executor::execute($schema, $ast, $data, [], 'Example');
$this->assertEquals(true, $gotHere); $this->assertEquals(true, $gotHere);
} }
@ -246,7 +248,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
] ]
] ]
])); ]));
Executor::execute($schema, null, $docAst, 'Example', []); Executor::execute($schema, $docAst, null, [], 'Example');
$this->assertSame($gotHere, true); $this->assertSame($gotHere, true);
} }
@ -255,6 +257,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
$doc = '{ $doc = '{
sync, sync,
syncError, syncError,
syncRawError,
async, async,
asyncReject, asyncReject,
asyncError asyncError
@ -265,9 +268,11 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
return 'sync'; return 'sync';
}, },
'syncError' => function () { 'syncError' => function () {
throw new \Exception('Error getting syncError'); throw new Error('Error getting syncError');
},
'syncRawError' => function() {
throw new \Exception('Error getting syncRawError');
}, },
// Following are inherited from JS reference implementation, but make no sense in this PHP impl // Following are inherited from JS reference implementation, but make no sense in this PHP impl
// leaving them just to simplify migrations from newer js versions // leaving them just to simplify migrations from newer js versions
'async' => function() { 'async' => function() {
@ -287,6 +292,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
'fields' => [ 'fields' => [
'sync' => ['type' => Type::string()], 'sync' => ['type' => Type::string()],
'syncError' => ['type' => Type::string()], 'syncError' => ['type' => Type::string()],
'syncRawError' => [ 'type' => Type::string() ],
'async' => ['type' => Type::string()], 'async' => ['type' => Type::string()],
'asyncReject' => ['type' => Type::string() ], 'asyncReject' => ['type' => Type::string() ],
'asyncError' => ['type' => Type::string()], 'asyncError' => ['type' => Type::string()],
@ -297,20 +303,22 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
'data' => [ 'data' => [
'sync' => 'sync', 'sync' => 'sync',
'syncError' => null, 'syncError' => null,
'syncRawError' => null,
'async' => 'async', 'async' => 'async',
'asyncReject' => null, 'asyncReject' => null,
'asyncError' => null, 'asyncError' => null,
], ],
'errors' => [ 'errors' => [
new FormattedError('Error getting syncError', [new SourceLocation(3, 7)]), FormattedError::create('Error getting syncError', [new SourceLocation(3, 7)]),
new FormattedError('Error getting asyncReject', [new SourceLocation(5, 7)]), FormattedError::create('Error getting syncRawError', [new SourceLocation(4, 7)]),
new FormattedError('Error getting asyncError', [new SourceLocation(6, 7)]) FormattedError::create('Error getting asyncReject', [new SourceLocation(6, 7)]),
FormattedError::create('Error getting asyncError', [new SourceLocation(7, 7)])
] ]
]; ];
$result = Executor::execute($schema, $data, $docAst); $result = Executor::execute($schema, $docAst, $data);
$this->assertEquals($expected, $result); $this->assertEquals($expected, $result->toArray());
} }
public function testUsesTheInlineOperationIfNoOperationIsProvided() public function testUsesTheInlineOperationIfNoOperationIsProvided()
@ -326,9 +334,9 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
] ]
])); ]));
$ex = Executor::execute($schema, $data, $ast); $ex = Executor::execute($schema, $ast, $data);
$this->assertEquals(['data' => ['a' => 'b']], $ex); $this->assertEquals(['data' => ['a' => 'b']], $ex->toArray());
} }
public function testUsesTheOnlyOperationIfNoOperationIsProvided() public function testUsesTheOnlyOperationIfNoOperationIsProvided()
@ -343,8 +351,8 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
] ]
])); ]));
$ex = Executor::execute($schema, $data, $ast); $ex = Executor::execute($schema, $ast, $data);
$this->assertEquals(['data' => ['a' => 'b']], $ex); $this->assertEquals(['data' => ['a' => 'b']], $ex->toArray());
} }
public function testThrowsIfNoOperationIsProvidedWithMultipleOperations() public function testThrowsIfNoOperationIsProvidedWithMultipleOperations()
@ -359,15 +367,12 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
] ]
])); ]));
$ex = Executor::execute($schema, $data, $ast); try {
Executor::execute($schema, $ast, $data);
$this->assertEquals( $this->fail('Expected exception is not thrown');
[ } catch (Error $err) {
'data' => null, $this->assertEquals('Must provide operation name if query contains multiple operations.', $err->getMessage());
'errors' => [new FormattedError('Must provide operation name if query contains multiple operations')] }
],
$ex
);
} }
public function testUsesTheQuerySchemaForQueries() public function testUsesTheQuerySchemaForQueries()
@ -390,8 +395,8 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
]) ])
); );
$queryResult = Executor::execute($schema, $data, $ast, 'Q'); $queryResult = Executor::execute($schema, $ast, $data, [], 'Q');
$this->assertEquals(['data' => ['a' => 'b']], $queryResult); $this->assertEquals(['data' => ['a' => 'b']], $queryResult->toArray());
} }
public function testUsesTheMutationSchemaForMutations() public function testUsesTheMutationSchemaForMutations()
@ -413,8 +418,8 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
] ]
]) ])
); );
$mutationResult = Executor::execute($schema, $data, $ast, 'M'); $mutationResult = Executor::execute($schema, $ast, $data, [], 'M');
$this->assertEquals(['data' => ['c' => 'd']], $mutationResult); $this->assertEquals(['data' => ['c' => 'd']], $mutationResult->toArray());
} }
public function testAvoidsRecursion() public function testAvoidsRecursion()
@ -440,8 +445,8 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
] ]
])); ]));
$queryResult = Executor::execute($schema, $data, $ast, 'Q'); $queryResult = Executor::execute($schema, $ast, $data, [], 'Q');
$this->assertEquals(['data' => ['a' => 'b']], $queryResult); $this->assertEquals(['data' => ['a' => 'b']], $queryResult->toArray());
} }
public function testDoesNotIncludeIllegalFieldsInOutput() public function testDoesNotIncludeIllegalFieldsInOutput()
@ -464,7 +469,106 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
] ]
]) ])
); );
$mutationResult = Executor::execute($schema, null, $ast); $mutationResult = Executor::execute($schema, $ast);
$this->assertEquals(['data' => []], $mutationResult); $this->assertEquals(['data' => []], $mutationResult->toArray());
}
public function testDoesNotIncludeArgumentsThatWereNotSet()
{
$schema = new Schema(
new ObjectType([
'name' => 'Type',
'fields' => [
'field' => [
'type' => Type::string(),
'resolve' => function($data, $args) {return $args ? json_encode($args) : '';},
'args' => [
'a' => ['type' => Type::boolean()],
'b' => ['type' => Type::boolean()],
'c' => ['type' => Type::boolean()],
'd' => ['type' => Type::int()],
'e' => ['type' => Type::int()]
]
]
]
])
);
$query = Parser::parse('{ field(a: true, c: false, e: 0) }');
$result = Executor::execute($schema, $query);
$expected = [
'data' => [
'field' => '{"a":true,"c":false,"e":0}'
]
];
$this->assertEquals($expected, $result->toArray());
/*
var query = parse('{ field(a: true, c: false, e: 0) }');
var result = await execute(schema, query);
expect(result).to.deep.equal({
data: {
field: '{"a":true,"c":false,"e":0}'
}
});
});
it('fails when an isTypeOf check is not met', async () => {
class Special {
constructor(value) {
this.value = value;
}
}
class NotSpecial {
constructor(value) {
this.value = value;
}
}
var SpecialType = new GraphQLObjectType({
name: 'SpecialType',
isTypeOf(obj) {
return obj instanceof Special;
},
fields: {
value: { type: GraphQLString }
}
});
var schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
specials: {
type: new GraphQLList(SpecialType),
resolve: rootValue => rootValue.specials
}
}
})
});
var query = parse('{ specials { value } }');
var value = {
specials: [ new Special('foo'), new NotSpecial('bar') ]
};
var result = await execute(schema, query, value);
expect(result.data).to.deep.equal({
specials: [
{ value: 'foo' },
null
]
});
expect(result.errors).to.have.lengthOf(1);
expect(result.errors).to.containSubset([
{ message:
'Expected value of type "SpecialType" but got: [object Object].',
locations: [ { line: 1, column: 3 } ] }
]);
});
*/
} }
} }

View File

@ -1,6 +1,7 @@
<?php <?php
namespace GraphQL\Executor; namespace GraphQL\Executor;
use GraphQL\Error;
use GraphQL\FormattedError; use GraphQL\FormattedError;
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
use GraphQL\Language\SourceLocation; use GraphQL\Language\SourceLocation;
@ -24,7 +25,7 @@ class ListsTest extends \PHPUnit_Framework_TestCase
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = ['data' => ['nest' => ['list' => [1,2]]]]; $expected = ['data' => ['nest' => ['list' => [1,2]]]];
$this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
} }
public function testHandlesListsOfNonNullsWhenTheyReturnNonNullValues() public function testHandlesListsOfNonNullsWhenTheyReturnNonNullValues()
@ -47,7 +48,7 @@ class ListsTest extends \PHPUnit_Framework_TestCase
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
} }
public function testHandlesNonNullListsOfWhenTheyReturnNonNullValues() public function testHandlesNonNullListsOfWhenTheyReturnNonNullValues()
@ -69,7 +70,7 @@ class ListsTest extends \PHPUnit_Framework_TestCase
] ]
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
} }
public function testHandlesNonNullListsOfNonNullsWhenTheyReturnNonNullValues() public function testHandlesNonNullListsOfNonNullsWhenTheyReturnNonNullValues()
@ -91,7 +92,7 @@ class ListsTest extends \PHPUnit_Framework_TestCase
] ]
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
} }
public function testHandlesListsWhenTheyReturnNullAsAValue() public function testHandlesListsWhenTheyReturnNullAsAValue()
@ -113,7 +114,7 @@ class ListsTest extends \PHPUnit_Framework_TestCase
] ]
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
} }
public function testHandlesListsOfNonNullsWhenTheyReturnNullAsAValue() public function testHandlesListsOfNonNullsWhenTheyReturnNullAsAValue()
@ -135,13 +136,13 @@ class ListsTest extends \PHPUnit_Framework_TestCase
] ]
], ],
'errors' => [ 'errors' => [
new FormattedError( FormattedError::create(
'Cannot return null for non-nullable type.', 'Cannot return null for non-nullable type.',
[new SourceLocation(4, 11)] [new SourceLocation(4, 11)]
) )
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
} }
public function testHandlesNonNullListsOfWhenTheyReturnNullAsAValue() public function testHandlesNonNullListsOfWhenTheyReturnNullAsAValue()
@ -160,7 +161,7 @@ class ListsTest extends \PHPUnit_Framework_TestCase
'nest' => ['nonNullListContainsNull' => [1, null, 2]] 'nest' => ['nonNullListContainsNull' => [1, null, 2]]
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
} }
public function testHandlesNonNullListsOfNonNullsWhenTheyReturnNullAsAValue() public function testHandlesNonNullListsOfNonNullsWhenTheyReturnNullAsAValue()
@ -180,13 +181,13 @@ class ListsTest extends \PHPUnit_Framework_TestCase
'nest' => null 'nest' => null
], ],
'errors' => [ 'errors' => [
new FormattedError( FormattedError::create(
'Cannot return null for non-nullable type.', 'Cannot return null for non-nullable type.',
[new SourceLocation(4, 11)] [new SourceLocation(4, 11)]
) )
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
} }
public function testHandlesListsWhenTheyReturnNull() public function testHandlesListsWhenTheyReturnNull()
@ -208,7 +209,7 @@ class ListsTest extends \PHPUnit_Framework_TestCase
] ]
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
} }
public function testHandlesListsOfNonNullsWhenTheyReturnNull() public function testHandlesListsOfNonNullsWhenTheyReturnNull()
@ -230,7 +231,7 @@ class ListsTest extends \PHPUnit_Framework_TestCase
] ]
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
} }
public function testHandlesNonNullListsOfWhenTheyReturnNull() public function testHandlesNonNullListsOfWhenTheyReturnNull()
@ -250,13 +251,13 @@ class ListsTest extends \PHPUnit_Framework_TestCase
'nest' => null, 'nest' => null,
], ],
'errors' => [ 'errors' => [
new FormattedError( FormattedError::create(
'Cannot return null for non-nullable type.', 'Cannot return null for non-nullable type.',
[new SourceLocation(4, 11)] [new SourceLocation(4, 11)]
) )
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
} }
public function testHandlesNonNullListsOfNonNullsWhenTheyReturnNull() public function testHandlesNonNullListsOfNonNullsWhenTheyReturnNull()
@ -276,13 +277,13 @@ class ListsTest extends \PHPUnit_Framework_TestCase
'nest' => null 'nest' => null
], ],
'errors' => [ 'errors' => [
new FormattedError( FormattedError::create(
'Cannot return null for non-nullable type.', 'Cannot return null for non-nullable type.',
[new SourceLocation(4, 11)] [new SourceLocation(4, 11)]
) )
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
} }

View File

@ -31,7 +31,7 @@ class MutationsTest extends \PHPUnit_Framework_TestCase
} }
}'; }';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$mutationResult = Executor::execute($this->schema(), new Root(6), $ast, 'M'); $mutationResult = Executor::execute($this->schema(), $ast, new Root(6), null, 'M');
$expected = [ $expected = [
'data' => [ 'data' => [
'first' => [ 'first' => [
@ -51,7 +51,7 @@ class MutationsTest extends \PHPUnit_Framework_TestCase
] ]
] ]
]; ];
$this->assertEquals($mutationResult, $expected); $this->assertEquals($expected, $mutationResult->toArray());
} }
public function testEvaluatesMutationsCorrectlyInThePresenseOfAFailedMutation() public function testEvaluatesMutationsCorrectlyInThePresenseOfAFailedMutation()
@ -77,7 +77,7 @@ class MutationsTest extends \PHPUnit_Framework_TestCase
} }
}'; }';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$mutationResult = Executor::execute($this->schema(), new Root(6), $ast, 'M'); $mutationResult = Executor::execute($this->schema(), $ast, new Root(6), null, 'M');
$expected = [ $expected = [
'data' => [ 'data' => [
'first' => [ 'first' => [
@ -96,17 +96,17 @@ class MutationsTest extends \PHPUnit_Framework_TestCase
'sixth' => null, 'sixth' => null,
], ],
'errors' => [ 'errors' => [
new FormattedError( FormattedError::create(
'Cannot change the number', 'Cannot change the number',
[new SourceLocation(8, 7)] [new SourceLocation(8, 7)]
), ),
new FormattedError( FormattedError::create(
'Cannot change the number', 'Cannot change the number',
[new SourceLocation(17, 7)] [new SourceLocation(17, 7)]
) )
] ]
]; ];
$this->assertEquals($expected, $mutationResult); $this->assertEquals($expected, $mutationResult->toArray());
} }
private function schema() private function schema()

View File

@ -86,13 +86,13 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
'sync' => null, 'sync' => null,
], ],
'errors' => [ 'errors' => [
new FormattedError( FormattedError::create(
$this->syncError->message, $this->syncError->message,
[new SourceLocation(3, 9)] [new SourceLocation(3, 9)]
) )
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema, $this->throwingData, $ast, 'Q', [])); $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, [], 'Q')->toArray());
} }
public function testNullsASynchronouslyReturnedObjectThatContainsANonNullableFieldThatThrowsSynchronously() public function testNullsASynchronouslyReturnedObjectThatContainsANonNullableFieldThatThrowsSynchronously()
@ -113,10 +113,10 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
'nest' => null 'nest' => null
], ],
'errors' => [ 'errors' => [
new FormattedError($this->nonNullSyncError->message, [new SourceLocation(4, 11)]) FormattedError::create($this->nonNullSyncError->message, [new SourceLocation(4, 11)])
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema, $this->throwingData, $ast, 'Q', [])); $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, [], 'Q')->toArray());
} }
public function testNullsAComplexTreeOfNullableFieldsThatThrow() public function testNullsAComplexTreeOfNullableFieldsThatThrow()
@ -144,11 +144,11 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
] ]
], ],
'errors' => [ 'errors' => [
new FormattedError($this->syncError->message, [new SourceLocation(4, 11)]), FormattedError::create($this->syncError->message, [new SourceLocation(4, 11)]),
new FormattedError($this->syncError->message, [new SourceLocation(6, 13)]), FormattedError::create($this->syncError->message, [new SourceLocation(6, 13)]),
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema, $this->throwingData, $ast, 'Q', [])); $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, [], 'Q')->toArray());
} }
public function testNullsANullableFieldThatSynchronouslyReturnsNull() public function testNullsANullableFieldThatSynchronouslyReturnsNull()
@ -166,7 +166,7 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
'sync' => null, 'sync' => null,
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema, $this->nullingData, $ast, 'Q', [])); $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, [], 'Q')->toArray());
} }
public function test4() public function test4()
@ -187,10 +187,10 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
'nest' => null 'nest' => null
], ],
'errors' => [ 'errors' => [
new FormattedError('Cannot return null for non-nullable type.', [new SourceLocation(4, 11)]) FormattedError::create('Cannot return null for non-nullable type.', [new SourceLocation(4, 11)])
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema, $this->nullingData, $ast, 'Q', [])); $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, [], 'Q')->toArray());
} }
public function test5() public function test5()
@ -226,7 +226,7 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
], ],
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema, $this->nullingData, $ast, 'Q', [])); $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, [], 'Q')->toArray());
} }
public function testNullsTheTopLevelIfSyncNonNullableFieldThrows() public function testNullsTheTopLevelIfSyncNonNullableFieldThrows()
@ -238,10 +238,10 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
$expected = [ $expected = [
'data' => null, 'data' => null,
'errors' => [ 'errors' => [
new FormattedError($this->nonNullSyncError->message, [new SourceLocation(2, 17)]) FormattedError::create($this->nonNullSyncError->message, [new SourceLocation(2, 17)])
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema, $this->throwingData, Parser::parse($doc))); $this->assertEquals($expected, Executor::execute($this->schema, Parser::parse($doc), $this->throwingData)->toArray());
} }
public function testNullsTheTopLevelIfSyncNonNullableFieldReturnsNull() public function testNullsTheTopLevelIfSyncNonNullableFieldReturnsNull()
@ -254,9 +254,9 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
$expected = [ $expected = [
'data' => null, 'data' => null,
'errors' => [ 'errors' => [
new FormattedError('Cannot return null for non-nullable type.', [new SourceLocation(2, 17)]), FormattedError::create('Cannot return null for non-nullable type.', [new SourceLocation(2, 17)]),
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema, $this->nullingData, Parser::parse($doc))); $this->assertEquals($expected, Executor::execute($this->schema, Parser::parse($doc), $this->nullingData)->toArray());
} }
} }

View File

@ -0,0 +1,78 @@
<?php
namespace GraphQL\Executor;
use GraphQL\Type\Definition\ScalarType;
class Dog
{
function __construct($name, $woofs)
{
$this->name = $name;
$this->woofs = $woofs;
}
}
class Cat
{
function __construct($name, $meows)
{
$this->name = $name;
$this->meows = $meows;
}
}
class Human
{
function __construct($name)
{
$this->name = $name;
}
}
class Person
{
public $name;
public $pets;
public $friends;
function __construct($name, $pets = null, $friends = null)
{
$this->name = $name;
$this->pets = $pets;
$this->friends = $friends;
}
}
class ComplexScalar extends ScalarType
{
public static function create()
{
return new self();
}
public $name = 'ComplexScalar';
public function serialize($value)
{
if ($value === 'DeserializedValue') {
return 'SerializedValue';
}
return null;
}
public function parseValue($value)
{
if ($value === 'SerializedValue') {
return 'DeserializedValue';
}
return null;
}
public function parseLiteral($valueAST)
{
if ($valueAST->value === 'SerializedValue') {
return 'DeserializedValue';
}
return null;
}
}

View File

@ -1,6 +1,8 @@
<?php <?php
namespace GraphQL\Executor; namespace GraphQL\Executor;
require_once __DIR__ . '/TestClasses.php';
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
use GraphQL\Schema; use GraphQL\Schema;
use GraphQL\Type\Definition\Config; use GraphQL\Type\Definition\Config;
@ -31,7 +33,7 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
'interfaces' => [$NamedType], 'interfaces' => [$NamedType],
'fields' => [ 'fields' => [
'name' => ['type' => Type::string()], 'name' => ['type' => Type::string()],
'barks' => ['type' => Type::boolean()] 'woofs' => ['type' => Type::boolean()]
], ],
'isTypeOf' => function ($value) { 'isTypeOf' => function ($value) {
return $value instanceof Dog; return $value instanceof Dog;
@ -143,7 +145,7 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
] ]
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema, null, $ast)); $this->assertEquals($expected, Executor::execute($this->schema, $ast)->toArray());
} }
public function testExecutesUsingUnionTypes() public function testExecutesUsingUnionTypes()
@ -156,7 +158,7 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
pets { pets {
__typename __typename
name name
barks woofs
meows meows
} }
} }
@ -167,12 +169,12 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
'name' => 'John', 'name' => 'John',
'pets' => [ 'pets' => [
['__typename' => 'Cat', 'name' => 'Garfield', 'meows' => false], ['__typename' => 'Cat', 'name' => 'Garfield', 'meows' => false],
['__typename' => 'Dog', 'name' => 'Odie', 'barks' => true] ['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true]
] ]
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema, $this->john, $ast)); $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray());
} }
public function testExecutesUnionTypesWithInlineFragments() public function testExecutesUnionTypesWithInlineFragments()
@ -186,7 +188,7 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
__typename __typename
... on Dog { ... on Dog {
name name
barks woofs
} }
... on Cat { ... on Cat {
name name
@ -201,12 +203,12 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
'name' => 'John', 'name' => 'John',
'pets' => [ 'pets' => [
['__typename' => 'Cat', 'name' => 'Garfield', 'meows' => false], ['__typename' => 'Cat', 'name' => 'Garfield', 'meows' => false],
['__typename' => 'Dog', 'name' => 'Odie', 'barks' => true] ['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true]
] ]
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema, $this->john, $ast)); $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray());
} }
public function testExecutesUsingInterfaceTypes() public function testExecutesUsingInterfaceTypes()
@ -219,7 +221,7 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
friends { friends {
__typename __typename
name name
barks woofs
meows meows
} }
} }
@ -230,12 +232,12 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
'name' => 'John', 'name' => 'John',
'friends' => [ 'friends' => [
['__typename' => 'Person', 'name' => 'Liz'], ['__typename' => 'Person', 'name' => 'Liz'],
['__typename' => 'Dog', 'name' => 'Odie', 'barks' => true] ['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true]
] ]
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema, $this->john, $ast)); $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray());
} }
public function testExecutesInterfaceTypesWithInlineFragments() public function testExecutesInterfaceTypesWithInlineFragments()
@ -249,7 +251,7 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
__typename __typename
name name
... on Dog { ... on Dog {
barks woofs
} }
... on Cat { ... on Cat {
meows meows
@ -263,12 +265,12 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
'name' => 'John', 'name' => 'John',
'friends' => [ 'friends' => [
['__typename' => 'Person', 'name' => 'Liz'], ['__typename' => 'Person', 'name' => 'Liz'],
['__typename' => 'Dog', 'name' => 'Odie', 'barks' => true] ['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true]
] ]
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema, $this->john, $ast)); $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray());
} }
public function testAllowsFragmentConditionsToBeAbstractTypes() public function testAllowsFragmentConditionsToBeAbstractTypes()
@ -285,7 +287,7 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
__typename __typename
... on Dog { ... on Dog {
name name
barks woofs
} }
... on Cat { ... on Cat {
name name
@ -297,7 +299,7 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
__typename __typename
name name
... on Dog { ... on Dog {
barks woofs
} }
... on Cat { ... on Cat {
meows meows
@ -311,54 +313,15 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
'name' => 'John', 'name' => 'John',
'pets' => [ 'pets' => [
['__typename' => 'Cat', 'name' => 'Garfield', 'meows' => false], ['__typename' => 'Cat', 'name' => 'Garfield', 'meows' => false],
['__typename' => 'Dog', 'name' => 'Odie', 'barks' => true] ['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true]
], ],
'friends' => [ 'friends' => [
['__typename' => 'Person', 'name' => 'Liz'], ['__typename' => 'Person', 'name' => 'Liz'],
['__typename' => 'Dog', 'name' => 'Odie', 'barks' => true] ['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true]
] ]
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema, $this->john, $ast)); $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray());
}
}
class Dog
{
public $name;
public $barks;
function __construct($name, $barks)
{
$this->name = $name;
$this->barks = $barks;
}
}
class Cat
{
public $name;
public $meows;
function __construct($name, $meows)
{
$this->name = $name;
$this->meows = $meows;
}
}
class Person
{
public $name;
public $pets;
public $friends;
function __construct($name, $pets = null, $friends = null)
{
$this->name = $name;
$this->pets = $pets;
$this->friends = $friends;
} }
} }

View File

@ -1,17 +1,21 @@
<?php <?php
namespace GraphQL\Executor; namespace GraphQL\Executor;
require_once __DIR__ . '/TestClasses.php';
use GraphQL\Error;
use GraphQL\FormattedError; use GraphQL\FormattedError;
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
use GraphQL\Language\SourceLocation; use GraphQL\Language\SourceLocation;
use GraphQL\Schema; use GraphQL\Schema;
use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
class InputObjectTest extends \PHPUnit_Framework_TestCase class VariablesTest extends \PHPUnit_Framework_TestCase
{ {
// Execute: Handles input objects // Execute: Handles inputs
// Handles objects and nullability // Handles objects and nullability
public function testUsingInlineStructs() public function testUsingInlineStructs()
@ -29,9 +33,9 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
'fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}' 'fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}'
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast)); $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
// properly coerces single value to array: // properly parses single value to list:
$doc = ' $doc = '
{ {
fieldWithObjectInput(input: {a: "foo", b: "bar", c: "baz"}) fieldWithObjectInput(input: {a: "foo", b: "bar", c: "baz"})
@ -40,7 +44,23 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = ['data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}']]; $expected = ['data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}']];
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast)); $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
}
public function testDoesNotUseIncorrectValue()
{
$doc = '
{
fieldWithObjectInput(input: ["foo", "bar", "baz"])
}
';
$ast = Parser::parse($doc);
$result = Executor::execute($this->schema(), $ast)->toArray();
$expected = [
'data' => ['fieldWithObjectInput' => null]
];
$this->assertEquals($expected, $result);
} }
public function testUsingVariables() public function testUsingVariables()
@ -57,46 +77,100 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
$this->assertEquals( $this->assertEquals(
['data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}']], ['data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}']],
Executor::execute($schema, null, $ast, null, $params) Executor::execute($schema, $ast, null, $params)->toArray()
); );
// properly coerces single value to array: // uses default value when not provided:
$withDefaultsAST = Parser::parse('
query q($input: TestInputObject = {a: "foo", b: ["bar"], c: "baz"}) {
fieldWithObjectInput(input: $input)
}
');
$result = Executor::execute($this->schema(), $withDefaultsAST)->toArray();
$expected = [
'data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}']
];
$this->assertEquals($expected, $result);
// properly parses single value to array:
$params = ['input' => ['a' => 'foo', 'b' => 'bar', 'c' => 'baz']]; $params = ['input' => ['a' => 'foo', 'b' => 'bar', 'c' => 'baz']];
$this->assertEquals( $this->assertEquals(
['data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}']], ['data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}']],
Executor::execute($schema, null, $ast, null, $params) Executor::execute($schema, $ast, null, $params)->toArray()
); );
// executes with complex scalar input:
$params = [ 'input' => [ 'c' => 'foo', 'd' => 'SerializedValue' ] ];
$result = Executor::execute($schema, $ast, null, $params)->toArray();
$expected = [
'data' => [
'fieldWithObjectInput' => '{"c":"foo","d":"DeserializedValue"}'
]
];
$this->assertEquals($expected, $result);
// errors on null for nested non-null: // errors on null for nested non-null:
$params = ['input' => ['a' => 'foo', 'b' => 'bar', 'c' => null]]; $params = ['input' => ['a' => 'foo', 'b' => 'bar', 'c' => null]];
$expected = [ $expected = FormattedError::create(
'data' => null,
'errors' => [
new FormattedError(
'Variable $input expected value of type ' . 'Variable $input expected value of type ' .
'TestInputObject but got: ' . 'TestInputObject but got: ' .
'{"a":"foo","b":"bar","c":null}.', '{"a":"foo","b":"bar","c":null}.',
[new SourceLocation(2, 17)] [new SourceLocation(2, 17)]
) );
]
];
$this->assertEquals($expected, Executor::execute($schema, null, $ast, null, $params)); try {
Executor::execute($schema, $ast, null, $params);
$this->fail('Expected exception not thrown');
} catch (Error $err) {
$this->assertEquals($expected, Error::formatError($err));
}
// errors on incorrect type:
$params = [ 'input' => 'foo bar' ];
try {
Executor::execute($schema, $ast, 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".',
[new SourceLocation(2, 17)]
);
$this->assertEquals($expected, Error::formatError($error));
}
// errors on omission of nested non-null: // errors on omission of nested non-null:
$params = ['input' => ['a' => 'foo', 'b' => 'bar']]; $params = ['input' => ['a' => 'foo', 'b' => 'bar']];
$expected = [
'data' => null, try {
'errors' => [ Executor::execute($schema, $ast, null, $params);
new FormattedError( $this->fail('Expected exception not thrown');
} catch (Error $e) {
$expected = FormattedError::create(
'Variable $input expected value of type ' . 'Variable $input expected value of type ' .
'TestInputObject but got: {"a":"foo","b":"bar"}.', 'TestInputObject but got: {"a":"foo","b":"bar"}.',
[new SourceLocation(2, 17)] [new SourceLocation(2, 17)]
) );
]
];
$this->assertEquals($expected, Executor::execute($schema, null, $ast, null, $params)); $this->assertEquals($expected, Error::formatError($e));
}
// errors on addition of unknown input field
$params = ['input' => [ 'a' => 'foo', 'b' => 'bar', 'c' => 'baz', 'd' => 'dog' ]];
try {
Executor::execute($schema, $ast, 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"}.',
[new SourceLocation(2, 17)]
);
$this->assertEquals($expected, Error::formatError($e));
}
} }
@ -110,10 +184,10 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = [ $expected = [
'data' => ['fieldWithNullableStringInput' => 'null'] 'data' => ['fieldWithNullableStringInput' => null]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast)); $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
} }
public function testAllowsNullableInputsToBeOmittedInAVariable() public function testAllowsNullableInputsToBeOmittedInAVariable()
@ -124,9 +198,9 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
} }
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = ['data' => ['fieldWithNullableStringInput' => 'null']]; $expected = ['data' => ['fieldWithNullableStringInput' => null]];
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast)); $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
} }
public function testAllowsNullableInputsToBeOmittedInAnUnlistedVariable() public function testAllowsNullableInputsToBeOmittedInAnUnlistedVariable()
@ -137,8 +211,8 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
} }
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = ['data' => ['fieldWithNullableStringInput' => 'null']]; $expected = ['data' => ['fieldWithNullableStringInput' => null]];
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast)); $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
} }
public function testAllowsNullableInputsToBeSetToNullInAVariable() public function testAllowsNullableInputsToBeSetToNullInAVariable()
@ -149,21 +223,9 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
} }
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = ['data' => ['fieldWithNullableStringInput' => 'null']]; $expected = ['data' => ['fieldWithNullableStringInput' => null]];
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['value' => null])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['value' => null])->toArray());
}
public function testAllowsNullableInputsToBeSetToNullDirectly()
{
$doc = '
{
fieldWithNullableStringInput(input: null)
}
';
$ast = Parser::parse($doc);
$expected = ['data' => ['fieldWithNullableStringInput' => 'null']];
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast));
} }
public function testAllowsNullableInputsToBeSetToAValueInAVariable() public function testAllowsNullableInputsToBeSetToAValueInAVariable()
@ -175,7 +237,7 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = ['data' => ['fieldWithNullableStringInput' => '"a"']]; $expected = ['data' => ['fieldWithNullableStringInput' => '"a"']];
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['value' => 'a'])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['value' => 'a'])->toArray());
} }
public function testAllowsNullableInputsToBeSetToAValueDirectly() public function testAllowsNullableInputsToBeSetToAValueDirectly()
@ -187,7 +249,7 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = ['data' => ['fieldWithNullableStringInput' => '"a"']]; $expected = ['data' => ['fieldWithNullableStringInput' => '"a"']];
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast)); $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
} }
@ -201,16 +263,16 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
} }
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = [ try {
'data' => null, Executor::execute($this->schema(), $ast);
'errors' => [ $this->fail('Expected exception not thrown');
new FormattedError( } catch (Error $e) {
$expected = FormattedError::create(
'Variable $value expected value of type String! but got: null.', 'Variable $value expected value of type String! but got: null.',
[new SourceLocation(2, 31)] [new SourceLocation(2, 31)]
) );
] $this->assertEquals($expected, Error::formatError($e));
]; }
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast));
} }
public function testDoesNotAllowNonNullableInputsToBeSetToNullInAVariable() public function testDoesNotAllowNonNullableInputsToBeSetToNullInAVariable()
@ -222,16 +284,17 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
} }
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = [
'data' => null, try {
'errors' => [ Executor::execute($this->schema(), $ast, null, ['value' => null]);
new FormattedError( $this->fail('Expected exception not thrown');
} catch (Error $e) {
$expected = FormattedError::create(
'Variable $value expected value of type String! but got: null.', 'Variable $value expected value of type String! but got: null.',
[new SourceLocation(2, 31)] [new SourceLocation(2, 31)]
) );
] $this->assertEquals($expected, Error::formatError($e));
]; }
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['value' => null]));
} }
public function testAllowsNonNullableInputsToBeSetToAValueInAVariable() public function testAllowsNonNullableInputsToBeSetToAValueInAVariable()
@ -243,7 +306,7 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = ['data' => ['fieldWithNonNullableStringInput' => '"a"']]; $expected = ['data' => ['fieldWithNonNullableStringInput' => '"a"']];
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['value' => 'a'])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['value' => 'a'])->toArray());
} }
public function testAllowsNonNullableInputsToBeSetToAValueDirectly() public function testAllowsNonNullableInputsToBeSetToAValueDirectly()
@ -256,7 +319,7 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = ['data' => ['fieldWithNonNullableStringInput' => '"a"']]; $expected = ['data' => ['fieldWithNonNullableStringInput' => '"a"']];
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast)); $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
} }
public function testPassesAlongNullForNonNullableInputsIfExplcitlySetInTheQuery() public function testPassesAlongNullForNonNullableInputsIfExplcitlySetInTheQuery()
@ -267,8 +330,8 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
} }
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = ['data' => ['fieldWithNonNullableStringInput' => 'null']]; $expected = ['data' => ['fieldWithNonNullableStringInput' => null]];
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast)); $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
} }
// Handles lists and nullability // Handles lists and nullability
@ -280,9 +343,9 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
} }
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = ['data' => ['list' => 'null']]; $expected = ['data' => ['list' => null]];
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => null])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => null])->toArray());
} }
public function testAllowsListsToContainValues() public function testAllowsListsToContainValues()
@ -294,7 +357,7 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = ['data' => ['list' => '["A"]']]; $expected = ['data' => ['list' => '["A"]']];
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => ['A']])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => ['A']])->toArray());
} }
public function testAllowsListsToContainNull() public function testAllowsListsToContainNull()
@ -306,7 +369,7 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = ['data' => ['list' => '["A",null,"B"]']]; $expected = ['data' => ['list' => '["A",null,"B"]']];
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => ['A',null,'B']])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => ['A',null,'B']])->toArray());
} }
public function testDoesNotAllowNonNullListsToBeNull() public function testDoesNotAllowNonNullListsToBeNull()
@ -317,17 +380,17 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
} }
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = [ $expected = FormattedError::create(
'data' => null,
'errors' => [
new FormattedError(
'Variable $input expected value of type [String]! but got: null.', 'Variable $input expected value of type [String]! but got: null.',
[new SourceLocation(2, 17)] [new SourceLocation(2, 17)]
) );
]
];
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => null])); try {
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => null])->toArray());
$this->fail('Expected exception not thrown');
} catch (Error $e) {
$this->assertEquals($expected, Error::formatError($e));
}
} }
public function testAllowsNonNullListsToContainValues() public function testAllowsNonNullListsToContainValues()
@ -339,7 +402,7 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = ['data' => ['nnList' => '["A"]']]; $expected = ['data' => ['nnList' => '["A"]']];
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => 'A'])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => 'A'])->toArray());
} }
public function testAllowsNonNullListsToContainNull() public function testAllowsNonNullListsToContainNull()
@ -352,7 +415,7 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = ['data' => ['nnList' => '["A",null,"B"]']]; $expected = ['data' => ['nnList' => '["A",null,"B"]']];
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => ['A',null,'B']])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => ['A',null,'B']])->toArray());
} }
public function testAllowsListsOfNonNullsToBeNull() public function testAllowsListsOfNonNullsToBeNull()
@ -363,8 +426,8 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
} }
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = ['data' => ['listNN' => 'null']]; $expected = ['data' => ['listNN' => null]];
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => null])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => null])->toArray());
} }
public function testAllowsListsOfNonNullsToContainValues() public function testAllowsListsOfNonNullsToContainValues()
@ -377,7 +440,7 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = ['data' => ['listNN' => '["A"]']]; $expected = ['data' => ['listNN' => '["A"]']];
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => 'A'])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => 'A'])->toArray());
} }
public function testDoesNotAllowListsOfNonNullsToContainNull() public function testDoesNotAllowListsOfNonNullsToContainNull()
@ -388,16 +451,17 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
} }
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = [ $expected = FormattedError::create(
'data' => null,
'errors' => [
new FormattedError(
'Variable $input expected value of type [String!] but got: ["A",null,"B"].', 'Variable $input expected value of type [String!] but got: ["A",null,"B"].',
[new SourceLocation(2, 17)] [new SourceLocation(2, 17)]
) );
]
]; try {
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => ['A', null, 'B']])); Executor::execute($this->schema(), $ast, null, ['input' => ['A', null, 'B']]);
$this->fail('Expected exception not thrown');
} catch (Error $e) {
$this->assertEquals($expected, Error::formatError($e));
}
} }
public function testDoesNotAllowNonNullListsOfNonNullsToBeNull() public function testDoesNotAllowNonNullListsOfNonNullsToBeNull()
@ -408,16 +472,15 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
} }
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = [ $expected = FormattedError::create(
'data' => null,
'errors' => [
new FormattedError(
'Variable $input expected value of type [String!]! but got: null.', 'Variable $input expected value of type [String!]! but got: null.',
[new SourceLocation(2, 17)] [new SourceLocation(2, 17)]
) );
] try {
]; Executor::execute($this->schema(), $ast, null, ['input' => null]);
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => null])); } catch (Error $e) {
$this->assertEquals($expected, Error::formatError($e));
}
} }
public function testAllowsNonNullListsOfNonNullsToContainValues() public function testAllowsNonNullListsOfNonNullsToContainValues()
@ -429,7 +492,7 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = ['data' => ['nnListNN' => '["A"]']]; $expected = ['data' => ['nnListNN' => '["A"]']];
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => ['A']])); $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => ['A']])->toArray());
} }
public function testDoesNotAllowNonNullListsOfNonNullsToContainNull() public function testDoesNotAllowNonNullListsOfNonNullsToContainNull()
@ -440,27 +503,29 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
} }
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
$expected = [ $expected = FormattedError::create(
'data' => null,
'errors' => [
new FormattedError(
'Variable $input expected value of type [String!]! but got: ["A",null,"B"].', 'Variable $input expected value of type [String!]! but got: ["A",null,"B"].',
[new SourceLocation(2, 17)] [new SourceLocation(2, 17)]
) );
] try {
]; Executor::execute($this->schema(), $ast, null, ['input' => ['A', null, 'B']]);
$this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => ['A',null,'B']])); } catch (Error $e) {
$this->assertEquals($expected, Error::formatError($e));
}
} }
public function schema() public function schema()
{ {
$ComplexScalarType = ComplexScalar::create();
$TestInputObject = new InputObjectType([ $TestInputObject = new InputObjectType([
'name' => 'TestInputObject', 'name' => 'TestInputObject',
'fields' => [ 'fields' => [
'a' => ['type' => Type::string()], 'a' => ['type' => Type::string()],
'b' => ['type' => Type::listOf(Type::string())], 'b' => ['type' => Type::listOf(Type::string())],
'c' => ['type' => Type::nonNull(Type::string())] 'c' => ['type' => Type::nonNull(Type::string())],
'd' => ['type' => $ComplexScalarType],
] ]
]); ]);
@ -471,49 +536,56 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
'type' => Type::string(), 'type' => Type::string(),
'args' => ['input' => ['type' => $TestInputObject]], 'args' => ['input' => ['type' => $TestInputObject]],
'resolve' => function ($_, $args) { 'resolve' => function ($_, $args) {
return json_encode($args['input']); return isset($args['input']) ? json_encode($args['input']) : null;
} }
], ],
'fieldWithNullableStringInput' => [ 'fieldWithNullableStringInput' => [
'type' => Type::string(), 'type' => Type::string(),
'args' => ['input' => ['type' => Type::string()]], 'args' => ['input' => ['type' => Type::string()]],
'resolve' => function ($_, $args) { 'resolve' => function ($_, $args) {
return json_encode($args['input']); return isset($args['input']) ? json_encode($args['input']) : null;
} }
], ],
'fieldWithNonNullableStringInput' => [ 'fieldWithNonNullableStringInput' => [
'type' => Type::string(), 'type' => Type::string(),
'args' => ['input' => ['type' => Type::nonNull(Type::string())]], 'args' => ['input' => ['type' => Type::nonNull(Type::string())]],
'resolve' => function ($_, $args) { 'resolve' => function ($_, $args) {
return json_encode($args['input']); return isset($args['input']) ? json_encode($args['input']) : null;
}
],
'fieldWithDefaultArgumentValue' => [
'type' => Type::string(),
'args' => [ 'input' => [ 'type' => Type::string(), 'defaultValue' => 'Hello World' ]],
'resolve' => function($_, $args) {
return isset($args['input']) ? json_encode($args['input']) : null;
} }
], ],
'list' => [ 'list' => [
'type' => Type::string(), 'type' => Type::string(),
'args' => ['input' => ['type' => Type::listOf(Type::string())]], 'args' => ['input' => ['type' => Type::listOf(Type::string())]],
'resolve' => function ($_, $args) { 'resolve' => function ($_, $args) {
return json_encode($args['input']); return isset($args['input']) ? json_encode($args['input']) : null;
} }
], ],
'nnList' => [ 'nnList' => [
'type' => Type::string(), 'type' => Type::string(),
'args' => ['input' => ['type' => Type::nonNull(Type::listOf(Type::string()))]], 'args' => ['input' => ['type' => Type::nonNull(Type::listOf(Type::string()))]],
'resolve' => function ($_, $args) { 'resolve' => function ($_, $args) {
return json_encode($args['input']); return isset($args['input']) ? json_encode($args['input']) : null;
} }
], ],
'listNN' => [ 'listNN' => [
'type' => Type::string(), 'type' => Type::string(),
'args' => ['input' => ['type' => Type::listOf(Type::nonNull(Type::string()))]], 'args' => ['input' => ['type' => Type::listOf(Type::nonNull(Type::string()))]],
'resolve' => function ($_, $args) { 'resolve' => function ($_, $args) {
return json_encode($args['input']); return isset($args['input']) ? json_encode($args['input']) : null;
} }
], ],
'nnListNN' => [ 'nnListNN' => [
'type' => Type::string(), 'type' => Type::string(),
'args' => ['input' => ['type' => Type::nonNull(Type::listOf(Type::nonNull(Type::string())))]], 'args' => ['input' => ['type' => Type::nonNull(Type::listOf(Type::nonNull(Type::string())))]],
'resolve' => function ($_, $args) { 'resolve' => function ($_, $args) {
return json_encode($args['input']); return isset($args['input']) ? json_encode($args['input']) : null;
} }
], ],
] ]

View File

@ -245,7 +245,7 @@ class StarWarsSchema
] ]
], ],
'resolve' => function ($root, $args) { 'resolve' => function ($root, $args) {
return StarWarsData::getHero($args['episode']); return StarWarsData::getHero(isset($args['episode']) ? $args['episode'] : null);
}, },
], ],
'human' => [ 'human' => [

View File

@ -28,8 +28,8 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase
appearsIn appearsIn
} }
'; ';
$result = $this->validationResult($query); $errors = $this->validationErrors($query);
$this->assertEquals(true, $result['isValid']); $this->assertEquals(true, empty($errors));
} }
public function testThatNonExistentFieldsAreInvalid() public function testThatNonExistentFieldsAreInvalid()
@ -42,7 +42,8 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase
} }
} }
'; ';
$this->assertEquals(false, $this->validationResult($query)['isValid']); $errors = $this->validationErrors($query);
$this->assertEquals(false, empty($errors));
} }
public function testRequiresFieldsOnObjects() public function testRequiresFieldsOnObjects()
@ -52,7 +53,9 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase
hero hero
} }
'; ';
$this->assertEquals(false, $this->validationResult($query)['isValid']);
$errors = $this->validationErrors($query);
$this->assertEquals(false, empty($errors));
} }
public function testDisallowsFieldsOnScalars() public function testDisallowsFieldsOnScalars()
@ -67,7 +70,8 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase
} }
} }
'; ';
$this->assertEquals(false, $this->validationResult($query)['isValid']); $errors = $this->validationErrors($query);
$this->assertEquals(false, empty($errors));
} }
public function testDisallowsObjectFieldsOnInterfaces() public function testDisallowsObjectFieldsOnInterfaces()
@ -80,7 +84,8 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase
} }
} }
'; ';
$this->assertEquals(false, $this->validationResult($query)['isValid']); $errors = $this->validationErrors($query);
$this->assertEquals(false, empty($errors));
} }
public function testAllowsObjectFieldsInFragments() public function testAllowsObjectFieldsInFragments()
@ -97,7 +102,8 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase
primaryFunction primaryFunction
} }
'; ';
$this->assertEquals(true, $this->validationResult($query)['isValid']); $errors = $this->validationErrors($query);
$this->assertEquals(true, empty($errors));
} }
public function testAllowsObjectFieldsInInlineFragments() public function testAllowsObjectFieldsInInlineFragments()
@ -112,13 +118,14 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase
} }
} }
'; ';
$this->assertEquals(true, $this->validationResult($query)['isValid']); $errors = $this->validationErrors($query);
$this->assertEquals(true, empty($errors));
} }
/** /**
* Helper function to test a query and the expected response. * Helper function to test a query and the expected response.
*/ */
private function validationResult($query) private function validationErrors($query)
{ {
$ast = Parser::parse($query); $ast = Parser::parse($query);
return DocumentValidator::validate(StarWarsSchema::build(), $ast); return DocumentValidator::validate(StarWarsSchema::build(), $ast);

View File

@ -6,7 +6,6 @@ use GraphQL\Type\Definition\Config;
use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\IntType;
use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ObjectType;
@ -67,7 +66,10 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase
public function setUp() public function setUp()
{ {
$this->objectType = new ObjectType(['name' => 'Object']); $this->objectType = new ObjectType([
'name' => 'Object',
'isTypeOf' => function() {return true;}
]);
$this->interfaceType = new InterfaceType(['name' => 'Interface']); $this->interfaceType = new InterfaceType(['name' => 'Interface']);
$this->unionType = new UnionType(['name' => 'Union', 'types' => [$this->objectType]]); $this->unionType = new UnionType(['name' => 'Union', 'types' => [$this->objectType]]);
$this->enumType = new EnumType(['name' => 'Enum']); $this->enumType = new EnumType(['name' => 'Enum']);
@ -176,20 +178,83 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase
$this->assertSame('writeArticle', $writeMutation->name); $this->assertSame('writeArticle', $writeMutation->name);
} }
public function testIncludesNestedInputObjectInTheMap()
{
$nestedInputObject = new InputObjectType([
'name' => 'NestedInputObject',
'fields' => ['value' => ['type' => Type::string()]]
]);
$someInputObject = new InputObjectType([
'name' => 'SomeInputObject',
'fields' => ['nested' => ['type' => $nestedInputObject]]
]);
$someMutation = new ObjectType([
'name' => 'SomeMutation',
'fields' => [
'mutateSomething' => [
'type' => $this->blogArticle,
'args' => ['input' => ['type' => $someInputObject]]
]
]
]);
$schema = new Schema($this->blogQuery, $someMutation);
$this->assertSame($nestedInputObject, $schema->getType('NestedInputObject'));
}
public function testIncludesInterfaceSubtypesInTheTypeMap() public function testIncludesInterfaceSubtypesInTheTypeMap()
{ {
$someInterface = new InterfaceType([ $someInterface = new InterfaceType([
'name' => 'SomeInterface', 'name' => 'SomeInterface',
'fields' => [] 'fields' => [
'f' => ['type' => Type::int()]
]
]); ]);
$someSubtype = new ObjectType([ $someSubtype = new ObjectType([
'name' => 'SomeSubtype', 'name' => 'SomeSubtype',
'fields' => [], 'fields' => [
'interfaces' => [$someInterface] 'f' => ['type' => Type::int()]
],
'interfaces' => [$someInterface],
'isTypeOf' => function() {return true;}
]); ]);
$schema = new Schema($someInterface); $schema = new Schema(new ObjectType([
'name' => 'Query',
'fields' => [
'iface' => ['type' => $someInterface]
]
]));
$this->assertSame($someSubtype, $schema->getType('SomeSubtype'));
}
public function testIncludesInterfacesThunkSubtypesInTheTypeMap()
{
// includes interfaces' thunk subtypes in the type map
$someInterface = new InterfaceType([
'name' => 'SomeInterface',
'fields' => [
'f' => ['type' => Type::int()]
]
]);
$someSubtype = new ObjectType([
'name' => 'SomeSubtype',
'fields' => [
'f' => ['type' => Type::int()]
],
'interfaces' => function() use ($someInterface) { return [$someInterface]; },
'isTypeOf' => function() {return true;}
]);
$schema = new Schema(new ObjectType([
'name' => 'Query',
'fields' => [
'iface' => ['type' => $someInterface]
]
]));
$this->assertSame($someSubtype, $schema->getType('SomeSubtype')); $this->assertSame($someSubtype, $schema->getType('SomeSubtype'));
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,63 +0,0 @@
<?php
namespace GraphQL\Type;
use GraphQL\Type\Definition\Type;
class ScalarCoercionTest extends \PHPUnit_Framework_TestCase
{
public function testCoercesOutputInt()
{
$intType = Type::int();
$this->assertSame(1, $intType->coerce(1));
$this->assertSame(0, $intType->coerce(0));
$this->assertSame(0, $intType->coerce(0.1));
$this->assertSame(1, $intType->coerce(1.1));
$this->assertSame(-1, $intType->coerce(-1.1));
$this->assertSame(100000, $intType->coerce(1e5));
$this->assertSame(null, $intType->coerce(1e100));
$this->assertSame(null, $intType->coerce(-1e100));
$this->assertSame(-1, $intType->coerce('-1.1'));
$this->assertSame(null, $intType->coerce('one'));
$this->assertSame(0, $intType->coerce(false));
}
public function testCoercesOutputFloat()
{
$floatType = Type::float();
$this->assertSame(1.0, $floatType->coerce(1));
$this->assertSame(-1.0, $floatType->coerce(-1));
$this->assertSame(0.1, $floatType->coerce(0.1));
$this->assertSame(1.1, $floatType->coerce(1.1));
$this->assertSame(-1.1, $floatType->coerce(-1.1));
$this->assertSame(null, $floatType->coerce('one'));
$this->assertSame(0.0, $floatType->coerce(false));
$this->assertSame(1.0, $floatType->coerce(true));
}
public function testCoercesOutputStrings()
{
$stringType = Type::string();
$this->assertSame('string', $stringType->coerce('string'));
$this->assertSame('1', $stringType->coerce(1));
$this->assertSame('-1.1', $stringType->coerce(-1.1));
$this->assertSame('true', $stringType->coerce(true));
$this->assertSame('false', $stringType->coerce(false));
}
public function testCoercesOutputBoolean()
{
$boolType = Type::boolean();
$this->assertSame(true, $boolType->coerce('string'));
$this->assertSame(false, $boolType->coerce(''));
$this->assertSame(true, $boolType->coerce(1));
$this->assertSame(false, $boolType->coerce(0));
$this->assertSame(true, $boolType->coerce(true));
$this->assertSame(false, $boolType->coerce(false));
// TODO: how should it behaive on '0'?
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace GraphQL\Type;
use GraphQL\Type\Definition\Type;
class ScalarSerializationTest extends \PHPUnit_Framework_TestCase
{
public function testCoercesOutputInt()
{
$intType = Type::int();
$this->assertSame(1, $intType->serialize(1));
$this->assertSame(0, $intType->serialize(0));
$this->assertSame(0, $intType->serialize(0.1));
$this->assertSame(1, $intType->serialize(1.1));
$this->assertSame(-1, $intType->serialize(-1.1));
$this->assertSame(100000, $intType->serialize(1e5));
$this->assertSame(null, $intType->serialize(1e100));
$this->assertSame(null, $intType->serialize(-1e100));
$this->assertSame(-1, $intType->serialize('-1.1'));
$this->assertSame(null, $intType->serialize('one'));
$this->assertSame(0, $intType->serialize(false));
}
public function testCoercesOutputFloat()
{
$floatType = Type::float();
$this->assertSame(1.0, $floatType->serialize(1));
$this->assertSame(-1.0, $floatType->serialize(-1));
$this->assertSame(0.1, $floatType->serialize(0.1));
$this->assertSame(1.1, $floatType->serialize(1.1));
$this->assertSame(-1.1, $floatType->serialize(-1.1));
$this->assertSame(null, $floatType->serialize('one'));
$this->assertSame(0.0, $floatType->serialize(false));
$this->assertSame(1.0, $floatType->serialize(true));
}
public function testCoercesOutputStrings()
{
$stringType = Type::string();
$this->assertSame('string', $stringType->serialize('string'));
$this->assertSame('1', $stringType->serialize(1));
$this->assertSame('-1.1', $stringType->serialize(-1.1));
$this->assertSame('true', $stringType->serialize(true));
$this->assertSame('false', $stringType->serialize(false));
}
public function testCoercesOutputBoolean()
{
$boolType = Type::boolean();
$this->assertSame(true, $boolType->serialize('string'));
$this->assertSame(false, $boolType->serialize(''));
$this->assertSame(true, $boolType->serialize(1));
$this->assertSame(false, $boolType->serialize(0));
$this->assertSame(true, $boolType->serialize(true));
$this->assertSame(false, $boolType->serialize(false));
// TODO: how should it behaive on '0'?
}
}

View File

@ -29,10 +29,8 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
public function testPassesOnTheIntrospectionSchema() public function testPassesOnTheIntrospectionSchema()
{ {
$schema = new Schema(Introspection::_schema()); $schema = new Schema(Introspection::_schema());
$validationResult = SchemaValidator::validate($schema); $errors = SchemaValidator::validate($schema);
$this->assertEmpty($errors);
$this->assertSame(true, $validationResult->isValid);
$this->assertSame(null, $validationResult->errors);
} }
@ -65,12 +63,11 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
$schema = new Schema($someOutputType); $schema = new Schema($someOutputType);
$validationResult = SchemaValidator::validate($schema, [SchemaValidator::noInputTypesAsOutputFieldsRule()]); $validationResult = SchemaValidator::validate($schema, [SchemaValidator::noInputTypesAsOutputFieldsRule()]);
$this->assertSame(false, $validationResult->isValid); $this->assertSame(1, count($validationResult));
$this->assertSame(1, count($validationResult->errors));
$this->assertSame( $this->assertSame(
'Field SomeOutputType.sneaky is of type SomeInputType, which is an ' . 'Field SomeOutputType.sneaky is of type SomeInputType, which is an ' .
'input type, but field types must be output types!', 'input type, but field types must be output types!',
$validationResult->errors[0]->message $validationResult[0]->message
); );
} }
} }
@ -93,17 +90,17 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
]); ]);
$schema = new Schema($someOutputType); $schema = new Schema($someOutputType);
$validationResult = SchemaValidator::validate($schema, [$rule]); $errors = SchemaValidator::validate($schema, [$rule]);
$this->assertSame(true, $validationResult->isValid); $this->assertEmpty($errors);
} }
private function checkValidationResult($validationResult, $operationType) private function checkValidationResult($validationErrors, $operationType)
{ {
$this->assertEquals(false, $validationResult->isValid); $this->assertNotEmpty($validationErrors, "Should not validate");
$this->assertEquals(1, count($validationResult->errors)); $this->assertEquals(1, count($validationErrors));
$this->assertEquals( $this->assertEquals(
"Schema $operationType type SomeInputType must be an object type!", "Schema $operationType type SomeInputType must be an object type!",
$validationResult->errors[0]->message $validationErrors[0]->message
); );
} }
@ -197,8 +194,8 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
private function assertAcceptingFieldArgOfType($fieldArgType) private function assertAcceptingFieldArgOfType($fieldArgType)
{ {
$schema = $this->schemaWithFieldArgOfType($fieldArgType); $schema = $this->schemaWithFieldArgOfType($fieldArgType);
$validationResult = SchemaValidator::validate($schema, [SchemaValidator::noOutputTypesAsInputArgsRule()]); $errors = SchemaValidator::validate($schema, [SchemaValidator::noOutputTypesAsInputArgsRule()]);
$this->assertSame(true, $validationResult->isValid); $this->assertEmpty($errors);
} }
private function schemaWithFieldArgOfType($argType) private function schemaWithFieldArgOfType($argType)
@ -223,14 +220,13 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
return new Schema($queryType); return new Schema($queryType);
} }
private function expectRejectionBecauseFieldIsNotInputType($validationResult, $fieldTypeName) private function expectRejectionBecauseFieldIsNotInputType($errors, $fieldTypeName)
{ {
$this->assertSame(false, $validationResult->isValid); $this->assertSame(1, count($errors));
$this->assertSame(1, count($validationResult->errors));
$this->assertSame( $this->assertSame(
"Input field SomeIncorrectInputType.val has type $fieldTypeName, " . "Input field SomeIncorrectInputType.val has type $fieldTypeName, " .
"which is not an input type!", "which is not an input type!",
$validationResult->errors[0]->message $errors[0]->message
); );
} }
@ -245,40 +241,9 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
public function testRejectsWhenAPossibleTypeDoesNotImplementTheInterface() public function testRejectsWhenAPossibleTypeDoesNotImplementTheInterface()
{ {
// rejects when a possible type does not implement the interface // TODO: Validation for interfaces / implementors
$InterfaceType = new InterfaceType([
'name' => 'InterfaceType',
'fields' => []
]);
$SubType = new ObjectType([
'name' => 'SubType',
'fields' => [],
'interfaces' => []
]);
InterfaceType::addImplementationToInterfaces($SubType, [$InterfaceType]);
// Sanity check.
$this->assertEquals(1, count($InterfaceType->getPossibleTypes()));
$this->assertEquals($SubType, $InterfaceType->getPossibleTypes()[0]);
$schema = new Schema($InterfaceType);
$validationResult = SchemaValidator::validate(
$schema,
[SchemaValidator::interfacePossibleTypesMustImplementTheInterfaceRule()]
);
$this->assertSame(false, $validationResult->isValid);
$this->assertSame(1, count($validationResult->errors));
$this->assertSame(
'SubType is a possible type of interface InterfaceType but does not ' .
'implement it!',
$validationResult->errors[0]->message
);
} }
private function assertAcceptingAnInterfaceWithANormalSubtype($rule) private function assertAcceptingAnInterfaceWithANormalSubtype($rule)
{ {
$interfaceType = new InterfaceType([ $interfaceType = new InterfaceType([
@ -294,8 +259,8 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
$schema = new Schema($interfaceType, $subType); $schema = new Schema($interfaceType, $subType);
$validationResult = SchemaValidator::validate($schema, [$rule]); $errors = SchemaValidator::validate($schema, [$rule]);
$this->assertSame(true, $validationResult->isValid); $this->assertEmpty($errors);
} }
@ -337,28 +302,12 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
// Another sanity check. // Another sanity check.
$this->assertSame($subType, $schema->getType('SubType')); $this->assertSame($subType, $schema->getType('SubType'));
$validationResult = SchemaValidator::validate($schema, [SchemaValidator::typesInterfacesMustShowThemAsPossibleRule()]); $errors = SchemaValidator::validate($schema, [SchemaValidator::typesInterfacesMustShowThemAsPossibleRule()]);
$this->assertSame(false, $validationResult->isValid); $this->assertSame(1, count($errors));
$this->assertSame(1, count($validationResult->errors));
$this->assertSame( $this->assertSame(
'SubType implements interface InterfaceType, but InterfaceType does ' . 'SubType implements interface InterfaceType, but InterfaceType does ' .
'not list it as possible!', 'not list it as possible!',
$validationResult->errors[0]->message $errors[0]->message
); );
/*
var validationResult = validateSchema(
schema,
[TypesInterfacesMustShowThemAsPossible]
);
expect(validationResult.isValid).to.equal(false);
expect(validationResult.errors.length).to.equal(1);
expect(validationResult.errors[0].message).to.equal(
'SubType implements interface InterfaceType, but InterfaceType does ' +
'not list it as possible!'
);
*/
} }
} }

View File

@ -9,7 +9,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase
{ {
function missingArg($fieldName, $argName, $typeName, $line, $column) function missingArg($fieldName, $argName, $typeName, $line, $column)
{ {
return new FormattedError( return FormattedError::create(
Messages::missingArgMessage($fieldName, $argName, $typeName), Messages::missingArgMessage($fieldName, $argName, $typeName),
[new SourceLocation($line, $column)] [new SourceLocation($line, $column)]
); );
@ -17,8 +17,8 @@ class ArgumentsOfCorrectTypeTest extends TestCase
function badValue($argName, $typeName, $value, $line, $column) function badValue($argName, $typeName, $value, $line, $column)
{ {
return new FormattedError( return FormattedError::create(
Messages::badValueMessage($argName, $typeName, $value), ArgumentsOfCorrectType::badValueMessage($argName, $typeName, $value),
[new SourceLocation($line, $column)] [new SourceLocation($line, $column)]
); );
} }
@ -632,33 +632,6 @@ class ArgumentsOfCorrectTypeTest extends TestCase
]); ]);
} }
public function testMissingOneNonNullableArgument()
{
$this->expectFailsRule(new ArgumentsOfCorrectType, '
{
complicatedArgs {
multipleReqs(req2: 2)
}
}
', [
$this->missingArg('multipleReqs', 'req1', 'Int!', 4, 13)
]);
}
public function testMissingMultipleNonNullableArguments()
{
$this->expectFailsRule(new ArgumentsOfCorrectType, '
{
complicatedArgs {
multipleReqs
}
}
', [
$this->missingArg('multipleReqs', 'req1', 'Int!', 4, 13),
$this->missingArg('multipleReqs', 'req2', 'Int!', 4, 13),
]);
}
public function testIncorrectValueAndMissingArgument() public function testIncorrectValueAndMissingArgument()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType, ' $this->expectFailsRule(new ArgumentsOfCorrectType, '
@ -668,7 +641,6 @@ class ArgumentsOfCorrectTypeTest extends TestCase
} }
} }
', [ ', [
$this->missingArg('multipleReqs', 'req2', 'Int!', 4, 13),
$this->badValue('req1', 'Int!', '"one"', 4, 32), $this->badValue('req1', 'Int!', '"one"', 4, 32),
]); ]);
} }

View File

@ -93,7 +93,7 @@ class DefaultValuesOfCorrectTypeTest extends TestCase
private function defaultForNonNullArg($varName, $typeName, $guessTypeName, $line, $column) private function defaultForNonNullArg($varName, $typeName, $guessTypeName, $line, $column)
{ {
return new FormattedError( return FormattedError::create(
Messages::defaultForNonNullArgMessage($varName, $typeName, $guessTypeName), Messages::defaultForNonNullArgMessage($varName, $typeName, $guessTypeName),
[ new SourceLocation($line, $column) ] [ new SourceLocation($line, $column) ]
); );
@ -101,7 +101,7 @@ class DefaultValuesOfCorrectTypeTest extends TestCase
private function badValue($varName, $typeName, $val, $line, $column) private function badValue($varName, $typeName, $val, $line, $column)
{ {
return new FormattedError( return FormattedError::create(
Messages::badValueForDefaultArgMessage($varName, $typeName, $val), Messages::badValueForDefaultArgMessage($varName, $typeName, $val),
[ new SourceLocation($line, $column) ] [ new SourceLocation($line, $column) ]
); );

View File

@ -56,6 +56,15 @@ class FieldsOnCorrectTypeTest extends TestCase
'); ');
} }
public function testIgnoresFieldsOnUnknownType()
{
$this->expectPassesRule(new FieldsOnCorrectType, '
fragment unknownSelection on UnknownType {
unknownField
}
');
}
public function testFieldNotDefinedOnFragment() public function testFieldNotDefinedOnFragment()
{ {
$this->expectFailsRule(new FieldsOnCorrectType, ' $this->expectFailsRule(new FieldsOnCorrectType, '
@ -184,7 +193,7 @@ class FieldsOnCorrectTypeTest extends TestCase
private function undefinedField($field, $type, $line, $column) private function undefinedField($field, $type, $line, $column)
{ {
return new FormattedError( return FormattedError::create(
Messages::undefinedFieldMessage($field, $type), Messages::undefinedFieldMessage($field, $type),
[new SourceLocation($line, $column)] [new SourceLocation($line, $column)]
); );

View File

@ -86,8 +86,8 @@ class FragmentsOnCompositeTypesTest extends TestCase
} }
} }
', ',
[new FormattedError( [FormattedError::create(
Messages::inlineFragmentOnNonCompositeErrorMessage('String'), FragmentsOnCompositeTypes::inlineFragmentOnNonCompositeErrorMessage('String'),
[new SourceLocation(3, 16)] [new SourceLocation(3, 16)]
)] )]
); );
@ -95,8 +95,8 @@ class FragmentsOnCompositeTypesTest extends TestCase
private function error($fragName, $typeName, $line, $column) private function error($fragName, $typeName, $line, $column)
{ {
return new FormattedError( return FormattedError::create(
Messages::fragmentOnNonCompositeErrorMessage($fragName, $typeName), FragmentsOnCompositeTypes::fragmentOnNonCompositeErrorMessage($fragName, $typeName),
[ new SourceLocation($line, $column) ] [ new SourceLocation($line, $column) ]
); );
} }

View File

@ -26,6 +26,15 @@ class KnownArgumentNamesTest extends TestCase
'); ');
} }
public function testIgnoresArgsOfUnknownFields()
{
$this->expectPassesRule(new KnownArgumentNames, '
fragment argOnUnknownField on Dog {
unknownField(unknownArg: SIT)
}
');
}
public function testMultipleArgsInReverseOrderAreKnown() public function testMultipleArgsInReverseOrderAreKnown()
{ {
$this->expectPassesRule(new KnownArgumentNames, ' $this->expectPassesRule(new KnownArgumentNames, '
@ -62,6 +71,26 @@ class KnownArgumentNamesTest extends TestCase
'); ');
} }
public function testDirectiveArgsAreKnown()
{
$this->expectPassesRule(new KnownArgumentNames, '
{
dog @skip(if: true)
}
');
}
public function testUndirectiveArgsAreInvalid()
{
$this->expectFailsRule(new KnownArgumentNames, '
{
dog @skip(unless: true)
}
', [
$this->unknownDirectiveArg('unless', 'skip', 3, 19),
]);
}
public function testInvalidArgName() public function testInvalidArgName()
{ {
$this->expectFailsRule(new KnownArgumentNames, ' $this->expectFailsRule(new KnownArgumentNames, '
@ -106,28 +135,18 @@ class KnownArgumentNamesTest extends TestCase
]); ]);
} }
public function testArgsMayBeOnObjectButNotInterface()
{
$this->expectFailsRule(new KnownArgumentNames, '
fragment nameSometimesHasArg on Being {
name(surname: true)
... on Human {
name(surname: true)
}
... on Dog {
name(surname: true)
}
}
', [
$this->unknownArg('surname', 'name', 'Being', 3, 14),
$this->unknownArg('surname', 'name', 'Dog', 8, 16)
]);
}
private function unknownArg($argName, $fieldName, $typeName, $line, $column) private function unknownArg($argName, $fieldName, $typeName, $line, $column)
{ {
return new FormattedError( return FormattedError::create(
Messages::unknownArgMessage($argName, $fieldName, $typeName), KnownArgumentNames::unknownArgMessage($argName, $fieldName, $typeName),
[new SourceLocation($line, $column)]
);
}
private function unknownDirectiveArg($argName, $directiveName, $line, $column)
{
return FormattedError::create(
KnownArgumentNames::unknownDirectiveArgMessage($argName, $directiveName),
[new SourceLocation($line, $column)] [new SourceLocation($line, $column)]
); );
} }

View File

@ -26,10 +26,10 @@ class KnownDirectivesTest extends TestCase
{ {
$this->expectPassesRule(new KnownDirectives, ' $this->expectPassesRule(new KnownDirectives, '
{ {
dog @if: true { dog @include(if: true) {
name name
} }
human @unless: false { human @skip(if: true) {
name name
} }
} }
@ -40,7 +40,7 @@ class KnownDirectivesTest extends TestCase
{ {
$this->expectFailsRule(new KnownDirectives, ' $this->expectFailsRule(new KnownDirectives, '
{ {
dog @unknown: "directive" { dog @unknown(directive: "value") {
name name
} }
} }
@ -53,12 +53,12 @@ class KnownDirectivesTest extends TestCase
{ {
$this->expectFailsRule(new KnownDirectives, ' $this->expectFailsRule(new KnownDirectives, '
{ {
dog @unknown: "directive" { dog @unknown(directive: "value") {
name name
} }
human @unknown: "directive" { human @unknown(directive: "value") {
name name
pets @unknown: "directive" { pets @unknown(directive: "value") {
name name
} }
} }
@ -70,30 +70,42 @@ class KnownDirectivesTest extends TestCase
]); ]);
} }
public function testWithWellPlacedDirectives()
{
$this->expectPassesRule(new KnownDirectives, '
query Foo {
name @include(if: true)
...Frag @include(if: true)
skippedField @skip(if: true)
...SkippedFrag @skip(if: true)
}
');
}
public function testWithMisplacedDirectives() public function testWithMisplacedDirectives()
{ {
$this->expectFailsRule(new KnownDirectives, ' $this->expectFailsRule(new KnownDirectives, '
query Foo @if: true { query Foo @include(if: true) {
name name
...Frag ...Frag
} }
', [ ', [
$this->misplacedDirective('if', 'operation', 2, 17) $this->misplacedDirective('include', 'operation', 2, 17)
]); ]);
} }
private function unknownDirective($directiveName, $line, $column) private function unknownDirective($directiveName, $line, $column)
{ {
return new FormattedError( return FormattedError::create(
Messages::unknownDirectiveMessage($directiveName), KnownDirectives::unknownDirectiveMessage($directiveName),
[ new SourceLocation($line, $column) ] [ new SourceLocation($line, $column) ]
); );
} }
function misplacedDirective($directiveName, $placement, $line, $column) function misplacedDirective($directiveName, $placement, $line, $column)
{ {
return new FormattedError( return FormattedError::create(
Messages::misplacedDirectiveMessage($directiveName, $placement), KnownDirectives::misplacedDirectiveMessage($directiveName, $placement),
[new SourceLocation($line, $column)] [new SourceLocation($line, $column)]
); );
} }

View File

@ -57,8 +57,8 @@ class KnownFragmentNamesTest extends TestCase
private function undefFrag($fragName, $line, $column) private function undefFrag($fragName, $line, $column)
{ {
return new FormattedError( return FormattedError::create(
"Undefined fragment $fragName.", KnownFragmentNames::unknownFragmentMessage($fragName),
[new SourceLocation($line, $column)] [new SourceLocation($line, $column)]
); );
} }

View File

@ -44,8 +44,8 @@ class KnownTypeNamesTest extends TestCase
private function unknownType($typeName, $line, $column) private function unknownType($typeName, $line, $column)
{ {
return new FormattedError( return FormattedError::create(
Messages::unknownTypeMessage($typeName), KnownTypeNames::unknownTypeMessage($typeName),
[new SourceLocation($line, $column)] [new SourceLocation($line, $column)]
); );
} }

View File

@ -2,9 +2,7 @@
namespace GraphQL\Validator; namespace GraphQL\Validator;
use GraphQL\FormattedError; use GraphQL\FormattedError;
use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation; use GraphQL\Language\SourceLocation;
use GraphQL\Type\Definition\Config;
use GraphQL\Validator\Rules\NoFragmentCycles; use GraphQL\Validator\Rules\NoFragmentCycles;
class NoFragmentCyclesTest extends TestCase class NoFragmentCyclesTest extends TestCase
@ -88,8 +86,8 @@ class NoFragmentCyclesTest extends TestCase
fragment fragA on Dog { ...fragB } fragment fragA on Dog { ...fragB }
fragment fragB on Dog { ...fragA } fragment fragB on Dog { ...fragA }
', [ ', [
new FormattedError( FormattedError::create(
Messages::cycleErrorMessage('fragA', ['fragB']), NoFragmentCycles::cycleErrorMessage('fragA', ['fragB']),
[ new SourceLocation(2, 31), new SourceLocation(3, 31) ] [ new SourceLocation(2, 31), new SourceLocation(3, 31) ]
) )
]); ]);
@ -101,8 +99,8 @@ class NoFragmentCyclesTest extends TestCase
fragment fragB on Dog { ...fragA } fragment fragB on Dog { ...fragA }
fragment fragA on Dog { ...fragB } fragment fragA on Dog { ...fragB }
', [ ', [
new FormattedError( FormattedError::create(
Messages::cycleErrorMessage('fragB', ['fragA']), NoFragmentCycles::cycleErrorMessage('fragB', ['fragA']),
[new SourceLocation(2, 31), new SourceLocation(3, 31)] [new SourceLocation(2, 31), new SourceLocation(3, 31)]
) )
]); ]);
@ -122,8 +120,8 @@ class NoFragmentCyclesTest extends TestCase
} }
} }
', [ ', [
new FormattedError( FormattedError::create(
Messages::cycleErrorMessage('fragA', ['fragB']), NoFragmentCycles::cycleErrorMessage('fragA', ['fragB']),
[new SourceLocation(4, 11), new SourceLocation(9, 11)] [new SourceLocation(4, 11), new SourceLocation(9, 11)]
) )
]); ]);
@ -140,8 +138,8 @@ class NoFragmentCyclesTest extends TestCase
fragment fragZ on Dog { ...fragO } fragment fragZ on Dog { ...fragO }
fragment fragO on Dog { ...fragA, ...fragX } fragment fragO on Dog { ...fragA, ...fragX }
', [ ', [
new FormattedError( FormattedError::create(
Messages::cycleErrorMessage('fragA', ['fragB', 'fragC', 'fragO']), NoFragmentCycles::cycleErrorMessage('fragA', ['fragB', 'fragC', 'fragO']),
[ [
new SourceLocation(2, 31), new SourceLocation(2, 31),
new SourceLocation(3, 31), new SourceLocation(3, 31),
@ -149,8 +147,8 @@ class NoFragmentCyclesTest extends TestCase
new SourceLocation(8, 31), new SourceLocation(8, 31),
] ]
), ),
new FormattedError( FormattedError::create(
Messages::cycleErrorMessage('fragX', ['fragY', 'fragZ', 'fragO']), NoFragmentCycles::cycleErrorMessage('fragX', ['fragY', 'fragZ', 'fragO']),
[ [
new SourceLocation(5, 31), new SourceLocation(5, 31),
new SourceLocation(6, 31), new SourceLocation(6, 31),
@ -168,12 +166,12 @@ class NoFragmentCyclesTest extends TestCase
fragment fragB on Dog { ...fragA } fragment fragB on Dog { ...fragA }
fragment fragC on Dog { ...fragA } fragment fragC on Dog { ...fragA }
', [ ', [
new FormattedError( FormattedError::create(
'Cannot spread fragment fragA within itself via fragB.', NoFragmentCycles::cycleErrorMessage('fragA', ['fragB']),
[new SourceLocation(2, 31), new SourceLocation(3, 31)] [new SourceLocation(2, 31), new SourceLocation(3, 31)]
), ),
new FormattedError( FormattedError::create(
'Cannot spread fragment fragA within itself via fragC.', NoFragmentCycles::cycleErrorMessage('fragA', ['fragC']),
[new SourceLocation(2, 41), new SourceLocation(4, 31)] [new SourceLocation(2, 41), new SourceLocation(4, 31)]
) )
]); ]);
@ -181,8 +179,8 @@ class NoFragmentCyclesTest extends TestCase
private function cycleError($fargment, $spreadNames, $line, $column) private function cycleError($fargment, $spreadNames, $line, $column)
{ {
return new FormattedError( return FormattedError::create(
Messages::cycleErrorMessage($fargment, $spreadNames), NoFragmentCycles::cycleErrorMessage($fargment, $spreadNames),
[new SourceLocation($line, $column)] [new SourceLocation($line, $column)]
); );
} }

View File

@ -302,16 +302,16 @@ class NoUndefinedVariablesTest extends TestCase
private function undefVar($varName, $line, $column) private function undefVar($varName, $line, $column)
{ {
return new FormattedError( return FormattedError::create(
Messages::undefinedVarMessage($varName), NoUndefinedVariables::undefinedVarMessage($varName),
[new SourceLocation($line, $column)] [new SourceLocation($line, $column)]
); );
} }
private function undefVarByOp($varName, $l1, $c1, $opName, $l2, $c2) private function undefVarByOp($varName, $l1, $c1, $opName, $l2, $c2)
{ {
return new FormattedError( return FormattedError::create(
Messages::undefinedVarByOpMessage($varName, $opName), NoUndefinedVariables::undefinedVarByOpMessage($varName, $opName),
[new SourceLocation($l1, $c1), new SourceLocation($l2, $c2)] [new SourceLocation($l1, $c1), new SourceLocation($l2, $c2)]
); );
} }

View File

@ -130,10 +130,27 @@ class NoUnusedFragmentsTest extends TestCase
]); ]);
} }
public function testContainsUnknownAndUndefFragments()
{
$this->expectFailsRule(new NoUnusedFragments, '
query Foo {
human(id: 4) {
...bar
}
}
fragment foo on Human {
name
}
', [
$this->unusedFrag('foo', 7, 7),
]);
}
private function unusedFrag($fragName, $line, $column) private function unusedFrag($fragName, $line, $column)
{ {
return new FormattedError( return FormattedError::create(
Messages::unusedFragMessage($fragName), NoUnusedFragments::unusedFragMessage($fragName),
[new SourceLocation($line, $column)] [new SourceLocation($line, $column)]
); );
} }

View File

@ -213,8 +213,8 @@ class NoUnusedVariablesTest extends TestCase
private function unusedVar($varName, $line, $column) private function unusedVar($varName, $line, $column)
{ {
return new FormattedError( return FormattedError::create(
Messages::unusedVariableMessage($varName), NoUnusedVariables::unusedVariableMessage($varName),
[new SourceLocation($line, $column)] [new SourceLocation($line, $column)]
); );
} }

View File

@ -48,8 +48,8 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
{ {
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, ' $this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
fragment mergeSameFieldsWithSameDirectives on Dog { fragment mergeSameFieldsWithSameDirectives on Dog {
name @if:true name @include(if: true)
name @if:true name @include(if: true)
} }
'); ');
} }
@ -68,8 +68,8 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
{ {
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, ' $this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
fragment differentDirectivesWithDifferentAliases on Dog { fragment differentDirectivesWithDifferentAliases on Dog {
nameIfTrue : name @if:true nameIfTrue : name @include(if: true)
nameIfFalse : name @if:false nameIfFalse : name @include(if: false)
} }
'); ');
} }
@ -82,8 +82,8 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
fido : nickname fido : nickname
} }
', [ ', [
new FormattedError( FormattedError::create(
Messages::fieldsConflictMessage('fido', 'name and nickname are different fields'), OverlappingFieldsCanBeMerged::fieldsConflictMessage('fido', 'name and nickname are different fields'),
[new SourceLocation(3, 9), new SourceLocation(4, 9)] [new SourceLocation(3, 9), new SourceLocation(4, 9)]
) )
]); ]);
@ -97,8 +97,8 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
name name
} }
', [ ', [
new FormattedError( FormattedError::create(
Messages::fieldsConflictMessage('name', 'nickname and name are different fields'), OverlappingFieldsCanBeMerged::fieldsConflictMessage('name', 'nickname and name are different fields'),
[new SourceLocation(3, 9), new SourceLocation(4, 9)] [new SourceLocation(3, 9), new SourceLocation(4, 9)]
) )
]); ]);
@ -112,8 +112,8 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
doesKnowCommand(dogCommand: HEEL) doesKnowCommand(dogCommand: HEEL)
} }
', [ ', [
new FormattedError( FormattedError::create(
Messages::fieldsConflictMessage('doesKnowCommand', 'they have differing arguments'), OverlappingFieldsCanBeMerged::fieldsConflictMessage('doesKnowCommand', 'they have differing arguments'),
[new SourceLocation(3,9), new SourceLocation(4,9)] [new SourceLocation(3,9), new SourceLocation(4,9)]
) )
]); ]);
@ -123,27 +123,43 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
{ {
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, ' $this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
fragment conflictingDirectiveArgs on Dog { fragment conflictingDirectiveArgs on Dog {
name @if: true name @include(if: true)
name @unless: false name @skip(if: true)
} }
', [ ', [
new FormattedError( FormattedError::create(
Messages::fieldsConflictMessage('name', 'they have differing directives'), OverlappingFieldsCanBeMerged::fieldsConflictMessage('name', 'they have differing directives'),
[new SourceLocation(3, 9), new SourceLocation(4, 9)] [new SourceLocation(3, 9), new SourceLocation(4, 9)]
) )
]); ]);
} }
public function testConflictingDirectiveArgs()
{
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
fragment conflictingDirectiveArgs on Dog {
name @include(if: true)
name @include(if: false)
}
', [
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage('name', 'they have differing directives'),
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
)
]
);
}
public function testConflictingArgsWithMatchingDirectives() public function testConflictingArgsWithMatchingDirectives()
{ {
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, ' $this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
fragment conflictingArgsWithMatchingDirectiveArgs on Dog { fragment conflictingArgsWithMatchingDirectiveArgs on Dog {
doesKnowCommand(dogCommand: SIT) @if:true doesKnowCommand(dogCommand: SIT) @include(if: true)
doesKnowCommand(dogCommand: HEEL) @if:true doesKnowCommand(dogCommand: HEEL) @include(if: true)
} }
', [ ', [
new FormattedError( FormattedError::create(
Messages::fieldsConflictMessage('doesKnowCommand', 'they have differing arguments'), OverlappingFieldsCanBeMerged::fieldsConflictMessage('doesKnowCommand', 'they have differing arguments'),
[new SourceLocation(3, 9), new SourceLocation(4, 9)] [new SourceLocation(3, 9), new SourceLocation(4, 9)]
) )
]); ]);
@ -153,12 +169,12 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
{ {
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, ' $this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
fragment conflictingDirectiveArgsWithMatchingArgs on Dog { fragment conflictingDirectiveArgsWithMatchingArgs on Dog {
doesKnowCommand(dogCommand: SIT) @if: true doesKnowCommand(dogCommand: SIT) @include(if: true)
doesKnowCommand(dogCommand: SIT) @unless: false doesKnowCommand(dogCommand: SIT) @skip(if: true)
} }
', [ ', [
new FormattedError( FormattedError::create(
Messages::fieldsConflictMessage('doesKnowCommand', 'they have differing directives'), OverlappingFieldsCanBeMerged::fieldsConflictMessage('doesKnowCommand', 'they have differing directives'),
[new SourceLocation(3, 9), new SourceLocation(4, 9)] [new SourceLocation(3, 9), new SourceLocation(4, 9)]
) )
]); ]);
@ -178,8 +194,8 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
x: b x: b
} }
', [ ', [
new FormattedError( FormattedError::create(
Messages::fieldsConflictMessage('x', 'a and b are different fields'), OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'a and b are different fields'),
[new SourceLocation(7, 9), new SourceLocation(10, 9)] [new SourceLocation(7, 9), new SourceLocation(10, 9)]
) )
]); ]);
@ -210,16 +226,16 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
x: b x: b
} }
', [ ', [
new FormattedError( FormattedError::create(
Messages::fieldsConflictMessage('x', 'a and b are different fields'), OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'a and b are different fields'),
[new SourceLocation(18, 9), new SourceLocation(21, 9)] [new SourceLocation(18, 9), new SourceLocation(21, 9)]
), ),
new FormattedError( FormattedError::create(
Messages::fieldsConflictMessage('x', 'a and c are different fields'), OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'a and c are different fields'),
[new SourceLocation(18, 9), new SourceLocation(14, 11)] [new SourceLocation(18, 9), new SourceLocation(14, 11)]
), ),
new FormattedError( FormattedError::create(
Messages::fieldsConflictMessage('x', 'b and c are different fields'), OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'b and c are different fields'),
[new SourceLocation(21, 9), new SourceLocation(14, 11)] [new SourceLocation(21, 9), new SourceLocation(14, 11)]
) )
]); ]);
@ -237,8 +253,8 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
} }
} }
', [ ', [
new FormattedError( FormattedError::create(
Messages::fieldsConflictMessage('field', [['x', 'a and b are different fields']]), OverlappingFieldsCanBeMerged::fieldsConflictMessage('field', [['x', 'a and b are different fields']]),
[ [
new SourceLocation(3, 9), new SourceLocation(3, 9),
new SourceLocation(6,9), new SourceLocation(6,9),
@ -263,8 +279,8 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
} }
} }
', [ ', [
new FormattedError( FormattedError::create(
Messages::fieldsConflictMessage('field', [ OverlappingFieldsCanBeMerged::fieldsConflictMessage('field', [
['x', 'a and b are different fields'], ['x', 'a and b are different fields'],
['y', 'c and d are different fields'] ['y', 'c and d are different fields']
]), ]),
@ -296,8 +312,8 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
} }
} }
', [ ', [
new FormattedError( FormattedError::create(
Messages::fieldsConflictMessage('field', [['deepField', [['x', 'a and b are different fields']]]]), OverlappingFieldsCanBeMerged::fieldsConflictMessage('field', [['deepField', [['x', 'a and b are different fields']]]]),
[ [
new SourceLocation(3,9), new SourceLocation(3,9),
new SourceLocation(8,9), new SourceLocation(8,9),
@ -329,8 +345,8 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
} }
} }
', [ ', [
new FormattedError( FormattedError::create(
Messages::fieldsConflictMessage('deepField', [['x', 'a and b are different fields']]), OverlappingFieldsCanBeMerged::fieldsConflictMessage('deepField', [['x', 'a and b are different fields']]),
[ [
new SourceLocation(4,11), new SourceLocation(4,11),
new SourceLocation(7,11), new SourceLocation(7,11),
@ -356,8 +372,8 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
} }
} }
', [ ', [
new FormattedError( FormattedError::create(
Messages::fieldsConflictMessage('scalar', 'they return differing types Int and String'), OverlappingFieldsCanBeMerged::fieldsConflictMessage('scalar', 'they return differing types Int and String'),
[ new SourceLocation(5,15), new SourceLocation(8,15) ] [ new SourceLocation(5,15), new SourceLocation(8,15) ]
) )
]); ]);
@ -379,6 +395,55 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
'); ');
} }
public function testComparesDeepTypesIncludingList()
{
$this->expectFailsRuleWithSchema($this->getTestSchema(), new OverlappingFieldsCanBeMerged, '
{
connection {
...edgeID
edges {
node {
id: name
}
}
}
}
fragment edgeID on Connection {
edges {
node {
id
}
}
}
', [
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage('edges', [['node', [['id', 'id and name are different fields']]]]),
[
new SourceLocation(14, 11), new SourceLocation(5, 13),
new SourceLocation(15, 13), new SourceLocation(6, 15),
new SourceLocation(16, 15), new SourceLocation(7, 17),
]
)
]);
}
public function testIgnoresUnknownTypes()
{
$this->expectPassesRuleWithSchema($this->getTestSchema(), new OverlappingFieldsCanBeMerged, '
{
boxUnion {
...on UnknownType {
scalar
}
...on NonNullStringBox2 {
scalar
}
}
}
');
}
private function getTestSchema() private function getTestSchema()
{ {
$StringBox = new ObjectType([ $StringBox = new ObjectType([
@ -411,13 +476,37 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
$BoxUnion = new UnionType([ $BoxUnion = new UnionType([
'name' => 'BoxUnion', 'name' => 'BoxUnion',
'resolveType' => function() use ($StringBox) {return $StringBox;},
'types' => [ $StringBox, $IntBox, $NonNullStringBox1, $NonNullStringBox2 ] 'types' => [ $StringBox, $IntBox, $NonNullStringBox1, $NonNullStringBox2 ]
]); ]);
$Connection = new ObjectType([
'name' => 'Connection',
'fields' => [
'edges' => [
'type' => Type::listOf(new ObjectType([
'name' => 'Edge',
'fields' => [
'node' => [
'type' => new ObjectType([
'name' => 'Node',
'fields' => [
'id' => ['type' => Type::id()],
'name' => ['type' => Type::string()]
]
])
]
]
]))
]
]
]);
$schema = new Schema(new ObjectType([ $schema = new Schema(new ObjectType([
'name' => 'QueryRoot', 'name' => 'QueryRoot',
'fields' => [ 'fields' => [
'boxUnion' => ['type' => $BoxUnion ] 'boxUnion' => ['type' => $BoxUnion ],
'connection' => ['type' => $Connection]
] ]
])); ]));

View File

@ -210,16 +210,16 @@ class PossibleFragmentSpreadsTest extends TestCase
private function error($fragName, $parentType, $fragType, $line, $column) private function error($fragName, $parentType, $fragType, $line, $column)
{ {
return new FormattedError( return FormattedError::create(
Messages::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType), PossibleFragmentSpreads::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType),
[new SourceLocation($line, $column)] [new SourceLocation($line, $column)]
); );
} }
private function errorAnon($parentType, $fragType, $line, $column) private function errorAnon($parentType, $fragType, $line, $column)
{ {
return new FormattedError( return FormattedError::create(
Messages::typeIncompatibleAnonSpreadMessage($parentType, $fragType), PossibleFragmentSpreads::typeIncompatibleAnonSpreadMessage($parentType, $fragType),
[new SourceLocation($line, $column)] [new SourceLocation($line, $column)]
); );
} }

View File

@ -0,0 +1,217 @@
<?php
namespace GraphQL\Validator;
use GraphQL\FormattedError;
use GraphQL\Language\SourceLocation;
use GraphQL\Validator\Rules\ProvidedNonNullArguments;
class ProvidedNonNullArgumentsTest extends TestCase
{
// Validate: Provided required arguments
public function testIgnoresUnknownArguments()
{
// ignores unknown arguments
$this->expectPassesRule(new ProvidedNonNullArguments, '
{
dog {
isHousetrained(unknownArgument: true)
}
}
');
}
// Valid non-nullable value:
public function testArgOnOptionalArg()
{
$this->expectPassesRule(new ProvidedNonNullArguments, '
{
dog {
isHousetrained(atOtherHomes: true)
}
}
');
}
public function testMultipleArgs()
{
$this->expectPassesRule(new ProvidedNonNullArguments, '
{
complicatedArgs {
multipleReqs(req1: 1, req2: 2)
}
}
');
}
public function testMultipleArgsReverseOrder()
{
$this->expectPassesRule(new ProvidedNonNullArguments, '
{
complicatedArgs {
multipleReqs(req2: 2, req1: 1)
}
}
');
}
public function testNoArgsOnMultipleOptional()
{
$this->expectPassesRule(new ProvidedNonNullArguments, '
{
complicatedArgs {
multipleOpts
}
}
');
}
public function testOneArgOnMultipleOptional()
{
$this->expectPassesRule(new ProvidedNonNullArguments, '
{
complicatedArgs {
multipleOpts(opt1: 1)
}
}
');
}
public function testSecondArgOnMultipleOptional()
{
$this->expectPassesRule(new ProvidedNonNullArguments, '
{
complicatedArgs {
multipleOpts(opt2: 1)
}
}
');
}
public function testMultipleReqsOnMixedList()
{
$this->expectPassesRule(new ProvidedNonNullArguments, '
{
complicatedArgs {
multipleOptAndReq(req1: 3, req2: 4)
}
}
');
}
public function testMultipleReqsAndOneOptOnMixedList()
{
$this->expectPassesRule(new ProvidedNonNullArguments, '
{
complicatedArgs {
multipleOptAndReq(req1: 3, req2: 4, opt1: 5)
}
}
');
}
public function testAllReqsAndOptsOnMixedList()
{
$this->expectPassesRule(new ProvidedNonNullArguments, '
{
complicatedArgs {
multipleOptAndReq(req1: 3, req2: 4, opt1: 5, opt2: 6)
}
}
');
}
// Invalid non-nullable value
public function testMissingOneNonNullableArgument()
{
$this->expectFailsRule(new ProvidedNonNullArguments, '
{
complicatedArgs {
multipleReqs(req2: 2)
}
}
', [
$this->missingFieldArg('multipleReqs', 'req1', 'Int!', 4, 13)
]);
}
public function testMissingMultipleNonNullableArguments()
{
$this->expectFailsRule(new ProvidedNonNullArguments, '
{
complicatedArgs {
multipleReqs
}
}
', [
$this->missingFieldArg('multipleReqs', 'req1', 'Int!', 4, 13),
$this->missingFieldArg('multipleReqs', 'req2', 'Int!', 4, 13),
]);
}
public function testIncorrectValueAndMissingArgument()
{
$this->expectFailsRule(new ProvidedNonNullArguments, '
{
complicatedArgs {
multipleReqs(req1: "one")
}
}
', [
$this->missingFieldArg('multipleReqs', 'req2', 'Int!', 4, 13),
]);
}
// Directive arguments
public function testIgnoresUnknownDirectives()
{
$this->expectPassesRule(new ProvidedNonNullArguments, '
{
dog @unknown
}
');
}
public function testWithDirectivesOfValidTypes()
{
$this->expectPassesRule(new ProvidedNonNullArguments, '
{
dog @include(if: true) {
name
}
human @skip(if: false) {
name
}
}
');
}
public function testWithDirectiveWithMissingTypes()
{
$this->expectFailsRule(new ProvidedNonNullArguments, '
{
dog @include {
name @skip
}
}
', [
$this->missingDirectiveArg('include', 'if', 'Boolean!', 3, 15),
$this->missingDirectiveArg('skip', 'if', 'Boolean!', 4, 18)
]);
}
private function missingFieldArg($fieldName, $argName, $typeName, $line, $column)
{
return FormattedError::create(
ProvidedNonNullArguments::missingFieldArgMessage($fieldName, $argName, $typeName),
[new SourceLocation($line, $column)]
);
}
private function missingDirectiveArg($directiveName, $argName, $typeName, $line, $column)
{
return FormattedError::create(
ProvidedNonNullArguments::missingDirectiveArgMessage($directiveName, $argName, $typeName),
[new SourceLocation($line, $column)]
);
}
}

View File

@ -81,10 +81,10 @@ class ScalarLeafsTest extends TestCase
{ {
$this->expectFailsRule(new ScalarLeafs, ' $this->expectFailsRule(new ScalarLeafs, '
fragment scalarSelectionsNotAllowedWithDirectives on Dog { fragment scalarSelectionsNotAllowedWithDirectives on Dog {
name @if: true { isAlsoHumanName } name @include(if: true) { isAlsoHumanName }
} }
', ',
[$this->noScalarSubselection('name', 'String', 3, 24)] [$this->noScalarSubselection('name', 'String', 3, 33)]
); );
} }
@ -92,25 +92,25 @@ class ScalarLeafsTest extends TestCase
{ {
$this->expectFailsRule(new ScalarLeafs, ' $this->expectFailsRule(new ScalarLeafs, '
fragment scalarSelectionsNotAllowedWithDirectivesAndArgs on Dog { fragment scalarSelectionsNotAllowedWithDirectivesAndArgs on Dog {
doesKnowCommand(dogCommand: SIT) @if: true { sinceWhen } doesKnowCommand(dogCommand: SIT) @include(if: true) { sinceWhen }
} }
', ',
[$this->noScalarSubselection('doesKnowCommand', 'Boolean', 3, 52)] [$this->noScalarSubselection('doesKnowCommand', 'Boolean', 3, 61)]
); );
} }
private function noScalarSubselection($field, $type, $line, $column) private function noScalarSubselection($field, $type, $line, $column)
{ {
return new FormattedError( return FormattedError::create(
Messages::noSubselectionAllowedMessage($field, $type), ScalarLeafs::noSubselectionAllowedMessage($field, $type),
[new SourceLocation($line, $column)] [new SourceLocation($line, $column)]
); );
} }
private function missingObjSubselection($field, $type, $line, $column) private function missingObjSubselection($field, $type, $line, $column)
{ {
return new FormattedError( return FormattedError::create(
Messages::requiredSubselectionMessage($field, $type), ScalarLeafs::requiredSubselectionMessage($field, $type),
[new SourceLocation($line, $column)] [new SourceLocation($line, $column)]
); );
} }

View File

@ -20,17 +20,25 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase
*/ */
protected function getDefaultSchema() protected function getDefaultSchema()
{ {
$FurColor = null;
$Being = new InterfaceType([ $Being = new InterfaceType([
'name' => 'Being', 'name' => 'Being',
'fields' => [ 'fields' => [
'name' => [ 'type' => Type::string() ] 'name' => [
'type' => Type::string(),
'args' => [ 'surname' => [ 'type' => Type::boolean() ] ]
]
], ],
]); ]);
$Pet = new InterfaceType([ $Pet = new InterfaceType([
'name' => 'Pet', 'name' => 'Pet',
'fields' => [ 'fields' => [
'name' => [ 'type' => Type::string() ] 'name' => [
'type' => Type::string(),
'args' => [ 'surname' => [ 'type' => Type::boolean() ] ]
]
], ],
]); ]);
@ -45,8 +53,12 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase
$Dog = new ObjectType([ $Dog = new ObjectType([
'name' => 'Dog', 'name' => 'Dog',
'isTypeOf' => function() {return true;},
'fields' => [ 'fields' => [
'name' => ['type' => Type::string()], 'name' => [
'type' => Type::string(),
'args' => [ 'surname' => [ 'type' => Type::boolean() ] ]
],
'nickname' => ['type' => Type::string()], 'nickname' => ['type' => Type::string()],
'barkVolume' => ['type' => Type::int()], 'barkVolume' => ['type' => Type::int()],
'barks' => ['type' => Type::boolean()], 'barks' => ['type' => Type::boolean()],
@ -66,24 +78,18 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase
'interfaces' => [$Being, $Pet] 'interfaces' => [$Being, $Pet]
]); ]);
$FurColor = new EnumType([
'name' => 'FurColor',
'values' => [
'BROWN' => [ 'value' => 0 ],
'BLACK' => [ 'value' => 1 ],
'TAN' => [ 'value' => 2 ],
'SPOTTED' => [ 'value' => 3 ],
],
]);
$Cat = new ObjectType([ $Cat = new ObjectType([
'name' => 'Cat', 'name' => 'Cat',
'isTypeOf' => function() {return true;},
'fields' => [ 'fields' => [
'name' => ['type' => Type::string()], 'name' => [
'type' => Type::string(),
'args' => [ 'surname' => [ 'type' => Type::boolean() ] ]
],
'nickname' => ['type' => Type::string()], 'nickname' => ['type' => Type::string()],
'meows' => ['type' => Type::boolean()], 'meows' => ['type' => Type::boolean()],
'meowVolume' => ['type' => Type::int()], 'meowVolume' => ['type' => Type::int()],
'furColor' => ['type' => $FurColor] 'furColor' => ['type' => function() use (&$FurColor) {return $FurColor;}]
], ],
'interfaces' => [$Being, $Pet] 'interfaces' => [$Being, $Pet]
]); ]);
@ -106,22 +112,29 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase
$Human = $this->humanType = new ObjectType([ $Human = $this->humanType = new ObjectType([
'name' => 'Human', 'name' => 'Human',
'isTypeOf' => function() {return true;},
'interfaces' => [$Being, $Intelligent], 'interfaces' => [$Being, $Intelligent],
'fields' => [ 'fields' => [
'name' => [ 'name' => [
'args' => ['surname' => ['type' => Type::boolean()]], 'type' => Type::string(),
'type' => Type::string() 'args' => ['surname' => ['type' => Type::boolean()]]
], ],
'pets' => ['type' => Type::listOf($Pet)], 'pets' => ['type' => Type::listOf($Pet)],
'relatives' => ['type' => function() {return Type::listOf($this->humanType); }] 'relatives' => ['type' => function() {return Type::listOf($this->humanType); }],
'iq' => ['type' => Type::int()]
] ]
]); ]);
$Alien = new ObjectType([ $Alien = new ObjectType([
'name' => 'Alien', 'name' => 'Alien',
'isTypeOf' => function() {return true;},
'interfaces' => [$Being, $Intelligent], 'interfaces' => [$Being, $Intelligent],
'fields' => [ 'fields' => [
'iq' => ['type' => Type::int()], 'iq' => ['type' => Type::int()],
'name' => [
'type' => Type::string(),
'args' => ['surname' => ['type' => Type::boolean()]]
],
'numEyes' => ['type' => Type::int()] 'numEyes' => ['type' => Type::int()]
] ]
]); ]);
@ -144,6 +157,16 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase
} }
]); ]);
$FurColor = new EnumType([
'name' => 'FurColor',
'values' => [
'BROWN' => [ 'value' => 0 ],
'BLACK' => [ 'value' => 1 ],
'TAN' => [ 'value' => 2 ],
'SPOTTED' => [ 'value' => 3 ],
],
]);
$ComplexInput = new InputObjectType([ $ComplexInput = new InputObjectType([
'name' => 'ComplexInput', 'name' => 'ComplexInput',
'fields' => [ 'fields' => [
@ -260,19 +283,20 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase
function expectValid($schema, $rules, $queryString) function expectValid($schema, $rules, $queryString)
{ {
$this->assertEquals( $this->assertEquals(
['isValid' => true, 'errors' => null], [],
DocumentValidator::validate($schema, Parser::parse($queryString), $rules) DocumentValidator::validate($schema, Parser::parse($queryString), $rules),
'Should validate'
); );
} }
function expectInvalid($schema, $rules, $queryString, $errors) function expectInvalid($schema, $rules, $queryString, $expectedErrors)
{ {
$result = DocumentValidator::validate($schema, Parser::parse($queryString), $rules); $errors = DocumentValidator::validate($schema, Parser::parse($queryString), $rules);
$this->assertEquals(false, $result['isValid'], 'GraphQL should not validate'); $this->assertNotEmpty($errors, 'GraphQL should not validate');
$this->assertEquals($errors, $result['errors']); $this->assertEquals($expectedErrors, array_map(['GraphQL\Error', 'formatError'], $errors));
return $result; return $errors;
} }
function expectPassesRule($rule, $queryString) function expectPassesRule($rule, $queryString)

View File

@ -20,20 +20,20 @@ class VariablesAreInputTypesTest extends TestCase
public function testOutputTypesAreInvalid() public function testOutputTypesAreInvalid()
{ {
$this->expectFailsRule(new VariablesAreInputTypes, ' $this->expectFailsRule(new VariablesAreInputTypes, '
query Foo($a: Dog, $b: [[DogOrCat!]]!, $c: Pet) { query Foo($a: Dog, $b: [[CatOrDog!]]!, $c: Pet) {
field(a: $a, b: $b, c: $c) field(a: $a, b: $b, c: $c)
} }
', [ ', [
new FormattedError( FormattedError::create(
Messages::nonInputTypeOnVarMessage('a', 'Dog'), VariablesAreInputTypes::nonInputTypeOnVarMessage('a', 'Dog'),
[new SourceLocation(2, 21)] [new SourceLocation(2, 21)]
), ),
new FormattedError( FormattedError::create(
Messages::nonInputTypeOnVarMessage('b', '[[DogOrCat!]]!'), VariablesAreInputTypes::nonInputTypeOnVarMessage('b', '[[CatOrDog!]]!'),
[new SourceLocation(2, 30)] [new SourceLocation(2, 30)]
), ),
new FormattedError( FormattedError::create(
Messages::nonInputTypeOnVarMessage('c', 'Pet'), VariablesAreInputTypes::nonInputTypeOnVarMessage('c', 'Pet'),
[new SourceLocation(2, 50)] [new SourceLocation(2, 50)]
) )
] ]

View File

@ -177,7 +177,7 @@ class VariablesInAllowedPositionTest extends TestCase
$this->expectPassesRule(new VariablesInAllowedPosition, ' $this->expectPassesRule(new VariablesInAllowedPosition, '
query Query($boolVar: Boolean!) query Query($boolVar: Boolean!)
{ {
dog @if: $boolVar dog @include(if: $boolVar)
} }
'); ');
} }
@ -188,7 +188,7 @@ class VariablesInAllowedPositionTest extends TestCase
$this->expectPassesRule(new VariablesInAllowedPosition, ' $this->expectPassesRule(new VariablesInAllowedPosition, '
query Query($boolVar: Boolean = false) query Query($boolVar: Boolean = false)
{ {
dog @if: $boolVar dog @include(if: $boolVar)
} }
'); ');
} }
@ -204,7 +204,7 @@ class VariablesInAllowedPositionTest extends TestCase
} }
} }
', [ ', [
new FormattedError( FormattedError::create(
Messages::badVarPosMessage('intArg', 'Int', 'Int!'), Messages::badVarPosMessage('intArg', 'Int', 'Int!'),
[new SourceLocation(5, 45)] [new SourceLocation(5, 45)]
) )
@ -226,7 +226,7 @@ class VariablesInAllowedPositionTest extends TestCase
} }
} }
', [ ', [
new FormattedError( FormattedError::create(
Messages::badVarPosMessage('intArg', 'Int', 'Int!'), Messages::badVarPosMessage('intArg', 'Int', 'Int!'),
[new SourceLocation(3, 43)] [new SourceLocation(3, 43)]
) )
@ -252,7 +252,7 @@ class VariablesInAllowedPositionTest extends TestCase
} }
} }
', [ ', [
new FormattedError( FormattedError::create(
Messages::badVarPosMessage('intArg', 'Int', 'Int!'), Messages::badVarPosMessage('intArg', 'Int', 'Int!'),
[new SourceLocation(7,43)] [new SourceLocation(7,43)]
) )
@ -270,7 +270,7 @@ class VariablesInAllowedPositionTest extends TestCase
} }
} }
', [ ', [
new FormattedError( FormattedError::create(
Messages::badVarPosMessage('stringVar', 'String', 'Boolean'), Messages::badVarPosMessage('stringVar', 'String', 'Boolean'),
[new SourceLocation(5,39)] [new SourceLocation(5,39)]
) )
@ -288,7 +288,7 @@ class VariablesInAllowedPositionTest extends TestCase
} }
} }
', [ ', [
new FormattedError( FormattedError::create(
Messages::badVarPosMessage('stringVar', 'String', '[String]'), Messages::badVarPosMessage('stringVar', 'String', '[String]'),
[new SourceLocation(5,45)] [new SourceLocation(5,45)]
) )
@ -301,12 +301,12 @@ class VariablesInAllowedPositionTest extends TestCase
$this->expectFailsRule(new VariablesInAllowedPosition, ' $this->expectFailsRule(new VariablesInAllowedPosition, '
query Query($boolVar: Boolean) query Query($boolVar: Boolean)
{ {
dog @if: $boolVar dog @include(if: $boolVar)
} }
', [ ', [
new FormattedError( FormattedError::create(
Messages::badVarPosMessage('boolVar', 'Boolean', 'Boolean!'), Messages::badVarPosMessage('boolVar', 'Boolean', 'Boolean!'),
[new SourceLocation(4,18)] [new SourceLocation(4,26)]
) )
]); ]);
} }
@ -317,12 +317,12 @@ class VariablesInAllowedPositionTest extends TestCase
$this->expectFailsRule(new VariablesInAllowedPosition, ' $this->expectFailsRule(new VariablesInAllowedPosition, '
query Query($stringVar: String) query Query($stringVar: String)
{ {
dog @if: $stringVar dog @include(if: $stringVar)
} }
', [ ', [
new FormattedError( FormattedError::create(
Messages::badVarPosMessage('stringVar', 'String', 'Boolean!'), Messages::badVarPosMessage('stringVar', 'String', 'Boolean!'),
[new SourceLocation(4,18)] [new SourceLocation(4,26)]
) )
]); ]);
} }