mirror of
https://github.com/retailcrm/graphql-php.git
synced 2024-11-21 20:36:05 +03:00
Pluggable executor implementations; new faster executor using coroutines
This commit is contained in:
parent
98807286f7
commit
b5d3341995
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
.phpcs-cache
|
||||
composer.lock
|
||||
composer.phar
|
||||
phpcs.xml
|
||||
phpstan.neon
|
||||
vendor/
|
||||
|
@ -6,6 +6,12 @@ php:
|
||||
- 7.2
|
||||
- nightly
|
||||
|
||||
env:
|
||||
matrix:
|
||||
- EXECUTOR=coroutine
|
||||
- EXECUTOR=
|
||||
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.composer/cache
|
||||
|
@ -2,7 +2,7 @@
|
||||
<phpunit
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
bootstrap="tests/bootstrap.php"
|
||||
>
|
||||
<php>
|
||||
<ini name="error_reporting" value="E_ALL"/>
|
||||
|
@ -58,7 +58,7 @@ class ExecutionResult implements JsonSerializable
|
||||
* @param Error[] $errors
|
||||
* @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->errors = $errors;
|
||||
|
File diff suppressed because it is too large
Load Diff
15
src/Executor/ExecutorImplementation.php
Normal file
15
src/Executor/ExecutorImplementation.php
Normal 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;
|
||||
}
|
1327
src/Executor/ReferenceExecutor.php
Normal file
1327
src/Executor/ReferenceExecutor.php
Normal file
File diff suppressed because it is too large
Load Diff
@ -12,6 +12,7 @@ use GraphQL\Language\AST\FieldDefinitionNode;
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\FragmentSpreadNode;
|
||||
use GraphQL\Language\AST\InlineFragmentNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeList;
|
||||
use GraphQL\Language\AST\ValueNode;
|
||||
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)
|
||||
{
|
||||
$argDefs = $def->args;
|
||||
$argNodes = $node->arguments;
|
||||
|
||||
if (empty($argDefs) || $argNodes === null) {
|
||||
if (empty($def->args)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$argumentNodes = $node->arguments;
|
||||
if (empty($argumentNodes)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$argumentValueMap = [];
|
||||
foreach ($argumentNodes as $argumentNode) {
|
||||
$argumentValueMap[$argumentNode->name->value] = $argumentNode->value;
|
||||
}
|
||||
|
||||
return static::getArgumentValuesForMap($def, $argumentValueMap, $variableValues, $node);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FieldDefinition|Directive $fieldDefinition
|
||||
* @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 = [];
|
||||
|
||||
/** @var ArgumentNode[] $argNodeMap */
|
||||
$argNodeMap = $argNodes ? Utils::keyMap(
|
||||
$argNodes,
|
||||
static function (ArgumentNode $arg) {
|
||||
return $arg->name->value;
|
||||
}
|
||||
) : [];
|
||||
foreach ($argumentDefinitions as $argumentDefinition) {
|
||||
$name = $argumentDefinition->name;
|
||||
$argType = $argumentDefinition->getType();
|
||||
$argumentValueNode = $argumentValueMap[$name] ?? null;
|
||||
|
||||
foreach ($argDefs as $argDef) {
|
||||
$name = $argDef->name;
|
||||
$argType = $argDef->getType();
|
||||
$argumentNode = $argNodeMap[$name] ?? null;
|
||||
|
||||
if (! $argumentNode) {
|
||||
if ($argDef->defaultValueExists()) {
|
||||
$coercedValues[$name] = $argDef->defaultValue;
|
||||
if (! $argumentValueNode) {
|
||||
if ($argumentDefinition->defaultValueExists()) {
|
||||
$coercedValues[$name] = $argumentDefinition->defaultValue;
|
||||
} elseif ($argType instanceof NonNull) {
|
||||
throw new Error(
|
||||
'Argument "' . $name . '" of required type ' .
|
||||
'"' . Utils::printSafe($argType) . '" was not provided.',
|
||||
[$node]
|
||||
$referenceNode
|
||||
);
|
||||
}
|
||||
} elseif ($argumentNode->value instanceof VariableNode) {
|
||||
$variableName = $argumentNode->value->name->value;
|
||||
} elseif ($argumentValueNode instanceof VariableNode) {
|
||||
$variableName = $argumentValueNode->name->value;
|
||||
|
||||
if ($variableValues && array_key_exists($variableName, $variableValues)) {
|
||||
// Note: this does not check that this variable value is correct.
|
||||
// This assumes that this query has been validated and the variable
|
||||
// usage here is of the correct type.
|
||||
$coercedValues[$name] = $variableValues[$variableName];
|
||||
} elseif ($argDef->defaultValueExists()) {
|
||||
$coercedValues[$name] = $argDef->defaultValue;
|
||||
} elseif ($argumentDefinition->defaultValueExists()) {
|
||||
$coercedValues[$name] = $argumentDefinition->defaultValue;
|
||||
} elseif ($argType instanceof NonNull) {
|
||||
throw new Error(
|
||||
'Argument "' . $name . '" of required type "' . Utils::printSafe($argType) . '" was ' .
|
||||
'provided the variable "$' . $variableName . '" which was not provided ' .
|
||||
'a runtime value.',
|
||||
[$argumentNode->value]
|
||||
[$argumentValueNode]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$valueNode = $argumentNode->value;
|
||||
$valueNode = $argumentValueNode;
|
||||
$coercedValue = AST::valueFromAST($valueNode, $argType, $variableValues);
|
||||
if (Utils::isInvalid($coercedValue)) {
|
||||
// Note: ValuesOfCorrectType validation should catch this before
|
||||
@ -213,7 +233,7 @@ class Values
|
||||
// continue with an invalid argument value.
|
||||
throw new Error(
|
||||
'Argument "' . $name . '" has invalid value ' . Printer::doPrint($valueNode) . '.',
|
||||
[$argumentNode->value]
|
||||
[$argumentValueNode]
|
||||
);
|
||||
}
|
||||
$coercedValues[$name] = $coercedValue;
|
||||
|
279
src/Experimental/Executor/Collector.php
Normal file
279
src/Experimental/Executor/Collector.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
57
src/Experimental/Executor/CoroutineContext.php
Normal file
57
src/Experimental/Executor/CoroutineContext.php
Normal 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;
|
||||
}
|
||||
}
|
62
src/Experimental/Executor/CoroutineContextShared.php
Normal file
62
src/Experimental/Executor/CoroutineContextShared.php
Normal 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;
|
||||
}
|
||||
}
|
931
src/Experimental/Executor/CoroutineExecutor.php
Normal file
931
src/Experimental/Executor/CoroutineExecutor.php
Normal 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;
|
||||
}
|
||||
}
|
18
src/Experimental/Executor/Runtime.php
Normal file
18
src/Experimental/Executor/Runtime.php
Normal 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);
|
||||
}
|
35
src/Experimental/Executor/Strand.php
Normal file
35
src/Experimental/Executor/Strand.php
Normal 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;
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class VariableNode extends Node
|
||||
class VariableNode extends Node implements ValueNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::VARIABLE;
|
||||
|
@ -8,6 +8,8 @@ use GraphQL\Language\AST\DirectiveDefinitionNode;
|
||||
use GraphQL\Language\DirectiveLocation;
|
||||
use GraphQL\Utils\Utils;
|
||||
use function array_key_exists;
|
||||
use function array_keys;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
|
||||
/**
|
||||
@ -17,6 +19,12 @@ class Directive
|
||||
{
|
||||
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[] */
|
||||
public static $internalDirectives;
|
||||
|
||||
@ -72,7 +80,6 @@ class Directive
|
||||
public static function includeDirective()
|
||||
{
|
||||
$internal = self::getInternalDirectives();
|
||||
|
||||
return $internal['include'];
|
||||
}
|
||||
|
||||
@ -84,7 +91,7 @@ class Directive
|
||||
if (! self::$internalDirectives) {
|
||||
self::$internalDirectives = [
|
||||
'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.',
|
||||
'locations' => [
|
||||
DirectiveLocation::FIELD,
|
||||
@ -92,14 +99,14 @@ class Directive
|
||||
DirectiveLocation::INLINE_FRAGMENT,
|
||||
],
|
||||
'args' => [new FieldArgument([
|
||||
'name' => 'if',
|
||||
'name' => self::IF_ARGUMENT_NAME,
|
||||
'type' => Type::nonNull(Type::boolean()),
|
||||
'description' => 'Included when true.',
|
||||
]),
|
||||
],
|
||||
]),
|
||||
'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.',
|
||||
'locations' => [
|
||||
DirectiveLocation::FIELD,
|
||||
@ -107,21 +114,21 @@ class Directive
|
||||
DirectiveLocation::INLINE_FRAGMENT,
|
||||
],
|
||||
'args' => [new FieldArgument([
|
||||
'name' => 'if',
|
||||
'name' => self::IF_ARGUMENT_NAME,
|
||||
'type' => Type::nonNull(Type::boolean()),
|
||||
'description' => 'Skipped when true.',
|
||||
]),
|
||||
],
|
||||
]),
|
||||
'deprecated' => new self([
|
||||
'name' => 'deprecated',
|
||||
'name' => self::DEPRECATED_NAME,
|
||||
'description' => 'Marks an element of a GraphQL schema as no longer supported.',
|
||||
'locations' => [
|
||||
DirectiveLocation::FIELD_DEFINITION,
|
||||
DirectiveLocation::ENUM_VALUE,
|
||||
],
|
||||
'args' => [new FieldArgument([
|
||||
'name' => 'reason',
|
||||
'name' => self::REASON_ARGUMENT_NAME,
|
||||
'type' => Type::string(),
|
||||
'description' =>
|
||||
'Explains why this element was deprecated, usually also including a ' .
|
||||
@ -133,30 +140,24 @@ class Directive
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
return self::$internalDirectives;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Directive
|
||||
*/
|
||||
public static function skipDirective()
|
||||
{
|
||||
$internal = self::getInternalDirectives();
|
||||
|
||||
return $internal['skip'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Directive
|
||||
*/
|
||||
public static function deprecatedDirective()
|
||||
{
|
||||
$internal = self::getInternalDirectives();
|
||||
|
||||
return $internal['deprecated'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
|
@ -59,7 +59,7 @@ class FieldDefinition
|
||||
public $config;
|
||||
|
||||
/** @var OutputType */
|
||||
private $type;
|
||||
public $type;
|
||||
|
||||
/** @var callable|string */
|
||||
private $complexityFn;
|
||||
|
@ -74,6 +74,20 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
|
||||
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[]
|
||||
*/
|
||||
|
@ -125,6 +125,20 @@ class ObjectType extends Type implements OutputType, CompositeType, NamedType
|
||||
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[]
|
||||
*
|
||||
|
@ -11,7 +11,6 @@ use GraphQL\Language\AST\InlineFragmentNode;
|
||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||
use GraphQL\Language\AST\SelectionSetNode;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Utils\Utils;
|
||||
use function array_merge_recursive;
|
||||
|
||||
/**
|
||||
@ -56,7 +55,7 @@ class ResolveInfo
|
||||
* Path to this field from the very root value
|
||||
*
|
||||
* @api
|
||||
* @var mixed[]|null
|
||||
* @var string[]
|
||||
*/
|
||||
public $path;
|
||||
|
||||
@ -100,12 +99,28 @@ class ResolveInfo
|
||||
*/
|
||||
public $variableValues;
|
||||
|
||||
/**
|
||||
* @param mixed[] $values
|
||||
*/
|
||||
public function __construct(array $values)
|
||||
{
|
||||
Utils::assign($this, $values);
|
||||
public function __construct(
|
||||
string $fieldName,
|
||||
$fieldNodes,
|
||||
$returnType,
|
||||
ObjectType $parentType,
|
||||
$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 bool[]
|
||||
*/
|
||||
private function foldSelectionSet(SelectionSetNode $selectionSet, int $descend) : array
|
||||
{
|
||||
$fields = [];
|
||||
|
||||
foreach ($selectionSet->selections as $selectionNode) {
|
||||
if ($selectionNode instanceof FieldNode) {
|
||||
$fields[$selectionNode->name->value] = $descend > 0 && ! empty($selectionNode->selectionSet)
|
||||
@ -186,7 +199,6 @@ class ResolveInfo
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,10 @@ use const E_USER_DEPRECATED;
|
||||
|
||||
class Introspection
|
||||
{
|
||||
const SCHEMA_FIELD_NAME = '__schema';
|
||||
const TYPE_FIELD_NAME = '__type';
|
||||
const TYPE_NAME_FIELD_NAME = '__typename';
|
||||
|
||||
/** @var Type[] */
|
||||
private static $map = [];
|
||||
|
||||
@ -691,9 +695,9 @@ EOD;
|
||||
|
||||
public static function schemaMetaFieldDef()
|
||||
{
|
||||
if (! isset(self::$map['__schema'])) {
|
||||
self::$map['__schema'] = FieldDefinition::create([
|
||||
'name' => '__schema',
|
||||
if (! isset(self::$map[self::SCHEMA_FIELD_NAME])) {
|
||||
self::$map[self::SCHEMA_FIELD_NAME] = FieldDefinition::create([
|
||||
'name' => self::SCHEMA_FIELD_NAME,
|
||||
'type' => Type::nonNull(self::_schema()),
|
||||
'description' => 'Access the current type schema of this server.',
|
||||
'args' => [],
|
||||
@ -708,14 +712,14 @@ EOD;
|
||||
]);
|
||||
}
|
||||
|
||||
return self::$map['__schema'];
|
||||
return self::$map[self::SCHEMA_FIELD_NAME];
|
||||
}
|
||||
|
||||
public static function typeMetaFieldDef()
|
||||
{
|
||||
if (! isset(self::$map['__type'])) {
|
||||
self::$map['__type'] = FieldDefinition::create([
|
||||
'name' => '__type',
|
||||
if (! isset(self::$map[self::TYPE_FIELD_NAME])) {
|
||||
self::$map[self::TYPE_FIELD_NAME] = FieldDefinition::create([
|
||||
'name' => self::TYPE_FIELD_NAME,
|
||||
'type' => self::_type(),
|
||||
'description' => 'Request the type information of a single type.',
|
||||
'args' => [
|
||||
@ -727,14 +731,14 @@ EOD;
|
||||
]);
|
||||
}
|
||||
|
||||
return self::$map['__type'];
|
||||
return self::$map[self::TYPE_FIELD_NAME];
|
||||
}
|
||||
|
||||
public static function typeNameMetaFieldDef()
|
||||
{
|
||||
if (! isset(self::$map['__typename'])) {
|
||||
self::$map['__typename'] = FieldDefinition::create([
|
||||
'name' => '__typename',
|
||||
if (! isset(self::$map[self::TYPE_NAME_FIELD_NAME])) {
|
||||
self::$map[self::TYPE_NAME_FIELD_NAME] = FieldDefinition::create([
|
||||
'name' => self::TYPE_NAME_FIELD_NAME,
|
||||
'type' => Type::nonNull(Type::string()),
|
||||
'description' => 'The name of the current Object type at runtime.',
|
||||
'args' => [],
|
||||
@ -749,6 +753,6 @@ EOD;
|
||||
]);
|
||||
}
|
||||
|
||||
return self::$map['__typename'];
|
||||
return self::$map[self::TYPE_NAME_FIELD_NAME];
|
||||
}
|
||||
}
|
||||
|
@ -309,6 +309,16 @@ class Schema
|
||||
return $this->resolvedTypes[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasType($name)
|
||||
{
|
||||
return $this->getType($name) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $typeName
|
||||
*
|
||||
|
@ -186,37 +186,30 @@ class QueryComplexity extends QuerySecurityRule
|
||||
if ($directiveNode->name->value === 'deprecated') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$variableValuesResult = Values::getVariableValues(
|
||||
[$errors, $variableValues] = Values::getVariableValues(
|
||||
$this->context->getSchema(),
|
||||
$this->variableDefs,
|
||||
$this->getRawVariableValues()
|
||||
);
|
||||
|
||||
if (! empty($variableValuesResult['errors'])) {
|
||||
if (! empty($errors)) {
|
||||
throw new Error(implode(
|
||||
"\n\n",
|
||||
array_map(
|
||||
static function ($error) {
|
||||
return $error->getMessage();
|
||||
},
|
||||
$variableValuesResult['errors']
|
||||
$errors
|
||||
)
|
||||
));
|
||||
}
|
||||
$variableValues = $variableValuesResult['coerced'];
|
||||
|
||||
if ($directiveNode->name->value === 'include') {
|
||||
$directive = Directive::includeDirective();
|
||||
/** @var bool $directiveArgsIf */
|
||||
$directiveArgsIf = Values::getArgumentValues($directive, $directiveNode, $variableValues)['if'];
|
||||
|
||||
return ! $directiveArgsIf;
|
||||
}
|
||||
|
||||
$directive = Directive::skipDirective();
|
||||
$directiveArgsIf = Values::getArgumentValues($directive, $directiveNode, $variableValues);
|
||||
|
||||
return $directiveArgsIf['if'];
|
||||
}
|
||||
}
|
||||
@ -243,24 +236,23 @@ class QueryComplexity extends QuerySecurityRule
|
||||
$args = [];
|
||||
|
||||
if ($fieldDef instanceof FieldDefinition) {
|
||||
$variableValuesResult = Values::getVariableValues(
|
||||
[$errors, $variableValues] = Values::getVariableValues(
|
||||
$this->context->getSchema(),
|
||||
$this->variableDefs,
|
||||
$rawVariableValues
|
||||
);
|
||||
|
||||
if (! empty($variableValuesResult['errors'])) {
|
||||
if (! empty($errors)) {
|
||||
throw new Error(implode(
|
||||
"\n\n",
|
||||
array_map(
|
||||
static function ($error) {
|
||||
return $error->getMessage();
|
||||
},
|
||||
$variableValuesResult['errors']
|
||||
$errors
|
||||
)
|
||||
));
|
||||
}
|
||||
$variableValues = $variableValuesResult['coerced'];
|
||||
|
||||
$args = Values::getArgumentValues($fieldDef, $node, $variableValues);
|
||||
}
|
||||
|
@ -13,7 +13,9 @@ use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Utils\Utils;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function json_encode;
|
||||
|
||||
class DeferredFieldsTest extends TestCase
|
||||
{
|
||||
@ -27,7 +29,7 @@ class DeferredFieldsTest extends TestCase
|
||||
private $categoryType;
|
||||
|
||||
/** @var */
|
||||
private $path;
|
||||
private $paths;
|
||||
|
||||
/** @var mixed[][] */
|
||||
private $storyDataSource;
|
||||
@ -68,7 +70,7 @@ class DeferredFieldsTest extends TestCase
|
||||
['id' => 3, 'name' => 'Category #3', 'topStoryId' => 9],
|
||||
];
|
||||
|
||||
$this->path = [];
|
||||
$this->paths = [];
|
||||
$this->userType = new ObjectType([
|
||||
'name' => 'User',
|
||||
'fields' => function () {
|
||||
@ -76,7 +78,7 @@ class DeferredFieldsTest extends TestCase
|
||||
'name' => [
|
||||
'type' => Type::string(),
|
||||
'resolve' => function ($user, $args, $context, ResolveInfo $info) {
|
||||
$this->path[] = $info->path;
|
||||
$this->paths[] = $info->path;
|
||||
|
||||
return $user['name'];
|
||||
},
|
||||
@ -84,10 +86,10 @@ class DeferredFieldsTest extends TestCase
|
||||
'bestFriend' => [
|
||||
'type' => $this->userType,
|
||||
'resolve' => function ($user, $args, $context, ResolveInfo $info) {
|
||||
$this->path[] = $info->path;
|
||||
$this->paths[] = $info->path;
|
||||
|
||||
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(
|
||||
$this->userDataSource,
|
||||
@ -108,7 +110,7 @@ class DeferredFieldsTest extends TestCase
|
||||
'title' => [
|
||||
'type' => Type::string(),
|
||||
'resolve' => function ($entry, $args, $context, ResolveInfo $info) {
|
||||
$this->path[] = $info->path;
|
||||
$this->paths[] = $info->path;
|
||||
|
||||
return $entry['title'];
|
||||
},
|
||||
@ -116,10 +118,10 @@ class DeferredFieldsTest extends TestCase
|
||||
'author' => [
|
||||
'type' => $this->userType,
|
||||
'resolve' => function ($story, $args, $context, ResolveInfo $info) {
|
||||
$this->path[] = $info->path;
|
||||
$this->paths[] = $info->path;
|
||||
|
||||
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(
|
||||
$this->userDataSource,
|
||||
@ -139,7 +141,7 @@ class DeferredFieldsTest extends TestCase
|
||||
'name' => [
|
||||
'type' => Type::string(),
|
||||
'resolve' => function ($category, $args, $context, ResolveInfo $info) {
|
||||
$this->path[] = $info->path;
|
||||
$this->paths[] = $info->path;
|
||||
|
||||
return $category['name'];
|
||||
},
|
||||
@ -148,7 +150,7 @@ class DeferredFieldsTest extends TestCase
|
||||
'stories' => [
|
||||
'type' => Type::listOf($this->storyType),
|
||||
'resolve' => function ($category, $args, $context, ResolveInfo $info) {
|
||||
$this->path[] = $info->path;
|
||||
$this->paths[] = $info->path;
|
||||
|
||||
return Utils::filter(
|
||||
$this->storyDataSource,
|
||||
@ -161,10 +163,10 @@ class DeferredFieldsTest extends TestCase
|
||||
'topStory' => [
|
||||
'type' => $this->storyType,
|
||||
'resolve' => function ($category, $args, $context, ResolveInfo $info) {
|
||||
$this->path[] = $info->path;
|
||||
$this->paths[] = $info->path;
|
||||
|
||||
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(
|
||||
$this->storyDataSource,
|
||||
@ -184,7 +186,7 @@ class DeferredFieldsTest extends TestCase
|
||||
'topStories' => [
|
||||
'type' => Type::listOf($this->storyType),
|
||||
'resolve' => function ($val, $args, $context, ResolveInfo $info) {
|
||||
$this->path[] = $info->path;
|
||||
$this->paths[] = $info->path;
|
||||
|
||||
return Utils::filter(
|
||||
$this->storyDataSource,
|
||||
@ -197,7 +199,7 @@ class DeferredFieldsTest extends TestCase
|
||||
'featuredCategory' => [
|
||||
'type' => $this->categoryType,
|
||||
'resolve' => function ($val, $args, $context, ResolveInfo $info) {
|
||||
$this->path[] = $info->path;
|
||||
$this->paths[] = $info->path;
|
||||
|
||||
return $this->categoryDataSource[0];
|
||||
},
|
||||
@ -205,7 +207,7 @@ class DeferredFieldsTest extends TestCase
|
||||
'categories' => [
|
||||
'type' => Type::listOf($this->categoryType),
|
||||
'resolve' => function ($val, $args, $context, ResolveInfo $info) {
|
||||
$this->path[] = $info->path;
|
||||
$this->paths[] = $info->path;
|
||||
|
||||
return $this->categoryDataSource;
|
||||
},
|
||||
@ -264,7 +266,7 @@ class DeferredFieldsTest extends TestCase
|
||||
$result = Executor::execute($schema, $query);
|
||||
self::assertEquals($expected, $result->toArray());
|
||||
|
||||
$expectedPath = [
|
||||
$expectedPaths = [
|
||||
['topStories'],
|
||||
['topStories', 0, 'title'],
|
||||
['topStories', 0, 'author'],
|
||||
@ -305,7 +307,10 @@ class DeferredFieldsTest extends TestCase
|
||||
['featuredCategory', 'stories', 2, '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
|
||||
@ -349,7 +354,7 @@ class DeferredFieldsTest extends TestCase
|
||||
$result = Executor::execute($schema, $query);
|
||||
self::assertEquals($expected, $result->toArray());
|
||||
|
||||
$expectedPath = [
|
||||
$expectedPaths = [
|
||||
['categories'],
|
||||
['categories', 0, 'name'],
|
||||
['categories', 0, 'topStory'],
|
||||
@ -382,7 +387,10 @@ class DeferredFieldsTest extends TestCase
|
||||
['categories', 1, '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
|
||||
@ -394,7 +402,7 @@ class DeferredFieldsTest extends TestCase
|
||||
'sync' => [
|
||||
'type' => Type::string(),
|
||||
'resolve' => function ($v, $a, $c, ResolveInfo $info) {
|
||||
$this->path[] = $info->path;
|
||||
$this->paths[] = $info->path;
|
||||
|
||||
return 'sync';
|
||||
},
|
||||
@ -402,10 +410,10 @@ class DeferredFieldsTest extends TestCase
|
||||
'deferred' => [
|
||||
'type' => Type::string(),
|
||||
'resolve' => function ($v, $a, $c, ResolveInfo $info) {
|
||||
$this->path[] = $info->path;
|
||||
$this->paths[] = $info->path;
|
||||
|
||||
return new Deferred(function () use ($info) {
|
||||
$this->path[] = ['!dfd for: ', $info->path];
|
||||
$this->paths[] = ['!dfd for: ', $info->path];
|
||||
|
||||
return 'deferred';
|
||||
});
|
||||
@ -414,7 +422,7 @@ class DeferredFieldsTest extends TestCase
|
||||
'nest' => [
|
||||
'type' => $complexType,
|
||||
'resolve' => function ($v, $a, $c, ResolveInfo $info) {
|
||||
$this->path[] = $info->path;
|
||||
$this->paths[] = $info->path;
|
||||
|
||||
return [];
|
||||
},
|
||||
@ -422,10 +430,10 @@ class DeferredFieldsTest extends TestCase
|
||||
'deferredNest' => [
|
||||
'type' => $complexType,
|
||||
'resolve' => function ($v, $a, $c, ResolveInfo $info) {
|
||||
$this->path[] = $info->path;
|
||||
$this->paths[] = $info->path;
|
||||
|
||||
return new Deferred(function () use ($info) {
|
||||
$this->path[] = ['!dfd nest for: ', $info->path];
|
||||
$this->paths[] = ['!dfd nest for: ', $info->path];
|
||||
|
||||
return [];
|
||||
});
|
||||
@ -497,7 +505,7 @@ class DeferredFieldsTest extends TestCase
|
||||
|
||||
self::assertEquals($expected, $result->toArray());
|
||||
|
||||
$expectedPath = [
|
||||
$expectedPaths = [
|
||||
['nest'],
|
||||
['nest', 'sync'],
|
||||
['nest', 'deferred'],
|
||||
@ -531,6 +539,9 @@ class DeferredFieldsTest extends TestCase
|
||||
['!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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -866,7 +866,7 @@ class ExecutorTest extends TestCase
|
||||
...Frag
|
||||
}
|
||||
|
||||
fragment Frag on DataType {
|
||||
fragment Frag on Type {
|
||||
a,
|
||||
...Frag
|
||||
}
|
||||
|
@ -14,7 +14,10 @@ use GraphQL\Language\SourceLocation;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Schema;
|
||||
use PHPUnit\Framework\ExpectationFailedException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use function count;
|
||||
use function json_encode;
|
||||
|
||||
class NonNullTest extends TestCase
|
||||
{
|
||||
@ -370,10 +373,24 @@ class NonNullTest extends TestCase
|
||||
],
|
||||
];
|
||||
|
||||
self::assertArraySubset(
|
||||
$expected,
|
||||
Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q')->toArray()
|
||||
);
|
||||
$result = 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
|
||||
|
@ -25,7 +25,7 @@ class ValuesTest extends TestCase
|
||||
{
|
||||
$this->expectInputVariablesMatchOutputVariables(['idInput' => '123456789']);
|
||||
self::assertEquals(
|
||||
['errors' => [], 'coerced' => ['idInput' => '123456789']],
|
||||
[null, ['idInput' => '123456789']],
|
||||
$this->runTestCase(['idInput' => 123456789]),
|
||||
'Integer ID was not converted to string'
|
||||
);
|
||||
@ -35,7 +35,7 @@ class ValuesTest extends TestCase
|
||||
{
|
||||
self::assertEquals(
|
||||
$variables,
|
||||
$this->runTestCase($variables)['coerced'],
|
||||
$this->runTestCase($variables)[1],
|
||||
'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
|
||||
{
|
||||
$result = $this->runTestCase($variables);
|
||||
self::assertGreaterThan(0, count($result['errors']));
|
||||
self::assertNotNull($result[0]);
|
||||
self::assertGreaterThan(0, count($result[0]));
|
||||
}
|
||||
|
||||
public function testFloatForIDVariableThrowsError() : void
|
||||
|
380
tests/Experimental/Executor/CollectorTest.php
Normal file
380
tests/Experimental/Executor/CollectorTest.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1 @@
|
||||
[]
|
@ -0,0 +1 @@
|
||||
[]
|
@ -0,0 +1 @@
|
||||
[]
|
@ -0,0 +1 @@
|
||||
[]
|
@ -0,0 +1 @@
|
||||
[]
|
@ -0,0 +1 @@
|
||||
[]
|
@ -0,0 +1 @@
|
||||
[]
|
@ -0,0 +1 @@
|
||||
[]
|
@ -0,0 +1 @@
|
||||
[]
|
15
tests/bootstrap.php
Normal file
15
tests/bootstrap.php
Normal 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']);
|
||||
}
|
Loading…
Reference in New Issue
Block a user