1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Date: 07.11.16 |
4
|
|
|
* |
5
|
|
|
* @author Portey Vasil <[email protected]> |
6
|
|
|
*/ |
7
|
|
|
|
8
|
|
|
namespace Youshido\GraphQL\Execution; |
9
|
|
|
|
10
|
|
|
|
11
|
|
|
use Youshido\GraphQL\Execution\Context\ExecutionContextInterface; |
12
|
|
|
use Youshido\GraphQL\Execution\Visitor\AbstractQueryVisitor; |
13
|
|
|
use Youshido\GraphQL\Field\Field; |
14
|
|
|
use Youshido\GraphQL\Field\FieldInterface; |
15
|
|
|
use Youshido\GraphQL\Parser\Ast\Field as FieldAst; |
16
|
|
|
use Youshido\GraphQL\Parser\Ast\FragmentReference; |
17
|
|
|
use Youshido\GraphQL\Parser\Ast\Interfaces\FragmentInterface; |
18
|
|
|
use Youshido\GraphQL\Parser\Ast\Mutation; |
19
|
|
|
use Youshido\GraphQL\Parser\Ast\Query; |
20
|
|
|
use Youshido\GraphQL\Type\AbstractType; |
21
|
|
|
use Youshido\GraphQL\Type\Object\AbstractObjectType; |
22
|
|
|
use Youshido\GraphQL\Type\Union\AbstractUnionType; |
23
|
|
|
|
24
|
|
|
class Reducer |
25
|
|
|
{ |
26
|
|
|
|
27
|
|
|
/** @var ExecutionContextInterface */ |
28
|
|
|
private $executionContext; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* Apply all of $reducers to this query. Example reducer operations: checking for maximum query complexity, |
32
|
|
|
* performing look-ahead query planning, etc. |
33
|
|
|
* |
34
|
|
|
* @param ExecutionContextInterface $executionContext |
35
|
|
|
* @param AbstractQueryVisitor[] $reducers |
36
|
|
|
*/ |
37
|
2 |
|
public function reduceQuery(ExecutionContextInterface $executionContext, array $reducers) |
38
|
|
|
{ |
39
|
2 |
|
$this->executionContext = $executionContext; |
40
|
2 |
|
$schema = $executionContext->getSchema(); |
41
|
|
|
|
42
|
2 |
|
foreach ($reducers as $reducer) { |
43
|
2 |
|
foreach ($executionContext->getRequest()->getAllOperations() as $operation) { |
44
|
2 |
|
$this->doVisit($operation, $operation instanceof Mutation ? $schema->getMutationType() : $schema->getQueryType(), $reducer); |
45
|
|
|
} |
46
|
|
|
} |
47
|
2 |
|
} |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* Entry point for the `walkQuery` routine. Execution bounces between here, where the reducer's ->visit() method |
51
|
|
|
* is invoked, and `walkQuery` where we send in the scores from the `visit` call. |
52
|
|
|
* |
53
|
|
|
* @param Query $query |
54
|
|
|
* @param AbstractType $currentLevelSchema |
55
|
|
|
* @param AbstractQueryVisitor $reducer |
56
|
|
|
*/ |
57
|
2 |
|
protected function doVisit(Query $query, $currentLevelSchema, $reducer) |
58
|
|
|
{ |
59
|
2 |
|
if (!($currentLevelSchema instanceof AbstractObjectType) || !$currentLevelSchema->hasField($query->getName())) { |
60
|
|
|
return; |
61
|
|
|
} |
62
|
|
|
|
63
|
2 |
|
if ($operationField = $currentLevelSchema->getField($query->getName())) { |
64
|
|
|
|
65
|
2 |
|
$coroutine = $this->walkQuery($query, $operationField); |
66
|
|
|
|
67
|
2 |
|
if ($results = $coroutine->current()) { |
68
|
2 |
|
$queryCost = 0; |
69
|
2 |
|
while ($results) { |
70
|
|
|
// initial values come from advancing the generator via ->current, subsequent values come from ->send() |
71
|
2 |
|
list($queryField, $astField, $childCost) = $results; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* @var Query|FieldAst $queryField |
75
|
|
|
* @var Field $astField |
76
|
|
|
*/ |
77
|
2 |
|
$cost = $reducer->visit($queryField->getKeyValueArguments(), $astField->getConfig(), $childCost); |
78
|
2 |
|
$queryCost += $cost; |
79
|
2 |
|
$results = $coroutine->send($cost); |
80
|
|
|
} |
81
|
|
|
} |
82
|
|
|
} |
83
|
2 |
|
} |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* Coroutine to walk the query and schema in DFS manner (see AbstractQueryVisitor docs for more info) and yield a |
87
|
|
|
* tuple of (queryNode, schemaNode, childScore) |
88
|
|
|
* |
89
|
|
|
* childScore costs are accumulated via values sent into the coroutine. |
90
|
|
|
* |
91
|
|
|
* Most of the branching in this function is just to handle the different types in a query: Queries, Unions, |
92
|
|
|
* Fragments (anonymous and named), and Fields. The core of the function is simple: recurse until we hit the base |
93
|
|
|
* case of a Field and yield that back up to the visitor up in `doVisit`. |
94
|
|
|
* |
95
|
|
|
* @param Query|Field|\Youshido\GraphQL\Parser\Ast\Interfaces\FragmentInterface $queryNode |
96
|
|
|
* @param FieldInterface $currentLevelAST |
97
|
|
|
* |
98
|
|
|
* @return \Generator |
99
|
|
|
*/ |
100
|
2 |
|
protected function walkQuery($queryNode, FieldInterface $currentLevelAST) |
101
|
|
|
{ |
102
|
2 |
|
$childrenScore = 0; |
103
|
2 |
|
if (!($queryNode instanceof FieldAst)) { |
104
|
2 |
|
foreach ($queryNode->getFields() as $queryField) { |
|
|
|
|
105
|
2 |
|
if ($queryField instanceof FragmentInterface) { |
106
|
1 |
|
if ($queryField instanceof FragmentReference) { |
107
|
|
|
$queryField = $this->executionContext->getRequest()->getFragment($queryField->getName()); |
108
|
|
|
} |
109
|
|
|
// the next 7 lines are essentially equivalent to `yield from $this->walkQuery(...)` in PHP7. |
110
|
|
|
// for backwards compatibility this is equivalent. |
111
|
|
|
// This pattern is repeated multiple times in this function, and unfortunately cannot be extracted or |
112
|
|
|
// made less verbose. |
113
|
1 |
|
$gen = $this->walkQuery($queryField, $currentLevelAST); |
|
|
|
|
114
|
1 |
|
$next = $gen->current(); |
115
|
1 |
|
while ($next) { |
116
|
1 |
|
$received = (yield $next); |
117
|
1 |
|
$childrenScore += (int)$received; |
118
|
1 |
|
$next = $gen->send($received); |
119
|
|
|
} |
120
|
|
|
} else { |
121
|
2 |
|
$fieldType = $currentLevelAST->getType()->getNamedType(); |
122
|
2 |
|
if ($fieldType instanceof AbstractUnionType) { |
123
|
1 |
|
foreach ($fieldType->getTypes() as $unionFieldType) { |
124
|
1 |
View Code Duplication |
if ($fieldAst = $unionFieldType->getField($queryField->getName())) { |
|
|
|
|
125
|
1 |
|
$gen = $this->walkQuery($queryField, $fieldAst); |
126
|
1 |
|
$next = $gen->current(); |
127
|
1 |
|
while ($next) { |
128
|
1 |
|
$received = (yield $next); |
129
|
1 |
|
$childrenScore += (int)$received; |
130
|
1 |
|
$next = $gen->send($received); |
131
|
|
|
} |
132
|
|
|
} |
133
|
|
|
} |
134
|
1 |
View Code Duplication |
} elseif ($fieldType instanceof AbstractObjectType && $fieldAst = $fieldType->getField($queryField->getName())) { |
|
|
|
|
135
|
1 |
|
$gen = $this->walkQuery($queryField, $fieldAst); |
136
|
1 |
|
$next = $gen->current(); |
137
|
2 |
|
while ($next) { |
138
|
1 |
|
$received = (yield $next); |
139
|
1 |
|
$childrenScore += (int)$received; |
140
|
1 |
|
$next = $gen->send($received); |
141
|
|
|
} |
142
|
|
|
} |
143
|
|
|
} |
144
|
|
|
} |
145
|
|
|
} |
146
|
|
|
// sanity check. don't yield fragments; they don't contribute to cost |
147
|
2 |
|
if ($queryNode instanceof Query || $queryNode instanceof FieldAst) { |
148
|
|
|
// BASE CASE. If we're here we're done recursing - |
149
|
|
|
// this node is either a field, or a query that we've finished recursing into. |
150
|
2 |
|
yield [$queryNode, $currentLevelAST, $childrenScore]; |
151
|
|
|
} |
152
|
2 |
|
} |
153
|
|
|
|
154
|
|
|
} |
It seems like the method you are trying to call exists only in some of the possible types.
Let’s take a look at an example:
Available Fixes
Add an additional type-check:
Only allow a single type to be passed if the variable comes from a parameter: