Passed
Pull Request — master (#314)
by Jakub
08:46 queued 02:41
created

Collector::collectFields()   B

Complexity

Conditions 9
Paths 5

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 9.0164

Importance

Changes 0
Metric Value
eloc 16
dl 0
loc 28
ccs 16
cts 17
cp 0.9412
rs 8.0555
c 0
b 0
f 0
cc 9
nc 5
nop 2
crap 9.0164
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Experimental\Executor;
6
7
use Generator;
8
use GraphQL\Error\Error;
9
use GraphQL\Language\AST\DefinitionNode;
10
use GraphQL\Language\AST\DocumentNode;
11
use GraphQL\Language\AST\FieldNode;
12
use GraphQL\Language\AST\FragmentDefinitionNode;
13
use GraphQL\Language\AST\FragmentSpreadNode;
14
use GraphQL\Language\AST\InlineFragmentNode;
15
use GraphQL\Language\AST\Node;
16
use GraphQL\Language\AST\NodeKind;
17
use GraphQL\Language\AST\OperationDefinitionNode;
18
use GraphQL\Language\AST\SelectionSetNode;
19
use GraphQL\Language\AST\ValueNode;
20
use GraphQL\Type\Definition\AbstractType;
21
use GraphQL\Type\Definition\Directive;
22
use GraphQL\Type\Definition\ObjectType;
23
use GraphQL\Type\Definition\Type;
24
use GraphQL\Type\Introspection;
25
use GraphQL\Type\Schema;
26
use function sprintf;
27
28
/**
29
 * @internal
30
 */
31
class Collector
32
{
33
    /** @var Schema */
34
    private $schema;
35
36
    /** @var Runtime */
37
    private $runtime;
38
39
    /** @var OperationDefinitionNode|null */
40
    public $operation = null;
41
42
    /** @var FragmentDefinitionNode[] */
43
    public $fragments = [];
44
45
    /** @var ObjectType|null */
46
    public $rootType;
47
48
    /** @var FieldNode[][] */
49
    private $fields;
50
51
    /** @var string[] */
52
    private $visitedFragments;
53
54 22
    public function __construct(Schema $schema, Runtime $runtime)
55
    {
56 22
        $this->schema  = $schema;
57 22
        $this->runtime = $runtime;
58 22
    }
59
60 22
    public function initialize(DocumentNode $documentNode, ?string $operationName = null)
61
    {
62 22
        $hasMultipleAssumedOperations = false;
63
64 22
        foreach ($documentNode->definitions as $definitionNode) {
65
            /** @var DefinitionNode|Node $definitionNode */
66
67 22
            if ($definitionNode->kind === NodeKind::OPERATION_DEFINITION) {
68
                /** @var OperationDefinitionNode $definitionNode */
69 22
                if ($operationName === null && $this->operation !== null) {
70
                    $hasMultipleAssumedOperations = true;
71
                }
72 22
                if ($operationName === null ||
73 22
                    (isset($definitionNode->name) && $definitionNode->name->value === $operationName)
74
                ) {
75 22
                    $this->operation = $definitionNode;
76
                }
77 6
            } elseif ($definitionNode->kind === NodeKind::FRAGMENT_DEFINITION) {
78
                /** @var FragmentDefinitionNode $definitionNode */
79 22
                $this->fragments[$definitionNode->name->value] = $definitionNode;
80
            }
81
        }
82
83 22
        if ($this->operation === null) {
84
            if ($operationName !== null) {
85
                $this->runtime->addError(new Error(sprintf('Unknown operation named "%s".', $operationName)));
86
            } else {
87
                $this->runtime->addError(new Error('Must provide an operation.'));
88
            }
89
            return;
90
        }
91
92 22
        if ($hasMultipleAssumedOperations) {
93
            $this->runtime->addError(new Error('Must provide operation name if query contains multiple operations.'));
94
            return;
95
        }
96
97 22
        if ($this->operation->operation === 'query') {
98 22
            $this->rootType = $this->schema->getQueryType();
99
        } elseif ($this->operation->operation === 'mutation') {
100
            $this->rootType = $this->schema->getMutationType();
101
        } elseif ($this->operation->operation === 'subscription') {
102
            $this->rootType = $this->schema->getSubscriptionType();
103
        } else {
104
            $this->runtime->addError(new Error(sprintf('Cannot initialize collector with operation type "%s".', $this->operation->operation)));
105
        }
106 22
    }
107
108
    /**
109
     * @return Generator
110
     */
111 22
    public function collectFields(ObjectType $runtimeType, ?SelectionSetNode $selectionSet)
112
    {
113 22
        $this->fields           = [];
114 22
        $this->visitedFragments = [];
115
116 22
        $this->doCollectFields($runtimeType, $selectionSet);
117
118 22
        foreach ($this->fields as $resultName => $fieldNodes) {
119 13
            $fieldNode = $fieldNodes[0];
120 13
            $fieldName = $fieldNode->name->value;
121
122 13
            $argumentValueMap = null;
123 13
            if (! empty($fieldNode->arguments)) {
124 13
                foreach ($fieldNode->arguments as $argumentNode) {
125 11
                    $argumentValueMap                             = $argumentValueMap ?? [];
126 11
                    $argumentValueMap[$argumentNode->name->value] = $argumentNode->value;
127
                }
128
            }
129
130 13
            if ($fieldName !== Introspection::TYPE_NAME_FIELD_NAME &&
131 13
                ! ($runtimeType === $this->schema->getQueryType() && ($fieldName === Introspection::SCHEMA_FIELD_NAME || $fieldName === Introspection::TYPE_FIELD_NAME)) &&
132 13
                ! $runtimeType->hasField($fieldName)
133
            ) {
134
                // do not emit error
135
                continue;
136
            }
137
138 13
            yield new CoroutineContextShared($fieldNodes, $fieldName, $resultName, $argumentValueMap);
139
        }
140 22
    }
141
142 22
    private function doCollectFields(ObjectType $runtimeType, ?SelectionSetNode $selectionSet)
143
    {
144 22
        if ($selectionSet === null) {
145
            return;
146
        }
147
148 22
        foreach ($selectionSet->selections as $selection) {
149
            /** @var FieldNode|FragmentSpreadNode|InlineFragmentNode $selection */
150
151 22
            if (! empty($selection->directives)) {
152 22
                foreach ($selection->directives as $directiveNode) {
153 16
                    if ($directiveNode->name->value === Directive::SKIP_NAME) {
154
                        /** @var ValueNode|null $condition */
155 8
                        $condition = null;
156 8
                        foreach ($directiveNode->arguments as $argumentNode) {
157 8
                            if ($argumentNode->name->value === Directive::IF_ARGUMENT_NAME) {
158 8
                                $condition = $argumentNode->value;
159 8
                                break;
160
                            }
161
                        }
162
163 8
                        if ($condition === null) {
164
                            $this->runtime->addError(new Error(
165
                                sprintf('@%s directive is missing "%s" argument.', Directive::SKIP_NAME, Directive::IF_ARGUMENT_NAME),
166
                                $selection
167
                            ));
168
                        } else {
169 8
                            if ($this->runtime->evaluate($condition, Type::boolean()) === true) {
170 8
                                continue 2; // !!! advances outer loop
171
                            }
172
                        }
173 10
                    } elseif ($directiveNode->name->value === Directive::INCLUDE_NAME) {
174
                        /** @var ValueNode|null $condition */
175 10
                        $condition = null;
176 10
                        foreach ($directiveNode->arguments as $argumentNode) {
177 10
                            if ($argumentNode->name->value === Directive::IF_ARGUMENT_NAME) {
178 10
                                $condition = $argumentNode->value;
179 10
                                break;
180
                            }
181
                        }
182
183 10
                        if ($condition === null) {
184
                            $this->runtime->addError(new Error(
185
                                sprintf('@%s directive is missing "%s" argument.', Directive::INCLUDE_NAME, Directive::IF_ARGUMENT_NAME),
186
                                $selection
187
                            ));
188
                        } else {
189 10
                            if ($this->runtime->evaluate($condition, Type::boolean()) !== true) {
190 13
                                continue 2; // !!! advances outer loop
191
                            }
192
                        }
193
                    }
194
                }
195
            }
196
197 13
            if ($selection->kind === NodeKind::FIELD) {
198
                /** @var FieldNode $selection */
199
200 13
                $resultName = $selection->alias ? $selection->alias->value : $selection->name->value;
201
202 13
                if (! isset($this->fields[$resultName])) {
203 13
                    $this->fields[$resultName] = [];
204
                }
205
206 13
                $this->fields[$resultName][] = $selection;
207 5
            } elseif ($selection->kind === NodeKind::FRAGMENT_SPREAD) {
208
                /** @var FragmentSpreadNode $selection */
209
210 3
                $fragmentName = $selection->name->value;
211
212 3
                if (isset($this->visitedFragments[$fragmentName])) {
213
                    continue;
214 3
                } elseif (! isset($this->fragments[$fragmentName])) {
215
                    $this->runtime->addError(new Error(
216
                        sprintf('Fragment "%s" does not exist.', $fragmentName),
217
                        $selection
218
                    ));
219
                    continue;
220
                }
221
222 3
                $this->visitedFragments[$fragmentName] = true;
223
224 3
                $fragmentDefinition = $this->fragments[$fragmentName];
225 3
                $conditionTypeName  = $fragmentDefinition->typeCondition->name->value;
226
227 3
                if (! $this->schema->hasType($conditionTypeName)) {
228
                    $this->runtime->addError(new Error(
229
                        sprintf('Cannot spread fragment "%s", type "%s" does not exist.', $fragmentName, $conditionTypeName),
230
                        $selection
231
                    ));
232
                    continue;
233
                }
234
235 3
                $conditionType = $this->schema->getType($conditionTypeName);
236
237 3
                if ($conditionType instanceof ObjectType) {
238 3
                    if ($runtimeType->name !== $conditionType->name) {
239 3
                        continue;
240
                    }
241
                } elseif ($conditionType instanceof AbstractType) {
242
                    if (! $this->schema->isPossibleType($conditionType, $runtimeType)) {
243
                        continue;
244
                    }
245
                }
246
247 3
                $this->doCollectFields($runtimeType, $fragmentDefinition->selectionSet);
248 2
            } elseif ($selection->kind === NodeKind::INLINE_FRAGMENT) {
249
                /** @var InlineFragmentNode $selection */
250
251 2
                if ($selection->typeCondition !== null) {
252 2
                    $conditionTypeName = $selection->typeCondition->name->value;
253
254 2
                    if (! $this->schema->hasType($conditionTypeName)) {
255
                        $this->runtime->addError(new Error(
256
                            sprintf('Cannot spread inline fragment, type "%s" does not exist.', $conditionTypeName),
257
                            $selection
258
                        ));
259
                        continue;
260
                    }
261
262 2
                    $conditionType = $this->schema->getType($conditionTypeName);
263
264 2
                    if ($conditionType instanceof ObjectType) {
265 2
                        if ($runtimeType->name !== $conditionType->name) {
266 2
                            continue;
267
                        }
268
                    } elseif ($conditionType instanceof AbstractType) {
269
                        if (! $this->schema->isPossibleType($conditionType, $runtimeType)) {
270
                            continue;
271
                        }
272
                    }
273
                }
274
275 13
                $this->doCollectFields($runtimeType, $selection->selectionSet);
276
            }
277
        }
278 22
    }
279
}
280