Completed
Push — master ( c9c830...8d8462 )
by Vladimir
89:58 queued 86:21
created

Collector::collectFields()   B

Complexity

Conditions 9
Paths 5

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 9

Importance

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