-
-
Notifications
You must be signed in to change notification settings - Fork 575
feat: add directive-aware field selection #1971
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
base: master
Are you sure you want to change the base?
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 |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
| use GraphQL\Language\AST\FragmentSpreadNode; | ||
| use GraphQL\Language\AST\InlineFragmentNode; | ||
| use GraphQL\Language\AST\OperationDefinitionNode; | ||
| use GraphQL\Language\AST\SelectionNode; | ||
| use GraphQL\Language\AST\SelectionSetNode; | ||
| use GraphQL\Type\Introspection; | ||
| use GraphQL\Type\Schema; | ||
|
|
@@ -214,6 +215,39 @@ public function getFieldSelection(int $depth = 0): array | |
| return $fields; | ||
| } | ||
|
|
||
| /** | ||
| * Returns names of all fields selected in query for `$this->fieldName` up to `$depth` levels, | ||
| * excluding selections disabled through `@skip` or `@include`. | ||
| * | ||
| * This method does not consider conditional typed fragments. | ||
| * Use it with care for fields of interface and union types. | ||
| * | ||
| * @param int $depth How many levels to include in the output beyond the first | ||
| * | ||
| * @throws \Exception | ||
| * @throws Error | ||
| * | ||
| * @return array<string, mixed> | ||
| * | ||
| * @api | ||
| */ | ||
| public function getFieldSelectionRespectingDirectives(int $depth = 0): array | ||
| { | ||
| $fields = []; | ||
|
|
||
| foreach ($this->fieldNodes as $fieldNode) { | ||
| $selectionSet = $fieldNode->selectionSet; | ||
| if ($selectionSet !== null) { | ||
| $fields = $this->mergeSelectionsRespectingDirectives( | ||
| $fields, | ||
| $this->foldSelectionSetRespectingDirectives($selectionSet, $depth) | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| return $fields; | ||
| } | ||
|
|
||
| /** | ||
| * Returns names and args of all fields selected in query for `$this->fieldName` up to `$depth` levels, including aliases. | ||
| * | ||
|
|
@@ -379,10 +413,9 @@ public function lookAhead(array $options = []): QueryPlan | |
| ); | ||
| } | ||
|
|
||
| /** @return array<string, bool> */ | ||
| /** @return array<string, mixed> */ | ||
| private function foldSelectionSet(SelectionSetNode $selectionSet, int $descend): array | ||
| { | ||
| /** @var array<string, bool> $fields */ | ||
| $fields = []; | ||
|
|
||
| foreach ($selectionSet->selections as $selection) { | ||
|
|
@@ -415,6 +448,109 @@ private function foldSelectionSet(SelectionSetNode $selectionSet, int $descend): | |
| return $fields; | ||
| } | ||
|
|
||
| /** | ||
| * @throws \Exception | ||
| * @throws Error | ||
| * | ||
| * @return array<string, mixed> | ||
| */ | ||
| protected function foldSelectionSetRespectingDirectives(SelectionSetNode $selectionSet, int $descend): array | ||
| { | ||
| $fields = []; | ||
|
|
||
| foreach ($selectionSet->selections as $selection) { | ||
| /** @var FragmentSpreadNode|FieldNode|InlineFragmentNode $selection */ | ||
| if (! $this->shouldIncludeSelectionNodeRespectingDirectives($selection)) { | ||
| continue; | ||
| } | ||
|
|
||
| if ($selection instanceof FieldNode) { | ||
| if ($descend > 0 && $selection->selectionSet !== null) { | ||
| $existingSelection = $fields[$selection->name->value] ?? []; | ||
| assert(is_array($existingSelection)); | ||
| $fields[$selection->name->value] = $this->mergeSelectionsRespectingDirectives( | ||
| $existingSelection, | ||
| $this->foldSelectionSetRespectingDirectives($selection->selectionSet, $descend - 1) | ||
| ); | ||
| } elseif (! isset($fields[$selection->name->value])) { | ||
| $fields[$selection->name->value] = true; | ||
| } | ||
|
|
||
| continue; | ||
| } | ||
|
|
||
| if ($selection instanceof FragmentSpreadNode) { | ||
| $spreadName = $selection->name->value; | ||
| $fragment = $this->fragments[$spreadName] ?? null; | ||
| if ($fragment === null) { | ||
| continue; | ||
| } | ||
|
|
||
| $fields = $this->mergeSelectionsRespectingDirectives( | ||
| $fields, | ||
| $this->foldSelectionSetRespectingDirectives($fragment->selectionSet, $descend) | ||
| ); | ||
|
|
||
| continue; | ||
| } | ||
|
|
||
| $fields = $this->mergeSelectionsRespectingDirectives( | ||
| $fields, | ||
| $this->foldSelectionSetRespectingDirectives($selection->selectionSet, $descend) | ||
| ); | ||
| } | ||
|
|
||
| return $fields; | ||
| } | ||
|
|
||
| /** | ||
| * @param array<string, mixed> $left | ||
| * @param array<string, mixed> $right | ||
| * | ||
| * @return array<string, mixed> | ||
| */ | ||
| protected function mergeSelectionsRespectingDirectives(array $left, array $right): array | ||
| { | ||
| foreach ($right as $field => $selection) { | ||
| $existingSelection = $left[$field] ?? null; | ||
| if (is_array($existingSelection) && is_array($selection)) { | ||
| $left[$field] = $this->mergeSelectionsRespectingDirectives($existingSelection, $selection); | ||
| } elseif ($existingSelection === null || is_array($selection)) { | ||
| $left[$field] = $selection; | ||
| } | ||
| } | ||
|
|
||
| return $left; | ||
| } | ||
|
|
||
| /** | ||
| * @param FragmentSpreadNode|FieldNode|InlineFragmentNode $node | ||
|
Collaborator
Author
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. The narrower PHPDoc is intentional. SelectionNode is an empty marker interface, while this method accepts the three concrete selection node types. This also matches the PHPDoc on ReferenceExecutor::shouldIncludeNode(), so keeping the union helps static analysis and preserves consistency. |
||
| * | ||
| * @throws \Exception | ||
| * @throws Error | ||
| */ | ||
| protected function shouldIncludeSelectionNodeRespectingDirectives(SelectionNode $node): bool | ||
| { | ||
| $skip = Values::getDirectiveValues( | ||
| Directive::skipDirective(), | ||
| $node, | ||
| $this->variableValues, | ||
| $this->schema | ||
| ); | ||
| if (isset($skip['if']) && $skip['if'] === true) { | ||
| return false; | ||
| } | ||
|
|
||
| $include = Values::getDirectiveValues( | ||
| Directive::includeDirective(), | ||
| $node, | ||
| $this->variableValues, | ||
| $this->schema | ||
| ); | ||
|
|
||
| return ! isset($include['if']) || $include['if'] !== false; | ||
| } | ||
|
|
||
| /** | ||
| * @throws \Exception | ||
| * @throws Error | ||
|
|
||
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.
Keeping these helpers protected is intentional. CONTRIBUTING.md requires protected over private for extensibility (Extensibility > protected over private). The existing private helper does not override that documented rule.