Skip to content

Commit aecf9c7

Browse files
authored
Merge pull request #327 from sowork/master
Fix args in sub queries
2 parents ae556fc + 204e591 commit aecf9c7

File tree

5 files changed

+183
-110
lines changed

5 files changed

+183
-110
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ Next release
2626
- Replace global helper `is_lumen` with static class call `\Rebing\GraphQL\Helpers::isLumen`
2727

2828
### Fixed
29+
- SelectFields correctly passes field arguments to the custom query [\#327](https://github.com/rebing/graphql-laravel/pull/327)
30+
- This also applies to privacy checks on fields, the callback now receives the field arguments too
31+
- Previously the initial query arguments would be used everywhere
2932
- SelectFields now works with wrapped types (nonNull, listOf)
3033

3134
### Removed

phpstan.neon

+4-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ parameters:
1818
- '/Strict comparison using === between null and array will always evaluate to false/'
1919
- '/Cannot access offset . on array\|Closure/'
2020
- '/Call to function is_array\(\) with string will always evaluate to false/' # TODO: fix \Rebing\GraphQL\Support\SelectFields::getSelectableFieldsAndRelations
21-
- '/Parameter #1 \$function of function call_user_func expects callable\(\): mixed, array\(mixed, string\) given/'
21+
# \Rebing\GraphQL\Support\SelectFields::handleFields
22+
- '/Parameter #1 \$function of function call_user_func expects callable\(\): mixed, array\(mixed, mixed\) given/'
2223
- '/Binary operation "." between string and array\|string\|null results in an error/'
2324
- '/Parameter #1 \$key of function array_key_exists expects int\|string, string\|false given/'
2425
- "/Parameter #1 \\$function of function call_user_func expects callable\\(\\): mixed, array\\(mixed, 'fire'\\) given/"
@@ -34,3 +35,5 @@ parameters:
3435
- '/Parameter #4 \$currentPage of class Illuminate\\Pagination\\LengthAwarePaginator constructor expects int\|null, float\|int given/'
3536
- '/Parameter #1 \$offset of method Illuminate\\Support\\Collection::slice\(\) expects int, float\|int given/'
3637
- '/Parameter #1 \$abstract of function app expects string\|null, mixed given/'
38+
# \Rebing\GraphQL\Support\ResolveInfoFieldsAndArguments::getValue
39+
- '/Access to an undefined property GraphQL\\Language\\AST\\ValueNode::\$value/'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rebing\GraphQL\Support;
6+
7+
use GraphQL\Language\AST\FieldNode;
8+
use GraphQL\Language\AST\ArgumentNode;
9+
use GraphQL\Language\AST\VariableNode;
10+
use GraphQL\Type\Definition\ResolveInfo;
11+
use GraphQL\Language\AST\SelectionSetNode;
12+
use GraphQL\Language\AST\FragmentSpreadNode;
13+
use GraphQL\Language\AST\InlineFragmentNode;
14+
15+
/**
16+
* This adapts \GraphQL\Type\Definition\ResolveInfo::getFieldSelection
17+
* but with support for both field *and* arguments.
18+
*/
19+
class ResolveInfoFieldsAndArguments
20+
{
21+
/** @var ResolveInfo */
22+
public $info;
23+
24+
public function __construct(ResolveInfo $info)
25+
{
26+
$this->info = $info;
27+
}
28+
29+
/**
30+
* Helper method that returns names of all fields with attributes selected in query for
31+
* $this->fieldName up to $depth levels.
32+
*
33+
* Example:
34+
* query MyQuery{
35+
* {
36+
* root {
37+
* nested(input:value) {
38+
* nested1
39+
* nested2 {
40+
* nested3(input:value)
41+
* }
42+
* }
43+
* }
44+
* }
45+
*
46+
* Given this ResolveInfo instance is a part of "root" field resolution, and $depth === 1,
47+
* method will return:
48+
* [
49+
* 'nested' => [
50+
* 'args' => [
51+
* 'input' => 'value',
52+
* ],
53+
* 'fields' => [
54+
* 'nested1' => [
55+
* 'args' => [],
56+
* 'fields' => true,
57+
* ],
58+
* 'nested2' => [
59+
* 'args' => [],
60+
* 'fields' => [
61+
* 'nested3' => [
62+
* 'args' => [
63+
* 'input' => 'value',
64+
* ],
65+
* 'fields' => true,
66+
* ],
67+
* ],
68+
* ],
69+
* ],
70+
* ],
71+
* ],
72+
*
73+
* Warning: this method it is a naive implementation which does not take into account
74+
* conditional typed fragments. So use it with care for fields of interface and union types.
75+
*
76+
* @param int $depth How many levels to include in output
77+
* @return array
78+
* @see \GraphQL\Type\Definition\ResolveInfo::getFieldSelection
79+
*/
80+
public function getFieldsAndArgumentsSelection(int $depth = 0): array
81+
{
82+
$fields = [];
83+
84+
foreach ($this->info->fieldNodes as $fieldNode) {
85+
if (! $fieldNode->selectionSet) {
86+
continue;
87+
}
88+
89+
$fields = array_merge_recursive($fields, $this->foldSelectionSet($fieldNode->selectionSet, $depth));
90+
}
91+
92+
return $fields;
93+
}
94+
95+
/**
96+
* @param SelectionSetNode $selectionSet
97+
* @param int $descend
98+
* @return array
99+
* @see \GraphQL\Type\Definition\ResolveInfo::foldSelectionSet
100+
*/
101+
private function foldSelectionSet(SelectionSetNode $selectionSet, int $descend): array
102+
{
103+
$fields = [];
104+
105+
foreach ($selectionSet->selections as $selectionNode) {
106+
if ($selectionNode instanceof FieldNode) {
107+
$name = $selectionNode->name->value;
108+
109+
$fields[$name] = [
110+
'args' => [],
111+
'fields' => $descend > 0 && ! empty($selectionNode->selectionSet)
112+
? $this->foldSelectionSet($selectionNode->selectionSet, $descend - 1)
113+
: true,
114+
];
115+
116+
foreach ($selectionNode->arguments ?? [] as $argumentNode) {
117+
$fields[$name]['args'][$argumentNode->name->value] = $this->getValue($argumentNode);
118+
}
119+
} elseif ($selectionNode instanceof FragmentSpreadNode) {
120+
$spreadName = $selectionNode->name->value;
121+
if (isset($this->info->fragments[$spreadName])) {
122+
$fragment = $this->info->fragments[$spreadName];
123+
$fields = array_merge_recursive($this->foldSelectionSet($fragment->selectionSet, $descend),
124+
$fields);
125+
}
126+
} elseif ($selectionNode instanceof InlineFragmentNode) {
127+
$fields = array_merge_recursive($this->foldSelectionSet($selectionNode->selectionSet, $descend),
128+
$fields);
129+
}
130+
}
131+
132+
return $fields;
133+
}
134+
135+
private function getValue(ArgumentNode $argumentNode)
136+
{
137+
$value = $argumentNode->value;
138+
if ($value instanceof VariableNode) {
139+
$variableName = $value->name->value;
140+
141+
return $this->info->variableValues[$variableName] ?? null;
142+
}
143+
144+
return $argumentNode->value->value;
145+
}
146+
}

src/Support/SelectFields.php

+22-17
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121

2222
class SelectFields
2323
{
24-
/** @var array */
25-
private static $args = [];
2624
/** @var array */
2725
private $select = [];
2826
/** @var array */
@@ -43,14 +41,20 @@ public function __construct(ResolveInfo $info, GraphqlType $parentType, array $a
4341
$parentType = $parentType->getWrappedType(true);
4442
}
4543

46-
if (! is_null($info->fieldNodes[0]->selectionSet)) {
47-
self::$args = $args;
44+
$requestedFields = $this->getFieldSelection($info, $args, 5);
45+
$fields = self::getSelectableFieldsAndRelations($requestedFields, $parentType);
46+
$this->select = $fields[0];
47+
$this->relations = $fields[1];
48+
}
4849

49-
$fields = self::getSelectableFieldsAndRelations($info->getFieldSelection(5), $parentType);
50+
private function getFieldSelection(ResolveInfo $resolveInfo, array $args, int $depth): array
51+
{
52+
$resolveInfoFieldsAndArguments = new ResolveInfoFieldsAndArguments($resolveInfo);
5053

51-
$this->select = $fields[0];
52-
$this->relations = $fields[1];
53-
}
54+
return [
55+
'args' => $args,
56+
'fields' => $resolveInfoFieldsAndArguments->getFieldsAndArgumentsSelection($depth),
57+
];
5458
}
5559

5660
/**
@@ -95,9 +99,9 @@ public static function getSelectableFieldsAndRelations(array $requestedFields, G
9599
if ($topLevel) {
96100
return [$select, $with];
97101
} else {
98-
return function ($query) use ($with, $select, $customQuery) {
102+
return function ($query) use ($with, $select, $customQuery, $requestedFields) {
99103
if ($customQuery) {
100-
$query = $customQuery(self::$args, $query);
104+
$query = $customQuery($requestedFields['args'], $query);
101105
}
102106

103107
$query->select($select);
@@ -118,7 +122,7 @@ protected static function handleFields(array $requestedFields, GraphqlType $pare
118122
{
119123
$parentTable = self::isMongodbInstance($parentType) ? null : self::getTableNameFromParentType($parentType);
120124

121-
foreach ($requestedFields as $key => $field) {
125+
foreach ($requestedFields['fields'] as $key => $field) {
122126
// Ignore __typename, as it's a special case
123127
if ($key === '__typename') {
124128
continue;
@@ -142,7 +146,7 @@ protected static function handleFields(array $requestedFields, GraphqlType $pare
142146
}
143147

144148
// First check if the field is even accessible
145-
$canSelect = self::validateField($fieldObject);
149+
$canSelect = self::validateField($fieldObject, $field['args']);
146150
if ($canSelect === true) {
147151
// Add a query, if it exists
148152
$customQuery = Arr::get($fieldObject->config, 'query');
@@ -155,7 +159,7 @@ protected static function handleFields(array $requestedFields, GraphqlType $pare
155159
self::handleFields($field, $fieldObject->config['type']->getWrappedType(), $select, $with);
156160
}
157161
// With
158-
elseif (is_array($field) && $queryable) {
162+
elseif (is_array($field['fields']) && $queryable) {
159163
if (isset($parentType->config['model'])) {
160164
// Get the next parent type, so that 'with' queries could be made
161165
// Both keys for the relation are required (e.g 'id' <-> 'user_id')
@@ -193,7 +197,7 @@ protected static function handleFields(array $requestedFields, GraphqlType $pare
193197
$segments = explode('.', $foreignKey);
194198
$foreignKey = end($segments);
195199
if (! array_key_exists($foreignKey, $field)) {
196-
$field[$foreignKey] = self::FOREIGN_KEY;
200+
$field['fields'][$foreignKey] = self::FOREIGN_KEY;
197201
}
198202
}
199203

@@ -240,10 +244,11 @@ protected static function handleFields(array $requestedFields, GraphqlType $pare
240244
* Check the privacy status, if it's given.
241245
*
242246
* @param FieldDefinition $fieldObject
247+
* @param array $fieldArgs Arguments given with the field
243248
* @return bool|null - true, if selectable; false, if not selectable, but allowed;
244249
* null, if not allowed
245250
*/
246-
protected static function validateField(FieldDefinition $fieldObject): ?bool
251+
protected static function validateField(FieldDefinition $fieldObject, array $fieldArgs): ?bool
247252
{
248253
$selectable = true;
249254

@@ -256,15 +261,15 @@ protected static function validateField(FieldDefinition $fieldObject): ?bool
256261
$privacyClass = $fieldObject->config['privacy'];
257262

258263
// If privacy given as a closure
259-
if (is_callable($privacyClass) && call_user_func($privacyClass, self::$args) === false) {
264+
if (is_callable($privacyClass) && call_user_func($privacyClass, $fieldArgs) === false) {
260265
$selectable = null;
261266
}
262267
// If Privacy class given
263268
elseif (is_string($privacyClass)) {
264269
if (Arr::has(self::$privacyValidations, $privacyClass)) {
265270
$validated = self::$privacyValidations[$privacyClass];
266271
} else {
267-
$validated = call_user_func([app($privacyClass), 'fire'], self::$args);
272+
$validated = call_user_func([app($privacyClass), 'fire'], $fieldArgs);
268273
self::$privacyValidations[$privacyClass] = $validated;
269274
}
270275

0 commit comments

Comments
 (0)