Completed
Pull Request — master (#270)
by Christoffer
02:27
created

ValidationContext::getFragmentSpreads()   B

Complexity

Conditions 7
Paths 2

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 13
nc 2
nop 1
dl 0
loc 26
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
namespace Digia\GraphQL\Validation;
4
5
use Digia\GraphQL\Error\ValidationException;
6
use Digia\GraphQL\Language\Node\DocumentNode;
7
use Digia\GraphQL\Language\Node\FieldNode;
8
use Digia\GraphQL\Language\Node\FragmentDefinitionNode;
9
use Digia\GraphQL\Language\Node\FragmentSpreadNode;
10
use Digia\GraphQL\Language\Node\NodeInterface;
11
use Digia\GraphQL\Language\Node\OperationDefinitionNode;
12
use Digia\GraphQL\Language\Node\SelectionSetAwareInterface;
13
use Digia\GraphQL\Language\Node\SelectionSetNode;
14
use Digia\GraphQL\Language\Node\VariableDefinitionNode;
15
use Digia\GraphQL\Language\Node\VariableNode;
16
use Digia\GraphQL\Language\Visitor\AcceptsVisitorsInterface;
17
use Digia\GraphQL\Language\Visitor\TypeInfoVisitor;
18
use Digia\GraphQL\Language\Visitor\Visitor;
19
use Digia\GraphQL\Schema\Schema;
20
use Digia\GraphQL\Type\Definition\Argument;
21
use Digia\GraphQL\Type\Definition\Directive;
22
use Digia\GraphQL\Type\Definition\Field;
23
use Digia\GraphQL\Type\Definition\TypeInterface;
24
use Digia\GraphQL\Util\TypeInfo;
25
26
class ValidationContext implements ValidationContextInterface
27
{
28
    /**
29
     * @var Schema
30
     */
31
    protected $schema;
32
33
    /**
34
     * @var DocumentNode
35
     */
36
    protected $document;
37
38
    /**
39
     * @var TypeInfo
40
     */
41
    protected $typeInfo;
42
43
    /**
44
     * @var array|ValidationException[]
45
     */
46
    protected $errors = [];
47
48
    /**
49
     * @var array|FragmentDefinitionNode[]
50
     */
51
    protected $fragments = [];
52
53
    /**
54
     * @var array
55
     */
56
    protected $fragmentSpreads = [];
57
58
    /**
59
     * @var array
60
     */
61
    protected $variableUsages = [];
62
63
    /**
64
     * @var array
65
     */
66
    protected $recursiveVariableUsages = [];
67
68
    /**
69
     * @var array
70
     */
71
    protected $recursivelyReferencedFragment = [];
72
73
    /**
74
     * ValidationContext constructor.
75
     * @param Schema       $schema
76
     * @param DocumentNode $document
77
     * @param TypeInfo     $typeInfo
78
     */
79
    public function __construct(Schema $schema, DocumentNode $document, TypeInfo $typeInfo)
80
    {
81
        $this->schema   = $schema;
82
        $this->document = $document;
83
        $this->typeInfo = $typeInfo;
84
    }
85
86
    /**
87
     * @inheritdoc
88
     */
89
    public function reportError(ValidationException $error): void
90
    {
91
        $this->errors[] = $error;
92
    }
93
94
    /**
95
     * @inheritdoc
96
     */
97
    public function getErrors(): array
98
    {
99
        return $this->errors;
100
    }
101
102
    /**
103
     * @return TypeInterface|null
104
     */
105
    public function getType(): ?TypeInterface
106
    {
107
        return $this->typeInfo->getType();
108
    }
109
110
    /**
111
     * @inheritdoc
112
     */
113
    public function getParentType(): ?TypeInterface
114
    {
115
        return $this->typeInfo->getParentType();
116
    }
117
118
    /**
119
     * @inheritdoc
120
     */
121
    public function getInputType(): ?TypeInterface
122
    {
123
        return $this->typeInfo->getInputType();
124
    }
125
126
    /**
127
     * @inheritdoc
128
     */
129
    public function getParentInputType(): ?TypeInterface
130
    {
131
        return $this->typeInfo->getParentInputType();
132
    }
133
134
    /**
135
     * @inheritdoc
136
     */
137
    public function getFieldDefinition(): ?Field
138
    {
139
        return $this->typeInfo->getFieldDefinition();
140
    }
141
142
    /**
143
     * @inheritdoc
144
     */
145
    public function getSchema(): Schema
146
    {
147
        return $this->schema;
148
    }
149
150
    /**
151
     * @inheritdoc
152
     */
153
    public function getArgument(): ?Argument
154
    {
155
        return $this->typeInfo->getArgument();
156
    }
157
158
    /**
159
     * @inheritdoc
160
     */
161
    public function getDirective(): ?Directive
162
    {
163
        return $this->typeInfo->getDirective();
164
    }
165
166
    /**
167
     * @inheritdoc
168
     */
169
    public function getFragment(string $name): ?FragmentDefinitionNode
170
    {
171
        if (empty($this->fragments)) {
172
            $this->fragments = array_reduce($this->document->getDefinitions(), function ($fragments, $definition) {
173
                if ($definition instanceof FragmentDefinitionNode) {
174
                    $fragments[$definition->getNameValue()] = $definition;
175
                }
176
                return $fragments;
177
            }, []);
178
        }
179
180
        return $this->fragments[$name] ?? null;
181
    }
182
183
    /**
184
     * @inheritdoc
185
     */
186
    public function getFragmentSpreads(SelectionSetNode $selectionSet): array
187
    {
188
        $spreads = $this->fragmentSpreads[(string)$selectionSet] ?? null;
189
190
        if (null === $spreads) {
191
            $spreads = [];
192
193
            $setsToVisit = [$selectionSet];
194
195
            while (!empty($setsToVisit)) {
196
                /** @var SelectionSetNode $set */
197
                $set = array_pop($setsToVisit);
198
199
                foreach ($set->getSelections() as $selection) {
200
                    if ($selection instanceof FragmentSpreadNode) {
201
                        $spreads[] = $selection;
202
                    } elseif ($selection instanceof SelectionSetAwareInterface && $selection->hasSelectionSet()) {
203
                        $setsToVisit[] = $selection->getSelectionSet();
204
                    }
205
                }
206
            }
207
208
            $this->fragmentSpreads[(string)$selectionSet] = $spreads;
209
        }
210
211
        return $spreads;
212
    }
213
214
    /**
215
     * @inheritdoc
216
     */
217
    public function getRecursiveVariableUsages(OperationDefinitionNode $operation): array
218
    {
219
        $usages = $this->recursiveVariableUsages[(string)$operation] ?? null;
220
221
        if (null === $usages) {
222
            $usages    = $this->getVariableUsages($operation);
223
            $fragments = $this->getRecursivelyReferencedFragments($operation);
224
225
            foreach ($fragments as $fragment) {
226
                // TODO: Figure out a more performance way to do this.
227
                /** @noinspection SlowArrayOperationsInLoopInspection */
228
                $usages = array_merge($usages, $this->getVariableUsages($fragment));
229
            }
230
231
            $this->recursiveVariableUsages[(string)$operation] = $usages;
232
        }
233
234
        return $usages;
235
    }
236
237
    /**
238
     * @inheritdoc
239
     */
240
    public function getVariableUsages($node): array
241
    {
242
        $usages = $this->variableUsages[(string)$node] ?? null;
243
244
        if (null === $usages) {
245
            $usages   = [];
246
            $typeInfo = new TypeInfo($this->schema);
247
            $visitor  = new TypeInfoVisitor($typeInfo, new Visitor(
248
                function (NodeInterface $node) use (&$usages, $typeInfo): ?NodeInterface {
249
                    if ($node instanceof VariableDefinitionNode) {
250
                        return null;
251
                    }
252
253
                    if ($node instanceof VariableNode) {
254
                        $usages[] = ['node' => $node, 'type' => $typeInfo->getInputType()];
255
                    }
256
257
                    return $node;
258
                }
259
            ));
260
261
            $node->acceptVisitor($visitor);
262
263
            $this->variableUsages[(string)$node] = $usages;
264
        }
265
266
        return $usages;
267
    }
268
269
    /**
270
     * @inheritdoc
271
     */
272
    public function getRecursivelyReferencedFragments(OperationDefinitionNode $operation): array
273
    {
274
        $fragments = $this->recursivelyReferencedFragment[(string)$operation] ?? null;
275
276
        if (null === $fragments) {
277
            $fragments      = [];
278
            $collectedNames = [];
279
            $nodesToVisit   = [$operation->getSelectionSet()];
280
281
            while (!empty($nodesToVisit)) {
282
                $node    = array_pop($nodesToVisit);
283
                $spreads = $this->getFragmentSpreads($node);
284
285
                foreach ($spreads as $spread) {
286
                    $fragmentName = $spread->getNameValue();
287
288
                    if (!isset($collectedNames[$fragmentName])) {
289
                        $collectedNames[$fragmentName] = true;
290
                        $fragment                      = $this->getFragment($fragmentName);
291
292
                        if (null !== $fragment) {
293
                            $fragments[]    = $fragment;
294
                            $nodesToVisit[] = $fragment->getSelectionSet();
295
                        }
296
                    }
297
                }
298
            }
299
300
            $this->recursivelyReferencedFragment[(string)$operation] = $fragments;
301
        }
302
303
        return $fragments;
304
    }
305
}
306