mirror of
https://github.com/retailcrm/graphql-php.git
synced 2024-11-25 14:26:08 +03:00
commit
29eba82093
239
src/Type/Definition/QueryPlan.php
Normal file
239
src/Type/Definition/QueryPlan.php
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace GraphQL\Type\Definition;
|
||||||
|
|
||||||
|
use GraphQL\Error\Error;
|
||||||
|
use GraphQL\Executor\Values;
|
||||||
|
use GraphQL\Language\AST\FieldNode;
|
||||||
|
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||||
|
use GraphQL\Language\AST\FragmentSpreadNode;
|
||||||
|
use GraphQL\Language\AST\InlineFragmentNode;
|
||||||
|
use GraphQL\Language\AST\SelectionSetNode;
|
||||||
|
use GraphQL\Type\Schema;
|
||||||
|
use function array_filter;
|
||||||
|
use function array_key_exists;
|
||||||
|
use function array_keys;
|
||||||
|
use function array_merge;
|
||||||
|
use function array_merge_recursive;
|
||||||
|
use function array_unique;
|
||||||
|
use function array_values;
|
||||||
|
use function count;
|
||||||
|
use function in_array;
|
||||||
|
use function is_array;
|
||||||
|
use function is_numeric;
|
||||||
|
|
||||||
|
class QueryPlan
|
||||||
|
{
|
||||||
|
/** @var string[][] */
|
||||||
|
private $types = [];
|
||||||
|
|
||||||
|
/** @var Schema */
|
||||||
|
private $schema;
|
||||||
|
|
||||||
|
/** @var mixed[] */
|
||||||
|
private $queryPlan = [];
|
||||||
|
|
||||||
|
/** @var mixed[] */
|
||||||
|
private $variableValues;
|
||||||
|
|
||||||
|
/** @var FragmentDefinitionNode[] */
|
||||||
|
private $fragments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param FieldNode[] $fieldNodes
|
||||||
|
* @param mixed[] $variableValues
|
||||||
|
* @param FragmentDefinitionNode[] $fragments
|
||||||
|
*/
|
||||||
|
public function __construct(ObjectType $parentType, Schema $schema, iterable $fieldNodes, array $variableValues, array $fragments)
|
||||||
|
{
|
||||||
|
$this->schema = $schema;
|
||||||
|
$this->variableValues = $variableValues;
|
||||||
|
$this->fragments = $fragments;
|
||||||
|
$this->analyzeQueryPlan($parentType, $fieldNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed[]
|
||||||
|
*/
|
||||||
|
public function queryPlan() : array
|
||||||
|
{
|
||||||
|
return $this->queryPlan;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getReferencedTypes() : array
|
||||||
|
{
|
||||||
|
return array_keys($this->types);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasType(string $type) : bool
|
||||||
|
{
|
||||||
|
return count(array_filter($this->getReferencedTypes(), static function (string $referencedType) use ($type) {
|
||||||
|
return $type === $referencedType;
|
||||||
|
})) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getReferencedFields() : array
|
||||||
|
{
|
||||||
|
return array_values(array_unique(array_merge(...array_values($this->types))));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasField(string $field) : bool
|
||||||
|
{
|
||||||
|
return count(array_filter($this->getReferencedFields(), static function (string $referencedField) use ($field) {
|
||||||
|
return $field === $referencedField;
|
||||||
|
})) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function subFields(string $typename) : array
|
||||||
|
{
|
||||||
|
if (! array_key_exists($typename, $this->types)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->types[$typename];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param FieldNode[] $fieldNodes
|
||||||
|
*/
|
||||||
|
private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes) : void
|
||||||
|
{
|
||||||
|
$queryPlan = [];
|
||||||
|
/** @var FieldNode $fieldNode */
|
||||||
|
foreach ($fieldNodes as $fieldNode) {
|
||||||
|
if (! $fieldNode->selectionSet) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = $parentType->getField($fieldNode->name->value)->getType();
|
||||||
|
if ($type instanceof WrappingType) {
|
||||||
|
$type = $type->getWrappedType();
|
||||||
|
}
|
||||||
|
|
||||||
|
$subfields = $this->analyzeSelectionSet($fieldNode->selectionSet, $type);
|
||||||
|
|
||||||
|
$this->types[$type->name] = array_unique(array_merge(
|
||||||
|
array_key_exists($type->name, $this->types) ? $this->types[$type->name] : [],
|
||||||
|
array_keys($subfields)
|
||||||
|
));
|
||||||
|
|
||||||
|
$queryPlan = array_merge_recursive(
|
||||||
|
$queryPlan,
|
||||||
|
$subfields
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->queryPlan = $queryPlan;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed[]
|
||||||
|
*
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
private function analyzeSelectionSet(SelectionSetNode $selectionSet, ObjectType $parentType) : array
|
||||||
|
{
|
||||||
|
$fields = [];
|
||||||
|
foreach ($selectionSet->selections as $selectionNode) {
|
||||||
|
if ($selectionNode instanceof FieldNode) {
|
||||||
|
$fieldName = $selectionNode->name->value;
|
||||||
|
$type = $parentType->getField($fieldName);
|
||||||
|
$selectionType = $type->getType();
|
||||||
|
|
||||||
|
$subfields = [];
|
||||||
|
if ($selectionNode->selectionSet) {
|
||||||
|
$subfields = $this->analyzeSubFields($selectionType, $selectionNode->selectionSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
$fields[$fieldName] = [
|
||||||
|
'type' => $selectionType,
|
||||||
|
'fields' => $subfields ?? [],
|
||||||
|
'args' => Values::getArgumentValues($type, $selectionNode, $this->variableValues),
|
||||||
|
];
|
||||||
|
} elseif ($selectionNode instanceof FragmentSpreadNode) {
|
||||||
|
$spreadName = $selectionNode->name->value;
|
||||||
|
if (isset($this->fragments[$spreadName])) {
|
||||||
|
$fragment = $this->fragments[$spreadName];
|
||||||
|
$type = $this->schema->getType($fragment->typeCondition->name->value);
|
||||||
|
$subfields = $this->analyzeSubFields($type, $fragment->selectionSet);
|
||||||
|
|
||||||
|
$fields = $this->arrayMergeDeep(
|
||||||
|
$subfields,
|
||||||
|
$fields
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} elseif ($selectionNode instanceof InlineFragmentNode) {
|
||||||
|
$type = $this->schema->getType($selectionNode->typeCondition->name->value);
|
||||||
|
$subfields = $this->analyzeSubFields($type, $selectionNode->selectionSet);
|
||||||
|
|
||||||
|
$fields = $this->arrayMergeDeep(
|
||||||
|
$subfields,
|
||||||
|
$fields
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed[]
|
||||||
|
*/
|
||||||
|
private function analyzeSubFields(Type $type, SelectionSetNode $selectionSet) : array
|
||||||
|
{
|
||||||
|
if ($type instanceof WrappingType) {
|
||||||
|
$type = $type->getWrappedType();
|
||||||
|
}
|
||||||
|
|
||||||
|
$subfields = [];
|
||||||
|
if ($type instanceof ObjectType) {
|
||||||
|
$subfields = $this->analyzeSelectionSet($selectionSet, $type);
|
||||||
|
$this->types[$type->name] = array_unique(array_merge(
|
||||||
|
array_key_exists($type->name, $this->types) ? $this->types[$type->name] : [],
|
||||||
|
array_keys($subfields)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $subfields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* similar to array_merge_recursive this merges nested arrays, but handles non array values differently
|
||||||
|
* while array_merge_recursive tries to merge non array values, in this implementation they will be overwritten
|
||||||
|
*
|
||||||
|
* @see https://stackoverflow.com/a/25712428
|
||||||
|
*
|
||||||
|
* @param mixed[] $array1
|
||||||
|
* @param mixed[] $array2
|
||||||
|
*
|
||||||
|
* @return mixed[]
|
||||||
|
*/
|
||||||
|
private function arrayMergeDeep(array $array1, array $array2) : array
|
||||||
|
{
|
||||||
|
$merged = $array1;
|
||||||
|
|
||||||
|
foreach ($array2 as $key => & $value) {
|
||||||
|
if (is_numeric($key)) {
|
||||||
|
if (! in_array($value, $merged, true)) {
|
||||||
|
$merged[] = $value;
|
||||||
|
}
|
||||||
|
} elseif (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
|
||||||
|
$merged[$key] = $this->arrayMergeDeep($merged[$key], $value);
|
||||||
|
} else {
|
||||||
|
$merged[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $merged;
|
||||||
|
}
|
||||||
|
}
|
@ -63,7 +63,7 @@ class ResolveInfo
|
|||||||
* Instance of a schema used for execution
|
* Instance of a schema used for execution
|
||||||
*
|
*
|
||||||
* @api
|
* @api
|
||||||
* @var Schema|null
|
* @var Schema
|
||||||
*/
|
*/
|
||||||
public $schema;
|
public $schema;
|
||||||
|
|
||||||
@ -99,6 +99,9 @@ class ResolveInfo
|
|||||||
*/
|
*/
|
||||||
public $variableValues = [];
|
public $variableValues = [];
|
||||||
|
|
||||||
|
/** @var QueryPlan */
|
||||||
|
private $queryPlan;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param FieldNode[] $fieldNodes
|
* @param FieldNode[] $fieldNodes
|
||||||
* @param ScalarType|ObjectType|InterfaceType|UnionType|EnumType|ListOfType|NonNull $returnType
|
* @param ScalarType|ObjectType|InterfaceType|UnionType|EnumType|ListOfType|NonNull $returnType
|
||||||
@ -109,7 +112,7 @@ class ResolveInfo
|
|||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
string $fieldName,
|
string $fieldName,
|
||||||
$fieldNodes,
|
iterable $fieldNodes,
|
||||||
$returnType,
|
$returnType,
|
||||||
ObjectType $parentType,
|
ObjectType $parentType,
|
||||||
array $path,
|
array $path,
|
||||||
@ -179,6 +182,22 @@ class ResolveInfo
|
|||||||
|
|
||||||
return $fields;
|
return $fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function lookAhead() : QueryPlan
|
||||||
|
{
|
||||||
|
if ($this->queryPlan === null) {
|
||||||
|
$this->queryPlan = new QueryPlan(
|
||||||
|
$this->parentType,
|
||||||
|
$this->schema,
|
||||||
|
$this->fieldNodes,
|
||||||
|
$this->variableValues,
|
||||||
|
$this->fragments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->queryPlan;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return bool[]
|
* @return bool[]
|
||||||
*/
|
*/
|
||||||
|
@ -284,22 +284,6 @@ class ExecutorTest extends TestCase
|
|||||||
|
|
||||||
Executor::execute($schema, $ast, $rootValue, null, ['var' => '123']);
|
Executor::execute($schema, $ast, $rootValue, null, ['var' => '123']);
|
||||||
|
|
||||||
self::assertEquals(
|
|
||||||
[
|
|
||||||
'fieldName',
|
|
||||||
'fieldNodes',
|
|
||||||
'returnType',
|
|
||||||
'parentType',
|
|
||||||
'path',
|
|
||||||
'schema',
|
|
||||||
'fragments',
|
|
||||||
'rootValue',
|
|
||||||
'operation',
|
|
||||||
'variableValues',
|
|
||||||
],
|
|
||||||
array_keys((array) $info)
|
|
||||||
);
|
|
||||||
|
|
||||||
self::assertEquals('test', $info->fieldName);
|
self::assertEquals('test', $info->fieldName);
|
||||||
self::assertEquals(1, count($info->fieldNodes));
|
self::assertEquals(1, count($info->fieldNodes));
|
||||||
self::assertSame($ast->definitions[0]->selectionSet->selections[0], $info->fieldNodes[0]);
|
self::assertSame($ast->definitions[0]->selectionSet->selections[0], $info->fieldNodes[0]);
|
||||||
|
588
tests/Type/QueryPlanTest.php
Normal file
588
tests/Type/QueryPlanTest.php
Normal file
@ -0,0 +1,588 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace GraphQL\Tests\Type;
|
||||||
|
|
||||||
|
use GraphQL\GraphQL;
|
||||||
|
use GraphQL\Type\Definition\ObjectType;
|
||||||
|
use GraphQL\Type\Definition\QueryPlan;
|
||||||
|
use GraphQL\Type\Definition\ResolveInfo;
|
||||||
|
use GraphQL\Type\Definition\Type;
|
||||||
|
use GraphQL\Type\Schema;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class QueryPlanTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testQueryPlan() : void
|
||||||
|
{
|
||||||
|
$image = new ObjectType([
|
||||||
|
'name' => 'Image',
|
||||||
|
'fields' => [
|
||||||
|
'url' => ['type' => Type::string()],
|
||||||
|
'width' => ['type' => Type::int()],
|
||||||
|
'height' => ['type' => Type::int()],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$article = null;
|
||||||
|
|
||||||
|
$author = new ObjectType([
|
||||||
|
'name' => 'Author',
|
||||||
|
'fields' => static function () use ($image, &$article) {
|
||||||
|
return [
|
||||||
|
'id' => ['type' => Type::string()],
|
||||||
|
'name' => ['type' => Type::string()],
|
||||||
|
'pic' => [
|
||||||
|
'type' => $image,
|
||||||
|
'args' => [
|
||||||
|
'width' => ['type' => Type::int()],
|
||||||
|
'height' => ['type' => Type::int()],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'recentArticle' => ['type' => $article],
|
||||||
|
];
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
$reply = new ObjectType([
|
||||||
|
'name' => 'Reply',
|
||||||
|
'fields' => [
|
||||||
|
'author' => ['type' => $author],
|
||||||
|
'body' => ['type' => Type::string()],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$article = new ObjectType([
|
||||||
|
'name' => 'Article',
|
||||||
|
'fields' => [
|
||||||
|
'id' => ['type' => Type::string()],
|
||||||
|
'isPublished' => ['type' => Type::boolean()],
|
||||||
|
'author' => ['type' => $author],
|
||||||
|
'title' => ['type' => Type::string()],
|
||||||
|
'body' => ['type' => Type::string()],
|
||||||
|
'image' => ['type' => $image],
|
||||||
|
'replies' => ['type' => Type::listOf($reply)],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$doc = '
|
||||||
|
query Test {
|
||||||
|
article {
|
||||||
|
author {
|
||||||
|
name
|
||||||
|
pic(width: 100, height: 200) {
|
||||||
|
url
|
||||||
|
width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
image {
|
||||||
|
width
|
||||||
|
height
|
||||||
|
...MyImage
|
||||||
|
}
|
||||||
|
replies {
|
||||||
|
body
|
||||||
|
author {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
pic {
|
||||||
|
url
|
||||||
|
width
|
||||||
|
... on Image {
|
||||||
|
height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recentArticle {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fragment MyImage on Image {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
';
|
||||||
|
$expectedQueryPlan = [
|
||||||
|
'author' => [
|
||||||
|
'type' => $author,
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [
|
||||||
|
'name' => [
|
||||||
|
'type' => Type::string(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
'pic' => [
|
||||||
|
'type' => $image,
|
||||||
|
'args' => [
|
||||||
|
'width' => 100,
|
||||||
|
'height' => 200,
|
||||||
|
],
|
||||||
|
'fields' => [
|
||||||
|
'url' => [
|
||||||
|
'type' => Type::string(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
'width' => [
|
||||||
|
'type' => Type::int(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'image' => [
|
||||||
|
'type' => $image,
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [
|
||||||
|
'url' => [
|
||||||
|
'type' => Type::string(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
'width' => [
|
||||||
|
'type' => Type::int(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
'height' => [
|
||||||
|
'type' => Type::int(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'replies' => [
|
||||||
|
'type' => Type::listOf($reply),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [
|
||||||
|
'body' => [
|
||||||
|
'type' => Type::string(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
'author' => [
|
||||||
|
'type' => $author,
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [
|
||||||
|
'id' => [
|
||||||
|
'type' => Type::string(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
'name' => [
|
||||||
|
'type' => Type::string(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
'pic' => [
|
||||||
|
'type' => $image,
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [
|
||||||
|
'url' => [
|
||||||
|
'type' => Type::string(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
'width' => [
|
||||||
|
'type' => Type::int(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
'height' => [
|
||||||
|
'type' => Type::int(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'recentArticle' => [
|
||||||
|
'type' => $article,
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [
|
||||||
|
'id' => [
|
||||||
|
'type' => Type::string(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
'title' => [
|
||||||
|
'type' => Type::string(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
'body' => [
|
||||||
|
'type' => Type::string(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$expectedReferencedTypes = [
|
||||||
|
'Image',
|
||||||
|
'Author',
|
||||||
|
'Article',
|
||||||
|
'Reply',
|
||||||
|
];
|
||||||
|
|
||||||
|
$expectedReferencedFields = [
|
||||||
|
'url',
|
||||||
|
'width',
|
||||||
|
'height',
|
||||||
|
'name',
|
||||||
|
'pic',
|
||||||
|
'id',
|
||||||
|
'recentArticle',
|
||||||
|
'title',
|
||||||
|
'body',
|
||||||
|
'author',
|
||||||
|
'image',
|
||||||
|
'replies',
|
||||||
|
];
|
||||||
|
|
||||||
|
$hasCalled = false;
|
||||||
|
/** @var QueryPlan $queryPlan */
|
||||||
|
$queryPlan = null;
|
||||||
|
|
||||||
|
$blogQuery = new ObjectType([
|
||||||
|
'name' => 'Query',
|
||||||
|
'fields' => [
|
||||||
|
'article' => [
|
||||||
|
'type' => $article,
|
||||||
|
'resolve' => static function (
|
||||||
|
$value,
|
||||||
|
$args,
|
||||||
|
$context,
|
||||||
|
ResolveInfo $info
|
||||||
|
) use (
|
||||||
|
&$hasCalled,
|
||||||
|
&$queryPlan
|
||||||
|
) {
|
||||||
|
$hasCalled = true;
|
||||||
|
$queryPlan = $info->lookAhead();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$schema = new Schema(['query' => $blogQuery]);
|
||||||
|
$result = GraphQL::executeQuery($schema, $doc)->toArray();
|
||||||
|
|
||||||
|
self::assertTrue($hasCalled);
|
||||||
|
self::assertEquals(['data' => ['article' => null]], $result);
|
||||||
|
self::assertEquals($expectedQueryPlan, $queryPlan->queryPlan());
|
||||||
|
self::assertEquals($expectedReferencedTypes, $queryPlan->getReferencedTypes());
|
||||||
|
self::assertEquals($expectedReferencedFields, $queryPlan->getReferencedFields());
|
||||||
|
self::assertEquals(['url', 'width', 'height'], $queryPlan->subFields('Image'));
|
||||||
|
|
||||||
|
self::assertTrue($queryPlan->hasField('url'));
|
||||||
|
self::assertFalse($queryPlan->hasField('test'));
|
||||||
|
|
||||||
|
self::assertTrue($queryPlan->hasType('Image'));
|
||||||
|
self::assertFalse($queryPlan->hasType('Test'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMergedFragmentsQueryPlan() : void
|
||||||
|
{
|
||||||
|
$image = new ObjectType([
|
||||||
|
'name' => 'Image',
|
||||||
|
'fields' => [
|
||||||
|
'url' => ['type' => Type::string()],
|
||||||
|
'width' => ['type' => Type::int()],
|
||||||
|
'height' => ['type' => Type::int()],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$article = null;
|
||||||
|
|
||||||
|
$author = new ObjectType([
|
||||||
|
'name' => 'Author',
|
||||||
|
'fields' => static function () use ($image, &$article) {
|
||||||
|
return [
|
||||||
|
'id' => ['type' => Type::string()],
|
||||||
|
'name' => ['type' => Type::string()],
|
||||||
|
'pic' => [
|
||||||
|
'type' => $image,
|
||||||
|
'args' => [
|
||||||
|
'width' => ['type' => Type::int()],
|
||||||
|
'height' => ['type' => Type::int()],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'recentArticle' => ['type' => $article],
|
||||||
|
];
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
$reply = new ObjectType([
|
||||||
|
'name' => 'Reply',
|
||||||
|
'fields' => [
|
||||||
|
'author' => ['type' => $author],
|
||||||
|
'body' => ['type' => Type::string()],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$article = new ObjectType([
|
||||||
|
'name' => 'Article',
|
||||||
|
'fields' => [
|
||||||
|
'id' => ['type' => Type::string()],
|
||||||
|
'isPublished' => ['type' => Type::boolean()],
|
||||||
|
'author' => ['type' => $author],
|
||||||
|
'title' => ['type' => Type::string()],
|
||||||
|
'body' => ['type' => Type::string()],
|
||||||
|
'image' => ['type' => $image],
|
||||||
|
'replies' => ['type' => Type::listOf($reply)],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$doc = '
|
||||||
|
query Test {
|
||||||
|
article {
|
||||||
|
author {
|
||||||
|
name
|
||||||
|
pic(width: 100, height: 200) {
|
||||||
|
url
|
||||||
|
width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
image {
|
||||||
|
width
|
||||||
|
height
|
||||||
|
...MyImage
|
||||||
|
}
|
||||||
|
...Replies01
|
||||||
|
...Replies02
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fragment MyImage on Image {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment Replies01 on Article {
|
||||||
|
_replies012: replies {
|
||||||
|
body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fragment Replies02 on Article {
|
||||||
|
_replies012: replies {
|
||||||
|
author {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
pic {
|
||||||
|
url
|
||||||
|
width
|
||||||
|
... on Image {
|
||||||
|
height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recentArticle {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
';
|
||||||
|
|
||||||
|
$expectedQueryPlan = [
|
||||||
|
'author' => [
|
||||||
|
'type' => $author,
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [
|
||||||
|
'name' => [
|
||||||
|
'type' => Type::string(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
'pic' => [
|
||||||
|
'type' => $image,
|
||||||
|
'args' => [
|
||||||
|
'width' => 100,
|
||||||
|
'height' => 200,
|
||||||
|
],
|
||||||
|
'fields' => [
|
||||||
|
'url' => [
|
||||||
|
'type' => Type::string(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
'width' => [
|
||||||
|
'type' => Type::int(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'image' => [
|
||||||
|
'type' => $image,
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [
|
||||||
|
'url' => [
|
||||||
|
'type' => Type::string(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
'width' => [
|
||||||
|
'type' => Type::int(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
'height' => [
|
||||||
|
'type' => Type::int(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'replies' => [
|
||||||
|
'type' => Type::listOf($reply),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [
|
||||||
|
'body' => [
|
||||||
|
'type' => Type::string(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
'author' => [
|
||||||
|
'type' => $author,
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [
|
||||||
|
'id' => [
|
||||||
|
'type' => Type::string(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
'name' => [
|
||||||
|
'type' => Type::string(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
'pic' => [
|
||||||
|
'type' => $image,
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [
|
||||||
|
'url' => [
|
||||||
|
'type' => Type::string(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
'width' => [
|
||||||
|
'type' => Type::int(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
'height' => [
|
||||||
|
'type' => Type::int(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'recentArticle' => [
|
||||||
|
'type' => $article,
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [
|
||||||
|
'id' => [
|
||||||
|
'type' => Type::string(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
'title' => [
|
||||||
|
'type' => Type::string(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
'body' => [
|
||||||
|
'type' => Type::string(),
|
||||||
|
'args' => [],
|
||||||
|
'fields' => [],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$expectedReferencedTypes = [
|
||||||
|
'Image',
|
||||||
|
'Author',
|
||||||
|
'Reply',
|
||||||
|
'Article',
|
||||||
|
];
|
||||||
|
|
||||||
|
$expectedReferencedFields = [
|
||||||
|
'url',
|
||||||
|
'width',
|
||||||
|
'height',
|
||||||
|
'name',
|
||||||
|
'pic',
|
||||||
|
'id',
|
||||||
|
'recentArticle',
|
||||||
|
'body',
|
||||||
|
'author',
|
||||||
|
'replies',
|
||||||
|
'title',
|
||||||
|
'image',
|
||||||
|
];
|
||||||
|
|
||||||
|
$hasCalled = false;
|
||||||
|
/** @var QueryPlan $queryPlan */
|
||||||
|
$queryPlan = null;
|
||||||
|
|
||||||
|
$blogQuery = new ObjectType([
|
||||||
|
'name' => 'Query',
|
||||||
|
'fields' => [
|
||||||
|
'article' => [
|
||||||
|
'type' => $article,
|
||||||
|
'resolve' => static function (
|
||||||
|
$value,
|
||||||
|
$args,
|
||||||
|
$context,
|
||||||
|
ResolveInfo $info
|
||||||
|
) use (
|
||||||
|
&$hasCalled,
|
||||||
|
&$queryPlan
|
||||||
|
) {
|
||||||
|
$hasCalled = true;
|
||||||
|
$queryPlan = $info->lookAhead();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$schema = new Schema(['query' => $blogQuery]);
|
||||||
|
$result = GraphQL::executeQuery($schema, $doc)->toArray();
|
||||||
|
|
||||||
|
self::assertTrue($hasCalled);
|
||||||
|
self::assertEquals(['data' => ['article' => null]], $result);
|
||||||
|
self::assertEquals($expectedQueryPlan, $queryPlan->queryPlan());
|
||||||
|
self::assertEquals($expectedReferencedTypes, $queryPlan->getReferencedTypes());
|
||||||
|
self::assertEquals($expectedReferencedFields, $queryPlan->getReferencedFields());
|
||||||
|
self::assertEquals(['url', 'width', 'height'], $queryPlan->subFields('Image'));
|
||||||
|
|
||||||
|
self::assertTrue($queryPlan->hasField('url'));
|
||||||
|
self::assertFalse($queryPlan->hasField('test'));
|
||||||
|
|
||||||
|
self::assertTrue($queryPlan->hasType('Image'));
|
||||||
|
self::assertFalse($queryPlan->hasType('Test'));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user