-
-
Notifications
You must be signed in to change notification settings - Fork 565
add queryplan #436
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add queryplan #436
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -284,22 +284,6 @@ public function testProvidesInfoAboutCurrentExecutionState() : void | |
|
||
Executor::execute($schema, $ast, $rootValue, null, ['var' => '123']); | ||
|
||
self::assertEquals( | ||
[ | ||
'fieldName', | ||
'fieldNodes', | ||
'returnType', | ||
'parentType', | ||
'path', | ||
'schema', | ||
'fragments', | ||
'rootValue', | ||
'operation', | ||
'variableValues', | ||
], | ||
array_keys((array) $info) | ||
); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why deleting this? Can't you just add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, missed your reply. Makes sense. |
||
self::assertEquals('test', $info->fieldName); | ||
self::assertEquals(1, count($info->fieldNodes)); | ||
self::assertSame($ast->definitions[0]->selectionSet->selections[0], $info->fieldNodes[0]); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please fix typehint or handle nullability, otherwise it's not safe to iterate
$fieldNodes
, not sure what's correct here :)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nullability can be dropped as it isn't nullable in ResolveInfo