Merge pull request #67 from mcg-web/promise

Initial implementation of promises (starting addressing N+1 problem)
This commit is contained in:
Vladimir Razuvaev 2016-12-01 16:22:15 +07:00 committed by GitHub
commit f3fca81e9d
11 changed files with 1732 additions and 228 deletions

View File

@ -13,7 +13,8 @@
"ext-mbstring": "*" "ext-mbstring": "*"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^4.8" "phpunit/phpunit": "^4.8",
"react/promise": "^2.4"
}, },
"config": { "config": {
"bin-dir": "bin" "bin-dir": "bin"
@ -30,5 +31,8 @@
"GraphQL\\Benchmarks\\": "benchmarks/", "GraphQL\\Benchmarks\\": "benchmarks/",
"GraphQL\\Examples\\Blog\\": "examples/01-blog/Blog/" "GraphQL\\Examples\\Blog\\": "examples/01-blog/Blog/"
} }
},
"suggest": {
"react/promise": "To use ReactPhp promise adapter"
} }
} }

View File

@ -3,13 +3,15 @@ namespace GraphQL\Executor;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\InvariantViolation;
use GraphQL\Executor\Promise\Promise;
use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\FieldNode; use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FragmentDefinitionNode; use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\SelectionSetNode; use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Executor\Promise\Adapter\GenericPromiseAdapter;
use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Schema; use GraphQL\Schema;
use GraphQL\Type\Definition\AbstractType; use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\Directive;
@ -48,6 +50,19 @@ class Executor
private static $defaultFieldResolver = [__CLASS__, 'defaultFieldResolver']; private static $defaultFieldResolver = [__CLASS__, 'defaultFieldResolver'];
/**
* @var PromiseAdapter
*/
private static $promiseAdapter;
/**
* @param PromiseAdapter|null $promiseAdapter
*/
public static function setPromiseAdapter(PromiseAdapter $promiseAdapter = null)
{
self::$promiseAdapter = $promiseAdapter;
}
/** /**
* Custom default resolve function * Custom default resolve function
* *
@ -66,7 +81,7 @@ class Executor
* @param $contextValue * @param $contextValue
* @param array|\ArrayAccess $variableValues * @param array|\ArrayAccess $variableValues
* @param null $operationName * @param null $operationName
* @return ExecutionResult * @return ExecutionResult|Promise
*/ */
public static function execute(Schema $schema, DocumentNode $ast, $rootValue = null, $contextValue = null, $variableValues = null, $operationName = null) public static function execute(Schema $schema, DocumentNode $ast, $rootValue = null, $contextValue = null, $variableValues = null, $operationName = null)
{ {
@ -88,9 +103,34 @@ class Executor
} }
$exeContext = self::buildExecutionContext($schema, $ast, $rootValue, $contextValue, $variableValues, $operationName); $exeContext = self::buildExecutionContext($schema, $ast, $rootValue, $contextValue, $variableValues, $operationName);
if (null === self::$promiseAdapter) {
static::setPromiseAdapter(new GenericPromiseAdapter());
}
try { try {
$data = self::executeOperation($exeContext, $exeContext->operation, $rootValue); $data = self::$promiseAdapter
->createPromise(function (callable $resolve) use ($exeContext, $rootValue) {
return $resolve(self::executeOperation($exeContext, $exeContext->operation, $rootValue));
});
if (self::$promiseAdapter->isPromise($data)) {
// Return a Promise that will eventually resolve to the data described by
// The "Response" section of the GraphQL specification.
//
// If errors are encountered while executing a GraphQL field, only that
// field and its descendants will be omitted, and sibling fields will still
// be executed. An execution which encounters errors will still result in a
// resolved Promise.
return $data->then(null, function ($error) use ($exeContext) {
// Errors from sub-fields of a NonNull type may propagate to the top level,
// at which point we still log the error and null the parent field, which
// in this case is the entire response.
$exeContext->addError($error);
return null;
})->then(function ($data) use ($exeContext) {
return new ExecutionResult((array) $data, $exeContext->errors);
});
}
} catch (Error $e) { } catch (Error $e) {
$exeContext->addError($e); $exeContext->addError($e);
$data = null; $data = null;
@ -102,6 +142,15 @@ class Executor
/** /**
* 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.
*
* @param Schema $schema
* @param DocumentNode $documentNode
* @param $rootValue
* @param $contextValue
* @param $rawVariableValues
* @param string $operationName
* @return ExecutionContext
* @throws Error
*/ */
private static function buildExecutionContext( private static function buildExecutionContext(
Schema $schema, Schema $schema,
@ -160,6 +209,11 @@ class Executor
/** /**
* Implements the "Evaluating operations" section of the spec. * Implements the "Evaluating operations" section of the spec.
*
* @param ExecutionContext $exeContext
* @param OperationDefinitionNode $operation
* @param $rootValue
* @return Promise|\stdClass|array
*/ */
private static function executeOperation(ExecutionContext $exeContext, OperationDefinitionNode $operation, $rootValue) private static function executeOperation(ExecutionContext $exeContext, OperationDefinitionNode $operation, $rootValue)
{ {
@ -217,38 +271,111 @@ 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.
*
* @param ExecutionContext $exeContext
* @param ObjectType $parentType
* @param $sourceValue
* @param $path
* @param $fields
* @return Promise|\stdClass|array
*/ */
private static function executeFieldsSerially(ExecutionContext $exeContext, ObjectType $parentType, $sourceValue, $path, $fields) private static function executeFieldsSerially(ExecutionContext $exeContext, ObjectType $parentType, $sourceValue, $path, $fields)
{ {
$results = []; $results = self::$promiseAdapter->createResolvedPromise([]);
foreach ($fields as $responseName => $fieldNodes) {
$process = function ($results, $responseName, $path, $exeContext, $parentType, $sourceValue, $fieldNodes) {
$fieldPath = $path; $fieldPath = $path;
$fieldPath[] = $responseName; $fieldPath[] = $responseName;
$result = self::resolveField($exeContext, $parentType, $sourceValue, $fieldNodes, $fieldPath); $result = self::resolveField($exeContext, $parentType, $sourceValue, $fieldNodes, $fieldPath);
if ($result === self::$UNDEFINED) {
return $results;
}
if (self::$promiseAdapter->isPromise($result)) {
return $result->then(function ($resolvedResult) use ($responseName, $results) {
$results[$responseName] = $resolvedResult;
return $results;
});
}
$results[$responseName] = $result;
return $results;
};
if ($result !== self::$UNDEFINED) { foreach ($fields as $responseName => $fieldNodes) {
// Undefined means that field is not defined in schema if (self::$promiseAdapter->isPromise($results)) {
$results[$responseName] = $result; $results = $results->then(function ($resolvedResults) use ($process, $responseName, $path, $exeContext, $parentType, $sourceValue, $fieldNodes) {
return $process($resolvedResults, $responseName, $path, $exeContext, $parentType, $sourceValue, $fieldNodes);
});
} else {
$results = $process($results, $responseName, $path, $exeContext, $parentType, $sourceValue, $fieldNodes);
} }
} }
// see #59
if ([] === $results) { if (self::$promiseAdapter->isPromise($results)) {
$results = new \stdClass(); return $results->then(function ($resolvedResults) {
return self::fixResultsIfEmptyArray($resolvedResults);
});
} }
return $results;
return self::fixResultsIfEmptyArray($results);
} }
/** /**
* Implements the "Evaluating selection sets" section of the spec * Implements the "Evaluating selection sets" section of the spec
* for "read" mode. * for "read" mode.
*
* @param ExecutionContext $exeContext
* @param ObjectType $parentType
* @param $source
* @param $path
* @param $fields
* @return Promise|\stdClass|array
*/ */
private static function executeFields(ExecutionContext $exeContext, ObjectType $parentType, $source, $path, $fields) private static function executeFields(ExecutionContext $exeContext, ObjectType $parentType, $source, $path, $fields)
{ {
// Native PHP doesn't support promises. $containsPromise = false;
// Custom executor should be built for platforms like ReactPHP $finalResults = [];
return self::executeFieldsSerially($exeContext, $parentType, $source, $path, $fields);
foreach ($fields as $responseName => $fieldNodes) {
$fieldPath = $path;
$fieldPath[] = $responseName;
$result = self::resolveField($exeContext, $parentType, $source, $fieldNodes, $fieldPath);
if ($result === self::$UNDEFINED) {
continue;
}
if (!$containsPromise && self::$promiseAdapter->isPromise($result)) {
$containsPromise = true;
}
$finalResults[$responseName] = $result;
}
// If there are no promises, we can just return the object
if (!$containsPromise) {
return self::fixResultsIfEmptyArray($finalResults);
}
// Otherwise, results is a map from field name to the result
// of resolving that field, which is possibly a promise. Return
// a promise that will return this same map, but with any
// promises replaced with the values they resolved to.
return self::$promiseAdapter->createPromiseAll($finalResults)->then(function ($resolvedResults) {
return self::fixResultsIfEmptyArray($resolvedResults);
});
} }
/**
* @see https://github.com/webonyx/graphql-php/issues/59
*
* @param $results
* @return \stdClass|array
*/
private static function fixResultsIfEmptyArray($results)
{
if ([] === $results) {
$results = new \stdClass();
}
return $results;
}
/** /**
* Given a selectionSet, adds all of the fields in that selection to * Given a selectionSet, adds all of the fields in that selection to
@ -258,6 +385,12 @@ class Executor
* returns and Interface or Union type, the "runtime type" will be the actual * returns and Interface or Union type, the "runtime type" will be the actual
* Object type returned by that field. * Object type returned by that field.
* *
* @param ExecutionContext $exeContext
* @param ObjectType $runtimeType
* @param SelectionSetNode $selectionSet
* @param $fields
* @param $visitedFragmentNames
*
* @return \ArrayObject * @return \ArrayObject
*/ */
private static function collectFields( private static function collectFields(
@ -322,6 +455,10 @@ class Executor
/** /**
* Determines if a field should be included based on the @include and @skip * Determines if a field should be included based on the @include and @skip
* directives, where @skip has higher precedence than @include. * directives, where @skip has higher precedence than @include.
*
* @param ExecutionContext $exeContext
* @param $directives
* @return bool
*/ */
private static function shouldIncludeNode(ExecutionContext $exeContext, $directives) private static function shouldIncludeNode(ExecutionContext $exeContext, $directives)
{ {
@ -361,6 +498,11 @@ class Executor
/** /**
* Determines if a fragment is applicable to the given type. * Determines if a fragment is applicable to the given type.
*
* @param ExecutionContext $exeContext
* @param $fragment
* @param ObjectType $type
* @return bool
*/ */
private static function doesFragmentConditionMatch(ExecutionContext $exeContext,/* FragmentDefinitionNode | InlineFragmentNode*/ $fragment, ObjectType $type) private static function doesFragmentConditionMatch(ExecutionContext $exeContext,/* FragmentDefinitionNode | InlineFragmentNode*/ $fragment, ObjectType $type)
{ {
@ -382,6 +524,9 @@ class Executor
/** /**
* Implements the logic to compute the key of a given fields entry * Implements the logic to compute the key of a given fields entry
*
* @param FieldNode $node
* @return string
*/ */
private static function getFieldEntryKey(FieldNode $node) private static function getFieldEntryKey(FieldNode $node)
{ {
@ -393,6 +538,14 @@ class Executor
* figures out the value that the field returns by calling its resolve function, * figures out the value that the field returns by calling its resolve function,
* then calls completeValue to complete promises, serialize scalars, or execute * then calls completeValue to complete promises, serialize scalars, or execute
* the sub-selection-set for objects. * the sub-selection-set for objects.
*
* @param ExecutionContext $exeContext
* @param ObjectType $parentType
* @param $source
* @param $fieldNodes
* @param $path
*
* @return array|\Exception|mixed|null
*/ */
private static function resolveField(ExecutionContext $exeContext, ObjectType $parentType, $source, $fieldNodes, $path) private static function resolveField(ExecutionContext $exeContext, ObjectType $parentType, $source, $fieldNodes, $path)
{ {
@ -501,7 +654,7 @@ class Executor
* @param ResolveInfo $info * @param ResolveInfo $info
* @param $path * @param $path
* @param $result * @param $result
* @return array|null * @return array|null|Promise
*/ */
private static function completeValueCatchingError( private static function completeValueCatchingError(
ExecutionContext $exeContext, ExecutionContext $exeContext,
@ -528,7 +681,7 @@ class Executor
// 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 {
return self::completeValueWithLocatedError( $completed = self::completeValueWithLocatedError(
$exeContext, $exeContext,
$returnType, $returnType,
$fieldNodes, $fieldNodes,
@ -536,6 +689,13 @@ class Executor
$path, $path,
$result $result
); );
if (self::$promiseAdapter->isPromise($completed)) {
return $completed->then(null, function ($error) use ($exeContext) {
$exeContext->addError($error);
return self::$promiseAdapter->createResolvedPromise(null);
});
}
return $completed;
} catch (Error $err) { } catch (Error $err) {
// If `completeValueWithLocatedError` returned abruptly (threw an error), log the error // If `completeValueWithLocatedError` returned abruptly (threw an error), log the error
// and return null. // and return null.
@ -555,10 +715,10 @@ class Executor
* @param ResolveInfo $info * @param ResolveInfo $info
* @param $path * @param $path
* @param $result * @param $result
* @return array|null * @return array|null|Promise
* @throws Error * @throws Error
*/ */
static function completeValueWithLocatedError( public static function completeValueWithLocatedError(
ExecutionContext $exeContext, ExecutionContext $exeContext,
Type $returnType, Type $returnType,
$fieldNodes, $fieldNodes,
@ -568,7 +728,7 @@ class Executor
) )
{ {
try { try {
return self::completeValue( $completed = self::completeValue(
$exeContext, $exeContext,
$returnType, $returnType,
$fieldNodes, $fieldNodes,
@ -576,6 +736,12 @@ class Executor
$path, $path,
$result $result
); );
if (self::$promiseAdapter->isPromise($completed)) {
return $completed->then(null, function ($error) use ($fieldNodes, $path) {
return self::$promiseAdapter->createRejectedPromise(Error::createLocatedError($error, $fieldNodes, $path));
});
}
return $completed;
} catch (\Exception $error) { } catch (\Exception $error) {
throw Error::createLocatedError($error, $fieldNodes, $path); throw Error::createLocatedError($error, $fieldNodes, $path);
} }
@ -608,7 +774,7 @@ class Executor
* @param ResolveInfo $info * @param ResolveInfo $info
* @param array $path * @param array $path
* @param $result * @param $result
* @return array|null * @return array|null|Promise
* @throws Error * @throws Error
* @throws \Exception * @throws \Exception
*/ */
@ -621,6 +787,13 @@ class Executor
&$result &$result
) )
{ {
// If result is a Promise, apply-lift over completeValue.
if (self::$promiseAdapter->isPromise($result)) {
return $result->then(function (&$resolved) use ($exeContext, $returnType, $fieldNodes, $info, $path) {
return self::completeValue($exeContext, $returnType, $fieldNodes, $info, $path, $resolved);
});
}
if ($result instanceof \Exception) { if ($result instanceof \Exception) {
throw $result; throw $result;
} }
@ -677,6 +850,13 @@ class Executor
* which takes the property of the source object of the same name as the field * which takes the property of the source object of the same name as the field
* and returns it as the result, or if it's a function, returns the result * and returns it as the result, or if it's a function, returns the result
* of calling that function while passing along args and context. * of calling that function while passing along args and context.
*
* @param $source
* @param $args
* @param $context
* @param ResolveInfo $info
*
* @return mixed|null
*/ */
public static function defaultFieldResolver($source, $args, $context, ResolveInfo $info) public static function defaultFieldResolver($source, $args, $context, ResolveInfo $info)
{ {
@ -705,6 +885,10 @@ class Executor
* added to the query type, but that would require mutating type * added to the query type, but that would require mutating type
* definitions, which would cause issues. * definitions, which would cause issues.
* *
* @param Schema $schema
* @param ObjectType $parentType
* @param $fieldName
*
* @return FieldDefinition * @return FieldDefinition
*/ */
private static function getFieldDef(Schema $schema, ObjectType $parentType, $fieldName) private static function getFieldDef(Schema $schema, ObjectType $parentType, $fieldName)
@ -781,7 +965,7 @@ class Executor
* @param ResolveInfo $info * @param ResolveInfo $info
* @param array $path * @param array $path
* @param $result * @param $result
* @return array * @return array|Promise
* @throws \Exception * @throws \Exception
*/ */
private static function completeListValue(ExecutionContext $exeContext, ListOfType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result) private static function completeListValue(ExecutionContext $exeContext, ListOfType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result)
@ -791,15 +975,20 @@ class Executor
is_array($result) || $result instanceof \Traversable, is_array($result) || $result instanceof \Traversable,
'User Error: expected iterable, but did not find one for field ' . $info->parentType . '.' . $info->fieldName . '.' 'User Error: expected iterable, but did not find one for field ' . $info->parentType . '.' . $info->fieldName . '.'
); );
$containsPromise = false;
$i = 0; $i = 0;
$tmp = []; $completedItems = [];
foreach ($result as $item) { foreach ($result as $item) {
$fieldPath = $path; $fieldPath = $path;
$fieldPath[] = $i++; $fieldPath[] = $i++;
$tmp[] = self::completeValueCatchingError($exeContext, $itemType, $fieldNodes, $info, $fieldPath, $item); $completedItem = self::completeValueCatchingError($exeContext, $itemType, $fieldNodes, $info, $fieldPath, $item);
if (!$containsPromise && self::$promiseAdapter->isPromise($completedItem)) {
$containsPromise = true;
}
$completedItems[] = $completedItem;
} }
return $tmp; return $containsPromise ? self::$promiseAdapter->createPromiseAll($completedItems) : $completedItems;
} }
/** /**
@ -832,7 +1021,7 @@ class Executor
* @param ResolveInfo $info * @param ResolveInfo $info
* @param array $path * @param array $path
* @param $result * @param $result
* @return array * @return array|Promise|\stdClass
* @throws Error * @throws Error
*/ */
private static function completeObjectValue(ExecutionContext $exeContext, ObjectType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result) private static function completeObjectValue(ExecutionContext $exeContext, ObjectType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result)
@ -888,7 +1077,13 @@ class Executor
} }
/** /**
* @deprecated as of 8.0 * @deprecated as of v0.8.0 should use self::defaultFieldResolver method
*
* @param $source
* @param $args
* @param $context
* @param ResolveInfo $info
* @return mixed|null
*/ */
public static function defaultResolveFn($source, $args, $context, ResolveInfo $info) public static function defaultResolveFn($source, $args, $context, ResolveInfo $info)
{ {
@ -897,7 +1092,9 @@ class Executor
} }
/** /**
* @deprecated as of 8.0 * @deprecated as of v0.8.0 should use self::setDefaultFieldResolver method
*
* @param callable $fn
*/ */
public static function setDefaultResolveFn($fn) public static function setDefaultResolveFn($fn)
{ {

View File

@ -0,0 +1,33 @@
<?php
namespace GraphQL\Executor\Promise\Adapter;
use GraphQL\Executor\Promise\PromiseAdapter;
class GenericPromiseAdapter implements PromiseAdapter
{
public function isPromise($value)
{
return false;
}
public function createPromise(callable $resolver)
{
return $resolver(function ($value) {
return $value;
});
}
public function createResolvedPromise($promiseOrValue = null)
{
return $promiseOrValue;
}
public function createRejectedPromise($reason)
{
}
public function createPromiseAll($promisesOrValues)
{
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace GraphQL\Executor\Promise\Adapter;
use GraphQL\Executor\Promise\PromiseAdapter;;
use React\Promise\FulfilledPromise;
use React\Promise\Promise;
use React\Promise\PromiseInterface;
class ReactPromiseAdapter implements PromiseAdapter
{
/**
* Return true if value is promise
*
* @param mixed $value
*
* @return bool
*/
public function isPromise($value)
{
return $value instanceof PromiseInterface;
}
/**
* @inheritdoc
*
* @return PromiseInterface
*/
public function createPromise(callable $resolver)
{
$promise = new Promise($resolver);
return $promise;
}
/**
* @inheritdoc
*
* @return FulfilledPromise
*/
public function createResolvedPromise($promiseOrValue = null)
{
return \React\Promise\resolve($promiseOrValue);
}
/**
* @inheritdoc
*
* @return \React\Promise\RejectedPromise
*/
public function createRejectedPromise($reason)
{
return \React\Promise\reject($reason);
}
/**
* Given an array of promises, return a promise that is fulfilled when all the
* items in the array are fulfilled.
*
* @param mixed $promisesOrValues Promises or values.
*
* @return mixed a Promise
*/
public function createPromiseAll($promisesOrValues)
{
return \React\Promise\all($promisesOrValues);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace GraphQL\Executor\Promise;
/**
* A simple Promise representation
* this interface helps to document the code
*/
interface Promise
{
/**
* @param callable|null $onFullFilled
* @param callable|null $onRejected
*
* @return Promise
*/
public function then(callable $onFullFilled = null, callable $onRejected = null);
}

View File

@ -0,0 +1,53 @@
<?php
namespace GraphQL\Executor\Promise;
interface PromiseAdapter
{
/**
* Return true if value is promise
*
* @param mixed $value
*
* @return bool
*/
public function isPromise($value);
/**
* Creates a Promise
*
* @param callable $resolver
*
* @return Promise
*/
public function createPromise(callable $resolver);
/**
* Creates a full filed Promise for a value if the value is not a promise.
*
* @param mixed $promiseOrValue
*
* @return Promise a full filed Promise
*/
public function createResolvedPromise($promiseOrValue = null);
/**
* Creates a rejected promise for a reason if the reason is not a promise. If
* the provided reason is a promise, then it is returned as-is.
*
* @param mixed $reason
*
* @return Promise a rejected promise
*/
public function createRejectedPromise($reason);
/**
* Given an array of promises, return a promise that is fulfilled when all the
* items in the array are fulfilled.
*
* @param mixed $promisesOrValues Promises or values.
*
* @return Promise equivalent to Promise.all result
*/
public function createPromiseAll($promisesOrValues);
}

View File

@ -4,9 +4,11 @@ namespace GraphQL;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Executor\ExecutionResult; use GraphQL\Executor\ExecutionResult;
use GraphQL\Executor\Executor; use GraphQL\Executor\Executor;
use GraphQL\Executor\Promise\Promise;
use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
use GraphQL\Language\Source; use GraphQL\Language\Source;
use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\Directive;
use GraphQL\Validator\DocumentValidator; use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\Rules\QueryComplexity; use GraphQL\Validator\Rules\QueryComplexity;
@ -19,11 +21,13 @@ class GraphQL
* @param mixed $rootValue * @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 Promise|array
*/ */
public static function execute(Schema $schema, $requestString, $rootValue = null, $contextValue = null, $variableValues = null, $operationName = null) public static function execute(Schema $schema, $requestString, $rootValue = null, $contextValue = null, $variableValues = null, $operationName = null)
{ {
return self::executeAndReturnResult($schema, $requestString, $rootValue, $contextValue, $variableValues, $operationName)->toArray(); $result = self::executeAndReturnResult($schema, $requestString, $rootValue, $contextValue, $variableValues, $operationName);
return $result instanceof ExecutionResult ? $result->toArray() : $result->then(function(ExecutionResult $executionResult) { return $executionResult->toArray(); });
} }
/** /**
@ -32,7 +36,7 @@ class GraphQL
* @param null $rootValue * @param null $rootValue
* @param null $variableValues * @param null $variableValues
* @param null $operationName * @param null $operationName
* @return array|ExecutionResult * @return ExecutionResult|Promise
*/ */
public static function executeAndReturnResult(Schema $schema, $requestString, $rootValue = null, $contextValue = null, $variableValues = null, $operationName = null) public static function executeAndReturnResult(Schema $schema, $requestString, $rootValue = null, $contextValue = null, $variableValues = null, $operationName = null)
{ {
@ -67,4 +71,12 @@ class GraphQL
{ {
return array_values(Directive::getInternalDirectives()); return array_values(Directive::getInternalDirectives());
} }
/**
* @param PromiseAdapter|null $promiseAdapter
*/
public static function setPromiseAdapter(PromiseAdapter $promiseAdapter = null)
{
Executor::setPromiseAdapter($promiseAdapter);
}
} }

View File

@ -4,8 +4,10 @@ namespace GraphQL\Tests\Executor;
require_once __DIR__ . '/TestClasses.php'; require_once __DIR__ . '/TestClasses.php';
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Executor\Executor; use GraphQL\Executor\Executor;
use GraphQL\Error\FormattedError; use GraphQL\Error\FormattedError;
use GraphQL\Executor\Promise\Adapter\ReactPromiseAdapter;
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
use GraphQL\Language\SourceLocation; use GraphQL\Language\SourceLocation;
use GraphQL\Schema; use GraphQL\Schema;
@ -16,9 +18,15 @@ use GraphQL\Type\Definition\ResolveInfo;
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 React\Promise\Promise;
class ExecutorTest extends \PHPUnit_Framework_TestCase class ExecutorTest extends \PHPUnit_Framework_TestCase
{ {
public function tearDown()
{
Executor::setPromiseAdapter(null);
}
// Execute: Handles basic execution tasks // Execute: Handles basic execution tasks
/** /**
@ -26,7 +34,16 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
*/ */
public function testExecutesArbitraryCode() public function testExecutesArbitraryCode()
{ {
Executor::setPromiseAdapter(new ReactPromiseAdapter());
$deepData = null; $deepData = null;
$data = null;
$promiseData = function () use (&$data) {
return new Promise(function (callable $resolve) use (&$data) {
return $resolve($data);
});
};
$data = [ $data = [
'a' => function () { return 'Apple';}, 'a' => function () { return 'Apple';},
'b' => function () {return 'Banana';}, 'b' => function () {return 'Banana';},
@ -37,8 +54,8 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
'pic' => function ($size = 50) { 'pic' => function ($size = 50) {
return 'Pic of size: ' . $size; return 'Pic of size: ' . $size;
}, },
'promise' => function() use (&$data) { 'promise' => function() use ($promiseData) {
return $data; return $promiseData();
}, },
'deep' => function () use (&$deepData) { 'deep' => function () use (&$deepData) {
return $deepData; return $deepData;
@ -51,7 +68,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
'c' => function () { 'c' => function () {
return ['Contrived', null, 'Confusing']; return ['Contrived', null, 'Confusing'];
}, },
'deeper' => function () use ($data) { 'deeper' => function () use (&$data) {
return [$data, null, $data]; return [$data, null, $data];
} }
]; ];
@ -148,7 +165,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
]); ]);
$schema = new Schema(['query' => $dataType]); $schema = new Schema(['query' => $dataType]);
$this->assertEquals($expected, Executor::execute($schema, $ast, $data, null, ['size' => 100], 'Example')->toArray()); $this->assertEquals($expected, self::awaitPromise(Executor::execute($schema, $ast, $data, null, ['size' => 100], 'Example')));
} }
/** /**
@ -342,12 +359,18 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
public function testNullsOutErrorSubtrees() public function testNullsOutErrorSubtrees()
{ {
$doc = '{ $doc = '{
sync, sync
syncError, syncError
syncRawError, syncRawError
async, syncReturnError
asyncReject, syncReturnErrorList
async
asyncReject
asyncRawReject
asyncEmptyReject
asyncError asyncError
asyncRawError
asyncReturnError
}'; }';
$data = [ $data = [
@ -360,17 +383,42 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
'syncRawError' => function() { 'syncRawError' => function() {
throw new \Exception('Error getting syncRawError'); throw new \Exception('Error getting syncRawError');
}, },
// Following are inherited from JS reference implementation, but make no sense in this PHP impl // inherited from JS reference implementation, but make no sense in this PHP impl
// leaving them just to simplify migrations from newer js versions // leaving it just to simplify migrations from newer js versions
'syncReturnError' => function() {
return new \Exception('Error getting syncReturnError');
},
'syncReturnErrorList' => function () {
return [
'sync0',
new \Exception('Error getting syncReturnErrorList1'),
'sync2',
new \Exception('Error getting syncReturnErrorList3')
];
},
'async' => function() { 'async' => function() {
return 'async'; return new Promise(function(callable $resolve) { return $resolve('async'); });
}, },
'asyncReject' => function() { 'asyncReject' => function() {
throw new \Exception('Error getting asyncReject'); return new Promise(function($_, callable $reject) { return $reject('Error getting asyncReject'); });
},
'asyncRawReject' => function () {
return \React\Promise\reject('Error getting asyncRawReject');
},
'asyncEmptyReject' => function () {
return \React\Promise\reject();
}, },
'asyncError' => function() { 'asyncError' => function() {
throw new \Exception('Error getting asyncError'); return new Promise(function() { throw new \Exception('Error getting asyncError'); });
} },
// inherited from JS reference implementation, but make no sense in this PHP impl
// leaving it just to simplify migrations from newer js versions
'asyncRawError' => function() {
return new Promise(function() { throw new \Exception('Error getting asyncRawError'); });
},
'asyncReturnError' => function() {
return \React\Promise\resolve(new \Exception('Error getting asyncReturnError'));
},
]; ];
$docAst = Parser::parse($doc); $docAst = Parser::parse($doc);
@ -380,10 +428,16 @@ 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() ], 'syncRawError' => ['type' => Type::string()],
'syncReturnError' => ['type' => Type::string()],
'syncReturnErrorList' => ['type' => Type::listOf(Type::string())],
'async' => ['type' => Type::string()], 'async' => ['type' => Type::string()],
'asyncReject' => ['type' => Type::string() ], 'asyncReject' => ['type' => Type::string() ],
'asyncRawReject' => ['type' => Type::string() ],
'asyncEmptyReject' => ['type' => Type::string() ],
'asyncError' => ['type' => Type::string()], 'asyncError' => ['type' => Type::string()],
'asyncRawError' => ['type' => Type::string()],
'asyncReturnError' => ['type' => Type::string()],
] ]
]) ])
]); ]);
@ -393,21 +447,79 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
'sync' => 'sync', 'sync' => 'sync',
'syncError' => null, 'syncError' => null,
'syncRawError' => null, 'syncRawError' => null,
'syncReturnError' => null,
'syncReturnErrorList' => ['sync0', null, 'sync2', null],
'async' => 'async', 'async' => 'async',
'asyncReject' => null, 'asyncReject' => null,
'asyncRawReject' => null,
'asyncEmptyReject' => null,
'asyncError' => null, 'asyncError' => null,
'asyncRawError' => null,
'asyncReturnError' => null,
], ],
'errors' => [ 'errors' => [
FormattedError::create('Error getting syncError', [new SourceLocation(3, 7)]), [
FormattedError::create('Error getting syncRawError', [new SourceLocation(4, 7)]), 'message' => 'Error getting syncError',
FormattedError::create('Error getting asyncReject', [new SourceLocation(6, 7)]), 'locations' => [['line' => 3, 'column' => 7]],
FormattedError::create('Error getting asyncError', [new SourceLocation(7, 7)]) 'path' => ['syncError']
],
[
'message' => 'Error getting syncRawError',
'locations' => [ [ 'line' => 4, 'column' => 7 ] ],
'path'=> [ 'syncRawError' ]
],
[
'message' => 'Error getting syncReturnError',
'locations' => [['line' => 5, 'column' => 7]],
'path' => ['syncReturnError']
],
[
'message' => 'Error getting syncReturnErrorList1',
'locations' => [['line' => 6, 'column' => 7]],
'path' => ['syncReturnErrorList', 1]
],
[
'message' => 'Error getting syncReturnErrorList3',
'locations' => [['line' => 6, 'column' => 7]],
'path' => ['syncReturnErrorList', 3]
],
[
'message' => 'Error getting asyncReject',
'locations' => [['line' => 8, 'column' => 7]],
'path' => ['asyncReject']
],
[
'message' => 'Error getting asyncRawReject',
'locations' => [['line' => 9, 'column' => 7]],
'path' => ['asyncRawReject']
],
[
'message' => 'An unknown error occurred.',
'locations' => [['line' => 10, 'column' => 7]],
'path' => ['asyncEmptyReject']
],
[
'message' => 'Error getting asyncError',
'locations' => [['line' => 11, 'column' => 7]],
'path' => ['asyncError']
],
[
'message' => 'Error getting asyncRawError',
'locations' => [ [ 'line' => 12, 'column' => 7 ] ],
'path' => [ 'asyncRawError' ]
],
[
'message' => 'Error getting asyncReturnError',
'locations' => [['line' => 13, 'column' => 7]],
'path' => ['asyncReturnError']
],
] ]
]; ];
Executor::setPromiseAdapter(new ReactPromiseAdapter());
$result = Executor::execute($schema, $docAst, $data); $result = Executor::execute($schema, $docAst, $data);
$this->assertArraySubset($expected, $result->toArray()); $this->assertEquals($expected, self::awaitPromise($result));
} }
/** /**
@ -629,6 +741,62 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(['data' => ['a' => 'b']], $subscriptionResult->toArray()); $this->assertEquals(['data' => ['a' => 'b']], $subscriptionResult->toArray());
} }
public function testCorrectFieldOrderingDespiteExecutionOrder()
{
Executor::setPromiseAdapter(new ReactPromiseAdapter());
$doc = '{
a,
b,
c,
d,
e
}';
$data = [
'a' => function () {
return 'a';
},
'b' => function () {
return new Promise(function (callable $resolve) { return $resolve('b'); });
},
'c' => function () {
return 'c';
},
'd' => function () {
return new Promise(function (callable $resolve) { return $resolve('d'); });
},
'e' => function () {
return 'e';
},
];
$ast = Parser::parse($doc);
$queryType = new ObjectType([
'name' => 'DeepDataType',
'fields' => [
'a' => [ 'type' => Type::string() ],
'b' => [ 'type' => Type::string() ],
'c' => [ 'type' => Type::string() ],
'd' => [ 'type' => Type::string() ],
'e' => [ 'type' => Type::string() ],
]
]);
$schema = new Schema(['query' => $queryType]);
$expected = [
'data' => [
'a' => 'a',
'b' => 'b',
'c' => 'c',
'd' => 'd',
'e' => 'e',
]
];
$this->assertEquals($expected, self::awaitPromise(Executor::execute($schema, $ast, $data)));
}
/** /**
* @it Avoids recursion * @it Avoids recursion
*/ */
@ -923,4 +1091,17 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
] ]
], $result->toArray()); ], $result->toArray());
} }
/**
* @param \GraphQL\Executor\Promise\Promise $promise
* @return array
*/
private static function awaitPromise($promise)
{
$results = null;
$promise->then(function (ExecutionResult $executionResult) use (&$results) {
$results = $executionResult->toArray();
});
return $results;
}
} }

View File

@ -1,9 +1,11 @@
<?php <?php
namespace GraphQL\Tests\Executor; namespace GraphQL\Tests\Executor;
use GraphQL\Error\Error; use GraphQL\Executor\ExecutionResult;
use GraphQL\Executor\Executor; use GraphQL\Executor\Executor;
use GraphQL\Error\FormattedError; use GraphQL\Error\FormattedError;
use GraphQL\Executor\Promise\Adapter\ReactPromiseAdapter;
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
use GraphQL\Language\SourceLocation; use GraphQL\Language\SourceLocation;
use GraphQL\Schema; use GraphQL\Schema;
@ -12,6 +14,521 @@ use GraphQL\Type\Definition\Type;
class ListsTest extends \PHPUnit_Framework_TestCase class ListsTest extends \PHPUnit_Framework_TestCase
{ {
public static function setUpBeforeClass()
{
Executor::setPromiseAdapter(new ReactPromiseAdapter());
}
public static function tearDownAfterClass()
{
Executor::setPromiseAdapter(null);
}
// Describe: Execute: Handles list nullability
/**
* @describe [T]
*/
public function testHandlesNullableListsWithArray()
{
// Contains values
$this->checkHandlesNullableLists(
[ 1, 2 ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
);
// Contains null
$this->checkHandlesNullableLists(
[ 1, null, 2 ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, null, 2 ] ] ] ]
);
// Returns null
$this->checkHandlesNullableLists(
null,
[ 'data' => [ 'nest' => [ 'test' => null ] ] ]
);
}
/**
* @describe [T]
*/
public function testHandlesNullableListsWithPromiseArray()
{
// Contains values
$this->checkHandlesNullableLists(
\React\Promise\resolve([ 1, 2 ]),
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
);
// Contains null
$this->checkHandlesNullableLists(
\React\Promise\resolve([ 1, null, 2 ]),
[ 'data' => [ 'nest' => [ 'test' => [ 1, null, 2 ] ] ] ]
);
// Returns null
$this->checkHandlesNullableLists(
\React\Promise\resolve(null),
[ 'data' => [ 'nest' => [ 'test' => null ] ] ]
);
// Rejected
$this->checkHandlesNullableLists(
function () {
return \React\Promise\reject(new \Exception('bad'));
},
[
'data' => ['nest' => ['test' => null]],
'errors' => [
[
'message' => 'bad',
'locations' => [['line' => 1, 'column' => 10]],
'path' => ['nest', 'test']
]
]
]
);
}
/**
* @describe [T]
*/
public function testHandlesNullableListsWithArrayPromise()
{
// Contains values
$this->checkHandlesNullableLists(
[ \React\Promise\resolve(1), \React\Promise\resolve(2) ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
);
// Contains null
$this->checkHandlesNullableLists(
[ \React\Promise\resolve(1), \React\Promise\resolve(null), \React\Promise\resolve(2) ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, null, 2 ] ] ] ]
);
// Returns null
$this->checkHandlesNullableLists(
\React\Promise\resolve(null),
[ 'data' => [ 'nest' => [ 'test' => null ] ] ]
);
// Contains reject
$this->checkHandlesNullableLists(
function () {
return [ \React\Promise\resolve(1), \React\Promise\reject(new \Exception('bad')), \React\Promise\resolve(2) ];
},
[
'data' => ['nest' => ['test' => [1, null, 2]]],
'errors' => [
[
'message' => 'bad',
'locations' => [['line' => 1, 'column' => 10]],
'path' => ['nest', 'test', 1]
]
]
]
);
}
/**
* @describe [T]!
*/
public function testHandlesNonNullableListsWithArray()
{
// Contains values
$this->checkHandlesNonNullableLists(
[ 1, 2 ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
);
// Contains null
$this->checkHandlesNonNullableLists(
[ 1, null, 2 ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, null, 2 ] ] ] ]
);
// Returns null
$this->checkHandlesNonNullableLists(
null,
[
'data' => [ 'nest' => null ],
'errors' => [
FormattedError::create(
'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ]
)
]
]
);
}
/**
* @describe [T]!
*/
public function testHandlesNonNullableListsWithPromiseArray()
{
// Contains values
$this->checkHandlesNonNullableLists(
\React\Promise\resolve([ 1, 2 ]),
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
);
// Contains null
$this->checkHandlesNonNullableLists(
\React\Promise\resolve([ 1, null, 2 ]),
[ 'data' => [ 'nest' => [ 'test' => [ 1, null, 2 ] ] ] ]
);
// Returns null
$this->checkHandlesNonNullableLists(
null,
[
'data' => [ 'nest' => null ],
'errors' => [
FormattedError::create(
'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ]
)
]
]
);
// Rejected
$this->checkHandlesNonNullableLists(
function () {
return \React\Promise\reject(new \Exception('bad'));
},
[
'data' => ['nest' => null],
'errors' => [
[
'message' => 'bad',
'locations' => [['line' => 1, 'column' => 10]],
'path' => ['nest', 'test']
]
]
]
);
}
/**
* @describe [T]!
*/
public function testHandlesNonNullableListsWithArrayPromise()
{
// Contains values
$this->checkHandlesNonNullableLists(
[ \React\Promise\resolve(1), \React\Promise\resolve(2) ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
);
// Contains null
$this->checkHandlesNonNullableLists(
[ \React\Promise\resolve(1), \React\Promise\resolve(null), \React\Promise\resolve(2) ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, null, 2 ] ] ] ]
);
// Contains reject
$this->checkHandlesNonNullableLists(
function () {
return [ \React\Promise\resolve(1), \React\Promise\reject(new \Exception('bad')), \React\Promise\resolve(2) ];
},
[
'data' => ['nest' => ['test' => [1, null, 2]]],
'errors' => [
[
'message' => 'bad',
'locations' => [['line' => 1, 'column' => 10]],
'path' => ['nest', 'test', 1]
]
]
]
);
}
/**
* @describe [T!]
*/
public function testHandlesListOfNonNullsWithArray()
{
// Contains values
$this->checkHandlesListOfNonNulls(
[ 1, 2 ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
);
// Contains null
$this->checkHandlesListOfNonNulls(
[ 1, null, 2 ],
[
'data' => [ 'nest' => [ 'test' => null ] ],
'errors' => [
FormattedError::create(
'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ]
)
]
]
);
// Returns null
$this->checkHandlesListOfNonNulls(
null,
[ 'data' => [ 'nest' => [ 'test' => null ] ] ]
);
}
/**
* @describe [T!]
*/
public function testHandlesListOfNonNullsWithPromiseArray()
{
// Contains values
$this->checkHandlesListOfNonNulls(
\React\Promise\resolve([ 1, 2 ]),
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
);
// Contains null
$this->checkHandlesListOfNonNulls(
\React\Promise\resolve([ 1, null, 2 ]),
[
'data' => [ 'nest' => [ 'test' => null ] ],
'errors' => [
FormattedError::create(
'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ]
)
]
]
);
// Returns null
$this->checkHandlesListOfNonNulls(
\React\Promise\resolve(null),
[ 'data' => [ 'nest' => [ 'test' => null ] ] ]
);
// Rejected
$this->checkHandlesListOfNonNulls(
function () {
return \React\Promise\reject(new \Exception('bad'));
},
[
'data' => ['nest' => ['test' => null]],
'errors' => [
[
'message' => 'bad',
'locations' => [['line' => 1, 'column' => 10]],
'path' => ['nest', 'test']
]
]
]
);
}
/**
* @describe [T]!
*/
public function testHandlesListOfNonNullsWithArrayPromise()
{
// Contains values
$this->checkHandlesListOfNonNulls(
[ \React\Promise\resolve(1), \React\Promise\resolve(2) ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
);
// Contains null
$this->checkHandlesListOfNonNulls(
[ \React\Promise\resolve(1), \React\Promise\resolve(null), \React\Promise\resolve(2) ],
[ 'data' => [ 'nest' => [ 'test' => null ] ] ]
);
// Contains reject
$this->checkHandlesListOfNonNulls(
function () {
return [ \React\Promise\resolve(1), \React\Promise\reject(new \Exception('bad')), \React\Promise\resolve(2) ];
},
[
'data' => ['nest' => ['test' => null]],
'errors' => [
[
'message' => 'bad',
'locations' => [['line' => 1, 'column' => 10]],
'path' => ['nest', 'test', 1]
]
]
]
);
}
/**
* @describe [T!]!
*/
public function testHandlesNonNullListOfNonNullsWithArray()
{
// Contains values
$this->checkHandlesNonNullListOfNonNulls(
[ 1, 2 ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
);
// Contains null
$this->checkHandlesNonNullListOfNonNulls(
[ 1, null, 2 ],
[
'data' => [ 'nest' => null ],
'errors' => [
FormattedError::create(
'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ]
)
]
]
);
// Returns null
$this->checkHandlesNonNullListOfNonNulls(
null,
[
'data' => [ 'nest' => null ],
'errors' => [
FormattedError::create(
'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ]
)
]
]
);
}
/**
* @describe [T!]!
*/
public function testHandlesNonNullListOfNonNullsWithPromiseArray()
{
// Contains values
$this->checkHandlesNonNullListOfNonNulls(
\React\Promise\resolve([ 1, 2 ]),
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
);
// Contains null
$this->checkHandlesNonNullListOfNonNulls(
\React\Promise\resolve([ 1, null, 2 ]),
[
'data' => [ 'nest' => null ],
'errors' => [
FormattedError::create(
'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ]
)
]
]
);
// Returns null
$this->checkHandlesNonNullListOfNonNulls(
\React\Promise\resolve(null),
[
'data' => [ 'nest' => null ],
'errors' => [
FormattedError::create(
'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ]
)
]
]
);
// Rejected
$this->checkHandlesNonNullListOfNonNulls(
function () {
return \React\Promise\reject(new \Exception('bad'));
},
[
'data' => ['nest' => null ],
'errors' => [
[
'message' => 'bad',
'locations' => [['line' => 1, 'column' => 10]],
'path' => ['nest', 'test']
]
]
]
);
}
/**
* @describe [T!]!
*/
public function testHandlesNonNullListOfNonNullsWithArrayPromise()
{
// Contains values
$this->checkHandlesNonNullListOfNonNulls(
[ \React\Promise\resolve(1), \React\Promise\resolve(2) ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
);
// Contains null
$this->checkHandlesNonNullListOfNonNulls(
[ \React\Promise\resolve(1), \React\Promise\resolve(null), \React\Promise\resolve(2) ],
[
'data' => [ 'nest' => null ],
'errors' => [
FormattedError::create(
'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ]
)
]
]
);
// Contains reject
$this->checkHandlesNonNullListOfNonNulls(
function () {
return [ \React\Promise\resolve(1), \React\Promise\reject(new \Exception('bad')), \React\Promise\resolve(2) ];
},
[
'data' => ['nest' => null ],
'errors' => [
[
'message' => 'bad',
'locations' => [['line' => 1, 'column' => 10]],
'path' => ['nest', 'test']
]
]
]
);
}
private function checkHandlesNullableLists($testData, $expected)
{
$testType = Type::listOf(Type::int());;
$this->check($testType, $testData, $expected);
}
private function checkHandlesNonNullableLists($testData, $expected)
{
$testType = Type::nonNull(Type::listOf(Type::int()));
$this->check($testType, $testData, $expected);
}
private function checkHandlesListOfNonNulls($testData, $expected)
{
$testType = Type::listOf(Type::nonNull(Type::int()));
$this->check($testType, $testData, $expected);
}
public function checkHandlesNonNullListOfNonNulls($testData, $expected)
{
$testType = Type::nonNull(Type::listOf(Type::nonNull(Type::int())));
$this->check($testType, $testData, $expected);
}
private function check($testType, $testData, $expected) private function check($testType, $testData, $expected)
{ {
$data = ['test' => $testData]; $data = ['test' => $testData];
@ -41,157 +558,19 @@ class ListsTest extends \PHPUnit_Framework_TestCase
$ast = Parser::parse('{ nest { test } }'); $ast = Parser::parse('{ nest { test } }');
$result = Executor::execute($schema, $ast, $data); $result = Executor::execute($schema, $ast, $data);
$this->assertArraySubset($expected, $result->toArray()); $this->assertArraySubset($expected, self::awaitPromise($result));
}
// Describe: Execute: Handles list nullability
/**
* @describe [T]
*/
public function testHandlesNullableLists()
{
$type = Type::listOf(Type::int());
// Contains values
$this->check(
$type,
[ 1, 2 ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
);
// Contains null
$this->check(
$type,
[ 1, null, 2 ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, null, 2 ] ] ] ]
);
// Returns null
$this->check(
$type,
null,
[ 'data' => [ 'nest' => [ 'test' => null ] ] ]
);
} }
/** /**
* @describe [T]! * @param \GraphQL\Executor\Promise\Promise $promise
* @return array
*/ */
public function testHandlesNonNullableLists() private static function awaitPromise($promise)
{ {
$type = Type::nonNull(Type::listOf(Type::int())); $results = null;
$promise->then(function (ExecutionResult $executionResult) use (&$results) {
// Contains values $results = $executionResult->toArray();
$this->check( });
$type, return $results;
[ 1, 2 ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
);
// Contains null
$this->check(
$type,
[ 1, null, 2 ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, null, 2 ] ] ] ]
);
// Returns null
$this->check(
$type,
null,
[
'data' => [ 'nest' => null ],
'errors' => [
FormattedError::create(
'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ]
)
]
]
);
}
/**
* @describe [T!]
*/
public function testHandlesListOfNonNulls()
{
$type = Type::listOf(Type::nonNull(Type::int()));
// Contains values
$this->check(
$type,
[ 1, 2 ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
);
// Contains null
$this->check(
$type,
[ 1, null, 2 ],
[
'data' => [ 'nest' => [ 'test' => null ] ],
'errors' => [
FormattedError::create(
'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ]
)
]
]
);
// Returns null
$this->check(
$type,
null,
[ 'data' => [ 'nest' => [ 'test' => null ] ] ]
);
}
/**
* @describe [T!]!
*/
public function testHandlesNonNullListOfNonNulls()
{
$type = Type::nonNull(Type::listOf(Type::nonNull(Type::int())));
// Contains values
$this->check(
$type,
[ 1, 2 ],
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
);
// Contains null
$this->check(
$type,
[ 1, null, 2 ],
[
'data' => [ 'nest' => null ],
'errors' => [
FormattedError::create(
'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ]
)
]
]
);
// Returns null
$this->check(
$type,
null,
[
'data' => [ 'nest' => null ],
'errors' => [
FormattedError::create(
'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ]
)
]
]
);
} }
} }

View File

@ -1,16 +1,29 @@
<?php <?php
namespace GraphQL\Tests\Executor; namespace GraphQL\Tests\Executor;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Executor\Executor; use GraphQL\Executor\Executor;
use GraphQL\Error\FormattedError; use GraphQL\Error\FormattedError;
use GraphQL\Executor\Promise\Adapter\ReactPromiseAdapter;
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\ObjectType; use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use React\Promise\Promise;
class MutationsTest extends \PHPUnit_Framework_TestCase class MutationsTest extends \PHPUnit_Framework_TestCase
{ {
public static function setUpBeforeClass()
{
Executor::setPromiseAdapter(new ReactPromiseAdapter());
}
public static function tearDownAfterClass()
{
Executor::setPromiseAdapter(null);
}
// Execute: Handles mutation execution ordering // Execute: Handles mutation execution ordering
/** /**
@ -56,7 +69,7 @@ class MutationsTest extends \PHPUnit_Framework_TestCase
] ]
] ]
]; ];
$this->assertEquals($expected, $mutationResult->toArray()); $this->assertEquals($expected, self::awaitPromise($mutationResult));
} }
/** /**
@ -114,7 +127,20 @@ class MutationsTest extends \PHPUnit_Framework_TestCase
) )
] ]
]; ];
$this->assertArraySubset($expected, $mutationResult->toArray()); $this->assertArraySubset($expected, self::awaitPromise($mutationResult));
}
/**
* @param \GraphQL\Executor\Promise\Promise $promise
* @return array
*/
private static function awaitPromise($promise)
{
$results = null;
$promise->then(function (ExecutionResult $executionResult) use (&$results) {
$results = $executionResult->toArray();
});
return $results;
} }
private function schema() private function schema()
@ -200,12 +226,14 @@ class Root {
/** /**
* @param $newNumber * @param $newNumber
* @return NumberHolder *
* @return Promise
*/ */
public function promiseToChangeTheNumber($newNumber) public function promiseToChangeTheNumber($newNumber)
{ {
// No promises return new Promise(function (callable $resolve) use ($newNumber) {
return $this->immediatelyChangeTheNumber($newNumber); return $resolve($this->immediatelyChangeTheNumber($newNumber));
});
} }
/** /**
@ -217,11 +245,12 @@ class Root {
} }
/** /**
* @throws \Exception * @return Promise
*/ */
public function promiseAndFailToChangeTheNumber() public function promiseAndFailToChangeTheNumber()
{ {
// No promises return new Promise(function (callable $resolve, callable $reject) {
throw new \Exception("Cannot change the number"); return $reject(new \Exception("Cannot change the number"));
});
} }
} }

View File

@ -1,14 +1,17 @@
<?php <?php
namespace GraphQL\Tests\Executor; namespace GraphQL\Tests\Executor;
use GraphQL\Error\Error;
use GraphQL\Executor\Executor; use GraphQL\Executor\Executor;
use GraphQL\Error\FormattedError; use GraphQL\Error\FormattedError;
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
use GraphQL\Language\SourceLocation; use GraphQL\Language\SourceLocation;
use GraphQL\Executor\Promise\Adapter\ReactPromiseAdapter;
use GraphQL\Schema; use GraphQL\Schema;
use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use React\Promise\Promise;
use React\Promise\PromiseInterface;
class NonNullTest extends \PHPUnit_Framework_TestCase class NonNullTest extends \PHPUnit_Framework_TestCase
{ {
@ -17,6 +20,13 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
/** @var \Exception */ /** @var \Exception */
public $nonNullSyncError; public $nonNullSyncError;
/** @var \Exception */
public $promiseError;
/** @var \Exception */
public $nonNullPromiseError;
public $throwingData; public $throwingData;
public $nullingData; public $nullingData;
public $schema; public $schema;
@ -25,6 +35,8 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
{ {
$this->syncError = new \Exception('sync'); $this->syncError = new \Exception('sync');
$this->nonNullSyncError = new \Exception('nonNullSync'); $this->nonNullSyncError = new \Exception('nonNullSync');
$this->promiseError = new \Exception('promise');
$this->nonNullPromiseError = new \Exception('nonNullPromise');
$this->throwingData = [ $this->throwingData = [
'sync' => function () { 'sync' => function () {
@ -33,12 +45,32 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
'nonNullSync' => function () { 'nonNullSync' => function () {
throw $this->nonNullSyncError; throw $this->nonNullSyncError;
}, },
'promise' => function () {
return new Promise(function () {
throw $this->promiseError;
});
},
'nonNullPromise' => function () {
return new Promise(function () {
throw $this->nonNullPromiseError;
});
},
'nest' => function () { 'nest' => function () {
return $this->throwingData; return $this->throwingData;
}, },
'nonNullNest' => function () { 'nonNullNest' => function () {
return $this->throwingData; return $this->throwingData;
}, },
'promiseNest' => function () {
return new Promise(function (callable $resolve) {
$resolve($this->throwingData);
});
},
'nonNullPromiseNest' => function () {
return new Promise(function (callable $resolve) {
$resolve($this->throwingData);
});
},
]; ];
$this->nullingData = [ $this->nullingData = [
@ -48,12 +80,32 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
'nonNullSync' => function () { 'nonNullSync' => function () {
return null; return null;
}, },
'promise' => function () {
return new Promise(function (callable $resolve) {
return $resolve(null);
});
},
'nonNullPromise' => function () {
return new Promise(function (callable $resolve) {
return $resolve(null);
});
},
'nest' => function () { 'nest' => function () {
return $this->nullingData; return $this->nullingData;
}, },
'nonNullNest' => function () { 'nonNullNest' => function () {
return $this->nullingData; return $this->nullingData;
}, },
'promiseNest' => function () {
return new Promise(function (callable $resolve) {
$resolve($this->nullingData);
});
},
'nonNullPromiseNest' => function () {
return new Promise(function (callable $resolve) {
$resolve($this->nullingData);
});
},
]; ];
$dataType = new ObjectType([ $dataType = new ObjectType([
@ -62,8 +114,12 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
return [ return [
'sync' => ['type' => Type::string()], 'sync' => ['type' => Type::string()],
'nonNullSync' => ['type' => Type::nonNull(Type::string())], 'nonNullSync' => ['type' => Type::nonNull(Type::string())],
'promise' => Type::string(),
'nonNullPromise' => Type::nonNull(Type::string()),
'nest' => $dataType, 'nest' => $dataType,
'nonNullNest' => Type::nonNull($dataType) 'nonNullNest' => Type::nonNull($dataType),
'promiseNest' => $dataType,
'nonNullPromiseNest' => Type::nonNull($dataType),
]; ];
} }
]); ]);
@ -71,6 +127,11 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
$this->schema = new Schema(['query' => $dataType]); $this->schema = new Schema(['query' => $dataType]);
} }
public function tearDown()
{
Executor::setPromiseAdapter(null);
}
// Execute: handles non-nullable types // Execute: handles non-nullable types
/** /**
@ -100,6 +161,32 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
$this->assertArraySubset($expected, Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q')->toArray()); $this->assertArraySubset($expected, Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q')->toArray());
} }
public function testNullsANullableFieldThatThrowsInAPromise()
{
$doc = '
query Q {
promise
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => [
'promise' => null,
],
'errors' => [
FormattedError::create(
$this->promiseError->getMessage(),
[new SourceLocation(3, 9)]
)
]
];
Executor::setPromiseAdapter(new ReactPromiseAdapter());
$this->assertArraySubsetPromise($expected, Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q'));
}
public function testNullsASynchronouslyReturnedObjectThatContainsANonNullableFieldThatThrowsSynchronously() public function testNullsASynchronouslyReturnedObjectThatContainsANonNullableFieldThatThrowsSynchronously()
{ {
// nulls a synchronously returned object that contains a non-nullable field that throws synchronously // nulls a synchronously returned object that contains a non-nullable field that throws synchronously
@ -124,18 +211,111 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
$this->assertArraySubset($expected, Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q')->toArray()); $this->assertArraySubset($expected, Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q')->toArray());
} }
public function testNullsAsynchronouslyReturnedObjectThatContainsANonNullableFieldThatThrowsInAPromise()
{
$doc = '
query Q {
nest {
nonNullPromise,
}
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => [
'nest' => null
],
'errors' => [
FormattedError::create($this->nonNullPromiseError->getMessage(), [new SourceLocation(4, 11)])
]
];
Executor::setPromiseAdapter(new ReactPromiseAdapter());
$this->assertArraySubsetPromise($expected, Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q'));
}
public function testNullsAnObjectReturnedInAPromiseThatContainsANonNullableFieldThatThrowsSynchronously()
{
$doc = '
query Q {
promiseNest {
nonNullSync,
}
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => [
'promiseNest' => null
],
'errors' => [
FormattedError::create($this->nonNullSyncError->getMessage(), [new SourceLocation(4, 11)])
]
];
Executor::setPromiseAdapter(new ReactPromiseAdapter());
$this->assertArraySubsetPromise($expected, Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q'));
}
public function testNullsAnObjectReturnedInAPromiseThatContainsANonNullableFieldThatThrowsInAPromise()
{
$doc = '
query Q {
promiseNest {
nonNullPromise,
}
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => [
'promiseNest' => null
],
'errors' => [
FormattedError::create($this->nonNullPromiseError->getMessage(), [new SourceLocation(4, 11)])
]
];
Executor::setPromiseAdapter(new ReactPromiseAdapter());
$this->assertArraySubsetPromise($expected, Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q'));
}
public function testNullsAComplexTreeOfNullableFieldsThatThrow() public function testNullsAComplexTreeOfNullableFieldsThatThrow()
{ {
$doc = ' $doc = '
query Q { query Q {
nest { nest {
sync sync
promise
nest { nest {
sync sync
promise
}
promiseNest {
sync
promise
}
}
promiseNest {
sync
promise
nest {
sync
promise
}
promiseNest {
sync
promise
} }
} }
} }
'; ';
$ast = Parser::parse($doc); $ast = Parser::parse($doc);
@ -143,17 +323,119 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
'data' => [ 'data' => [
'nest' => [ 'nest' => [
'sync' => null, 'sync' => null,
'promise' => null,
'nest' => [ 'nest' => [
'sync' => null, 'sync' => null,
] 'promise' => null,
] ],
'promiseNest' => [
'sync' => null,
'promise' => null,
],
],
'promiseNest' => [
'sync' => null,
'promise' => null,
'nest' => [
'sync' => null,
'promise' => null,
],
'promiseNest' => [
'sync' => null,
'promise' => null,
],
],
], ],
'errors' => [ 'errors' => [
FormattedError::create($this->syncError->getMessage(), [new SourceLocation(4, 11)]), FormattedError::create($this->syncError->getMessage(), [new SourceLocation(4, 11)]),
FormattedError::create($this->syncError->getMessage(), [new SourceLocation(6, 13)]), FormattedError::create($this->promiseError->getMessage(), [new SourceLocation(5, 11)]),
FormattedError::create($this->syncError->getMessage(), [new SourceLocation(7, 13)]),
FormattedError::create($this->promiseError->getMessage(), [new SourceLocation(8, 13)]),
FormattedError::create($this->syncError->getMessage(), [new SourceLocation(11, 13)]),
FormattedError::create($this->promiseError->getMessage(), [new SourceLocation(12, 13)]),
FormattedError::create($this->syncError->getMessage(), [new SourceLocation(16, 11)]),
FormattedError::create($this->promiseError->getMessage(), [new SourceLocation(17, 11)]),
FormattedError::create($this->syncError->getMessage(), [new SourceLocation(19, 13)]),
FormattedError::create($this->promiseError->getMessage(), [new SourceLocation(20, 13)]),
FormattedError::create($this->syncError->getMessage(), [new SourceLocation(23, 13)]),
FormattedError::create($this->promiseError->getMessage(), [new SourceLocation(24, 13)]),
] ]
]; ];
$this->assertArraySubset($expected, Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q')->toArray());
Executor::setPromiseAdapter(new ReactPromiseAdapter());
$this->assertArraySubsetPromise($expected, Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q'));
}
public function testNullsTheFirstNullableObjectAfterAFieldThrowsInALongChainOfFieldsThatAreNonNull()
{
$doc = '
query Q {
nest {
nonNullNest {
nonNullPromiseNest {
nonNullNest {
nonNullPromiseNest {
nonNullSync
}
}
}
}
}
promiseNest {
nonNullNest {
nonNullPromiseNest {
nonNullNest {
nonNullPromiseNest {
nonNullSync
}
}
}
}
}
anotherNest: nest {
nonNullNest {
nonNullPromiseNest {
nonNullNest {
nonNullPromiseNest {
nonNullPromise
}
}
}
}
}
anotherPromiseNest: promiseNest {
nonNullNest {
nonNullPromiseNest {
nonNullNest {
nonNullPromiseNest {
nonNullPromise
}
}
}
}
}
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => [
'nest' => null,
'promiseNest' => null,
'anotherNest' => null,
'anotherPromiseNest' => null,
],
'errors' => [
FormattedError::create($this->nonNullSyncError->getMessage(), [new SourceLocation(8, 19)]),
FormattedError::create($this->nonNullSyncError->getMessage(), [new SourceLocation(19, 19)]),
FormattedError::create($this->nonNullPromiseError->getMessage(), [new SourceLocation(30, 19)]),
FormattedError::create($this->nonNullPromiseError->getMessage(), [new SourceLocation(41, 19)]),
]
];
Executor::setPromiseAdapter(new ReactPromiseAdapter());
$this->assertArraySubsetPromise($expected, Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q'));
} }
public function testNullsANullableFieldThatSynchronouslyReturnsNull() public function testNullsANullableFieldThatSynchronouslyReturnsNull()
@ -174,9 +456,28 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray()); $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray());
} }
public function test4() public function testNullsANullableFieldThatReturnsNullInAPromise()
{
$doc = '
query Q {
promise
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => [
'promise' => null,
]
];
Executor::setPromiseAdapter(new ReactPromiseAdapter());
$this->assertArraySubsetPromise($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q'));
}
public function testNullsASynchronouslyReturnedObjectThatContainsANonNullableFieldThatReturnsNullSynchronously()
{ {
// nulls a synchronously returned object that contains a non-nullable field that returns null synchronously
$doc = ' $doc = '
query Q { query Q {
nest { nest {
@ -198,19 +499,107 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
$this->assertArraySubset($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray()); $this->assertArraySubset($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray());
} }
public function test5() public function testNullsASynchronouslyReturnedObjectThatContainsANonNullableFieldThatReturnsNullInAPromise()
{ {
// nulls a complex tree of nullable fields that return null $doc = '
query Q {
nest {
nonNullPromise,
}
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => [
'nest' => null,
],
'errors' => [
FormattedError::create('Cannot return null for non-nullable field DataType.nonNullPromise.', [new SourceLocation(4, 11)]),
]
];
Executor::setPromiseAdapter(new ReactPromiseAdapter());
$this->assertArraySubsetPromise($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q'));
}
public function testNullsAnObjectReturnedInAPromiseThatContainsANonNullableFieldThatReturnsNullSynchronously()
{
$doc = '
query Q {
promiseNest {
nonNullSync,
}
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => [
'promiseNest' => null,
],
'errors' => [
FormattedError::create('Cannot return null for non-nullable field DataType.nonNullSync.', [new SourceLocation(4, 11)]),
]
];
Executor::setPromiseAdapter(new ReactPromiseAdapter());
$this->assertArraySubsetPromise($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q'));
}
public function testNullsAnObjectReturnedInAPromiseThatContainsANonNullableFieldThatReturnsNullInaAPromise()
{
$doc = '
query Q {
promiseNest {
nonNullPromise,
}
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => [
'promiseNest' => null,
],
'errors' => [
FormattedError::create('Cannot return null for non-nullable field DataType.nonNullPromise.', [new SourceLocation(4, 11)]),
]
];
Executor::setPromiseAdapter(new ReactPromiseAdapter());
$this->assertArraySubsetPromise($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q'));
}
public function testNullsAComplexTreeOfNullableFieldsThatReturnNull()
{
$doc = ' $doc = '
query Q { query Q {
nest { nest {
sync sync
promise
nest { nest {
sync sync
nest { promise
sync }
} promiseNest {
sync
promise
}
}
promiseNest {
sync
promise
nest {
sync
promise
}
promiseNest {
sync
promise
} }
} }
} }
@ -222,16 +611,107 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
'data' => [ 'data' => [
'nest' => [ 'nest' => [
'sync' => null, 'sync' => null,
'promise' => null,
'nest' => [ 'nest' => [
'sync' => null, 'sync' => null,
'nest' => [ 'promise' => null,
'sync' => null
]
], ],
'promiseNest' => [
'sync' => null,
'promise' => null,
]
], ],
'promiseNest' => [
'sync' => null,
'promise' => null,
'nest' => [
'sync' => null,
'promise' => null,
],
'promiseNest' => [
'sync' => null,
'promise' => null,
]
]
] ]
]; ];
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray());
Executor::setPromiseAdapter(new ReactPromiseAdapter());
Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->then(function ($actual) use ($expected) {
$this->assertEquals($expected, $actual);
});
}
public function testNullsTheFirstNullableObjectAfterAFieldReturnsNullInALongChainOfFieldsThatAreNonNull()
{
$doc = '
query Q {
nest {
nonNullNest {
nonNullPromiseNest {
nonNullNest {
nonNullPromiseNest {
nonNullSync
}
}
}
}
}
promiseNest {
nonNullNest {
nonNullPromiseNest {
nonNullNest {
nonNullPromiseNest {
nonNullSync
}
}
}
}
}
anotherNest: nest {
nonNullNest {
nonNullPromiseNest {
nonNullNest {
nonNullPromiseNest {
nonNullPromise
}
}
}
}
}
anotherPromiseNest: promiseNest {
nonNullNest {
nonNullPromiseNest {
nonNullNest {
nonNullPromiseNest {
nonNullPromise
}
}
}
}
}
}
';
$ast = Parser::parse($doc);
$expected = [
'data' => [
'nest' => null,
'promiseNest' => null,
'anotherNest' => null,
'anotherPromiseNest' => null,
],
'errors' => [
FormattedError::create('Cannot return null for non-nullable field DataType.nonNullSync.', [new SourceLocation(8, 19)]),
FormattedError::create('Cannot return null for non-nullable field DataType.nonNullSync.', [new SourceLocation(19, 19)]),
FormattedError::create('Cannot return null for non-nullable field DataType.nonNullPromise.', [new SourceLocation(30, 19)]),
FormattedError::create('Cannot return null for non-nullable field DataType.nonNullPromise.', [new SourceLocation(41, 19)]),
]
];
Executor::setPromiseAdapter(new ReactPromiseAdapter());
$this->assertArraySubsetPromise($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q'));
} }
public function testNullsTheTopLevelIfSyncNonNullableFieldThrows() public function testNullsTheTopLevelIfSyncNonNullableFieldThrows()
@ -241,11 +721,32 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
'; ';
$expected = [ $expected = [
'data' => null,
'errors' => [ 'errors' => [
FormattedError::create($this->nonNullSyncError->getMessage(), [new SourceLocation(2, 17)]) FormattedError::create($this->nonNullSyncError->getMessage(), [new SourceLocation(2, 17)])
] ]
]; ];
$this->assertArraySubset($expected, Executor::execute($this->schema, Parser::parse($doc), $this->throwingData)->toArray()); $actual = Executor::execute($this->schema, Parser::parse($doc), $this->throwingData)->toArray();
$this->assertArraySubset($expected, $actual);
}
public function testNullsTheTopLevelIfAsyncNonNullableFieldErrors()
{
$doc = '
query Q { nonNullPromise }
';
$ast = Parser::parse($doc);
$expected = [
'data' => null,
'errors' => [
FormattedError::create($this->nonNullPromiseError->getMessage(), [new SourceLocation(2, 17)]),
]
];
Executor::setPromiseAdapter(new ReactPromiseAdapter());
$this->assertArraySubsetPromise($expected, Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q'));
} }
public function testNullsTheTopLevelIfSyncNonNullableFieldReturnsNull() public function testNullsTheTopLevelIfSyncNonNullableFieldReturnsNull()
@ -262,4 +763,33 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
]; ];
$this->assertArraySubset($expected, Executor::execute($this->schema, Parser::parse($doc), $this->nullingData)->toArray()); $this->assertArraySubset($expected, Executor::execute($this->schema, Parser::parse($doc), $this->nullingData)->toArray());
} }
public function testNullsTheTopLevelIfAsyncNonNullableFieldResolvesNull()
{
$doc = '
query Q { nonNullPromise }
';
$ast = Parser::parse($doc);
$expected = [
'data' => null,
'errors' => [
FormattedError::create('Cannot return null for non-nullable field DataType.nonNullPromise.', [new SourceLocation(2, 17)]),
]
];
Executor::setPromiseAdapter(new ReactPromiseAdapter());
$this->assertArraySubsetPromise($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q'));
}
private function assertArraySubsetPromise($subset, PromiseInterface $promise)
{
$array = null;
$promise->then(function ($value) use (&$array) {
$array = $value->toArray();
});
$this->assertArraySubset($subset, $array);
}
} }