Skip to content

Commit

Permalink
Feature getAlias args => refactored and enhanced from (ResolveInfo)$i…
Browse files Browse the repository at this point in the history
…nfo->lookAhead()->aliasArgs() to $info->getFieldSelectionWithAlias()

Add tests
  • Loading branch information
zephyx committed Dec 2, 2024
1 parent 8b68ead commit 39e07e4
Show file tree
Hide file tree
Showing 3 changed files with 438 additions and 32 deletions.
32 changes: 0 additions & 32 deletions src/Type/Definition/QueryPlan.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,6 @@ class QueryPlan
/** @var array<string, FragmentDefinitionNode> */
private array $fragments;

private array $fieldArgs = [];

private array $aliasArgs = [];

private bool $groupImplementorFields;

/**
Expand Down Expand Up @@ -118,17 +114,6 @@ public function subFields(string $typename): array
return array_keys($this->typeToFields[$typename] ?? []);
}

/**
* Return an array with keys representing the fields which have been aliased.
* The value for each of those aliased fields is an associative array with $aliasName => related arguments for this aliased field.
*
* @return array
*/
public function aliasArgs()
{
return $this->aliasArgs;
}

/**
* @param iterable<FieldNode> $fieldNodes
*
Expand Down Expand Up @@ -203,23 +188,6 @@ private function analyzeSelectionSet(SelectionSetNode $selectionSet, Type $paren
'fields' => $subfields,
'args' => Values::getArgumentValues($type, $selectionNode, $this->variableValues),
];

if (isset($selectionNode->alias)) {
// If a previous field of the same name has not been aliased, we will lose its args,
// so insert it inside the alias list with its own name.
if (isset($this->fieldArgs[$fieldName])) {
$this->aliasArgs[$fieldName][$fieldName] = $this->fieldArgs[$fieldName];
}
$this->aliasArgs[$fieldName][$selectionNode->alias->value] = $fields[$fieldName]['args'];
} else {
// If a previous field of the same name has been aliased and not this one,
// Add it to the alias list in order to regroup all variety of the same field.
if (isset($this->aliasArgs[$fieldName])) {
$this->aliasArgs[$fieldName][$fieldName] = $fields[$fieldName]['args'];
}
$this->fieldArgs[$fieldName] = $fields[$fieldName]['args'];
}

if ($this->groupImplementorFields && $subImplementors) {
$fields[$fieldName]['implementors'] = $subImplementors;
}
Expand Down
179 changes: 179 additions & 0 deletions src/Type/Definition/ResolveInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@

use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation;
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\OperationDefinitionNode;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Type\Introspection;
use GraphQL\Type\Schema;

/**
Expand Down Expand Up @@ -214,6 +216,126 @@ public function getFieldSelection(int $depth = 0): array
return $fields;
}

/**
* Helper method that returns names of all fields selected in query for
* $this->fieldName up to $depth levels.
* For each field there is an "aliases" key regrouping all aliases of this field
* (if a field is not aliased, its name is present here as a key)
* For each of those "alias" you can find "args" key containing the arguments of the alias and the "fields" key
* containing the subfield of this field/alias. Each of those field have the same structure as described above.
*
* Example:
* query MyQuery{
* {
* root {
* id,
* nested {
* nested1(myArg:1)
* nested1Bis:nested1
* }
* alias1:nested {
* nested1(myArg:2, mySecondAg:"test")
* }
* }
* }
*
* Given this ResolveInfo instance is a part of "root" field resolution, and $depth === 1,
* method will return:
* [
* 'id' => [
* 'aliases' => [
* 'id' => [
* [args] => []
* ]
* ]
* ],
* 'nested' => [
* 'aliases' => [
* 'nested' => [
* ['args'] => [],
* ['fields'] => [
* 'nested1' => [
* 'aliases' => [
* 'nested1' => [
* ['args'] => [
* 'myArg' => 1
* ]
* ],
* 'nested1Bis' => [
* ['args'] => []
* ]
* ]
* ]
* ]
* ],
* 'alias1' => [
* ['args'] => [],
* ['fields'] => [
* 'nested1' => [
* 'aliases' => [
* 'nested1' => [
* ['args'] => [
* 'myArg' => 2,
* 'mySecondAg' => "test"
* ]
* ]
* ]
* ]
* ]
* ]
* ]
* ]
* ]
*
* Warning: this method it is a naive implementation which does not take into account
* conditional typed fragments. So use it with care for fields of interface and union types.
* You still can alias the union type fields with the same name in order to extract their corresponding args.
*
* Example:
* query MyQuery{
* {
* root {
* id,
* unionPerson {
* ...on Child {
* name
* birthdate(format:"d/m/Y")
* }
* ...on Adult {
* adultName:name
* adultBirthDate:birthdate(format:"Y-m-d")
* job
* }
* }
* }
* }
*
* @param int $depth How many levels to include in output
*
* @throws \Exception
* @throws Error
* @throws InvariantViolation
*
* @return array<string, mixed>
*
* @api
*/
public function getFieldSelectionWithAlias(int $depth = 0): array
{
$fields = [];

foreach ($this->fieldNodes as $fieldNode) {
if (isset($fieldNode->selectionSet)) {
$fields = \array_merge_recursive(
$fields,
$this->foldSelectionWithAlias($fieldNode->selectionSet, $depth, $this->parentType->getField($fieldNode->name->value)->getType())
);
}
}

return $fields;
}

/**
* @param QueryPlanOptions $options
*
Expand Down Expand Up @@ -266,4 +388,61 @@ private function foldSelectionSet(SelectionSetNode $selectionSet, int $descend):

return $fields;
}

/**
* @throws InvariantViolation|Error|\Exception
*
* @return array<string>
*/
private function foldSelectionWithAlias(SelectionSetNode $selectionSet, int $descend, Type $parentType): array
{
/** @var array<string, bool> $fields */
$fields = [];

foreach ($selectionSet->selections as $selectionNode) {
if ($selectionNode instanceof FieldNode) {
$fieldName = $selectionNode->name->value;
$aliasName = $selectionNode->alias->value ?? $fieldName;

if ($fieldName === Introspection::TYPE_NAME_FIELD_NAME) {
continue;

Check warning on line 408 in src/Type/Definition/ResolveInfo.php

View check run for this annotation

Codecov / codecov/patch

src/Type/Definition/ResolveInfo.php#L408

Added line #L408 was not covered by tests
}

assert($parentType instanceof HasFieldsType, 'ensured by query validation and the check above which excludes union types');

$fieldDef = $parentType->getField($fieldName);
$fieldType = Type::getNamedType($fieldDef->getType());
$fields[$fieldName]['aliases'][$aliasName]['args'] = Values::getArgumentValues($fieldDef, $selectionNode, $this->variableValues);

if ($descend > 0 && $selectionNode->selectionSet !== null) {
$fields[$fieldName]['aliases'][$aliasName]['fields'] = $this->foldSelectionWithAlias($selectionNode->selectionSet, $descend - 1, $fieldType);

Check failure on line 418 in src/Type/Definition/ResolveInfo.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (7.4)

Parameter #3 $parentType of method GraphQL\Type\Definition\ResolveInfo::foldSelectionWithAlias() expects GraphQL\Type\Definition\Type, (GraphQL\Type\Definition\NamedType&GraphQL\Type\Definition\Type)|null given.

Check failure on line 418 in src/Type/Definition/ResolveInfo.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (8.0)

Parameter #3 $parentType of method GraphQL\Type\Definition\ResolveInfo::foldSelectionWithAlias() expects GraphQL\Type\Definition\Type, (GraphQL\Type\Definition\NamedType&GraphQL\Type\Definition\Type)|null given.

Check failure on line 418 in src/Type/Definition/ResolveInfo.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (8.1)

Parameter #3 $parentType of method GraphQL\Type\Definition\ResolveInfo::foldSelectionWithAlias() expects GraphQL\Type\Definition\Type, (GraphQL\Type\Definition\NamedType&GraphQL\Type\Definition\Type)|null given.

Check failure on line 418 in src/Type/Definition/ResolveInfo.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (8.2)

Parameter #3 $parentType of method GraphQL\Type\Definition\ResolveInfo::foldSelectionWithAlias() expects GraphQL\Type\Definition\Type, (GraphQL\Type\Definition\NamedType&GraphQL\Type\Definition\Type)|null given.

Check failure on line 418 in src/Type/Definition/ResolveInfo.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (8.3)

Parameter #3 $parentType of method GraphQL\Type\Definition\ResolveInfo::foldSelectionWithAlias() expects GraphQL\Type\Definition\Type, (GraphQL\Type\Definition\NamedType&GraphQL\Type\Definition\Type)|null given.
}
} elseif ($selectionNode instanceof FragmentSpreadNode) {
$spreadName = $selectionNode->name->value;
if (isset($this->fragments[$spreadName])) {
$fragment = $this->fragments[$spreadName];
$fieldType = $this->schema->getType($fragment->typeCondition->name->value);
assert($fieldType instanceof Type, 'ensured by query validation');

$fields = \array_merge_recursive(
$this->foldSelectionWithAlias($fragment->selectionSet, $descend, $fieldType),
$fields
);
}
} elseif ($selectionNode instanceof InlineFragmentNode) {
$typeCondition = $selectionNode->typeCondition;
$fieldType = $typeCondition === null
? $parentType
: $this->schema->getType($typeCondition->name->value);
assert($fieldType instanceof Type, 'ensured by query validation');

Check warning on line 437 in src/Type/Definition/ResolveInfo.php

View check run for this annotation

Codecov / codecov/patch

src/Type/Definition/ResolveInfo.php#L432-L437

Added lines #L432 - L437 were not covered by tests

$fields = \array_merge_recursive(
$this->foldSelectionWithAlias($selectionNode->selectionSet, $descend, $fieldType),
$fields
);

Check warning on line 442 in src/Type/Definition/ResolveInfo.php

View check run for this annotation

Codecov / codecov/patch

src/Type/Definition/ResolveInfo.php#L439-L442

Added lines #L439 - L442 were not covered by tests
}
}

return $fields;
}
}
Loading

0 comments on commit 39e07e4

Please sign in to comment.