Skip to content

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

Merged
merged 1 commit into from
Mar 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 239 additions & 0 deletions src/Type/Definition/QueryPlan.php
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) {
Copy link
Collaborator

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 :)

Copy link
Contributor Author

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

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;
}
}
23 changes: 21 additions & 2 deletions src/Type/Definition/ResolveInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class ResolveInfo
* Instance of a schema used for execution
*
* @api
* @var Schema|null
* @var Schema
*/
public $schema;

Expand Down Expand Up @@ -99,6 +99,9 @@ class ResolveInfo
*/
public $variableValues = [];

/** @var QueryPlan */
private $queryPlan;

/**
* @param FieldNode[] $fieldNodes
* @param ScalarType|ObjectType|InterfaceType|UnionType|EnumType|ListOfType|NonNull $returnType
Expand All @@ -109,7 +112,7 @@ class ResolveInfo
*/
public function __construct(
string $fieldName,
$fieldNodes,
iterable $fieldNodes,
$returnType,
ObjectType $parentType,
array $path,
Expand Down Expand Up @@ -179,6 +182,22 @@ public function getFieldSelection($depth = 0)

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[]
*/
Expand Down
16 changes: 0 additions & 16 deletions tests/Executor/ExecutorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why deleting this? Can't you just add queryPlan key to this list?

Copy link
Contributor Author

@keulinho keulinho Feb 26, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

queryPlan is declared private because it should only be loaded lazy when you call lookAhead()
When you cast the resolveInfo to array now it cpontains some binary string instead of queryPlan because it is private.
Furthermore i really saw no value added by this test, because every key mentionied there would be used for own assertions, so the test will fail anyway if you remove one of the public properties.

Copy link
Member

Choose a reason for hiding this comment

The 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]);
Expand Down
Loading