From c35379cee88bc785c46e18331a27ad8da8ffb8f7 Mon Sep 17 00:00:00 2001 From: yura3d Date: Fri, 28 Jun 2019 23:32:32 +0300 Subject: [PATCH 01/11] Added inline fragments support to ResolveInfo --- src/Type/Definition/ResolveInfo.php | 5 +---- tests/Type/ResolveInfoTest.php | 8 ++++++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Type/Definition/ResolveInfo.php b/src/Type/Definition/ResolveInfo.php index f2525dea5..2badd3ffd 100644 --- a/src/Type/Definition/ResolveInfo.php +++ b/src/Type/Definition/ResolveInfo.php @@ -220,10 +220,7 @@ private function foldSelectionSet(SelectionSetNode $selectionSet, int $descend) ); } } elseif ($selectionNode instanceof InlineFragmentNode) { - $fields = array_merge_recursive( - $this->foldSelectionSet($selectionNode->selectionSet, $descend), - $fields - ); + $fields['__inlineFragments'][$selectionNode->typeCondition->name->value] = $this->foldSelectionSet($selectionNode->selectionSet, $descend); } } diff --git a/tests/Type/ResolveInfoTest.php b/tests/Type/ResolveInfoTest.php index d228a82b8..446147326 100644 --- a/tests/Type/ResolveInfoTest.php +++ b/tests/Type/ResolveInfoTest.php @@ -131,7 +131,9 @@ public function testFieldSelection() : void 'pic' => [ 'url' => true, 'width' => true, - 'height' => true, + '__inlineFragments' => [ + 'Image' => ['height' => true], + ], ], 'recentArticle' => [ 'id' => true, @@ -304,7 +306,9 @@ public function testMergedFragmentsFieldSelection() : void 'pic' => [ 'url' => true, 'width' => true, - 'height' => true, + '__inlineFragments' => [ + 'Image' => ['height' => true], + ], ], 'recentArticle' => [ 'id' => true, From 628e7eeae0470806b7a16eb3b3093bd6dbab6295 Mon Sep 17 00:00:00 2001 From: yura3d Date: Mon, 1 Jul 2019 23:10:23 +0300 Subject: [PATCH 02/11] Revert "Added inline fragments support to ResolveInfo" This reverts commit c35379cee88bc785c46e18331a27ad8da8ffb8f7. --- src/Type/Definition/ResolveInfo.php | 5 ++++- tests/Type/ResolveInfoTest.php | 8 ++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Type/Definition/ResolveInfo.php b/src/Type/Definition/ResolveInfo.php index 2badd3ffd..f2525dea5 100644 --- a/src/Type/Definition/ResolveInfo.php +++ b/src/Type/Definition/ResolveInfo.php @@ -220,7 +220,10 @@ private function foldSelectionSet(SelectionSetNode $selectionSet, int $descend) ); } } elseif ($selectionNode instanceof InlineFragmentNode) { - $fields['__inlineFragments'][$selectionNode->typeCondition->name->value] = $this->foldSelectionSet($selectionNode->selectionSet, $descend); + $fields = array_merge_recursive( + $this->foldSelectionSet($selectionNode->selectionSet, $descend), + $fields + ); } } diff --git a/tests/Type/ResolveInfoTest.php b/tests/Type/ResolveInfoTest.php index 446147326..d228a82b8 100644 --- a/tests/Type/ResolveInfoTest.php +++ b/tests/Type/ResolveInfoTest.php @@ -131,9 +131,7 @@ public function testFieldSelection() : void 'pic' => [ 'url' => true, 'width' => true, - '__inlineFragments' => [ - 'Image' => ['height' => true], - ], + 'height' => true, ], 'recentArticle' => [ 'id' => true, @@ -306,9 +304,7 @@ public function testMergedFragmentsFieldSelection() : void 'pic' => [ 'url' => true, 'width' => true, - '__inlineFragments' => [ - 'Image' => ['height' => true], - ], + 'height' => true, ], 'recentArticle' => [ 'id' => true, From 15ab709cc66a1019f5160cc5a7ff535c5efb9126 Mon Sep 17 00:00:00 2001 From: yura3d Date: Mon, 1 Jul 2019 23:20:06 +0300 Subject: [PATCH 03/11] Inline fragments and union type support for QueryPlan --- src/Type/Definition/QueryPlan.php | 16 +++++++++---- tests/Type/QueryPlanTest.php | 40 ++++++++++++++++++++----------- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/src/Type/Definition/QueryPlan.php b/src/Type/Definition/QueryPlan.php index 641686d67..232e8ef40 100644 --- a/src/Type/Definition/QueryPlan.php +++ b/src/Type/Definition/QueryPlan.php @@ -23,6 +23,8 @@ use function in_array; use function is_array; use function is_numeric; +use function strlen; +use function substr; class QueryPlan { @@ -125,7 +127,9 @@ private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes) $this->types[$type->name] = array_unique(array_merge( array_key_exists($type->name, $this->types) ? $this->types[$type->name] : [], - array_keys($subfields) + array_filter(array_keys($subfields), static function ($fieldName) { + return substr($fieldName, 0, 2) !== '__'; + }) )); $queryPlan = array_merge_recursive( @@ -177,7 +181,9 @@ private function analyzeSelectionSet(SelectionSetNode $selectionSet, Type $paren } } elseif ($selectionNode instanceof InlineFragmentNode) { $type = $this->schema->getType($selectionNode->typeCondition->name->value); - $subfields = $this->analyzeSubFields($type, $selectionNode->selectionSet); + $subfields = [ + '__inlineFragments' => [$type->name => $this->analyzeSubFields($type, $selectionNode->selectionSet)], + ]; $fields = $this->arrayMergeDeep( $subfields, @@ -199,11 +205,13 @@ private function analyzeSubFields(Type $type, SelectionSetNode $selectionSet) : } $subfields = []; - if ($type instanceof ObjectType) { + if ($type instanceof ObjectType || $type instanceof UnionType) { $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) + array_filter(array_keys($subfields), static function ($fieldName) { + return substr($fieldName, 0, 2) !== '__'; + }) )); } diff --git a/tests/Type/QueryPlanTest.php b/tests/Type/QueryPlanTest.php index 73f8ae7ca..63a8049b7 100644 --- a/tests/Type/QueryPlanTest.php +++ b/tests/Type/QueryPlanTest.php @@ -197,10 +197,14 @@ public function testQueryPlan() : void 'args' => [], 'fields' => [], ], - 'height' => [ - 'type' => Type::int(), - 'args' => [], - 'fields' => [], + '__inlineFragments' => [ + 'Image' => [ + 'height' => [ + 'type' => Type::int(), + 'args' => [], + 'fields' => [], + ], + ], ], ], ], @@ -332,16 +336,20 @@ public function testQueryPlanOnInterface() : void }'; $expectedQueryPlan = [ - 'woofs' => [ - 'type' => Type::boolean(), - 'fields' => [], - 'args' => [], - ], - 'name' => [ + 'name' => [ 'type' => Type::string(), 'args' => [], 'fields' => [], ], + '__inlineFragments' => [ + 'Dog' => [ + 'woofs' => [ + 'type' => Type::boolean(), + 'fields' => [], + 'args' => [], + ], + ], + ], ]; $expectedReferencedTypes = [ @@ -599,10 +607,14 @@ public function testMergedFragmentsQueryPlan() : void 'args' => [], 'fields' => [], ], - 'height' => [ - 'type' => Type::int(), - 'args' => [], - 'fields' => [], + '__inlineFragments' => [ + 'Image' => [ + 'height' => [ + 'type' => Type::int(), + 'args' => [], + 'fields' => [], + ], + ], ], ], ], From 7e63017c4b3d1ec94d6bdf4d651d54e70ec972a3 Mon Sep 17 00:00:00 2001 From: yura3d Date: Sun, 14 Jul 2019 19:13:23 +0300 Subject: [PATCH 04/11] Introduced $options to prevent BC --- src/Type/Definition/QueryPlan.php | 42 ++++++--- src/Type/Definition/ResolveInfo.php | 24 ++--- tests/Type/QueryPlanTest.php | 139 ++++++++++++++++++++++++---- 3 files changed, 159 insertions(+), 46 deletions(-) diff --git a/src/Type/Definition/QueryPlan.php b/src/Type/Definition/QueryPlan.php index 232e8ef40..930bea1cd 100644 --- a/src/Type/Definition/QueryPlan.php +++ b/src/Type/Definition/QueryPlan.php @@ -23,7 +23,6 @@ use function in_array; use function is_array; use function is_numeric; -use function strlen; use function substr; class QueryPlan @@ -43,16 +42,21 @@ class QueryPlan /** @var FragmentDefinitionNode[] */ private $fragments; + /** @var bool */ + private $withMetaFields; + /** * @param FieldNode[] $fieldNodes * @param mixed[] $variableValues * @param FragmentDefinitionNode[] $fragments + * @param string[] $options */ - public function __construct(ObjectType $parentType, Schema $schema, iterable $fieldNodes, array $variableValues, array $fragments) + public function __construct(ObjectType $parentType, Schema $schema, iterable $fieldNodes, array $variableValues, array $fragments, array $options) { $this->schema = $schema; $this->variableValues = $variableValues; $this->fragments = $fragments; + $this->withMetaFields = in_array('with-meta-fields', $options, true); $this->analyzeQueryPlan($parentType, $fieldNodes); } @@ -125,11 +129,15 @@ private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes) $subfields = $this->analyzeSelectionSet($fieldNode->selectionSet, $type); + $subfieldsNames = array_keys($subfields); + if ($this->withMetaFields) { + $subfieldsNames = array_filter($subfieldsNames, static function ($fieldName) { + return substr($fieldName, 0, 2) !== '__'; + }); + } $this->types[$type->name] = array_unique(array_merge( array_key_exists($type->name, $this->types) ? $this->types[$type->name] : [], - array_filter(array_keys($subfields), static function ($fieldName) { - return substr($fieldName, 0, 2) !== '__'; - }) + $subfieldsNames )); $queryPlan = array_merge_recursive( @@ -180,10 +188,14 @@ private function analyzeSelectionSet(SelectionSetNode $selectionSet, Type $paren ); } } elseif ($selectionNode instanceof InlineFragmentNode) { - $type = $this->schema->getType($selectionNode->typeCondition->name->value); - $subfields = [ - '__inlineFragments' => [$type->name => $this->analyzeSubFields($type, $selectionNode->selectionSet)], - ]; + $type = $this->schema->getType($selectionNode->typeCondition->name->value); + if ($this->withMetaFields) { + $subfields = [ + '__inlineFragments' => [$type->name => $this->analyzeSubFields($type, $selectionNode->selectionSet)], + ]; + } else { + $subfields = $this->analyzeSubFields($type, $selectionNode->selectionSet); + } $fields = $this->arrayMergeDeep( $subfields, @@ -206,12 +218,16 @@ private function analyzeSubFields(Type $type, SelectionSetNode $selectionSet) : $subfields = []; if ($type instanceof ObjectType || $type instanceof UnionType) { - $subfields = $this->analyzeSelectionSet($selectionSet, $type); + $subfields = $this->analyzeSelectionSet($selectionSet, $type); + $subfieldsNames = array_keys($subfields); + if ($this->withMetaFields) { + $subfieldsNames = array_filter($subfieldsNames, static function ($fieldName) { + return substr($fieldName, 0, 2) !== '__'; + }); + } $this->types[$type->name] = array_unique(array_merge( array_key_exists($type->name, $this->types) ? $this->types[$type->name] : [], - array_filter(array_keys($subfields), static function ($fieldName) { - return substr($fieldName, 0, 2) !== '__'; - }) + $subfieldsNames )); } diff --git a/src/Type/Definition/ResolveInfo.php b/src/Type/Definition/ResolveInfo.php index 0f453e03b..b185a2932 100644 --- a/src/Type/Definition/ResolveInfo.php +++ b/src/Type/Definition/ResolveInfo.php @@ -100,9 +100,6 @@ class ResolveInfo */ public $variableValues = []; - /** @var QueryPlan */ - private $queryPlan; - /** * @param FieldNode[] $fieldNodes * @param ScalarType|ObjectType|InterfaceType|UnionType|EnumType|ListOfType|NonNull $returnType @@ -187,19 +184,16 @@ public function getFieldSelection($depth = 0) return $fields; } - public function lookAhead() : QueryPlan + public function lookAhead(string ...$options) : QueryPlan { - if ($this->queryPlan === null) { - $this->queryPlan = new QueryPlan( - $this->parentType, - $this->schema, - $this->fieldNodes, - $this->variableValues, - $this->fragments - ); - } - - return $this->queryPlan; + return new QueryPlan( + $this->parentType, + $this->schema, + $this->fieldNodes, + $this->variableValues, + $this->fragments, + $options + ); } /** diff --git a/tests/Type/QueryPlanTest.php b/tests/Type/QueryPlanTest.php index fd8eb0fe4..bb4b90db9 100644 --- a/tests/Type/QueryPlanTest.php +++ b/tests/Type/QueryPlanTest.php @@ -197,14 +197,10 @@ public function testQueryPlan() : void 'args' => [], 'fields' => [], ], - '__inlineFragments' => [ - 'Image' => [ - 'height' => [ - 'type' => Type::int(), - 'args' => [], - 'fields' => [], - ], - ], + 'height' => [ + 'type' => Type::int(), + 'args' => [], + 'fields' => [], ], ], ], @@ -336,7 +332,118 @@ public function testQueryPlanOnInterface() : void }'; $expectedQueryPlan = [ - 'name' => [ + 'woofs' => [ + 'type' => Type::boolean(), + 'fields' => [], + 'args' => [], + ], + 'name' => [ + 'type' => Type::string(), + 'args' => [], + 'fields' => [], + ], + ]; + + $expectedReferencedTypes = [ + 'Dog', + 'Pet', + ]; + + $expectedReferencedFields = [ + 'woofs', + 'name', + ]; + + /** @var QueryPlan $queryPlan */ + $queryPlan = null; + $hasCalled = false; + + $petsQuery = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'pets' => [ + 'type' => Type::listOf($petType), + 'resolve' => static function ( + $value, + $args, + $context, + ResolveInfo $info + ) use ( + &$hasCalled, + &$queryPlan +) { + $hasCalled = true; + $queryPlan = $info->lookAhead(); + + return []; + }, + ], + ], + ]); + + $schema = new Schema([ + 'query' => $petsQuery, + 'types' => [$dogType], + 'typeLoader' => static function ($name) use ($dogType, $petType) { + switch ($name) { + case 'Dog': + return $dogType; + case 'Pet': + return $petType; + } + }, + ]); + GraphQL::executeQuery($schema, $query)->toArray(); + + self::assertTrue($hasCalled); + self::assertEquals($expectedQueryPlan, $queryPlan->queryPlan()); + self::assertEquals($expectedReferencedTypes, $queryPlan->getReferencedTypes()); + self::assertEquals($expectedReferencedFields, $queryPlan->getReferencedFields()); + self::assertEquals(['woofs'], $queryPlan->subFields('Dog')); + + self::assertTrue($queryPlan->hasField('name')); + self::assertFalse($queryPlan->hasField('test')); + + self::assertTrue($queryPlan->hasType('Dog')); + self::assertFalse($queryPlan->hasType('Test')); + } + + public function testQueryPlanOnUnionInInlineFragment() : void + { + $petType = new InterfaceType([ + 'name' => 'Pet', + 'fields' => static function () { + return [ + 'name' => ['type' => Type::string()], + ]; + }, + ]); + + $dogType = new ObjectType([ + 'name' => 'Dog', + 'interfaces' => [$petType], + 'isTypeOf' => static function ($obj) { + return $obj instanceof Dog; + }, + 'fields' => static function () { + return [ + 'name' => ['type' => Type::string()], + 'woofs' => ['type' => Type::boolean()], + ]; + }, + ]); + + $query = 'query Test { + pets { + name + ... on Dog { + woofs + } + } + }'; + + $expectedQueryPlan = [ + 'name' => [ 'type' => Type::string(), 'args' => [], 'fields' => [], @@ -381,7 +488,7 @@ public function testQueryPlanOnInterface() : void &$queryPlan ) { $hasCalled = true; - $queryPlan = $info->lookAhead(); + $queryPlan = $info->lookAhead('with-meta-fields'); return []; }, @@ -607,14 +714,10 @@ public function testMergedFragmentsQueryPlan() : void 'args' => [], 'fields' => [], ], - '__inlineFragments' => [ - 'Image' => [ - 'height' => [ - 'type' => Type::int(), - 'args' => [], - 'fields' => [], - ], - ], + 'height' => [ + 'type' => Type::int(), + 'args' => [], + 'fields' => [], ], ], ], From 13ac891d073555c2d96103dc5a9791cc500b33d5 Mon Sep 17 00:00:00 2001 From: yura3d Date: Mon, 15 Jul 2019 21:03:25 +0300 Subject: [PATCH 05/11] No more underscores for fragments data --- src/Type/Definition/QueryPlan.php | 102 +++++----- tests/Type/QueryPlanTest.php | 320 +++++++++++++++++++++++++++++- 2 files changed, 358 insertions(+), 64 deletions(-) diff --git a/src/Type/Definition/QueryPlan.php b/src/Type/Definition/QueryPlan.php index 930bea1cd..f5bba52f3 100644 --- a/src/Type/Definition/QueryPlan.php +++ b/src/Type/Definition/QueryPlan.php @@ -23,7 +23,6 @@ use function in_array; use function is_array; use function is_numeric; -use function substr; class QueryPlan { @@ -79,7 +78,7 @@ public function getReferencedTypes() : array public function hasType(string $type) : bool { return count(array_filter($this->getReferencedTypes(), static function (string $referencedType) use ($type) { - return $type === $referencedType; + return $type === $referencedType; })) > 0; } @@ -116,6 +115,7 @@ public function subFields(string $typename) : array private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes) : void { $queryPlan = []; + /** @var FieldNode $fieldNode */ foreach ($fieldNodes as $fieldNode) { if (! $fieldNode->selectionSet) { @@ -127,22 +127,16 @@ private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes) $type = $type->getWrappedType(); } - $subfields = $this->analyzeSelectionSet($fieldNode->selectionSet, $type); + $data = $this->analyzeSelectionSet($fieldNode->selectionSet, $type); - $subfieldsNames = array_keys($subfields); - if ($this->withMetaFields) { - $subfieldsNames = array_filter($subfieldsNames, static function ($fieldName) { - return substr($fieldName, 0, 2) !== '__'; - }); - } $this->types[$type->name] = array_unique(array_merge( array_key_exists($type->name, $this->types) ? $this->types[$type->name] : [], - $subfieldsNames + array_keys($this->withMetaFields ? $data['fields'] : $data) )); $queryPlan = array_merge_recursive( $queryPlan, - $subfields + $data ); } @@ -158,51 +152,59 @@ private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes) */ private function analyzeSelectionSet(SelectionSetNode $selectionSet, Type $parentType) : array { - $fields = []; + $fields = []; + $fragments = []; + $inlineFragments = []; + 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); + $fieldData = & $fields[$fieldName]; + $fieldData['type'] = $selectionType; + $fieldData['args'] = Values::getArgumentValues($type, $selectionNode, $this->variableValues); + if ($this->withMetaFields) { + $fieldData += $selectionNode->selectionSet ? $this->analyzeSubFields($selectionType, $selectionNode->selectionSet) : ['fields' => []]; + } else { + $fieldData['fields'] = $selectionNode->selectionSet ? $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 - ); + $fragment = $this->fragments[$spreadName] ?? null; + if ($fragment) { + $type = $this->schema->getType($fragment->typeCondition->name->value); + + if ($this->withMetaFields) { + $fragmentData = &$fragments[$spreadName]; + $fragmentData['type'] = $type; + $fragmentData += $this->analyzeSubFields($type, $fragment->selectionSet); + } else { + $fields = $this->arrayMergeDeep( + $this->analyzeSubFields($type, $fragment->selectionSet), + $fields + ); + } } } elseif ($selectionNode instanceof InlineFragmentNode) { $type = $this->schema->getType($selectionNode->typeCondition->name->value); + if ($this->withMetaFields) { - $subfields = [ - '__inlineFragments' => [$type->name => $this->analyzeSubFields($type, $selectionNode->selectionSet)], - ]; + $inlineFragments[$type->name] = $this->analyzeSubFields($type, $selectionNode->selectionSet); } else { - $subfields = $this->analyzeSubFields($type, $selectionNode->selectionSet); + $fields = $this->arrayMergeDeep( + $this->analyzeSubFields($type, $selectionNode->selectionSet), + $fields + ); } - - $fields = $this->arrayMergeDeep( - $subfields, - $fields - ); } } + unset($fieldData); + + if ($this->withMetaFields) { + return ['fields' => $fields] + array_filter(['fragments' => $fragments, 'inlineFragments' => $inlineFragments]); + } return $fields; } @@ -216,22 +218,14 @@ private function analyzeSubFields(Type $type, SelectionSetNode $selectionSet) : $type = $type->getWrappedType(); } - $subfields = []; - if ($type instanceof ObjectType || $type instanceof UnionType) { - $subfields = $this->analyzeSelectionSet($selectionSet, $type); - $subfieldsNames = array_keys($subfields); - if ($this->withMetaFields) { - $subfieldsNames = array_filter($subfieldsNames, static function ($fieldName) { - return substr($fieldName, 0, 2) !== '__'; - }); - } - $this->types[$type->name] = array_unique(array_merge( - array_key_exists($type->name, $this->types) ? $this->types[$type->name] : [], - $subfieldsNames - )); - } + $data = $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($this->withMetaFields ? $data['fields'] : $data) + )); - return $subfields; + return $data; } /** diff --git a/tests/Type/QueryPlanTest.php b/tests/Type/QueryPlanTest.php index bb4b90db9..2c9aedfe1 100644 --- a/tests/Type/QueryPlanTest.php +++ b/tests/Type/QueryPlanTest.php @@ -297,6 +297,302 @@ public function testQueryPlan() : void self::assertFalse($queryPlan->hasType('Test')); } + public function testQueryPlanWithMetaFields() : 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 = [ + 'fields' => [ + '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' => [ + 'width' => [ + 'type' => Type::int(), + 'args' => [], + 'fields' => [], + ], + 'height' => [ + 'type' => Type::int(), + 'args' => [], + 'fields' => [], + ], + ], + 'fragments' => [ + 'MyImage' => [ + 'type' => $image, + 'fields' => [ + 'url' => [ + 'type' => Type::string(), + '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' => [], + ], + ], + 'inlineFragments' => [ + 'Image' => [ + '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('with-meta-fields'); + + 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 testQueryPlanOnInterface() : void { $petType = new InterfaceType([ @@ -408,7 +704,7 @@ public function testQueryPlanOnInterface() : void self::assertFalse($queryPlan->hasType('Test')); } - public function testQueryPlanOnUnionInInlineFragment() : void + public function testQueryPlanOnInterfaceWithMetaFields() : void { $petType = new InterfaceType([ 'name' => 'Pet', @@ -443,17 +739,21 @@ public function testQueryPlanOnUnionInInlineFragment() : void }'; $expectedQueryPlan = [ - 'name' => [ - 'type' => Type::string(), - 'args' => [], - 'fields' => [], + 'fields' => [ + 'name' => [ + 'type' => Type::string(), + 'args' => [], + 'fields' => [], + ], ], - '__inlineFragments' => [ + 'inlineFragments' => [ 'Dog' => [ - 'woofs' => [ - 'type' => Type::boolean(), - 'fields' => [], - 'args' => [], + 'fields' => [ + 'woofs' => [ + 'type' => Type::boolean(), + 'fields' => [], + 'args' => [], + ], ], ], ], From f3ad01391e2e773b3c1f024d34161713198ad6b2 Mon Sep 17 00:00:00 2001 From: yura3d Date: Mon, 15 Jul 2019 21:32:21 +0300 Subject: [PATCH 06/11] Fixed Scrutinizer --- src/Type/Definition/QueryPlan.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Type/Definition/QueryPlan.php b/src/Type/Definition/QueryPlan.php index f5bba52f3..243612937 100644 --- a/src/Type/Definition/QueryPlan.php +++ b/src/Type/Definition/QueryPlan.php @@ -144,9 +144,9 @@ private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes) } /** - * @return mixed[] + * @param InterfaceType|ObjectType $parentType * - * $parentType InterfaceType|ObjectType. + * @return mixed[] * * @throws Error */ From 2ff1d01609a1c7e69562f650730117499764faf3 Mon Sep 17 00:00:00 2001 From: yura3d Date: Mon, 15 Jul 2019 22:17:27 +0300 Subject: [PATCH 07/11] Fixed Scrutinizer #2 --- src/Type/Definition/QueryPlan.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/Definition/QueryPlan.php b/src/Type/Definition/QueryPlan.php index 243612937..6506eaac3 100644 --- a/src/Type/Definition/QueryPlan.php +++ b/src/Type/Definition/QueryPlan.php @@ -157,7 +157,7 @@ private function analyzeSelectionSet(SelectionSetNode $selectionSet, Type $paren $inlineFragments = []; foreach ($selectionSet->selections as $selectionNode) { - if ($selectionNode instanceof FieldNode) { + if ($selectionNode instanceof FieldNode && ($parentType instanceof InterfaceType || $parentType instanceof ObjectType)) { $fieldName = $selectionNode->name->value; $type = $parentType->getField($fieldName); $selectionType = $type->getType(); From c1795f9b69e50a118a5213ebf493aab972c52deb Mon Sep 17 00:00:00 2001 From: yura3d Date: Mon, 15 Jul 2019 22:51:01 +0300 Subject: [PATCH 08/11] Fixed Scrutinizer #3 --- src/Type/Definition/QueryPlan.php | 77 ++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 26 deletions(-) diff --git a/src/Type/Definition/QueryPlan.php b/src/Type/Definition/QueryPlan.php index 6506eaac3..5cdf0a21a 100644 --- a/src/Type/Definition/QueryPlan.php +++ b/src/Type/Definition/QueryPlan.php @@ -127,7 +127,8 @@ private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes) $type = $type->getWrappedType(); } - $data = $this->analyzeSelectionSet($fieldNode->selectionSet, $type); + $selectionSet = $fieldNode->selectionSet; + $data = $this->withMetaFields ? $this->analyzeSelectionSetWithMetaFields($selectionSet, $type) : $this->analyzeSelectionSet($selectionSet, $type); $this->types[$type->name] = array_unique(array_merge( array_key_exists($type->name, $this->types) ? $this->types[$type->name] : [], @@ -150,7 +151,7 @@ private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes) * * @throws Error */ - private function analyzeSelectionSet(SelectionSetNode $selectionSet, Type $parentType) : array + private function analyzeSelectionSetWithMetaFields(SelectionSetNode $selectionSet, Type $parentType) : array { $fields = []; $fragments = []; @@ -165,47 +166,71 @@ private function analyzeSelectionSet(SelectionSetNode $selectionSet, Type $paren $fieldData = & $fields[$fieldName]; $fieldData['type'] = $selectionType; $fieldData['args'] = Values::getArgumentValues($type, $selectionNode, $this->variableValues); - if ($this->withMetaFields) { - $fieldData += $selectionNode->selectionSet ? $this->analyzeSubFields($selectionType, $selectionNode->selectionSet) : ['fields' => []]; - } else { - $fieldData['fields'] = $selectionNode->selectionSet ? $this->analyzeSubFields($selectionType, $selectionNode->selectionSet) : []; - } + $fieldData += $selectionNode->selectionSet ? $this->analyzeSubFields($selectionType, $selectionNode->selectionSet) : ['fields' => []]; } elseif ($selectionNode instanceof FragmentSpreadNode) { $spreadName = $selectionNode->name->value; $fragment = $this->fragments[$spreadName] ?? null; if ($fragment) { $type = $this->schema->getType($fragment->typeCondition->name->value); - if ($this->withMetaFields) { - $fragmentData = &$fragments[$spreadName]; - $fragmentData['type'] = $type; - $fragmentData += $this->analyzeSubFields($type, $fragment->selectionSet); - } else { - $fields = $this->arrayMergeDeep( - $this->analyzeSubFields($type, $fragment->selectionSet), - $fields - ); - } + $fragmentData = &$fragments[$spreadName]; + $fragmentData['type'] = $type; + $fragmentData += $this->analyzeSubFields($type, $fragment->selectionSet); } } elseif ($selectionNode instanceof InlineFragmentNode) { $type = $this->schema->getType($selectionNode->typeCondition->name->value); - if ($this->withMetaFields) { - $inlineFragments[$type->name] = $this->analyzeSubFields($type, $selectionNode->selectionSet); - } else { + $inlineFragments[$type->name] = $this->analyzeSubFields($type, $selectionNode->selectionSet); + } + } + unset($fieldData, $fragmentData); + + return ['fields' => $fields] + array_filter(['fragments' => $fragments, 'inlineFragments' => $inlineFragments]); + } + + /** + * @param InterfaceType|ObjectType $parentType + * + * @return mixed[] + * + * @throws Error + */ + private function analyzeSelectionSet(SelectionSetNode $selectionSet, Type $parentType) : array + { + $fields = []; + + foreach ($selectionSet->selections as $selectionNode) { + if ($selectionNode instanceof FieldNode && ($parentType instanceof InterfaceType || $parentType instanceof ObjectType)) { + $fieldName = $selectionNode->name->value; + $type = $parentType->getField($fieldName); + $selectionType = $type->getType(); + + $fieldData = & $fields[$fieldName]; + $fieldData['type'] = $selectionType; + $fieldData['args'] = Values::getArgumentValues($type, $selectionNode, $this->variableValues); + $fieldData['fields'] = $selectionNode->selectionSet ? $this->analyzeSubFields($selectionType, $selectionNode->selectionSet) : []; + } elseif ($selectionNode instanceof FragmentSpreadNode) { + $spreadName = $selectionNode->name->value; + $fragment = $this->fragments[$spreadName] ?? null; + if ($fragment) { + $type = $this->schema->getType($fragment->typeCondition->name->value); + $fields = $this->arrayMergeDeep( - $this->analyzeSubFields($type, $selectionNode->selectionSet), + $this->analyzeSubFields($type, $fragment->selectionSet), $fields ); } + } elseif ($selectionNode instanceof InlineFragmentNode) { + $type = $this->schema->getType($selectionNode->typeCondition->name->value); + + $fields = $this->arrayMergeDeep( + $this->analyzeSubFields($type, $selectionNode->selectionSet), + $fields + ); } } unset($fieldData); - if ($this->withMetaFields) { - return ['fields' => $fields] + array_filter(['fragments' => $fragments, 'inlineFragments' => $inlineFragments]); - } - return $fields; } @@ -218,7 +243,7 @@ private function analyzeSubFields(Type $type, SelectionSetNode $selectionSet) : $type = $type->getWrappedType(); } - $data = $this->analyzeSelectionSet($selectionSet, $type); + $data = $this->withMetaFields ? $this->analyzeSelectionSetWithMetaFields($selectionSet, $type) : $this->analyzeSelectionSet($selectionSet, $type); $this->types[$type->name] = array_unique(array_merge( array_key_exists($type->name, $this->types) ? $this->types[$type->name] : [], From 6d9fff30ad54582ed6ce26567852ac34028ebbc2 Mon Sep 17 00:00:00 2001 From: yura3d Date: Tue, 16 Jul 2019 20:21:04 +0300 Subject: [PATCH 09/11] Renamings; merging fields inside fragments --- src/Type/Definition/QueryPlan.php | 45 ++-- tests/Type/QueryPlanTest.php | 328 +++++++++++++++++++++++++++++- 2 files changed, 345 insertions(+), 28 deletions(-) diff --git a/src/Type/Definition/QueryPlan.php b/src/Type/Definition/QueryPlan.php index 5cdf0a21a..9f395d414 100644 --- a/src/Type/Definition/QueryPlan.php +++ b/src/Type/Definition/QueryPlan.php @@ -42,7 +42,7 @@ class QueryPlan private $fragments; /** @var bool */ - private $withMetaFields; + private $withTypeConditions; /** * @param FieldNode[] $fieldNodes @@ -52,10 +52,10 @@ class QueryPlan */ public function __construct(ObjectType $parentType, Schema $schema, iterable $fieldNodes, array $variableValues, array $fragments, array $options) { - $this->schema = $schema; - $this->variableValues = $variableValues; - $this->fragments = $fragments; - $this->withMetaFields = in_array('with-meta-fields', $options, true); + $this->schema = $schema; + $this->variableValues = $variableValues; + $this->fragments = $fragments; + $this->withTypeConditions = in_array('with-type-conditions', $options, true); $this->analyzeQueryPlan($parentType, $fieldNodes); } @@ -128,11 +128,11 @@ private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes) } $selectionSet = $fieldNode->selectionSet; - $data = $this->withMetaFields ? $this->analyzeSelectionSetWithMetaFields($selectionSet, $type) : $this->analyzeSelectionSet($selectionSet, $type); + $data = $this->withTypeConditions ? $this->analyzeSelectionSetWithTypeConditions($selectionSet, $type) : $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($this->withMetaFields ? $data['fields'] : $data) + array_keys($this->withTypeConditions ? $data['fields'] : $data) )); $queryPlan = array_merge_recursive( @@ -151,11 +151,10 @@ private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes) * * @throws Error */ - private function analyzeSelectionSetWithMetaFields(SelectionSetNode $selectionSet, Type $parentType) : array + private function analyzeSelectionSetWithTypeConditions(SelectionSetNode $selectionSet, Type $parentType) : array { - $fields = []; - $fragments = []; - $inlineFragments = []; + $fields = []; + $typeConditions = []; foreach ($selectionSet->selections as $selectionNode) { if ($selectionNode instanceof FieldNode && ($parentType instanceof InterfaceType || $parentType instanceof ObjectType)) { @@ -173,19 +172,23 @@ private function analyzeSelectionSetWithMetaFields(SelectionSetNode $selectionSe if ($fragment) { $type = $this->schema->getType($fragment->typeCondition->name->value); - $fragmentData = &$fragments[$spreadName]; - $fragmentData['type'] = $type; - $fragmentData += $this->analyzeSubFields($type, $fragment->selectionSet); + $typeConditions[$type->name] = $this->arrayMergeDeep( + $this->analyzeSubFields($type, $fragment->selectionSet), + $typeConditions[$type->name] ?? [] + ); } } elseif ($selectionNode instanceof InlineFragmentNode) { $type = $this->schema->getType($selectionNode->typeCondition->name->value); - $inlineFragments[$type->name] = $this->analyzeSubFields($type, $selectionNode->selectionSet); + $typeConditions[$type->name] = $this->arrayMergeDeep( + $this->analyzeSubFields($type, $selectionNode->selectionSet), + $typeConditions[$type->name] ?? [] + ); } } - unset($fieldData, $fragmentData); + unset($fieldData); - return ['fields' => $fields] + array_filter(['fragments' => $fragments, 'inlineFragments' => $inlineFragments]); + return ['fields' => $fields] + array_filter(['typeConditions' => $typeConditions]); } /** @@ -243,11 +246,15 @@ private function analyzeSubFields(Type $type, SelectionSetNode $selectionSet) : $type = $type->getWrappedType(); } - $data = $this->withMetaFields ? $this->analyzeSelectionSetWithMetaFields($selectionSet, $type) : $this->analyzeSelectionSet($selectionSet, $type); + if (! $this->withTypeConditions && ! $type instanceof ObjectType) { + return []; + } + + $data = $this->withTypeConditions ? $this->analyzeSelectionSetWithTypeConditions($selectionSet, $type) : $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($this->withMetaFields ? $data['fields'] : $data) + array_keys($this->withTypeConditions ? $data['fields'] : $data) )); return $data; diff --git a/tests/Type/QueryPlanTest.php b/tests/Type/QueryPlanTest.php index 2c9aedfe1..2b208314e 100644 --- a/tests/Type/QueryPlanTest.php +++ b/tests/Type/QueryPlanTest.php @@ -297,7 +297,7 @@ public function testQueryPlan() : void self::assertFalse($queryPlan->hasType('Test')); } - public function testQueryPlanWithMetaFields() : void + public function testQueryPlanWithTypeConditions() : void { $image = new ObjectType([ 'name' => 'Image', @@ -436,9 +436,8 @@ public function testQueryPlanWithMetaFields() : void 'fields' => [], ], ], - 'fragments' => [ - 'MyImage' => [ - 'type' => $image, + 'typeConditions' => [ + 'Image' => [ 'fields' => [ 'url' => [ 'type' => Type::string(), @@ -487,7 +486,7 @@ public function testQueryPlanWithMetaFields() : void 'fields' => [], ], ], - 'inlineFragments' => [ + 'typeConditions' => [ 'Image' => [ 'fields' => [ 'height' => [ @@ -568,7 +567,7 @@ public function testQueryPlanWithMetaFields() : void &$queryPlan ) { $hasCalled = true; - $queryPlan = $info->lookAhead('with-meta-fields'); + $queryPlan = $info->lookAhead('with-type-conditions'); return null; }, @@ -704,7 +703,7 @@ public function testQueryPlanOnInterface() : void self::assertFalse($queryPlan->hasType('Test')); } - public function testQueryPlanOnInterfaceWithMetaFields() : void + public function testQueryPlanOnInterfaceWithTypeConditions() : void { $petType = new InterfaceType([ 'name' => 'Pet', @@ -746,7 +745,7 @@ public function testQueryPlanOnInterfaceWithMetaFields() : void 'fields' => [], ], ], - 'inlineFragments' => [ + 'typeConditions' => [ 'Dog' => [ 'fields' => [ 'woofs' => [ @@ -788,7 +787,7 @@ public function testQueryPlanOnInterfaceWithMetaFields() : void &$queryPlan ) { $hasCalled = true; - $queryPlan = $info->lookAhead('with-meta-fields'); + $queryPlan = $info->lookAhead('with-type-conditions'); return []; }, @@ -1113,4 +1112,315 @@ public function testMergedFragmentsQueryPlan() : void self::assertTrue($queryPlan->hasType('Image')); self::assertFalse($queryPlan->hasType('Test')); } + + public function testMergedFragmentsQueryPlanWithTypeConditions() : 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 = [ + 'fields' => [ + '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' => [ + 'width' => [ + 'type' => Type::int(), + 'args' => [], + 'fields' => [], + ], + 'height' => [ + 'type' => Type::int(), + 'args' => [], + 'fields' => [], + ], + ], + 'typeConditions' => [ + 'Image' => [ + 'fields' => [ + 'url' => [ + 'type' => Type::string(), + 'args' => [], + 'fields' => [], + ], + ], + ], + ], + ], + ], + 'typeConditions' => [ + 'Article' => [ + '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' => [], + ], + ], + 'typeConditions' => [ + 'Image' => [ + '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('with-type-conditions'); + + 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')); + } } From 1179de70777af9797ae88c895938e8d9168023ae Mon Sep 17 00:00:00 2001 From: Yury Antonau Date: Mon, 5 Aug 2019 20:05:01 +0300 Subject: [PATCH 10/11] Fixed fields order for merged types --- src/Type/Definition/QueryPlan.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Type/Definition/QueryPlan.php b/src/Type/Definition/QueryPlan.php index 9f395d414..d40f2b93c 100644 --- a/src/Type/Definition/QueryPlan.php +++ b/src/Type/Definition/QueryPlan.php @@ -173,16 +173,16 @@ private function analyzeSelectionSetWithTypeConditions(SelectionSetNode $selecti $type = $this->schema->getType($fragment->typeCondition->name->value); $typeConditions[$type->name] = $this->arrayMergeDeep( - $this->analyzeSubFields($type, $fragment->selectionSet), - $typeConditions[$type->name] ?? [] + $typeConditions[$type->name] ?? [], + $this->analyzeSubFields($type, $fragment->selectionSet) ); } } elseif ($selectionNode instanceof InlineFragmentNode) { $type = $this->schema->getType($selectionNode->typeCondition->name->value); $typeConditions[$type->name] = $this->arrayMergeDeep( - $this->analyzeSubFields($type, $selectionNode->selectionSet), - $typeConditions[$type->name] ?? [] + $typeConditions[$type->name] ?? [], + $this->analyzeSubFields($type, $selectionNode->selectionSet) ); } } From 6c5c4fc15c88b4598e99ac3e60350c6aabfe48c3 Mon Sep 17 00:00:00 2001 From: Yury Antonau Date: Thu, 3 Oct 2019 16:50:08 +0300 Subject: [PATCH 11/11] Grouping implementor fields for abstract types --- src/Type/Definition/QueryPlan.php | 77 ++++++-- src/Type/Definition/ResolveInfo.php | 8 +- tests/Type/QueryPlanTest.php | 266 ++++++++++++++++++++++++++++ 3 files changed, 331 insertions(+), 20 deletions(-) diff --git a/src/Type/Definition/QueryPlan.php b/src/Type/Definition/QueryPlan.php index 9cd0aec00..500bc9cb5 100644 --- a/src/Type/Definition/QueryPlan.php +++ b/src/Type/Definition/QueryPlan.php @@ -12,7 +12,9 @@ use GraphQL\Language\AST\InlineFragmentNode; use GraphQL\Language\AST\SelectionSetNode; use GraphQL\Type\Schema; +use function array_diff_key; use function array_filter; +use function array_intersect_key; use function array_key_exists; use function array_keys; use function array_merge; @@ -41,16 +43,21 @@ class QueryPlan /** @var FragmentDefinitionNode[] */ private $fragments; + /** @var bool */ + private $groupImplementorFields; + /** * @param FieldNode[] $fieldNodes * @param mixed[] $variableValues * @param FragmentDefinitionNode[] $fragments + * @param mixed[] $options */ - public function __construct(ObjectType $parentType, Schema $schema, iterable $fieldNodes, array $variableValues, array $fragments) + public function __construct(ObjectType $parentType, Schema $schema, iterable $fieldNodes, array $variableValues, array $fragments, array $options = []) { - $this->schema = $schema; - $this->variableValues = $variableValues; - $this->fragments = $fragments; + $this->schema = $schema; + $this->variableValues = $variableValues; + $this->fragments = $fragments; + $this->groupImplementorFields = in_array('group-implementor-fields', $options, true); $this->analyzeQueryPlan($parentType, $fieldNodes); } @@ -109,7 +116,8 @@ public function subFields(string $typename) : array */ private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes) : void { - $queryPlan = []; + $queryPlan = []; + $implementors = []; /** @var FieldNode $fieldNode */ foreach ($fieldNodes as $fieldNode) { if (! $fieldNode->selectionSet) { @@ -121,7 +129,7 @@ private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes) $type = $type->getWrappedType(); } - $subfields = $this->analyzeSelectionSet($fieldNode->selectionSet, $type); + $subfields = $this->analyzeSelectionSet($fieldNode->selectionSet, $type, $implementors); $this->types[$type->name] = array_unique(array_merge( array_key_exists($type->name, $this->types) ? $this->types[$type->name] : [], @@ -134,17 +142,26 @@ private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes) ); } - $this->queryPlan = $queryPlan; + if ($this->groupImplementorFields) { + $this->queryPlan = ['fields' => $queryPlan]; + + if ($implementors) { + $this->queryPlan['implementors'] = $implementors; + } + } else { + $this->queryPlan = $queryPlan; + } } /** * @param InterfaceType|ObjectType $parentType + * @param mixed[] $implementors * * @return mixed[] * * @throws Error */ - private function analyzeSelectionSet(SelectionSetNode $selectionSet, Type $parentType) : array + private function analyzeSelectionSet(SelectionSetNode $selectionSet, Type $parentType, array &$implementors = []) : array { $fields = []; foreach ($selectionSet->selections as $selectionNode) { @@ -169,20 +186,12 @@ private function analyzeSelectionSet(SelectionSetNode $selectionSet, Type $paren $fragment = $this->fragments[$spreadName]; $type = $this->schema->getType($fragment->typeCondition->name->value); $subfields = $this->analyzeSubFields($type, $fragment->selectionSet); - - $fields = $this->arrayMergeDeep( - $subfields, - $fields - ); + $fields = $this->mergeFields($parentType, $type, $fields, $subfields, $implementors); } } elseif ($selectionNode instanceof InlineFragmentNode) { $type = $this->schema->getType($selectionNode->typeCondition->name->value); $subfields = $this->analyzeSubFields($type, $selectionNode->selectionSet); - - $fields = $this->arrayMergeDeep( - $subfields, - $fields - ); + $fields = $this->mergeFields($parentType, $type, $fields, $subfields, $implementors); } } @@ -210,6 +219,38 @@ private function analyzeSubFields(Type $type, SelectionSetNode $selectionSet) : return $subfields; } + /** + * @param mixed[] $fields + * @param mixed[] $subfields + * @param mixed[] $implementors + * + * @return mixed[] + */ + private function mergeFields(Type $parentType, Type $type, array $fields, array $subfields, array &$implementors) : array + { + if ($this->groupImplementorFields && $parentType instanceof AbstractType && ! $type instanceof AbstractType) { + $implementors[$type->name] = [ + 'type' => $type, + 'fields' => $this->arrayMergeDeep( + $implementors[$type->name]['fields'] ?? [], + array_diff_key($subfields, $fields) + ), + ]; + + $fields = $this->arrayMergeDeep( + $fields, + array_intersect_key($subfields, $fields) + ); + } else { + $fields = $this->arrayMergeDeep( + $subfields, + $fields + ); + } + + return $fields; + } + /** * 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 diff --git a/src/Type/Definition/ResolveInfo.php b/src/Type/Definition/ResolveInfo.php index 4ca536ad3..7c73d537a 100644 --- a/src/Type/Definition/ResolveInfo.php +++ b/src/Type/Definition/ResolveInfo.php @@ -198,7 +198,10 @@ public function getFieldSelection($depth = 0) return $fields; } - public function lookAhead() : QueryPlan + /** + * @param mixed[] $options + */ + public function lookAhead(array $options = []) : QueryPlan { if ($this->queryPlan === null) { $this->queryPlan = new QueryPlan( @@ -206,7 +209,8 @@ public function lookAhead() : QueryPlan $this->schema, $this->fieldNodes, $this->variableValues, - $this->fragments + $this->fragments, + $options ); } diff --git a/tests/Type/QueryPlanTest.php b/tests/Type/QueryPlanTest.php index a0d05fd40..0e0e5abfa 100644 --- a/tests/Type/QueryPlanTest.php +++ b/tests/Type/QueryPlanTest.php @@ -11,6 +11,7 @@ use GraphQL\Type\Definition\QueryPlan; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; +use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Schema; use PHPUnit\Framework\TestCase; @@ -698,4 +699,269 @@ public function testMergedFragmentsQueryPlan() : void self::assertTrue($queryPlan->hasType('Image')); self::assertFalse($queryPlan->hasType('Test')); } + + public function testQueryPlanOnInterfaceGroupingImplementorFields() : void + { + $car = null; + + $item = new InterfaceType([ + 'name' => 'Item', + 'fields' => [ + 'id' => Type::int(), + 'owner' => Type::string(), + ], + 'resolveType' => static function () use (&$car) { + return $car; + }, + ]); + + $car = new ObjectType([ + 'name' => 'Car', + 'fields' => [ + 'id' => Type::int(), + 'owner' => Type::string(), + 'mark' => Type::string(), + 'model' => Type::string(), + ], + 'interfaces' => [$item], + ]); + + $building = new ObjectType([ + 'name' => 'Building', + 'fields' => [ + 'id' => Type::int(), + 'owner' => Type::string(), + 'city' => Type::string(), + 'address' => Type::string(), + ], + 'interfaces' => [$item], + ]); + + $query = '{ + item { + id + owner + ... on Car { + mark + model + } + ... on Building { + city + } + ...BuildingFragment + } + } + fragment BuildingFragment on Building { + address + }'; + + $expectedResult = [ + 'data' => ['item' => null], + ]; + + $expectedQueryPlan = [ + 'fields' => [ + 'id' => [ + 'type' => Type::int(), + 'fields' => [], + 'args' => [], + ], + 'owner' => [ + 'type' => Type::string(), + 'fields' => [], + 'args' => [], + ], + ], + 'implementors' => [ + 'Car' => [ + 'type' => $car, + 'fields' => [ + 'mark' => [ + 'type' => Type::string(), + 'fields' => [], + 'args' => [], + ], + 'model' => [ + 'type' => Type::string(), + 'fields' => [], + 'args' => [], + ], + ], + ], + 'Building' => [ + 'type' => $building, + 'fields' => [ + 'city' => [ + 'type' => Type::string(), + 'fields' => [], + 'args' => [], + ], + 'address' => [ + 'type' => Type::string(), + 'fields' => [], + 'args' => [], + ], + ], + ], + ], + ]; + + $expectedReferencedTypes = ['Car', 'Building', 'Item']; + + $expectedReferencedFields = ['mark', 'model', 'city', 'address', 'id', 'owner']; + + $expectedItemSubFields = ['id', 'owner']; + $expectedBuildingSubFields = ['city', 'address']; + + $hasCalled = false; + /** @var QueryPlan $queryPlan */ + $queryPlan = null; + + $root = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'item' => [ + 'type' => $item, + 'resolve' => static function ($value, $args, $context, ResolveInfo $info) use (&$hasCalled, &$queryPlan) { + $hasCalled = true; + $queryPlan = $info->lookAhead(['group-implementor-fields']); + + return null; + }, + ], + ], + ]); + + $schema = new Schema([ + 'query' => $root, + 'types' => [$car, $building], + ]); + $result = GraphQL::executeQuery($schema, $query)->toArray(); + + self::assertTrue($hasCalled); + self::assertEquals($expectedResult, $result); + self::assertEquals($expectedQueryPlan, $queryPlan->queryPlan()); + self::assertEquals($expectedReferencedTypes, $queryPlan->getReferencedTypes()); + self::assertEquals($expectedReferencedFields, $queryPlan->getReferencedFields()); + self::assertEquals($expectedItemSubFields, $queryPlan->subFields('Item')); + self::assertEquals($expectedBuildingSubFields, $queryPlan->subFields('Building')); + } + + public function testQueryPlanOnUnionGroupingImplementorFields() : void + { + $car = new ObjectType([ + 'name' => 'Car', + 'fields' => [ + 'mark' => Type::string(), + 'model' => Type::string(), + ], + ]); + + $building = new ObjectType([ + 'name' => 'Building', + 'fields' => [ + 'city' => Type::string(), + 'address' => Type::string(), + ], + ]); + + $item = new UnionType([ + 'name' => 'Item', + 'types' => [$car, $building], + 'resolveType' => static function () use ($car) { + return $car; + }, + ]); + + $query = '{ + item { + ... on Car { + mark + model + } + ... on Building { + city + } + ...BuildingFragment + } + } + fragment BuildingFragment on Building { + address + }'; + + $expectedResult = [ + 'data' => ['item' => null], + ]; + + $expectedQueryPlan = [ + 'fields' => [], + 'implementors' => [ + 'Car' => [ + 'type' => $car, + 'fields' => [ + 'mark' => [ + 'type' => Type::string(), + 'fields' => [], + 'args' => [], + ], + 'model' => [ + 'type' => Type::string(), + 'fields' => [], + 'args' => [], + ], + ], + ], + 'Building' => [ + 'type' => $building, + 'fields' => [ + 'city' => [ + 'type' => Type::string(), + 'fields' => [], + 'args' => [], + ], + 'address' => [ + 'type' => Type::string(), + 'fields' => [], + 'args' => [], + ], + ], + ], + ], + ]; + + $expectedReferencedTypes = ['Car', 'Building', 'Item']; + + $expectedReferencedFields = ['mark', 'model', 'city', 'address']; + + $expectedBuildingSubFields = ['city', 'address']; + + $hasCalled = false; + /** @var QueryPlan $queryPlan */ + $queryPlan = null; + + $root = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'item' => [ + 'type' => $item, + 'resolve' => static function ($value, $args, $context, ResolveInfo $info) use (&$hasCalled, &$queryPlan) { + $hasCalled = true; + $queryPlan = $info->lookAhead(['group-implementor-fields']); + + return null; + }, + ], + ], + ]); + + $schema = new Schema(['query' => $root]); + $result = GraphQL::executeQuery($schema, $query)->toArray(); + + self::assertTrue($hasCalled); + self::assertEquals($expectedResult, $result); + self::assertEquals($expectedQueryPlan, $queryPlan->queryPlan()); + self::assertEquals($expectedReferencedTypes, $queryPlan->getReferencedTypes()); + self::assertEquals($expectedReferencedFields, $queryPlan->getReferencedFields()); + self::assertEquals($expectedBuildingSubFields, $queryPlan->subFields('Building')); + } }