Completed
Pull Request — master (#45)
by Christoffer
02:17
created

ValidationContext::getVariableUsages()   B

Complexity

Conditions 4
Paths 2

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 14
nc 2
nop 1
dl 0
loc 27
rs 8.5806
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\AST\Node\DocumentNode;
7
use Digia\GraphQL\Language\AST\Node\FieldNode;
8
use Digia\GraphQL\Language\AST\Node\FragmentDefinitionNode;
9
use Digia\GraphQL\Language\AST\Node\FragmentSpreadNode;
10
use Digia\GraphQL\Language\AST\Node\NodeInterface;
11
use Digia\GraphQL\Language\AST\Node\OperationDefinitionNode;
12
use Digia\GraphQL\Language\AST\Node\SelectionSetNode;
13
use Digia\GraphQL\Language\AST\Node\VariableDefinitionNode;
14
use Digia\GraphQL\Language\AST\Node\VariableNode;
15
use Digia\GraphQL\Language\AST\Visitor\TypeInfoVisitor;
16
use Digia\GraphQL\Language\AST\Visitor\Visitor;
17
use Digia\GraphQL\Type\Definition\Argument;
18
use Digia\GraphQL\Type\Definition\Directive;
19
use Digia\GraphQL\Type\Definition\Field;
20
use Digia\GraphQL\Type\Definition\TypeInterface;
21
use Digia\GraphQL\Type\SchemaInterface;
22
use Digia\GraphQL\Util\TypeInfo;
23
24
class ValidationContext
25
{
26
    /**
27
     * @var SchemaInterface
28
     */
29
    protected $schema;
30
31
    /**
32
     * @var DocumentNode
33
     */
34
    protected $documentNode;
35
36
    /**
37
     * @var TypeInfo
38
     */
39
    protected $typeInfo;
40
41
    /**
42
     * @var array|ValidationException[]
43
     */
44
    protected $errors = [];
45
46
    /**
47
     * @var array|FragmentDefinitionNode[]
48
     */
49
    protected $fragments = [];
50
51
    /**
52
     * @var array
53
     */
54
    protected $fragmentSpreads = [];
55
56
    /**
57
     * @var array
58
     */
59
    protected $variableUsages = [];
60
61
    /**
62
     * @var array
63
     */
64
    protected $recursiveVariableUsages = [];
65
66
    /**
67
     * @var array
68
     */
69
    protected $recursivelyReferencedFragment = [];
70
71
    /**
72
     * ValidationContext constructor.
73
     * @param SchemaInterface $schema
74
     * @param DocumentNode    $documentNode
75
     * @param TypeInfo        $typeInfo
76
     */
77
    public function __construct(SchemaInterface $schema, DocumentNode $documentNode, TypeInfo $typeInfo)
78
    {
79
        $this->schema       = $schema;
80
        $this->documentNode = $documentNode;
81
        $this->typeInfo     = $typeInfo;
82
    }
83
84
    /**
85
     * @param ValidationException $error
86
     */
87
    public function reportError(ValidationException $error): void
88
    {
89
        $this->errors[] = $error;
90
    }
91
92
    /**
93
     * @return array|ValidationException[]
94
     */
95
    public function getErrors(): array
96
    {
97
        return $this->errors;
98
    }
99
100
    /**
101
     * @return TypeInterface|null
102
     */
103
    public function getParentType(): ?TypeInterface
104
    {
105
        return $this->typeInfo->getParentType();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->typeInfo->getParentType() could return the type Digia\GraphQL\Type\Defin...\CompositeTypeInterface which is incompatible with the type-hinted return null|Digia\GraphQL\Type\Definition\TypeInterface. Consider adding an additional type-check to rule them out.
Loading history...
106
    }
107
108
    /**
109
     * @return Field|null
110
     */
111
    public function getFieldDefinition(): ?Field
112
    {
113
        return $this->typeInfo->getFieldDefinition();
114
    }
115
116
    /**
117
     * @return SchemaInterface
118
     */
119
    public function getSchema(): SchemaInterface
120
    {
121
        return $this->schema;
122
    }
123
124
    /**
125
     * @return Argument|null
126
     */
127
    public function getArgument(): ?Argument
128
    {
129
        return $this->typeInfo->getArgument();
130
    }
131
132
    /**
133
     * @return Directive|null
134
     */
135
    public function getDirective(): ?Directive
136
    {
137
        return $this->typeInfo->getDirective();
138
    }
139
140
    /**
141
     * @param string $name
142
     * @return FragmentDefinitionNode|null
143
     */
144
    public function getFragment(string $name): ?FragmentDefinitionNode
145
    {
146
        if (empty($this->fragments)) {
147
            $this->fragments = array_reduce($this->documentNode->getDefinitions(), function ($fragments, $definition) {
148
                if ($definition instanceof FragmentDefinitionNode) {
149
                    $fragments[$definition->getNameValue()] = $definition;
150
                }
151
                return $fragments;
152
            }, []);
153
        }
154
155
        return $this->fragments[$name] ?? null;
156
    }
157
158
    /**
159
     * @param SelectionSetNode $selectionSet
160
     * @return array|FragmentSpreadNode[]
161
     */
162
    public function getFragmentSpreads(SelectionSetNode $selectionSet): array
163
    {
164
        $spreads = $this->fragmentSpreads[(string)$selectionSet] ?? null;
165
166
        if (null === $spreads) {
167
            $spreads = [];
168
169
            $setsToVisit = [$selectionSet];
170
171
            while (!empty($setsToVisit)) {
172
                /** @var SelectionSetNode $set */
173
                $set = array_pop($setsToVisit);
174
175
                /** @var FieldNode $selection */
176
                foreach ($set->getSelections() as $selection) {
177
                    if ($selection instanceof FragmentSpreadNode) {
178
                        $spreads[] = $selection;
179
                    } elseif ($selection->hasSelectionSet()) {
180
                        $setsToVisit[] = $selection->getSelectionSet();
181
                    }
182
                }
183
            }
184
185
            $this->fragmentSpreads[(string)$selectionSet] = $spreads;
186
        }
187
188
        return $spreads;
189
    }
190
191
    /**
192
     * @param OperationDefinitionNode $operation
193
     * @return array
194
     */
195
    public function getRecursiveVariableUsages(OperationDefinitionNode $operation): array
196
    {
197
        $usages = $this->recursiveVariableUsages[(string)$operation] ?? null;
198
199
        if (null === $usages) {
200
            $usages    = $this->getVariableUsages($operation);
201
            $fragments = $this->getRecursivelyReferencedFragments($operation);
202
203
            foreach ($fragments as $fragment) {
204
                // TODO: Figure out a more performance way to do this.
205
                /** @noinspection SlowArrayOperationsInLoopInspection */
206
                $usages = array_merge($usages, $this->getVariableUsages($fragment));
207
            }
208
209
            $this->recursiveVariableUsages[(string)$operation] = $usages;
210
        }
211
212
        return $usages;
213
    }
214
215
    /**
216
     * @param OperationDefinitionNode|FragmentDefinitionNode $node
217
     * @return array
218
     */
219
    public function getVariableUsages(NodeInterface $node): array
220
    {
221
        $usages = $this->variableUsages[(string)$node] ?? null;
222
223
        if (null === $usages) {
224
            $usages   = [];
225
            $typeInfo = new TypeInfo($this->schema);
226
            $visitor  = new TypeInfoVisitor($typeInfo, new Visitor(
227
                function (NodeInterface $node) use (&$usages, $typeInfo): ?NodeInterface {
228
                    if ($node instanceof VariableDefinitionNode) {
229
                        return null;
230
                    }
231
232
                    if ($node instanceof VariableNode) {
233
                        $usages[] = ['node' => $node, 'type' => $typeInfo->getInputType()];
234
                    }
235
236
                    return $node;
237
                }
238
            ));
239
240
            $node->accept($visitor);
0 ignored issues
show
Bug introduced by
The method accept() does not exist on Digia\GraphQL\Language\AST\Node\NodeInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Digia\GraphQL\Language\A...DefinitionNodeInterface or Digia\GraphQL\Language\A...eExtensionNodeInterface or Digia\GraphQL\Language\AST\Node\ValueNodeInterface or Digia\GraphQL\Language\A...\SelectionNodeInterface or Digia\GraphQL\Language\AST\Node\TypeNodeInterface or Digia\GraphQL\Language\A...DefinitionNodeInterface or Digia\GraphQL\Language\A...DefinitionNodeInterface or Digia\GraphQL\Language\A...DefinitionNodeInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

240
            $node->/** @scrutinizer ignore-call */ 
241
                   accept($visitor);
Loading history...
241
242
            $this->variableUsages[(string)$node] = $usages;
243
        }
244
245
        return $usages;
246
    }
247
248
    /**
249
     * @param OperationDefinitionNode $operation
250
     * @return array|FragmentDefinitionNode[]
251
     */
252
    public function getRecursivelyReferencedFragments(OperationDefinitionNode $operation): array
253
    {
254
        $fragments = $this->recursivelyReferencedFragment[(string)$operation] ?? null;
255
256
        if (null === $fragments) {
257
            $fragments      = [];
258
            $collectedNames = [];
259
            $nodesToVisit   = [$operation->getSelectionSet()];
260
261
            while (!empty($nodesToVisit)) {
262
                $node    = array_pop($nodesToVisit);
263
                $spreads = $this->getFragmentSpreads($node);
264
265
                foreach ($spreads as $spread) {
266
                    $fragmentName = $spread->getNameValue();
267
268
                    if (!isset($collectedNames[$fragmentName])) {
269
                        $collectedNames[$fragmentName] = true;
270
                        $fragment                      = $this->getFragment($fragmentName);
271
272
                        if (null !== $fragment) {
273
                            $fragments[]    = $fragment;
274
                            $nodesToVisit[] = $fragment->getSelectionSet();
275
                        }
276
                    }
277
                }
278
            }
279
280
            $this->recursivelyReferencedFragment[(string)$operation] = $fragments;
281
        }
282
283
        return $fragments;
284
    }
285
}
286