Pluggable executor implementations; new faster executor using coroutines

This commit is contained in:
Jakub Kulhan 2018-07-25 16:51:01 +02:00
parent 98807286f7
commit b5d3341995
52 changed files with 4193 additions and 1520 deletions

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
.phpcs-cache .phpcs-cache
composer.lock composer.lock
composer.phar
phpcs.xml phpcs.xml
phpstan.neon phpstan.neon
vendor/ vendor/

View File

@ -6,6 +6,12 @@ php:
- 7.2 - 7.2
- nightly - nightly
env:
matrix:
- EXECUTOR=coroutine
- EXECUTOR=
cache: cache:
directories: directories:
- $HOME/.composer/cache - $HOME/.composer/cache

View File

@ -26,7 +26,7 @@
"config": { "config": {
"preferred-install": "dist", "preferred-install": "dist",
"sort-packages": true "sort-packages": true
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"GraphQL\\": "src/" "GraphQL\\": "src/"

View File

@ -2,7 +2,7 @@
<phpunit <phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php" bootstrap="tests/bootstrap.php"
> >
<php> <php>
<ini name="error_reporting" value="E_ALL"/> <ini name="error_reporting" value="E_ALL"/>

View File

@ -58,7 +58,7 @@ class ExecutionResult implements JsonSerializable
* @param Error[] $errors * @param Error[] $errors
* @param mixed[] $extensions * @param mixed[] $extensions
*/ */
public function __construct(?array $data = null, array $errors = [], array $extensions = []) public function __construct($data = null, array $errors = [], array $extensions = [])
{ {
$this->data = $data; $this->data = $data;
$this->errors = $errors; $this->errors = $errors;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace GraphQL\Executor;
use GraphQL\Executor\Promise\Promise;
interface ExecutorImplementation
{
/**
* Returns promise of {@link ExecutionResult}. Promise should always resolve, never reject.
*/
public function doExecute() : Promise;
}

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\FieldNode; use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FragmentSpreadNode; use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode; use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeList; use GraphQL\Language\AST\NodeList;
use GraphQL\Language\AST\ValueNode; use GraphQL\Language\AST\ValueNode;
use GraphQL\Language\AST\VariableDefinitionNode; use GraphQL\Language\AST\VariableDefinitionNode;
@ -107,7 +108,11 @@ class Values
} }
} }
return ['errors' => $errors, 'coerced' => $errors ? null : $coercedValues]; if (! empty($errors)) {
return [$errors, null];
}
return [null, $coercedValues];
} }
/** /**
@ -154,58 +159,73 @@ class Values
*/ */
public static function getArgumentValues($def, $node, $variableValues = null) public static function getArgumentValues($def, $node, $variableValues = null)
{ {
$argDefs = $def->args; if (empty($def->args)) {
$argNodes = $node->arguments;
if (empty($argDefs) || $argNodes === null) {
return []; return [];
} }
$coercedValues = []; $argumentNodes = $node->arguments;
if (empty($argumentNodes)) {
return [];
}
/** @var ArgumentNode[] $argNodeMap */ $argumentValueMap = [];
$argNodeMap = $argNodes ? Utils::keyMap( foreach ($argumentNodes as $argumentNode) {
$argNodes, $argumentValueMap[$argumentNode->name->value] = $argumentNode->value;
static function (ArgumentNode $arg) { }
return $arg->name->value;
}
) : [];
foreach ($argDefs as $argDef) { return static::getArgumentValuesForMap($def, $argumentValueMap, $variableValues, $node);
$name = $argDef->name; }
$argType = $argDef->getType();
$argumentNode = $argNodeMap[$name] ?? null;
if (! $argumentNode) { /**
if ($argDef->defaultValueExists()) { * @param FieldDefinition|Directive $fieldDefinition
$coercedValues[$name] = $argDef->defaultValue; * @param ArgumentNode[] $argumentValueMap
* @param mixed[] $variableValues
* @param Node|null $referenceNode
*
* @return mixed[]
*
* @throws Error
*/
public static function getArgumentValuesForMap($fieldDefinition, $argumentValueMap, $variableValues = null, $referenceNode = null)
{
$argumentDefinitions = $fieldDefinition->args;
$coercedValues = [];
foreach ($argumentDefinitions as $argumentDefinition) {
$name = $argumentDefinition->name;
$argType = $argumentDefinition->getType();
$argumentValueNode = $argumentValueMap[$name] ?? null;
if (! $argumentValueNode) {
if ($argumentDefinition->defaultValueExists()) {
$coercedValues[$name] = $argumentDefinition->defaultValue;
} elseif ($argType instanceof NonNull) { } elseif ($argType instanceof NonNull) {
throw new Error( throw new Error(
'Argument "' . $name . '" of required type ' . 'Argument "' . $name . '" of required type ' .
'"' . Utils::printSafe($argType) . '" was not provided.', '"' . Utils::printSafe($argType) . '" was not provided.',
[$node] $referenceNode
); );
} }
} elseif ($argumentNode->value instanceof VariableNode) { } elseif ($argumentValueNode instanceof VariableNode) {
$variableName = $argumentNode->value->name->value; $variableName = $argumentValueNode->name->value;
if ($variableValues && array_key_exists($variableName, $variableValues)) { if ($variableValues && array_key_exists($variableName, $variableValues)) {
// Note: this does not check that this variable value is correct. // Note: this does not check that this variable value is correct.
// This assumes that this query has been validated and the variable // This assumes that this query has been validated and the variable
// usage here is of the correct type. // usage here is of the correct type.
$coercedValues[$name] = $variableValues[$variableName]; $coercedValues[$name] = $variableValues[$variableName];
} elseif ($argDef->defaultValueExists()) { } elseif ($argumentDefinition->defaultValueExists()) {
$coercedValues[$name] = $argDef->defaultValue; $coercedValues[$name] = $argumentDefinition->defaultValue;
} elseif ($argType instanceof NonNull) { } elseif ($argType instanceof NonNull) {
throw new Error( throw new Error(
'Argument "' . $name . '" of required type "' . Utils::printSafe($argType) . '" was ' . 'Argument "' . $name . '" of required type "' . Utils::printSafe($argType) . '" was ' .
'provided the variable "$' . $variableName . '" which was not provided ' . 'provided the variable "$' . $variableName . '" which was not provided ' .
'a runtime value.', 'a runtime value.',
[$argumentNode->value] [$argumentValueNode]
); );
} }
} else { } else {
$valueNode = $argumentNode->value; $valueNode = $argumentValueNode;
$coercedValue = AST::valueFromAST($valueNode, $argType, $variableValues); $coercedValue = AST::valueFromAST($valueNode, $argType, $variableValues);
if (Utils::isInvalid($coercedValue)) { if (Utils::isInvalid($coercedValue)) {
// Note: ValuesOfCorrectType validation should catch this before // Note: ValuesOfCorrectType validation should catch this before
@ -213,7 +233,7 @@ class Values
// continue with an invalid argument value. // continue with an invalid argument value.
throw new Error( throw new Error(
'Argument "' . $name . '" has invalid value ' . Printer::doPrint($valueNode) . '.', 'Argument "' . $name . '" has invalid value ' . Printer::doPrint($valueNode) . '.',
[$argumentNode->value] [$argumentValueNode]
); );
} }
$coercedValues[$name] = $coercedValue; $coercedValues[$name] = $coercedValue;

View File

@ -0,0 +1,279 @@
<?php
declare(strict_types=1);
namespace GraphQL\Experimental\Executor;
use Generator;
use GraphQL\Error\Error;
use GraphQL\Language\AST\DefinitionNode;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Language\AST\ValueNode;
use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Introspection;
use GraphQL\Type\Schema;
use function sprintf;
/**
* @internal
*/
class Collector
{
/** @var Schema */
private $schema;
/** @var Runtime */
private $runtime;
/** @var OperationDefinitionNode|null */
public $operation = null;
/** @var FragmentDefinitionNode[] */
public $fragments = [];
/** @var ObjectType|null */
public $rootType;
/** @var FieldNode[][] */
private $fields;
/** @var string[] */
private $visitedFragments;
public function __construct(Schema $schema, Runtime $runtime)
{
$this->schema = $schema;
$this->runtime = $runtime;
}
public function initialize(DocumentNode $documentNode, ?string $operationName = null)
{
$hasMultipleAssumedOperations = false;
foreach ($documentNode->definitions as $definitionNode) {
/** @var DefinitionNode|Node $definitionNode */
if ($definitionNode->kind === NodeKind::OPERATION_DEFINITION) {
/** @var OperationDefinitionNode $definitionNode */
if ($operationName === null && $this->operation !== null) {
$hasMultipleAssumedOperations = true;
}
if ($operationName === null ||
(isset($definitionNode->name) && $definitionNode->name->value === $operationName)
) {
$this->operation = $definitionNode;
}
} elseif ($definitionNode->kind === NodeKind::FRAGMENT_DEFINITION) {
/** @var FragmentDefinitionNode $definitionNode */
$this->fragments[$definitionNode->name->value] = $definitionNode;
}
}
if ($this->operation === null) {
if ($operationName !== null) {
$this->runtime->addError(new Error(sprintf('Unknown operation named "%s".', $operationName)));
} else {
$this->runtime->addError(new Error('Must provide an operation.'));
}
return;
}
if ($hasMultipleAssumedOperations) {
$this->runtime->addError(new Error('Must provide operation name if query contains multiple operations.'));
return;
}
if ($this->operation->operation === 'query') {
$this->rootType = $this->schema->getQueryType();
} elseif ($this->operation->operation === 'mutation') {
$this->rootType = $this->schema->getMutationType();
} elseif ($this->operation->operation === 'subscription') {
$this->rootType = $this->schema->getSubscriptionType();
} else {
$this->runtime->addError(new Error(sprintf('Cannot initialize collector with operation type "%s".', $this->operation->operation)));
}
}
/**
* @return Generator
*/
public function collectFields(ObjectType $runtimeType, ?SelectionSetNode $selectionSet)
{
$this->fields = [];
$this->visitedFragments = [];
$this->doCollectFields($runtimeType, $selectionSet);
foreach ($this->fields as $resultName => $fieldNodes) {
$fieldNode = $fieldNodes[0];
$fieldName = $fieldNode->name->value;
$argumentValueMap = null;
if (! empty($fieldNode->arguments)) {
foreach ($fieldNode->arguments as $argumentNode) {
$argumentValueMap = $argumentValueMap ?? [];
$argumentValueMap[$argumentNode->name->value] = $argumentNode->value;
}
}
if ($fieldName !== Introspection::TYPE_NAME_FIELD_NAME &&
! ($runtimeType === $this->schema->getQueryType() && ($fieldName === Introspection::SCHEMA_FIELD_NAME || $fieldName === Introspection::TYPE_FIELD_NAME)) &&
! $runtimeType->hasField($fieldName)
) {
// do not emit error
continue;
}
yield new CoroutineContextShared($fieldNodes, $fieldName, $resultName, $argumentValueMap);
}
}
private function doCollectFields(ObjectType $runtimeType, ?SelectionSetNode $selectionSet)
{
if ($selectionSet === null) {
return;
}
foreach ($selectionSet->selections as $selection) {
/** @var FieldNode|FragmentSpreadNode|InlineFragmentNode $selection */
if (! empty($selection->directives)) {
foreach ($selection->directives as $directiveNode) {
if ($directiveNode->name->value === Directive::SKIP_NAME) {
/** @var ValueNode|null $condition */
$condition = null;
foreach ($directiveNode->arguments as $argumentNode) {
if ($argumentNode->name->value === Directive::IF_ARGUMENT_NAME) {
$condition = $argumentNode->value;
break;
}
}
if ($condition === null) {
$this->runtime->addError(new Error(
sprintf('@%s directive is missing "%s" argument.', Directive::SKIP_NAME, Directive::IF_ARGUMENT_NAME),
$selection
));
} else {
if ($this->runtime->evaluate($condition, Type::boolean()) === true) {
continue 2; // !!! advances outer loop
}
}
} elseif ($directiveNode->name->value === Directive::INCLUDE_NAME) {
/** @var ValueNode|null $condition */
$condition = null;
foreach ($directiveNode->arguments as $argumentNode) {
if ($argumentNode->name->value === Directive::IF_ARGUMENT_NAME) {
$condition = $argumentNode->value;
break;
}
}
if ($condition === null) {
$this->runtime->addError(new Error(
sprintf('@%s directive is missing "%s" argument.', Directive::INCLUDE_NAME, Directive::IF_ARGUMENT_NAME),
$selection
));
} else {
if ($this->runtime->evaluate($condition, Type::boolean()) !== true) {
continue 2; // !!! advances outer loop
}
}
}
}
}
if ($selection->kind === NodeKind::FIELD) {
/** @var FieldNode $selection */
$resultName = $selection->alias ? $selection->alias->value : $selection->name->value;
if (! isset($this->fields[$resultName])) {
$this->fields[$resultName] = [];
}
$this->fields[$resultName][] = $selection;
} elseif ($selection->kind === NodeKind::FRAGMENT_SPREAD) {
/** @var FragmentSpreadNode $selection */
$fragmentName = $selection->name->value;
if (isset($this->visitedFragments[$fragmentName])) {
continue;
} elseif (! isset($this->fragments[$fragmentName])) {
$this->runtime->addError(new Error(
sprintf('Fragment "%s" does not exist.', $fragmentName),
$selection
));
continue;
}
$this->visitedFragments[$fragmentName] = true;
$fragmentDefinition = $this->fragments[$fragmentName];
$conditionTypeName = $fragmentDefinition->typeCondition->name->value;
if (! $this->schema->hasType($conditionTypeName)) {
$this->runtime->addError(new Error(
sprintf('Cannot spread fragment "%s", type "%s" does not exist.', $fragmentName, $conditionTypeName),
$selection
));
continue;
}
$conditionType = $this->schema->getType($conditionTypeName);
if ($conditionType instanceof ObjectType) {
if ($runtimeType->name !== $conditionType->name) {
continue;
}
} elseif ($conditionType instanceof AbstractType) {
if (! $this->schema->isPossibleType($conditionType, $runtimeType)) {
continue;
}
}
$this->doCollectFields($runtimeType, $fragmentDefinition->selectionSet);
} elseif ($selection->kind === NodeKind::INLINE_FRAGMENT) {
/** @var InlineFragmentNode $selection */
if ($selection->typeCondition !== null) {
$conditionTypeName = $selection->typeCondition->name->value;
if (! $this->schema->hasType($conditionTypeName)) {
$this->runtime->addError(new Error(
sprintf('Cannot spread inline fragment, type "%s" does not exist.', $conditionTypeName),
$selection
));
continue;
}
$conditionType = $this->schema->getType($conditionTypeName);
if ($conditionType instanceof ObjectType) {
if ($runtimeType->name !== $conditionType->name) {
continue;
}
} elseif ($conditionType instanceof AbstractType) {
if (! $this->schema->isPossibleType($conditionType, $runtimeType)) {
continue;
}
}
}
$this->doCollectFields($runtimeType, $selection->selectionSet);
}
}
}
}

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace GraphQL\Experimental\Executor;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
/**
* @internal
*/
class CoroutineContext
{
/** @var CoroutineContextShared */
public $shared;
/** @var ObjectType */
public $type;
/** @var mixed */
public $value;
/** @var object */
public $result;
/** @var string[] */
public $path;
/** @var ResolveInfo|null */
public $resolveInfo;
/** @var string[]|null */
public $nullFence;
/**
* @param mixed $value
* @param object $result
* @param string[] $path
* @param string[]|null $nullFence
*/
public function __construct(
CoroutineContextShared $shared,
ObjectType $type,
$value,
$result,
array $path,
?array $nullFence = null
) {
$this->shared = $shared;
$this->type = $type;
$this->value = $value;
$this->result = $result;
$this->path = $path;
$this->nullFence = $nullFence;
}
}

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace GraphQL\Experimental\Executor;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Language\AST\ValueNode;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
/**
* @internal
*/
class CoroutineContextShared
{
/** @var FieldNode[] */
public $fieldNodes;
/** @var string */
public $fieldName;
/** @var string */
public $resultName;
/** @var ValueNode[]|null */
public $argumentValueMap;
/** @var SelectionSetNode|null */
public $mergedSelectionSet;
/** @var ObjectType|null */
public $typeGuard1;
/** @var callable|null */
public $resolveIfType1;
/** @var mixed */
public $argumentsIfType1;
/** @var ResolveInfo|null */
public $resolveInfoIfType1;
/** @var ObjectType|null */
public $typeGuard2;
/** @var CoroutineContext[]|null */
public $childContextsIfType2;
/**
* @param FieldNode[] $fieldNodes
* @param mixed[]|null $argumentValueMap
*/
public function __construct(array $fieldNodes, string $fieldName, string $resultName, ?array $argumentValueMap)
{
$this->fieldNodes = $fieldNodes;
$this->fieldName = $fieldName;
$this->resultName = $resultName;
$this->argumentValueMap = $argumentValueMap;
}
}

View File

@ -0,0 +1,931 @@
<?php
declare(strict_types=1);
namespace GraphQL\Experimental\Executor;
use Generator;
use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation;
use GraphQL\Error\Warning;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Executor\ExecutorImplementation;
use GraphQL\Executor\Promise\Promise;
use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Executor\Values;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Language\AST\ValueNode;
use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\CompositeType;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\LeafType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Introspection;
use GraphQL\Type\Schema;
use GraphQL\Utils\AST;
use GraphQL\Utils\Utils;
use SplQueue;
use stdClass;
use Throwable;
use function is_array;
use function is_string;
use function sprintf;
class CoroutineExecutor implements Runtime, ExecutorImplementation
{
/** @var object */
private static $undefined;
/** @var Schema */
private $schema;
/** @var callable */
private $fieldResolver;
/** @var PromiseAdapter */
private $promiseAdapter;
/** @var mixed|null */
private $rootValue;
/** @var mixed|null */
private $contextValue;
/** @var mixed|null */
private $rawVariableValues;
/** @var mixed|null */
private $variableValues;
/** @var DocumentNode */
private $documentNode;
/** @var string|null */
private $operationName;
/** @var Collector */
private $collector;
/** @var Error[] */
private $errors;
/** @var SplQueue */
private $queue;
/** @var SplQueue */
private $schedule;
/** @var stdClass */
private $rootResult;
/** @var int */
private $pending;
/** @var callable */
private $doResolve;
public function __construct(
PromiseAdapter $promiseAdapter,
Schema $schema,
DocumentNode $documentNode,
$rootValue,
$contextValue,
$rawVariableValues,
?string $operationName,
callable $fieldResolver
) {
if (self::$undefined === null) {
self::$undefined = Utils::undefined();
}
$this->schema = $schema;
$this->fieldResolver = $fieldResolver;
$this->promiseAdapter = $promiseAdapter;
$this->rootValue = $rootValue;
$this->contextValue = $contextValue;
$this->rawVariableValues = $rawVariableValues;
$this->documentNode = $documentNode;
$this->operationName = $operationName;
}
public static function create(
PromiseAdapter $promiseAdapter,
Schema $schema,
DocumentNode $documentNode,
$rootValue,
$contextValue,
$variableValues,
?string $operationName,
callable $fieldResolver
) {
return new static(
$promiseAdapter,
$schema,
$documentNode,
$rootValue,
$contextValue,
$variableValues,
$operationName,
$fieldResolver
);
}
private static function resultToArray($value, $emptyObjectAsStdClass = true)
{
if ($value instanceof stdClass) {
$array = [];
foreach ($value as $propertyName => $propertyValue) {
$array[$propertyName] = self::resultToArray($propertyValue);
}
if ($emptyObjectAsStdClass && empty($array)) {
return new stdClass();
}
return $array;
}
if (is_array($value)) {
$array = [];
foreach ($value as $item) {
$array[] = self::resultToArray($item);
}
return $array;
}
return $value;
}
public function doExecute() : Promise
{
$this->rootResult = new stdClass();
$this->errors = [];
$this->queue = new SplQueue();
$this->schedule = new SplQueue();
$this->pending = 0;
$this->collector = new Collector($this->schema, $this);
$this->collector->initialize($this->documentNode, $this->operationName);
if (! empty($this->errors)) {
return $this->promiseAdapter->createFulfilled($this->finishExecute(null, $this->errors));
}
[$errors, $coercedVariableValues] = Values::getVariableValues(
$this->schema,
$this->collector->operation->variableDefinitions ?: [],
$this->rawVariableValues ?: []
);
if (! empty($errors)) {
return $this->promiseAdapter->createFulfilled($this->finishExecute(null, $errors));
}
$this->variableValues = $coercedVariableValues;
foreach ($this->collector->collectFields($this->collector->rootType, $this->collector->operation->selectionSet) as $shared) {
/** @var CoroutineContextShared $shared */
// !!! assign to keep object keys sorted
$this->rootResult->{$shared->resultName} = null;
$ctx = new CoroutineContext(
$shared,
$this->collector->rootType,
$this->rootValue,
$this->rootResult,
[$shared->resultName]
);
$fieldDefinition = $this->findFieldDefinition($ctx);
if (! $fieldDefinition->getType() instanceof NonNull) {
$ctx->nullFence = [$shared->resultName];
}
if ($this->collector->operation->operation === 'mutation' && ! $this->queue->isEmpty()) {
$this->schedule->enqueue($ctx);
} else {
$this->queue->enqueue(new Strand($this->spawn($ctx)));
}
}
$this->run();
if ($this->pending > 0) {
return $this->promiseAdapter->create(function (callable $resolve) {
$this->doResolve = $resolve;
});
}
return $this->promiseAdapter->createFulfilled($this->finishExecute($this->rootResult, $this->errors));
}
/**
* @param object|null $value
* @param Error[] $errors
*/
private function finishExecute($value, array $errors) : ExecutionResult
{
$this->rootResult = null;
$this->errors = null;
$this->queue = null;
$this->schedule = null;
$this->pending = null;
$this->collector = null;
$this->variableValues = null;
if ($value !== null) {
$value = self::resultToArray($value, false);
}
return new ExecutionResult($value, $errors);
}
/**
* @internal
*/
public function evaluate(ValueNode $valueNode, InputType $type)
{
return AST::valueFromAST($valueNode, $type, $this->variableValues);
}
/**
* @internal
*/
public function addError($error)
{
$this->errors[] = $error;
}
private function run()
{
RUN:
while (! $this->queue->isEmpty()) {
/** @var Strand $strand */
$strand = $this->queue->dequeue();
try {
if ($strand->success !== null) {
RESUME:
if ($strand->success) {
$strand->current->send($strand->value);
} else {
$strand->current->throw($strand->value);
}
$strand->success = null;
$strand->value = null;
}
START:
if ($strand->current->valid()) {
$value = $strand->current->current();
if ($value instanceof Generator) {
$strand->stack[$strand->depth++] = $strand->current;
$strand->current = $value;
goto START;
} elseif ($this->promiseAdapter->isThenable($value)) {
// !!! increment pending before calling ->then() as it may invoke the callback right away
++$this->pending;
$this->promiseAdapter
->convertThenable($value)
->then(
function ($value) use ($strand) {
$strand->success = true;
$strand->value = $value;
$this->queue->enqueue($strand);
$this->done();
},
function (Throwable $throwable) use ($strand) {
$strand->success = false;
$strand->value = $throwable;
$this->queue->enqueue($strand);
$this->done();
}
);
continue;
} else {
$strand->success = true;
$strand->value = $value;
goto RESUME;
}
}
$strand->success = true;
$strand->value = $strand->current->getReturn();
} catch (Throwable $reason) {
$strand->success = false;
$strand->value = $reason;
}
if ($strand->depth <= 0) {
continue;
}
$current = &$strand->stack[--$strand->depth];
$strand->current = $current;
$current = null;
goto RESUME;
}
if ($this->pending > 0 || $this->schedule->isEmpty()) {
return;
}
/** @var CoroutineContext $ctx */
$ctx = $this->schedule->dequeue();
$this->queue->enqueue(new Strand($this->spawn($ctx)));
goto RUN;
}
private function done()
{
--$this->pending;
$this->run();
if ($this->pending > 0) {
return;
}
$doResolve = $this->doResolve;
$doResolve($this->finishExecute($this->rootResult, $this->errors));
}
private function spawn(CoroutineContext $ctx)
{
// short-circuit evaluation for __typename
if ($ctx->shared->fieldName === Introspection::TYPE_NAME_FIELD_NAME) {
$ctx->result->{$ctx->shared->resultName} = $ctx->type->name;
return;
}
try {
if ($ctx->shared->typeGuard1 === $ctx->type) {
$resolve = $ctx->shared->resolveIfType1;
$ctx->resolveInfo = clone $ctx->shared->resolveInfoIfType1;
$ctx->resolveInfo->path = $ctx->path;
$arguments = $ctx->shared->argumentsIfType1;
$returnType = $ctx->resolveInfo->returnType;
} else {
$fieldDefinition = $this->findFieldDefinition($ctx);
if ($fieldDefinition->resolveFn !== null) {
$resolve = $fieldDefinition->resolveFn;
} elseif ($ctx->type->resolveFieldFn !== null) {
$resolve = $ctx->type->resolveFieldFn;
} else {
$resolve = $this->fieldResolver;
}
$returnType = $fieldDefinition->getType();
$ctx->resolveInfo = new ResolveInfo(
$ctx->shared->fieldName,
$ctx->shared->fieldNodes,
$returnType,
$ctx->type,
$ctx->path,
$this->schema,
$this->collector->fragments,
$this->rootValue,
$this->collector->operation,
$this->variableValues
);
$arguments = Values::getArgumentValuesForMap(
$fieldDefinition,
$ctx->shared->argumentValueMap,
$this->variableValues
);
// !!! assign only in batch when no exception can be thrown in-between
$ctx->shared->typeGuard1 = $ctx->type;
$ctx->shared->resolveIfType1 = $resolve;
$ctx->shared->argumentsIfType1 = $arguments;
$ctx->shared->resolveInfoIfType1 = $ctx->resolveInfo;
}
$value = $resolve($ctx->value, $arguments, $this->contextValue, $ctx->resolveInfo);
if (! $this->completeValueFast($ctx, $returnType, $value, $ctx->path, $returnValue)) {
$returnValue = yield $this->completeValue(
$ctx,
$returnType,
$value,
$ctx->path,
$ctx->nullFence
);
}
} catch (Throwable $reason) {
$this->addError(Error::createLocatedError(
$reason,
$ctx->shared->fieldNodes,
$ctx->path
));
$returnValue = self::$undefined;
}
if ($returnValue !== self::$undefined) {
$ctx->result->{$ctx->shared->resultName} = $returnValue;
} elseif ($ctx->resolveInfo !== null && $ctx->resolveInfo->returnType instanceof NonNull) { // !!! $ctx->resolveInfo might not have been initialized yet
$result =& $this->rootResult;
foreach ($ctx->nullFence ?? [] as $key) {
if (is_string($key)) {
$result =& $result->{$key};
} else {
$result =& $result[$key];
}
}
$result = null;
}
}
private function findFieldDefinition(CoroutineContext $ctx)
{
if ($ctx->shared->fieldName === Introspection::SCHEMA_FIELD_NAME && $ctx->type === $this->schema->getQueryType()) {
return Introspection::schemaMetaFieldDef();
}
if ($ctx->shared->fieldName === Introspection::TYPE_FIELD_NAME && $ctx->type === $this->schema->getQueryType()) {
return Introspection::typeMetaFieldDef();
}
if ($ctx->shared->fieldName === Introspection::TYPE_NAME_FIELD_NAME) {
return Introspection::typeNameMetaFieldDef();
}
return $ctx->type->getField($ctx->shared->fieldName);
}
/**
* @param mixed $value
* @param string[] $path
* @param mixed $returnValue
*/
private function completeValueFast(CoroutineContext $ctx, Type $type, $value, array $path, &$returnValue) : bool
{
// special handling of Throwable inherited from JS reference implementation, but makes no sense in this PHP
if ($this->promiseAdapter->isThenable($value) || $value instanceof Throwable) {
return false;
}
$nonNull = false;
if ($type instanceof NonNull) {
$nonNull = true;
$type = $type->getWrappedType();
}
if (! $type instanceof LeafType) {
return false;
}
if ($type !== $this->schema->getType($type->name)) {
$hint = '';
if ($this->schema->getConfig()->typeLoader) {
$hint = sprintf(
'Make sure that type loader returns the same instance as defined in %s.%s',
$ctx->type,
$ctx->shared->fieldName
);
}
$this->addError(Error::createLocatedError(
new InvariantViolation(
sprintf(
'Schema must contain unique named types but contains multiple types named "%s". %s ' .
'(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
$type->name,
$hint
)
),
$ctx->shared->fieldNodes,
$path
));
$value = null;
}
if ($value === null) {
$returnValue = null;
} else {
try {
$returnValue = $type->serialize($value);
} catch (Throwable $error) {
$this->addError(Error::createLocatedError(
new InvariantViolation(
'Expected a value of type "' . Utils::printSafe($type) . '" but received: ' . Utils::printSafe($value),
0,
$error
),
$ctx->shared->fieldNodes,
$path
));
$returnValue = null;
}
}
if ($nonNull && $returnValue === null) {
$this->addError(Error::createLocatedError(
new InvariantViolation(sprintf(
'Cannot return null for non-nullable field %s.%s.',
$ctx->type->name,
$ctx->shared->fieldName
)),
$ctx->shared->fieldNodes,
$path
));
$returnValue = self::$undefined;
}
return true;
}
/**
* @param mixed $value
* @param string[] $path
* @param string[]|null $nullFence
*
* @return mixed
*/
private function completeValue(CoroutineContext $ctx, Type $type, $value, array $path, ?array $nullFence)
{
$nonNull = false;
$returnValue = null;
if ($type instanceof NonNull) {
$nonNull = true;
$type = $type->getWrappedType();
} else {
$nullFence = $path;
}
// !!! $value might be promise, yield to resolve
try {
if ($this->promiseAdapter->isThenable($value)) {
$value = yield $value;
}
} catch (Throwable $reason) {
$this->addError(Error::createLocatedError(
$reason,
$ctx->shared->fieldNodes,
$path
));
if ($nonNull) {
$returnValue = self::$undefined;
} else {
$returnValue = null;
}
goto CHECKED_RETURN;
}
if ($value === null) {
$returnValue = $value;
goto CHECKED_RETURN;
} elseif ($value instanceof Throwable) {
// special handling of Throwable inherited from JS reference implementation, but makes no sense in this PHP
$this->addError(Error::createLocatedError(
$value,
$ctx->shared->fieldNodes,
$path
));
if ($nonNull) {
$returnValue = self::$undefined;
} else {
$returnValue = null;
}
goto CHECKED_RETURN;
}
if ($type instanceof ListOfType) {
$returnValue = [];
$index = -1;
$itemType = $type->getWrappedType();
foreach ($value as $itemValue) {
++$index;
$itemPath = $path;
$itemPath[] = $index; // !!! use arrays COW semantics
try {
if (! $this->completeValueFast($ctx, $itemType, $itemValue, $itemPath, $itemReturnValue)) {
$itemReturnValue = yield $this->completeValue($ctx, $itemType, $itemValue, $itemPath, $nullFence);
}
} catch (Throwable $reason) {
$this->addError(Error::createLocatedError(
$reason,
$ctx->shared->fieldNodes,
$itemPath
));
$itemReturnValue = null;
}
if ($itemReturnValue === self::$undefined) {
$returnValue = self::$undefined;
goto CHECKED_RETURN;
}
$returnValue[$index] = $itemReturnValue;
}
goto CHECKED_RETURN;
} else {
if ($type !== $this->schema->getType($type->name)) {
$hint = '';
if ($this->schema->getConfig()->typeLoader) {
$hint = sprintf(
'Make sure that type loader returns the same instance as defined in %s.%s',
$ctx->type,
$ctx->shared->fieldName
);
}
$this->addError(Error::createLocatedError(
new InvariantViolation(
sprintf(
'Schema must contain unique named types but contains multiple types named "%s". %s ' .
'(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
$type->name,
$hint
)
),
$ctx->shared->fieldNodes,
$path
));
$returnValue = null;
goto CHECKED_RETURN;
}
if ($type instanceof LeafType) {
try {
$returnValue = $type->serialize($value);
} catch (Throwable $error) {
$this->addError(Error::createLocatedError(
new InvariantViolation(
'Expected a value of type "' . Utils::printSafe($type) . '" but received: ' . Utils::printSafe($value),
0,
$error
),
$ctx->shared->fieldNodes,
$path
));
$returnValue = null;
}
goto CHECKED_RETURN;
} elseif ($type instanceof CompositeType) {
/** @var ObjectType|null $objectType */
$objectType = null;
if ($type instanceof InterfaceType || $type instanceof UnionType) {
$objectType = $type->resolveType($value, $this->contextValue, $ctx->resolveInfo);
if ($objectType === null) {
$objectType = yield $this->resolveTypeSlow($ctx, $value, $type);
}
// !!! $objectType->resolveType() might return promise, yield to resolve
$objectType = yield $objectType;
if (is_string($objectType)) {
$objectType = $this->schema->getType($objectType);
}
if ($objectType === null) {
$this->addError(Error::createLocatedError(
sprintf(
'Composite type "%s" did not resolve concrete object type for value: %s.',
$type->name,
Utils::printSafe($value)
),
$ctx->shared->fieldNodes,
$path
));
$returnValue = self::$undefined;
goto CHECKED_RETURN;
} elseif (! $objectType instanceof ObjectType) {
$this->addError(Error::createLocatedError(
new InvariantViolation(sprintf(
'Abstract type %s must resolve to an Object type at ' .
'runtime for field %s.%s with value "%s", received "%s". ' .
'Either the %s type should provide a "resolveType" ' .
'function or each possible type should provide an "isTypeOf" function.',
$type,
$ctx->resolveInfo->parentType,
$ctx->resolveInfo->fieldName,
Utils::printSafe($value),
Utils::printSafe($objectType),
$type
)),
$ctx->shared->fieldNodes,
$path
));
$returnValue = null;
goto CHECKED_RETURN;
} elseif (! $this->schema->isPossibleType($type, $objectType)) {
$this->addError(Error::createLocatedError(
new InvariantViolation(sprintf(
'Runtime Object type "%s" is not a possible type for "%s".',
$objectType,
$type
)),
$ctx->shared->fieldNodes,
$path
));
$returnValue = null;
goto CHECKED_RETURN;
} elseif ($objectType !== $this->schema->getType($objectType->name)) {
$this->addError(Error::createLocatedError(
new InvariantViolation(
sprintf(
'Schema must contain unique named types but contains multiple types named "%s". ' .
'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
'type instance as referenced anywhere else within the schema ' .
'(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
$objectType,
$type
)
),
$ctx->shared->fieldNodes,
$path
));
$returnValue = null;
goto CHECKED_RETURN;
}
} elseif ($type instanceof ObjectType) {
$objectType = $type;
} else {
$this->addError(Error::createLocatedError(
sprintf(
'Unexpected field type "%s".',
Utils::printSafe($type)
),
$ctx->shared->fieldNodes,
$path
));
$returnValue = self::$undefined;
goto CHECKED_RETURN;
}
$typeCheck = $objectType->isTypeOf($value, $this->contextValue, $ctx->resolveInfo);
if ($typeCheck !== null) {
// !!! $objectType->isTypeOf() might return promise, yield to resolve
$typeCheck = yield $typeCheck;
if (! $typeCheck) {
$this->addError(Error::createLocatedError(
sprintf('Expected value of type "%s" but got: %s.', $type->name, Utils::printSafe($value)),
$ctx->shared->fieldNodes,
$path
));
$returnValue = null;
goto CHECKED_RETURN;
}
}
$returnValue = new stdClass();
if ($ctx->shared->typeGuard2 === $objectType) {
foreach ($ctx->shared->childContextsIfType2 as $childCtx) {
$childCtx = clone $childCtx;
$childCtx->type = $objectType;
$childCtx->value = $value;
$childCtx->result = $returnValue;
$childCtx->path = $path;
$childCtx->path[] = $childCtx->shared->resultName; // !!! uses array COW semantics
$childCtx->nullFence = $nullFence;
$childCtx->resolveInfo = null;
$this->queue->enqueue(new Strand($this->spawn($childCtx)));
// !!! assign null to keep object keys sorted
$returnValue->{$childCtx->shared->resultName} = null;
}
} else {
$childContexts = [];
foreach ($this->collector->collectFields($objectType, $ctx->shared->mergedSelectionSet ?? $this->mergeSelectionSets($ctx)) as $childShared) {
/** @var CoroutineContextShared $childShared */
$childPath = $path;
$childPath[] = $childShared->resultName; // !!! uses array COW semantics
$childCtx = new CoroutineContext(
$childShared,
$objectType,
$value,
$returnValue,
$childPath,
$nullFence
);
$childContexts[] = $childCtx;
$this->queue->enqueue(new Strand($this->spawn($childCtx)));
// !!! assign null to keep object keys sorted
$returnValue->{$childShared->resultName} = null;
}
$ctx->shared->typeGuard2 = $objectType;
$ctx->shared->childContextsIfType2 = $childContexts;
}
goto CHECKED_RETURN;
} else {
$this->addError(Error::createLocatedError(
sprintf('Unhandled type "%s".', Utils::printSafe($type)),
$ctx->shared->fieldNodes,
$path
));
$returnValue = null;
goto CHECKED_RETURN;
}
}
CHECKED_RETURN:
if ($nonNull && $returnValue === null) {
$this->addError(Error::createLocatedError(
new InvariantViolation(sprintf(
'Cannot return null for non-nullable field %s.%s.',
$ctx->type->name,
$ctx->shared->fieldName
)),
$ctx->shared->fieldNodes,
$path
));
return self::$undefined;
}
return $returnValue;
}
private function mergeSelectionSets(CoroutineContext $ctx)
{
$selections = [];
foreach ($ctx->shared->fieldNodes as $fieldNode) {
if ($fieldNode->selectionSet === null) {
continue;
}
foreach ($fieldNode->selectionSet->selections as $selection) {
$selections[] = $selection;
}
}
return $ctx->shared->mergedSelectionSet = new SelectionSetNode(['selections' => $selections]);
}
private function resolveTypeSlow(CoroutineContext $ctx, $value, AbstractType $abstractType)
{
if ($value !== null &&
is_array($value) &&
isset($value['__typename']) &&
is_string($value['__typename'])
) {
return $this->schema->getType($value['__typename']);
}
if ($abstractType instanceof InterfaceType && $this->schema->getConfig()->typeLoader) {
Warning::warnOnce(
sprintf(
'GraphQL Interface Type `%s` returned `null` from it`s `resolveType` function ' .
'for value: %s. Switching to slow resolution method using `isTypeOf` ' .
'of all possible implementations. It requires full schema scan and degrades query performance significantly. ' .
' Make sure your `resolveType` always returns valid implementation or throws.',
$abstractType->name,
Utils::printSafe($value)
),
Warning::WARNING_FULL_SCHEMA_SCAN
);
}
$possibleTypes = $this->schema->getPossibleTypes($abstractType);
// to be backward-compatible with old executor, ->isTypeOf() is called for all possible types,
// it cannot short-circuit when the match is found
$selectedType = null;
foreach ($possibleTypes as $type) {
$typeCheck = yield $type->isTypeOf($value, $this->contextValue, $ctx->resolveInfo);
if ($selectedType !== null || $typeCheck !== true) {
continue;
}
$selectedType = $type;
}
return $selectedType;
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace GraphQL\Experimental\Executor;
use GraphQL\Language\AST\ValueNode;
use GraphQL\Type\Definition\InputType;
/**
* @internal
*/
interface Runtime
{
public function evaluate(ValueNode $valueNode, InputType $type);
public function addError($error);
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace GraphQL\Experimental\Executor;
use Generator;
/**
* @internal
*/
class Strand
{
/** @var Generator */
public $current;
/** @var Generator[] */
public $stack;
/** @var int */
public $depth;
/** @var bool|null */
public $success;
/** @var mixed */
public $value;
public function __construct(Generator $coroutine)
{
$this->current = $coroutine;
$this->stack = [];
$this->depth = 0;
}
}

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace GraphQL\Language\AST; namespace GraphQL\Language\AST;
class VariableNode extends Node class VariableNode extends Node implements ValueNode
{ {
/** @var string */ /** @var string */
public $kind = NodeKind::VARIABLE; public $kind = NodeKind::VARIABLE;

View File

@ -8,6 +8,8 @@ use GraphQL\Language\AST\DirectiveDefinitionNode;
use GraphQL\Language\DirectiveLocation; use GraphQL\Language\DirectiveLocation;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
use function array_key_exists; use function array_key_exists;
use function array_keys;
use function in_array;
use function is_array; use function is_array;
/** /**
@ -17,6 +19,12 @@ class Directive
{ {
public const DEFAULT_DEPRECATION_REASON = 'No longer supported'; public const DEFAULT_DEPRECATION_REASON = 'No longer supported';
const INCLUDE_NAME = 'include';
const IF_ARGUMENT_NAME = 'if';
const SKIP_NAME = 'skip';
const DEPRECATED_NAME = 'deprecated';
const REASON_ARGUMENT_NAME = 'reason';
/** @var Directive[] */ /** @var Directive[] */
public static $internalDirectives; public static $internalDirectives;
@ -72,7 +80,6 @@ class Directive
public static function includeDirective() public static function includeDirective()
{ {
$internal = self::getInternalDirectives(); $internal = self::getInternalDirectives();
return $internal['include']; return $internal['include'];
} }
@ -84,7 +91,7 @@ class Directive
if (! self::$internalDirectives) { if (! self::$internalDirectives) {
self::$internalDirectives = [ self::$internalDirectives = [
'include' => new self([ 'include' => new self([
'name' => 'include', 'name' => self::INCLUDE_NAME,
'description' => 'Directs the executor to include this field or fragment only when the `if` argument is true.', 'description' => 'Directs the executor to include this field or fragment only when the `if` argument is true.',
'locations' => [ 'locations' => [
DirectiveLocation::FIELD, DirectiveLocation::FIELD,
@ -92,14 +99,14 @@ class Directive
DirectiveLocation::INLINE_FRAGMENT, DirectiveLocation::INLINE_FRAGMENT,
], ],
'args' => [new FieldArgument([ 'args' => [new FieldArgument([
'name' => 'if', 'name' => self::IF_ARGUMENT_NAME,
'type' => Type::nonNull(Type::boolean()), 'type' => Type::nonNull(Type::boolean()),
'description' => 'Included when true.', 'description' => 'Included when true.',
]), ]),
], ],
]), ]),
'skip' => new self([ 'skip' => new self([
'name' => 'skip', 'name' => self::SKIP_NAME,
'description' => 'Directs the executor to skip this field or fragment when the `if` argument is true.', 'description' => 'Directs the executor to skip this field or fragment when the `if` argument is true.',
'locations' => [ 'locations' => [
DirectiveLocation::FIELD, DirectiveLocation::FIELD,
@ -107,21 +114,21 @@ class Directive
DirectiveLocation::INLINE_FRAGMENT, DirectiveLocation::INLINE_FRAGMENT,
], ],
'args' => [new FieldArgument([ 'args' => [new FieldArgument([
'name' => 'if', 'name' => self::IF_ARGUMENT_NAME,
'type' => Type::nonNull(Type::boolean()), 'type' => Type::nonNull(Type::boolean()),
'description' => 'Skipped when true.', 'description' => 'Skipped when true.',
]), ]),
], ],
]), ]),
'deprecated' => new self([ 'deprecated' => new self([
'name' => 'deprecated', 'name' => self::DEPRECATED_NAME,
'description' => 'Marks an element of a GraphQL schema as no longer supported.', 'description' => 'Marks an element of a GraphQL schema as no longer supported.',
'locations' => [ 'locations' => [
DirectiveLocation::FIELD_DEFINITION, DirectiveLocation::FIELD_DEFINITION,
DirectiveLocation::ENUM_VALUE, DirectiveLocation::ENUM_VALUE,
], ],
'args' => [new FieldArgument([ 'args' => [new FieldArgument([
'name' => 'reason', 'name' => self::REASON_ARGUMENT_NAME,
'type' => Type::string(), 'type' => Type::string(),
'description' => 'description' =>
'Explains why this element was deprecated, usually also including a ' . 'Explains why this element was deprecated, usually also including a ' .
@ -133,30 +140,24 @@ class Directive
]), ]),
]; ];
} }
return self::$internalDirectives; return self::$internalDirectives;
} }
/** /**
* @return Directive * @return Directive
*/ */
public static function skipDirective() public static function skipDirective()
{ {
$internal = self::getInternalDirectives(); $internal = self::getInternalDirectives();
return $internal['skip']; return $internal['skip'];
} }
/** /**
* @return Directive * @return Directive
*/ */
public static function deprecatedDirective() public static function deprecatedDirective()
{ {
$internal = self::getInternalDirectives(); $internal = self::getInternalDirectives();
return $internal['deprecated']; return $internal['deprecated'];
} }
/** /**
* @return bool * @return bool
*/ */

View File

@ -59,7 +59,7 @@ class FieldDefinition
public $config; public $config;
/** @var OutputType */ /** @var OutputType */
private $type; public $type;
/** @var callable|string */ /** @var callable|string */
private $complexityFn; private $complexityFn;

View File

@ -74,6 +74,20 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
return $this->fields[$name]; return $this->fields[$name];
} }
/**
* @param string $name
*
* @return bool
*/
public function hasField($name)
{
if ($this->fields === null) {
$this->getFields();
}
return isset($this->fields[$name]);
}
/** /**
* @return FieldDefinition[] * @return FieldDefinition[]
*/ */

View File

@ -125,6 +125,20 @@ class ObjectType extends Type implements OutputType, CompositeType, NamedType
return $this->fields[$name]; return $this->fields[$name];
} }
/**
* @param string $name
*
* @return bool
*/
public function hasField($name)
{
if ($this->fields === null) {
$this->getFields();
}
return isset($this->fields[$name]);
}
/** /**
* @return FieldDefinition[] * @return FieldDefinition[]
* *

View File

@ -11,7 +11,6 @@ use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\SelectionSetNode; use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Type\Schema; use GraphQL\Type\Schema;
use GraphQL\Utils\Utils;
use function array_merge_recursive; use function array_merge_recursive;
/** /**
@ -56,7 +55,7 @@ class ResolveInfo
* Path to this field from the very root value * Path to this field from the very root value
* *
* @api * @api
* @var mixed[]|null * @var string[]
*/ */
public $path; public $path;
@ -100,12 +99,28 @@ class ResolveInfo
*/ */
public $variableValues; public $variableValues;
/** public function __construct(
* @param mixed[] $values string $fieldName,
*/ $fieldNodes,
public function __construct(array $values) $returnType,
{ ObjectType $parentType,
Utils::assign($this, $values); $path,
Schema $schema,
$fragments,
$rootValue,
?OperationDefinitionNode $operation,
$variableValues
) {
$this->fieldName = $fieldName;
$this->fieldNodes = $fieldNodes;
$this->returnType = $returnType;
$this->parentType = $parentType;
$this->path = $path;
$this->schema = $schema;
$this->fragments = $fragments;
$this->rootValue = $rootValue;
$this->operation = $operation;
$this->variableValues = $variableValues;
} }
/** /**
@ -156,14 +171,12 @@ class ResolveInfo
return $fields; return $fields;
} }
/** /**
* @return bool[] * @return bool[]
*/ */
private function foldSelectionSet(SelectionSetNode $selectionSet, int $descend) : array private function foldSelectionSet(SelectionSetNode $selectionSet, int $descend) : array
{ {
$fields = []; $fields = [];
foreach ($selectionSet->selections as $selectionNode) { foreach ($selectionSet->selections as $selectionNode) {
if ($selectionNode instanceof FieldNode) { if ($selectionNode instanceof FieldNode) {
$fields[$selectionNode->name->value] = $descend > 0 && ! empty($selectionNode->selectionSet) $fields[$selectionNode->name->value] = $descend > 0 && ! empty($selectionNode->selectionSet)
@ -186,7 +199,6 @@ class ResolveInfo
); );
} }
} }
return $fields; return $fields;
} }
} }

View File

@ -35,6 +35,10 @@ use const E_USER_DEPRECATED;
class Introspection class Introspection
{ {
const SCHEMA_FIELD_NAME = '__schema';
const TYPE_FIELD_NAME = '__type';
const TYPE_NAME_FIELD_NAME = '__typename';
/** @var Type[] */ /** @var Type[] */
private static $map = []; private static $map = [];
@ -691,9 +695,9 @@ EOD;
public static function schemaMetaFieldDef() public static function schemaMetaFieldDef()
{ {
if (! isset(self::$map['__schema'])) { if (! isset(self::$map[self::SCHEMA_FIELD_NAME])) {
self::$map['__schema'] = FieldDefinition::create([ self::$map[self::SCHEMA_FIELD_NAME] = FieldDefinition::create([
'name' => '__schema', 'name' => self::SCHEMA_FIELD_NAME,
'type' => Type::nonNull(self::_schema()), 'type' => Type::nonNull(self::_schema()),
'description' => 'Access the current type schema of this server.', 'description' => 'Access the current type schema of this server.',
'args' => [], 'args' => [],
@ -708,14 +712,14 @@ EOD;
]); ]);
} }
return self::$map['__schema']; return self::$map[self::SCHEMA_FIELD_NAME];
} }
public static function typeMetaFieldDef() public static function typeMetaFieldDef()
{ {
if (! isset(self::$map['__type'])) { if (! isset(self::$map[self::TYPE_FIELD_NAME])) {
self::$map['__type'] = FieldDefinition::create([ self::$map[self::TYPE_FIELD_NAME] = FieldDefinition::create([
'name' => '__type', 'name' => self::TYPE_FIELD_NAME,
'type' => self::_type(), 'type' => self::_type(),
'description' => 'Request the type information of a single type.', 'description' => 'Request the type information of a single type.',
'args' => [ 'args' => [
@ -727,14 +731,14 @@ EOD;
]); ]);
} }
return self::$map['__type']; return self::$map[self::TYPE_FIELD_NAME];
} }
public static function typeNameMetaFieldDef() public static function typeNameMetaFieldDef()
{ {
if (! isset(self::$map['__typename'])) { if (! isset(self::$map[self::TYPE_NAME_FIELD_NAME])) {
self::$map['__typename'] = FieldDefinition::create([ self::$map[self::TYPE_NAME_FIELD_NAME] = FieldDefinition::create([
'name' => '__typename', 'name' => self::TYPE_NAME_FIELD_NAME,
'type' => Type::nonNull(Type::string()), 'type' => Type::nonNull(Type::string()),
'description' => 'The name of the current Object type at runtime.', 'description' => 'The name of the current Object type at runtime.',
'args' => [], 'args' => [],
@ -749,6 +753,6 @@ EOD;
]); ]);
} }
return self::$map['__typename']; return self::$map[self::TYPE_NAME_FIELD_NAME];
} }
} }

View File

@ -309,6 +309,16 @@ class Schema
return $this->resolvedTypes[$name]; return $this->resolvedTypes[$name];
} }
/**
* @param string $name
*
* @return bool
*/
public function hasType($name)
{
return $this->getType($name) !== null;
}
/** /**
* @param string $typeName * @param string $typeName
* *

View File

@ -186,37 +186,30 @@ class QueryComplexity extends QuerySecurityRule
if ($directiveNode->name->value === 'deprecated') { if ($directiveNode->name->value === 'deprecated') {
return false; return false;
} }
[$errors, $variableValues] = Values::getVariableValues(
$variableValuesResult = Values::getVariableValues(
$this->context->getSchema(), $this->context->getSchema(),
$this->variableDefs, $this->variableDefs,
$this->getRawVariableValues() $this->getRawVariableValues()
); );
if (! empty($errors)) {
if (! empty($variableValuesResult['errors'])) {
throw new Error(implode( throw new Error(implode(
"\n\n", "\n\n",
array_map( array_map(
static function ($error) { static function ($error) {
return $error->getMessage(); return $error->getMessage();
}, },
$variableValuesResult['errors'] $errors
) )
)); ));
} }
$variableValues = $variableValuesResult['coerced'];
if ($directiveNode->name->value === 'include') { if ($directiveNode->name->value === 'include') {
$directive = Directive::includeDirective(); $directive = Directive::includeDirective();
/** @var bool $directiveArgsIf */ /** @var bool $directiveArgsIf */
$directiveArgsIf = Values::getArgumentValues($directive, $directiveNode, $variableValues)['if']; $directiveArgsIf = Values::getArgumentValues($directive, $directiveNode, $variableValues)['if'];
return ! $directiveArgsIf; return ! $directiveArgsIf;
} }
$directive = Directive::skipDirective(); $directive = Directive::skipDirective();
$directiveArgsIf = Values::getArgumentValues($directive, $directiveNode, $variableValues); $directiveArgsIf = Values::getArgumentValues($directive, $directiveNode, $variableValues);
return $directiveArgsIf['if']; return $directiveArgsIf['if'];
} }
} }
@ -243,24 +236,23 @@ class QueryComplexity extends QuerySecurityRule
$args = []; $args = [];
if ($fieldDef instanceof FieldDefinition) { if ($fieldDef instanceof FieldDefinition) {
$variableValuesResult = Values::getVariableValues( [$errors, $variableValues] = Values::getVariableValues(
$this->context->getSchema(), $this->context->getSchema(),
$this->variableDefs, $this->variableDefs,
$rawVariableValues $rawVariableValues
); );
if (! empty($variableValuesResult['errors'])) { if (! empty($errors)) {
throw new Error(implode( throw new Error(implode(
"\n\n", "\n\n",
array_map( array_map(
static function ($error) { static function ($error) {
return $error->getMessage(); return $error->getMessage();
}, },
$variableValuesResult['errors'] $errors
) )
)); ));
} }
$variableValues = $variableValuesResult['coerced'];
$args = Values::getArgumentValues($fieldDef, $node, $variableValues); $args = Values::getArgumentValues($fieldDef, $node, $variableValues);
} }

View File

@ -13,7 +13,9 @@ use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema; use GraphQL\Type\Schema;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use function count;
use function in_array; use function in_array;
use function json_encode;
class DeferredFieldsTest extends TestCase class DeferredFieldsTest extends TestCase
{ {
@ -27,7 +29,7 @@ class DeferredFieldsTest extends TestCase
private $categoryType; private $categoryType;
/** @var */ /** @var */
private $path; private $paths;
/** @var mixed[][] */ /** @var mixed[][] */
private $storyDataSource; private $storyDataSource;
@ -68,7 +70,7 @@ class DeferredFieldsTest extends TestCase
['id' => 3, 'name' => 'Category #3', 'topStoryId' => 9], ['id' => 3, 'name' => 'Category #3', 'topStoryId' => 9],
]; ];
$this->path = []; $this->paths = [];
$this->userType = new ObjectType([ $this->userType = new ObjectType([
'name' => 'User', 'name' => 'User',
'fields' => function () { 'fields' => function () {
@ -76,7 +78,7 @@ class DeferredFieldsTest extends TestCase
'name' => [ 'name' => [
'type' => Type::string(), 'type' => Type::string(),
'resolve' => function ($user, $args, $context, ResolveInfo $info) { 'resolve' => function ($user, $args, $context, ResolveInfo $info) {
$this->path[] = $info->path; $this->paths[] = $info->path;
return $user['name']; return $user['name'];
}, },
@ -84,10 +86,10 @@ class DeferredFieldsTest extends TestCase
'bestFriend' => [ 'bestFriend' => [
'type' => $this->userType, 'type' => $this->userType,
'resolve' => function ($user, $args, $context, ResolveInfo $info) { 'resolve' => function ($user, $args, $context, ResolveInfo $info) {
$this->path[] = $info->path; $this->paths[] = $info->path;
return new Deferred(function () use ($user) { return new Deferred(function () use ($user) {
$this->path[] = 'deferred-for-best-friend-of-' . $user['id']; $this->paths[] = 'deferred-for-best-friend-of-' . $user['id'];
return Utils::find( return Utils::find(
$this->userDataSource, $this->userDataSource,
@ -108,7 +110,7 @@ class DeferredFieldsTest extends TestCase
'title' => [ 'title' => [
'type' => Type::string(), 'type' => Type::string(),
'resolve' => function ($entry, $args, $context, ResolveInfo $info) { 'resolve' => function ($entry, $args, $context, ResolveInfo $info) {
$this->path[] = $info->path; $this->paths[] = $info->path;
return $entry['title']; return $entry['title'];
}, },
@ -116,10 +118,10 @@ class DeferredFieldsTest extends TestCase
'author' => [ 'author' => [
'type' => $this->userType, 'type' => $this->userType,
'resolve' => function ($story, $args, $context, ResolveInfo $info) { 'resolve' => function ($story, $args, $context, ResolveInfo $info) {
$this->path[] = $info->path; $this->paths[] = $info->path;
return new Deferred(function () use ($story) { return new Deferred(function () use ($story) {
$this->path[] = 'deferred-for-story-' . $story['id'] . '-author'; $this->paths[] = 'deferred-for-story-' . $story['id'] . '-author';
return Utils::find( return Utils::find(
$this->userDataSource, $this->userDataSource,
@ -139,7 +141,7 @@ class DeferredFieldsTest extends TestCase
'name' => [ 'name' => [
'type' => Type::string(), 'type' => Type::string(),
'resolve' => function ($category, $args, $context, ResolveInfo $info) { 'resolve' => function ($category, $args, $context, ResolveInfo $info) {
$this->path[] = $info->path; $this->paths[] = $info->path;
return $category['name']; return $category['name'];
}, },
@ -148,7 +150,7 @@ class DeferredFieldsTest extends TestCase
'stories' => [ 'stories' => [
'type' => Type::listOf($this->storyType), 'type' => Type::listOf($this->storyType),
'resolve' => function ($category, $args, $context, ResolveInfo $info) { 'resolve' => function ($category, $args, $context, ResolveInfo $info) {
$this->path[] = $info->path; $this->paths[] = $info->path;
return Utils::filter( return Utils::filter(
$this->storyDataSource, $this->storyDataSource,
@ -161,10 +163,10 @@ class DeferredFieldsTest extends TestCase
'topStory' => [ 'topStory' => [
'type' => $this->storyType, 'type' => $this->storyType,
'resolve' => function ($category, $args, $context, ResolveInfo $info) { 'resolve' => function ($category, $args, $context, ResolveInfo $info) {
$this->path[] = $info->path; $this->paths[] = $info->path;
return new Deferred(function () use ($category) { return new Deferred(function () use ($category) {
$this->path[] = 'deferred-for-category-' . $category['id'] . '-topStory'; $this->paths[] = 'deferred-for-category-' . $category['id'] . '-topStory';
return Utils::find( return Utils::find(
$this->storyDataSource, $this->storyDataSource,
@ -184,7 +186,7 @@ class DeferredFieldsTest extends TestCase
'topStories' => [ 'topStories' => [
'type' => Type::listOf($this->storyType), 'type' => Type::listOf($this->storyType),
'resolve' => function ($val, $args, $context, ResolveInfo $info) { 'resolve' => function ($val, $args, $context, ResolveInfo $info) {
$this->path[] = $info->path; $this->paths[] = $info->path;
return Utils::filter( return Utils::filter(
$this->storyDataSource, $this->storyDataSource,
@ -197,7 +199,7 @@ class DeferredFieldsTest extends TestCase
'featuredCategory' => [ 'featuredCategory' => [
'type' => $this->categoryType, 'type' => $this->categoryType,
'resolve' => function ($val, $args, $context, ResolveInfo $info) { 'resolve' => function ($val, $args, $context, ResolveInfo $info) {
$this->path[] = $info->path; $this->paths[] = $info->path;
return $this->categoryDataSource[0]; return $this->categoryDataSource[0];
}, },
@ -205,7 +207,7 @@ class DeferredFieldsTest extends TestCase
'categories' => [ 'categories' => [
'type' => Type::listOf($this->categoryType), 'type' => Type::listOf($this->categoryType),
'resolve' => function ($val, $args, $context, ResolveInfo $info) { 'resolve' => function ($val, $args, $context, ResolveInfo $info) {
$this->path[] = $info->path; $this->paths[] = $info->path;
return $this->categoryDataSource; return $this->categoryDataSource;
}, },
@ -264,7 +266,7 @@ class DeferredFieldsTest extends TestCase
$result = Executor::execute($schema, $query); $result = Executor::execute($schema, $query);
self::assertEquals($expected, $result->toArray()); self::assertEquals($expected, $result->toArray());
$expectedPath = [ $expectedPaths = [
['topStories'], ['topStories'],
['topStories', 0, 'title'], ['topStories', 0, 'title'],
['topStories', 0, 'author'], ['topStories', 0, 'author'],
@ -305,7 +307,10 @@ class DeferredFieldsTest extends TestCase
['featuredCategory', 'stories', 2, 'author', 'name'], ['featuredCategory', 'stories', 2, 'author', 'name'],
['featuredCategory', 'stories', 3, 'author', 'name'], ['featuredCategory', 'stories', 3, 'author', 'name'],
]; ];
self::assertEquals($expectedPath, $this->path); self::assertCount(count($expectedPaths), $this->paths);
foreach ($expectedPaths as $expectedPath) {
self::assertTrue(in_array($expectedPath, $this->paths, true), 'Missing path: ' . json_encode($expectedPath));
}
} }
public function testNestedDeferredFields() : void public function testNestedDeferredFields() : void
@ -349,7 +354,7 @@ class DeferredFieldsTest extends TestCase
$result = Executor::execute($schema, $query); $result = Executor::execute($schema, $query);
self::assertEquals($expected, $result->toArray()); self::assertEquals($expected, $result->toArray());
$expectedPath = [ $expectedPaths = [
['categories'], ['categories'],
['categories', 0, 'name'], ['categories', 0, 'name'],
['categories', 0, 'topStory'], ['categories', 0, 'topStory'],
@ -382,7 +387,10 @@ class DeferredFieldsTest extends TestCase
['categories', 1, 'topStory', 'author', 'bestFriend', 'name'], ['categories', 1, 'topStory', 'author', 'bestFriend', 'name'],
['categories', 2, 'topStory', 'author', 'bestFriend', 'name'], ['categories', 2, 'topStory', 'author', 'bestFriend', 'name'],
]; ];
self::assertEquals($expectedPath, $this->path); self::assertCount(count($expectedPaths), $this->paths);
foreach ($expectedPaths as $expectedPath) {
self::assertTrue(in_array($expectedPath, $this->paths, true), 'Missing path: ' . json_encode($expectedPath));
}
} }
public function testComplexRecursiveDeferredFields() : void public function testComplexRecursiveDeferredFields() : void
@ -394,7 +402,7 @@ class DeferredFieldsTest extends TestCase
'sync' => [ 'sync' => [
'type' => Type::string(), 'type' => Type::string(),
'resolve' => function ($v, $a, $c, ResolveInfo $info) { 'resolve' => function ($v, $a, $c, ResolveInfo $info) {
$this->path[] = $info->path; $this->paths[] = $info->path;
return 'sync'; return 'sync';
}, },
@ -402,10 +410,10 @@ class DeferredFieldsTest extends TestCase
'deferred' => [ 'deferred' => [
'type' => Type::string(), 'type' => Type::string(),
'resolve' => function ($v, $a, $c, ResolveInfo $info) { 'resolve' => function ($v, $a, $c, ResolveInfo $info) {
$this->path[] = $info->path; $this->paths[] = $info->path;
return new Deferred(function () use ($info) { return new Deferred(function () use ($info) {
$this->path[] = ['!dfd for: ', $info->path]; $this->paths[] = ['!dfd for: ', $info->path];
return 'deferred'; return 'deferred';
}); });
@ -414,7 +422,7 @@ class DeferredFieldsTest extends TestCase
'nest' => [ 'nest' => [
'type' => $complexType, 'type' => $complexType,
'resolve' => function ($v, $a, $c, ResolveInfo $info) { 'resolve' => function ($v, $a, $c, ResolveInfo $info) {
$this->path[] = $info->path; $this->paths[] = $info->path;
return []; return [];
}, },
@ -422,10 +430,10 @@ class DeferredFieldsTest extends TestCase
'deferredNest' => [ 'deferredNest' => [
'type' => $complexType, 'type' => $complexType,
'resolve' => function ($v, $a, $c, ResolveInfo $info) { 'resolve' => function ($v, $a, $c, ResolveInfo $info) {
$this->path[] = $info->path; $this->paths[] = $info->path;
return new Deferred(function () use ($info) { return new Deferred(function () use ($info) {
$this->path[] = ['!dfd nest for: ', $info->path]; $this->paths[] = ['!dfd nest for: ', $info->path];
return []; return [];
}); });
@ -497,7 +505,7 @@ class DeferredFieldsTest extends TestCase
self::assertEquals($expected, $result->toArray()); self::assertEquals($expected, $result->toArray());
$expectedPath = [ $expectedPaths = [
['nest'], ['nest'],
['nest', 'sync'], ['nest', 'sync'],
['nest', 'deferred'], ['nest', 'deferred'],
@ -531,6 +539,9 @@ class DeferredFieldsTest extends TestCase
['!dfd for: ', ['deferredNest', 'deferredNest', 'deferred']], ['!dfd for: ', ['deferredNest', 'deferredNest', 'deferred']],
]; ];
self::assertEquals($expectedPath, $this->path); self::assertCount(count($expectedPaths), $this->paths);
foreach ($expectedPaths as $expectedPath) {
self::assertTrue(in_array($expectedPath, $this->paths, true), 'Missing path: ' . json_encode($expectedPath));
}
} }
} }

View File

@ -866,7 +866,7 @@ class ExecutorTest extends TestCase
...Frag ...Frag
} }
fragment Frag on DataType { fragment Frag on Type {
a, a,
...Frag ...Frag
} }

View File

@ -14,7 +14,10 @@ use GraphQL\Language\SourceLocation;
use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema; use GraphQL\Type\Schema;
use PHPUnit\Framework\ExpectationFailedException;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use function count;
use function json_encode;
class NonNullTest extends TestCase class NonNullTest extends TestCase
{ {
@ -370,10 +373,24 @@ class NonNullTest extends TestCase
], ],
]; ];
self::assertArraySubset( $result = Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q')->toArray();
$expected,
Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q')->toArray() self::assertEquals($expected['data'], $result['data']);
);
self::assertCount(count($expected['errors']), $result['errors']);
foreach ($expected['errors'] as $expectedError) {
$found = false;
foreach ($result['errors'] as $error) {
try {
self::assertArraySubset($expectedError, $error);
$found = true;
break;
} catch (ExpectationFailedException $e) {
continue;
}
}
self::assertTrue($found, 'Did not find error: ' . json_encode($expectedError));
}
} }
public function testNullsTheFirstNullableObjectAfterAFieldThrowsInALongChainOfFieldsThatAreNonNull() : void public function testNullsTheFirstNullableObjectAfterAFieldThrowsInALongChainOfFieldsThatAreNonNull() : void

View File

@ -25,7 +25,7 @@ class ValuesTest extends TestCase
{ {
$this->expectInputVariablesMatchOutputVariables(['idInput' => '123456789']); $this->expectInputVariablesMatchOutputVariables(['idInput' => '123456789']);
self::assertEquals( self::assertEquals(
['errors' => [], 'coerced' => ['idInput' => '123456789']], [null, ['idInput' => '123456789']],
$this->runTestCase(['idInput' => 123456789]), $this->runTestCase(['idInput' => 123456789]),
'Integer ID was not converted to string' 'Integer ID was not converted to string'
); );
@ -35,7 +35,7 @@ class ValuesTest extends TestCase
{ {
self::assertEquals( self::assertEquals(
$variables, $variables,
$this->runTestCase($variables)['coerced'], $this->runTestCase($variables)[1],
'Output variables did not match input variables' . "\n" . var_export($variables, true) . "\n" 'Output variables did not match input variables' . "\n" . var_export($variables, true) . "\n"
); );
} }
@ -148,7 +148,8 @@ class ValuesTest extends TestCase
private function expectGraphQLError($variables) : void private function expectGraphQLError($variables) : void
{ {
$result = $this->runTestCase($variables); $result = $this->runTestCase($variables);
self::assertGreaterThan(0, count($result['errors'])); self::assertNotNull($result[0]);
self::assertGreaterThan(0, count($result[0]));
} }
public function testFloatForIDVariableThrowsError() : void public function testFloatForIDVariableThrowsError() : void

View File

@ -0,0 +1,380 @@
<?php
declare(strict_types=1);
namespace GraphQL\Tests\Experimental\Executor;
use GraphQL\Error\FormattedError;
use GraphQL\Experimental\Executor\Collector;
use GraphQL\Experimental\Executor\Runtime;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\ValueNode;
use GraphQL\Language\Parser;
use GraphQL\Tests\StarWarsSchema;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Schema;
use GraphQL\Utils\AST;
use PHPUnit\Framework\TestCase;
use stdClass;
use Throwable;
use function array_map;
use function basename;
use function file_exists;
use function file_put_contents;
use function json_encode;
use function strlen;
use function strncmp;
use const DIRECTORY_SEPARATOR;
use const JSON_PRETTY_PRINT;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;
class CollectorTest extends TestCase
{
/**
* @param mixed[]|null $variableValues
*
* @dataProvider provideForTestCollectFields
*/
public function testCollectFields(Schema $schema, DocumentNode $documentNode, string $operationName, ?array $variableValues)
{
$runtime = new class($variableValues) implements Runtime
{
/** @var Throwable[] */
public $errors = [];
/** @var mixed[]|null */
public $variableValues;
public function __construct($variableValues)
{
$this->variableValues = $variableValues;
}
public function evaluate(ValueNode $valueNode, InputType $type)
{
return AST::valueFromAST($valueNode, $type, $this->variableValues);
}
public function addError($error)
{
$this->errors[] = $error;
}
};
$collector = new Collector($schema, $runtime);
$collector->initialize($documentNode, $operationName);
$pipeline = [];
foreach ($collector->collectFields($collector->rootType, $collector->operation->selectionSet) as $shared) {
$execution = new stdClass();
if (! empty($shared->fieldNodes)) {
$execution->fieldNodes = array_map(static function (Node $node) {
return $node->toArray(true);
}, $shared->fieldNodes);
}
if (! empty($shared->fieldName)) {
$execution->fieldName = $shared->fieldName;
}
if (! empty($shared->resultName)) {
$execution->resultName = $shared->resultName;
}
if (! empty($shared->argumentValueMap)) {
$execution->argumentValueMap = [];
foreach ($shared->argumentValueMap as $argumentName => $valueNode) {
/** @var Node $valueNode */
$execution->argumentValueMap[$argumentName] = $valueNode->toArray(true);
}
}
$pipeline[] = $execution;
}
if (strncmp($operationName, 'ShouldEmitError', strlen('ShouldEmitError')) === 0) {
self::assertNotEmpty($runtime->errors, 'There should be errors.');
} else {
self::assertEmpty($runtime->errors, 'There must be no errors. Got: ' . json_encode($runtime->errors, JSON_PRETTY_PRINT));
if (strncmp($operationName, 'ShouldNotEmit', strlen('ShouldNotEmit')) === 0) {
self::assertEmpty($pipeline, 'No instructions should be emitted.');
} else {
self::assertNotEmpty($pipeline, 'There should be some instructions emitted.');
}
}
$result = [];
if (! empty($runtime->errors)) {
$result['errors'] = array_map(
FormattedError::prepareFormatter(null, false),
$runtime->errors
);
}
if (! empty($pipeline)) {
$result['pipeline'] = $pipeline;
}
$json = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
$fileName = __DIR__ . DIRECTORY_SEPARATOR . basename(__FILE__, '.php') . 'Snapshots' . DIRECTORY_SEPARATOR . $operationName . '.json';
if (! file_exists($fileName)) {
file_put_contents($fileName, $json);
}
self::assertStringEqualsFile($fileName, $json);
}
public function provideForTestCollectFields()
{
$testCases = [
[
StarWarsSchema::build(),
'query ShouldEmitFieldWithoutArguments {
human {
name
}
}',
null,
],
[
StarWarsSchema::build(),
'query ShouldEmitFieldThatHasArguments($id: ID!) {
human(id: $id) {
name
}
}',
null,
],
[
StarWarsSchema::build(),
'query ShouldEmitForInlineFragment($id: ID!) {
...HumanById
}
fragment HumanById on Query {
human(id: $id) {
... on Human {
name
}
}
}',
null,
],
[
StarWarsSchema::build(),
'query ShouldEmitObjectFieldForFragmentSpread($id: ID!) {
human(id: $id) {
...HumanName
}
}
fragment HumanName on Human {
name
}',
null,
],
[
StarWarsSchema::build(),
'query ShouldEmitTypeName {
queryTypeName: __typename
__typename
}',
null,
],
[
StarWarsSchema::build(),
'query ShouldEmitIfIncludeConditionTrue($id: ID!, $condition: Boolean!) {
droid(id: $id) @include(if: $condition) {
id
}
}',
['condition' => true],
],
[
StarWarsSchema::build(),
'query ShouldNotEmitIfIncludeConditionFalse($id: ID!, $condition: Boolean!) {
droid(id: $id) @include(if: $condition) {
id
}
}',
['condition' => false],
],
[
StarWarsSchema::build(),
'query ShouldNotEmitIfSkipConditionTrue($id: ID!, $condition: Boolean!) {
droid(id: $id) @skip(if: $condition) {
id
}
}',
['condition' => true],
],
[
StarWarsSchema::build(),
'query ShouldEmitIfSkipConditionFalse($id: ID!, $condition: Boolean!) {
droid(id: $id) @skip(if: $condition) {
id
}
}',
['condition' => false],
],
[
StarWarsSchema::build(),
'query ShouldNotEmitIncludeSkipTT($id: ID!, $includeCondition: Boolean!, $skipCondition: Boolean!) {
droid(id: $id) @include(if: $includeCondition) @skip(if: $skipCondition) {
id
}
}',
['includeCondition' => true, 'skipCondition' => true],
],
[
StarWarsSchema::build(),
'query ShouldEmitIncludeSkipTF($id: ID!, $includeCondition: Boolean!, $skipCondition: Boolean!) {
droid(id: $id) @include(if: $includeCondition) @skip(if: $skipCondition) {
id
}
}',
['includeCondition' => true, 'skipCondition' => false],
],
[
StarWarsSchema::build(),
'query ShouldNotEmitIncludeSkipFT($id: ID!, $includeCondition: Boolean!, $skipCondition: Boolean!) {
droid(id: $id) @include(if: $includeCondition) @skip(if: $skipCondition) {
id
}
}',
['includeCondition' => false, 'skipCondition' => true],
],
[
StarWarsSchema::build(),
'query ShouldNotEmitIncludeSkipFF($id: ID!, $includeCondition: Boolean!, $skipCondition: Boolean!) {
droid(id: $id) @include(if: $includeCondition) @skip(if: $skipCondition) {
id
}
}',
['includeCondition' => false, 'skipCondition' => false],
],
[
StarWarsSchema::build(),
'query ShouldNotEmitSkipAroundInlineFragment {
... on Query @skip(if: true) {
hero(episode: 5) {
name
}
}
}',
null,
],
[
StarWarsSchema::build(),
'query ShouldEmitSkipAroundInlineFragment {
... on Query @skip(if: false) {
hero(episode: 5) {
name
}
}
}',
null,
],
[
StarWarsSchema::build(),
'query ShouldEmitIncludeAroundInlineFragment {
... on Query @include(if: true) {
hero(episode: 5) {
name
}
}
}',
null,
],
[
StarWarsSchema::build(),
'query ShouldNotEmitIncludeAroundInlineFragment {
... on Query @include(if: false) {
hero(episode: 5) {
name
}
}
}',
null,
],
[
StarWarsSchema::build(),
'query ShouldNotEmitSkipFragmentSpread {
...Hero @skip(if: true)
}
fragment Hero on Query {
hero(episode: 5) {
name
}
}',
null,
],
[
StarWarsSchema::build(),
'query ShouldEmitSkipFragmentSpread {
...Hero @skip(if: false)
}
fragment Hero on Query {
hero(episode: 5) {
name
}
}',
null,
],
[
StarWarsSchema::build(),
'query ShouldEmitIncludeFragmentSpread {
...Hero @include(if: true)
}
fragment Hero on Query {
hero(episode: 5) {
name
}
}',
null,
],
[
StarWarsSchema::build(),
'query ShouldNotEmitIncludeFragmentSpread {
...Hero @include(if: false)
}
fragment Hero on Query {
hero(episode: 5) {
name
}
}',
null,
],
[
StarWarsSchema::build(),
'query ShouldEmitSingleInstrictionForSameResultName($id: ID!) {
human(id: $id) {
name
name: secretBackstory
}
}',
null,
],
];
$data = [];
foreach ($testCases as [$schema, $query, $variableValues]) {
$documentNode = Parser::parse($query, ['noLocation' => true]);
$operationName = null;
foreach ($documentNode->definitions as $definitionNode) {
/** @var Node $definitionNode */
if ($definitionNode->kind === NodeKind::OPERATION_DEFINITION) {
/** @var OperationDefinitionNode $definitionNode */
self::assertNotNull($definitionNode->name);
$operationName = $definitionNode->name->value;
break;
}
}
self::assertArrayNotHasKey($operationName, $data);
$data[$operationName] = [$schema, $documentNode, $operationName, $variableValues];
}
return $data;
}
}

View File

@ -0,0 +1,57 @@
{
"pipeline": [
{
"fieldNodes": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "human"
},
"arguments": [
{
"kind": "Argument",
"value": {
"kind": "Variable",
"name": {
"kind": "Name",
"value": "id"
}
},
"name": {
"kind": "Name",
"value": "id"
}
}
],
"directives": [],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "name"
},
"arguments": [],
"directives": []
}
]
}
}
],
"fieldName": "human",
"resultName": "human",
"argumentValueMap": {
"id": {
"kind": "Variable",
"name": {
"kind": "Name",
"value": "id"
}
}
}
}
]
}

View File

@ -0,0 +1,33 @@
{
"pipeline": [
{
"fieldNodes": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "human"
},
"arguments": [],
"directives": [],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "name"
},
"arguments": [],
"directives": []
}
]
}
}
],
"fieldName": "human",
"resultName": "human"
}
]
}

View File

@ -0,0 +1,73 @@
{
"pipeline": [
{
"fieldNodes": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "human"
},
"arguments": [
{
"kind": "Argument",
"value": {
"kind": "Variable",
"name": {
"kind": "Name",
"value": "id"
}
},
"name": {
"kind": "Name",
"value": "id"
}
}
],
"directives": [],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "InlineFragment",
"typeCondition": {
"kind": "NamedType",
"name": {
"kind": "Name",
"value": "Human"
}
},
"directives": [],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "name"
},
"arguments": [],
"directives": []
}
]
}
}
]
}
}
],
"fieldName": "human",
"resultName": "human",
"argumentValueMap": {
"id": {
"kind": "Variable",
"name": {
"kind": "Name",
"value": "id"
}
}
}
}
]
}

View File

@ -0,0 +1,81 @@
{
"pipeline": [
{
"fieldNodes": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "droid"
},
"arguments": [
{
"kind": "Argument",
"value": {
"kind": "Variable",
"name": {
"kind": "Name",
"value": "id"
}
},
"name": {
"kind": "Name",
"value": "id"
}
}
],
"directives": [
{
"kind": "Directive",
"name": {
"kind": "Name",
"value": "include"
},
"arguments": [
{
"kind": "Argument",
"value": {
"kind": "Variable",
"name": {
"kind": "Name",
"value": "condition"
}
},
"name": {
"kind": "Name",
"value": "if"
}
}
]
}
],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "id"
},
"arguments": [],
"directives": []
}
]
}
}
],
"fieldName": "droid",
"resultName": "droid",
"argumentValueMap": {
"id": {
"kind": "Variable",
"name": {
"kind": "Name",
"value": "id"
}
}
}
}
]
}

View File

@ -0,0 +1,81 @@
{
"pipeline": [
{
"fieldNodes": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "droid"
},
"arguments": [
{
"kind": "Argument",
"value": {
"kind": "Variable",
"name": {
"kind": "Name",
"value": "id"
}
},
"name": {
"kind": "Name",
"value": "id"
}
}
],
"directives": [
{
"kind": "Directive",
"name": {
"kind": "Name",
"value": "skip"
},
"arguments": [
{
"kind": "Argument",
"value": {
"kind": "Variable",
"name": {
"kind": "Name",
"value": "condition"
}
},
"name": {
"kind": "Name",
"value": "if"
}
}
]
}
],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "id"
},
"arguments": [],
"directives": []
}
]
}
}
],
"fieldName": "droid",
"resultName": "droid",
"argumentValueMap": {
"id": {
"kind": "Variable",
"name": {
"kind": "Name",
"value": "id"
}
}
}
}
]
}

View File

@ -0,0 +1,51 @@
{
"pipeline": [
{
"fieldNodes": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "hero"
},
"arguments": [
{
"kind": "Argument",
"value": {
"kind": "IntValue",
"value": "5"
},
"name": {
"kind": "Name",
"value": "episode"
}
}
],
"directives": [],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "name"
},
"arguments": [],
"directives": []
}
]
}
}
],
"fieldName": "hero",
"resultName": "hero",
"argumentValueMap": {
"episode": {
"kind": "IntValue",
"value": "5"
}
}
}
]
}

View File

@ -0,0 +1,51 @@
{
"pipeline": [
{
"fieldNodes": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "hero"
},
"arguments": [
{
"kind": "Argument",
"value": {
"kind": "IntValue",
"value": "5"
},
"name": {
"kind": "Name",
"value": "episode"
}
}
],
"directives": [],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "name"
},
"arguments": [],
"directives": []
}
]
}
}
],
"fieldName": "hero",
"resultName": "hero",
"argumentValueMap": {
"episode": {
"kind": "IntValue",
"value": "5"
}
}
}
]
}

View File

@ -0,0 +1,104 @@
{
"pipeline": [
{
"fieldNodes": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "droid"
},
"arguments": [
{
"kind": "Argument",
"value": {
"kind": "Variable",
"name": {
"kind": "Name",
"value": "id"
}
},
"name": {
"kind": "Name",
"value": "id"
}
}
],
"directives": [
{
"kind": "Directive",
"name": {
"kind": "Name",
"value": "include"
},
"arguments": [
{
"kind": "Argument",
"value": {
"kind": "Variable",
"name": {
"kind": "Name",
"value": "includeCondition"
}
},
"name": {
"kind": "Name",
"value": "if"
}
}
]
},
{
"kind": "Directive",
"name": {
"kind": "Name",
"value": "skip"
},
"arguments": [
{
"kind": "Argument",
"value": {
"kind": "Variable",
"name": {
"kind": "Name",
"value": "skipCondition"
}
},
"name": {
"kind": "Name",
"value": "if"
}
}
]
}
],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "id"
},
"arguments": [],
"directives": []
}
]
}
}
],
"fieldName": "droid",
"resultName": "droid",
"argumentValueMap": {
"id": {
"kind": "Variable",
"name": {
"kind": "Name",
"value": "id"
}
}
}
}
]
}

View File

@ -0,0 +1,56 @@
{
"pipeline": [
{
"fieldNodes": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "human"
},
"arguments": [
{
"kind": "Argument",
"value": {
"kind": "Variable",
"name": {
"kind": "Name",
"value": "id"
}
},
"name": {
"kind": "Name",
"value": "id"
}
}
],
"directives": [],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "FragmentSpread",
"name": {
"kind": "Name",
"value": "HumanName"
},
"directives": []
}
]
}
}
],
"fieldName": "human",
"resultName": "human",
"argumentValueMap": {
"id": {
"kind": "Variable",
"name": {
"kind": "Name",
"value": "id"
}
}
}
}
]
}

View File

@ -0,0 +1,70 @@
{
"pipeline": [
{
"fieldNodes": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "human"
},
"arguments": [
{
"kind": "Argument",
"value": {
"kind": "Variable",
"name": {
"kind": "Name",
"value": "id"
}
},
"name": {
"kind": "Name",
"value": "id"
}
}
],
"directives": [],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "name"
},
"arguments": [],
"directives": []
},
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "secretBackstory"
},
"alias": {
"kind": "Name",
"value": "name"
},
"arguments": [],
"directives": []
}
]
}
}
],
"fieldName": "human",
"resultName": "human",
"argumentValueMap": {
"id": {
"kind": "Variable",
"name": {
"kind": "Name",
"value": "id"
}
}
}
}
]
}

View File

@ -0,0 +1,51 @@
{
"pipeline": [
{
"fieldNodes": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "hero"
},
"arguments": [
{
"kind": "Argument",
"value": {
"kind": "IntValue",
"value": "5"
},
"name": {
"kind": "Name",
"value": "episode"
}
}
],
"directives": [],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "name"
},
"arguments": [],
"directives": []
}
]
}
}
],
"fieldName": "hero",
"resultName": "hero",
"argumentValueMap": {
"episode": {
"kind": "IntValue",
"value": "5"
}
}
}
]
}

View File

@ -0,0 +1,51 @@
{
"pipeline": [
{
"fieldNodes": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "hero"
},
"arguments": [
{
"kind": "Argument",
"value": {
"kind": "IntValue",
"value": "5"
},
"name": {
"kind": "Name",
"value": "episode"
}
}
],
"directives": [],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "name"
},
"arguments": [],
"directives": []
}
]
}
}
],
"fieldName": "hero",
"resultName": "hero",
"argumentValueMap": {
"episode": {
"kind": "IntValue",
"value": "5"
}
}
}
]
}

View File

@ -0,0 +1,38 @@
{
"pipeline": [
{
"fieldNodes": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "__typename"
},
"alias": {
"kind": "Name",
"value": "queryTypeName"
},
"arguments": [],
"directives": []
}
],
"fieldName": "__typename",
"resultName": "queryTypeName"
},
{
"fieldNodes": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "__typename"
},
"arguments": [],
"directives": []
}
],
"fieldName": "__typename",
"resultName": "__typename"
}
]
}

15
tests/bootstrap.php Normal file
View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace GraphQL\Tests;
use GraphQL\Executor\Executor;
use GraphQL\Experimental\Executor\CoroutineExecutor;
use function getenv;
require_once __DIR__ . '/../vendor/autoload.php';
if (getenv('EXECUTOR') === 'coroutine') {
Executor::setImplementationFactory([CoroutineExecutor::class, 'create']);
}