Completed
Pull Request — master (#274)
by Christoffer
02:34
created

AcceptsVisitorsTrait::setIsEdited()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
3
namespace Digia\GraphQL\Language\Visitor;
4
5
use Digia\GraphQL\Language\Node\NodeInterface;
6
use Digia\GraphQL\Language\NodeBuilder;
7
use Digia\GraphQL\Language\NodeBuilderInterface;
8
use Digia\GraphQL\Util\SerializationInterface;
9
10
trait AcceptsVisitorsTrait
11
{
12
13
    /**
14
     * @var NodeBuilderInterface
15
     */
16
    private static $nodeBuilder;
17
18
    /**
19
     * @var VisitorInterface
20
     */
21
    protected $visitor;
22
23
    /**
24
     * @var string|int|null
25
     */
26
    protected $key;
27
28
    /**
29
     * @var NodeInterface|null
30
     */
31
    protected $parent;
32
33
    /**
34
     * @var array
35
     */
36
    protected $path;
37
38
    /**
39
     * @var array
40
     */
41
    protected $ancestors = [];
42
43
    /**
44
     * @var bool
45
     */
46
    protected $isEdited = false;
47
48
    /**
49
     * @param VisitorInterface   $visitor
50
     * @param mixed              $key
51
     * @param NodeInterface|null $parent
52
     * @param string[]           $path
53
     * @param NodeInterface[]    $ancestors
54
     * @return NodeInterface|SerializationInterface|null
55
     */
56
    public function acceptVisitor(
57
        VisitorInterface $visitor,
58
        $key = null,
59
        ?NodeInterface $parent = null,
60
        array $path = [],
61
        array $ancestors = []
62
    ): ?NodeInterface {
63
        $this->visitor   = $visitor;
64
        $this->key       = $key;
65
        $this->parent    = $parent;
66
        $this->path      = $path;
67
        $this->ancestors = $ancestors;
68
69
        /** @var NodeInterface $newNode */
70
        $newNode = clone $this; // TODO: Benchmark cloning
71
72
        // If the result was null, it means that we should not traverse this branch.
73
        if (null === ($newNode = $visitor->enterNode($newNode))) {
74
            return null;
75
        }
76
77
        // If the node was edited, we want to return early
78
        // to avoid visiting its sub-tree completely.
79
        if ($newNode instanceof AcceptsVisitorsInterface && $newNode->determineIsEdited($this)) {
80
            return $newNode;
81
        }
82
83
        $newAst = $newNode->toAST();
84
85
        foreach (self::$kindToNodesToVisitMap[$this->kind] as $propertyName) {
86
            $nodeOrNodes = $this->{$propertyName};
87
88
            if (empty($nodeOrNodes)) {
89
                continue;
90
            }
91
92
            $propertyAst = $this->visitNodeOrNodes($nodeOrNodes, $propertyName, $newNode);
93
94
            if (empty($propertyAst)) {
95
                continue;
96
            }
97
98
            $newAst[$propertyName] = $propertyAst;
99
        }
100
101
        $newNode = $this->createNode($newAst);
102
103
        return $visitor->leaveNode($newNode);
104
    }
105
106
    /**
107
     * @param array $ast
108
     * @return NodeInterface
109
     */
110
    protected function createNode(array $ast): NodeInterface
111
    {
112
        /** @var NodeInterface|AcceptsVisitorsInterface $newNode */
113
        $newNode = $this->getNodeBuilder()->build($ast);
114
115
        $newNode->setVisitor($this->visitor);
0 ignored issues
show
Bug introduced by
The method setVisitor() does not exist on Digia\GraphQL\Language\Node\NodeInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Digia\GraphQL\Language\N...mExtensionNodeInterface or Digia\GraphQL\Language\Node\SelectionNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\Node\FragmentNodeInterface or Digia\GraphQL\Language\Node\ValueNodeInterface or Digia\GraphQL\Language\Node\TypeNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\Node\NamedTypeNodeInterface. 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

115
        $newNode->/** @scrutinizer ignore-call */ 
116
                  setVisitor($this->visitor);
Loading history...
116
        $newNode->setKey($this->key);
0 ignored issues
show
Bug introduced by
The method setKey() does not exist on Digia\GraphQL\Language\Node\NodeInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Digia\GraphQL\Language\N...mExtensionNodeInterface or Digia\GraphQL\Language\Node\SelectionNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\Node\FragmentNodeInterface or Digia\GraphQL\Language\Node\ValueNodeInterface or Digia\GraphQL\Language\Node\TypeNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\Node\NamedTypeNodeInterface. 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

116
        $newNode->/** @scrutinizer ignore-call */ 
117
                  setKey($this->key);
Loading history...
117
        $newNode->setParent($this->parent);
0 ignored issues
show
Bug introduced by
The method setParent() does not exist on Digia\GraphQL\Language\Node\NodeInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Digia\GraphQL\Language\N...mExtensionNodeInterface or Digia\GraphQL\Language\Node\SelectionNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\Node\FragmentNodeInterface or Digia\GraphQL\Language\Node\ValueNodeInterface or Digia\GraphQL\Language\Node\TypeNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\Node\NamedTypeNodeInterface. 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

117
        $newNode->/** @scrutinizer ignore-call */ 
118
                  setParent($this->parent);
Loading history...
118
        $newNode->setPath($this->path);
0 ignored issues
show
Bug introduced by
The method setPath() does not exist on Digia\GraphQL\Language\Node\NodeInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Digia\GraphQL\Language\N...mExtensionNodeInterface or Digia\GraphQL\Language\Node\SelectionNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\Node\FragmentNodeInterface or Digia\GraphQL\Language\Node\ValueNodeInterface or Digia\GraphQL\Language\Node\TypeNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\Node\NamedTypeNodeInterface. 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

118
        $newNode->/** @scrutinizer ignore-call */ 
119
                  setPath($this->path);
Loading history...
119
        $newNode->setAncestors($this->ancestors);
0 ignored issues
show
Bug introduced by
The method setAncestors() does not exist on Digia\GraphQL\Language\Node\NodeInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Digia\GraphQL\Language\N...mExtensionNodeInterface or Digia\GraphQL\Language\Node\SelectionNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\Node\FragmentNodeInterface or Digia\GraphQL\Language\Node\ValueNodeInterface or Digia\GraphQL\Language\Node\TypeNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\Node\NamedTypeNodeInterface. 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

119
        $newNode->/** @scrutinizer ignore-call */ 
120
                  setAncestors($this->ancestors);
Loading history...
120
        $newNode->setIsEdited($this->isEdited);
0 ignored issues
show
Bug introduced by
The method setIsEdited() does not exist on Digia\GraphQL\Language\Node\NodeInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Digia\GraphQL\Language\N...mExtensionNodeInterface or Digia\GraphQL\Language\Node\SelectionNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\Node\FragmentNodeInterface or Digia\GraphQL\Language\Node\ValueNodeInterface or Digia\GraphQL\Language\Node\TypeNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\Node\NamedTypeNodeInterface. 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

120
        $newNode->/** @scrutinizer ignore-call */ 
121
                  setIsEdited($this->isEdited);
Loading history...
121
122
        return $newNode;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $newNode could return the type Digia\GraphQL\Language\V...cceptsVisitorsInterface which is incompatible with the type-hinted return Digia\GraphQL\Language\Node\NodeInterface. Consider adding an additional type-check to rule them out.
Loading history...
123
    }
124
125
    /**
126
     * @inheritdoc
127
     */
128
    public function determineIsEdited($node): bool
129
    {
130
        $this->isEdited = $this->isEdited || !$this->compareNode($node);
131
        return $this->isEdited;
132
    }
133
134
    /**
135
     * @return bool
136
     */
137
    public function isEdited(): bool
138
    {
139
        return $this->isEdited;
140
    }
141
142
    /**
143
     * @inheritdoc
144
     */
145
    public function getAncestor(int $depth = 1): ?NodeInterface
146
    {
147
        if (empty($this->ancestors)) {
148
            return null;
149
        }
150
151
        $index = \count($this->ancestors) - $depth;
152
153
        return $this->ancestors[$index] ?? null;
154
    }
155
156
    /**
157
     * @inheritdoc
158
     */
159
    public function getAncestors(): array
160
    {
161
        return $this->ancestors;
162
    }
163
164
    /**
165
     * @inheritdoc
166
     */
167
    public function getKey()
168
    {
169
        return $this->key;
170
    }
171
172
    /**
173
     * @inheritdoc
174
     */
175
    public function getParent(): ?NodeInterface
176
    {
177
        return $this->parent;
178
    }
179
180
    /**
181
     * @inheritdoc
182
     */
183
    public function getPath(): array
184
    {
185
        return $this->path;
186
    }
187
188
    /**
189
     * @param VisitorInterface $visitor
190
     * @return $this
191
     */
192
    public function setVisitor(VisitorInterface $visitor)
193
    {
194
        $this->visitor = $visitor;
195
        return $this;
196
    }
197
198
    /**
199
     * @param mixed $key
200
     * @return $this
201
     */
202
    public function setKey($key)
203
    {
204
        $this->key = $key;
205
        return $this;
206
    }
207
208
    /**
209
     * @param NodeInterface|null $parent
210
     * @return $this
211
     */
212
    public function setParent(?NodeInterface $parent)
213
    {
214
        $this->parent = $parent;
215
        return $this;
216
    }
217
218
    /**
219
     * @param array $path
220
     * @return $this
221
     */
222
    public function setPath(array $path)
223
    {
224
        $this->path = $path;
225
        return $this;
226
    }
227
228
    /**
229
     * @param array $ancestors
230
     * @return $this
231
     */
232
    public function setAncestors(array $ancestors)
233
    {
234
        $this->ancestors = $ancestors;
235
        return $this;
236
    }
237
238
    /**
239
     * @param bool $isEdited
240
     * @return $this
241
     */
242
    public function setIsEdited(bool $isEdited)
243
    {
244
        $this->isEdited = $isEdited;
245
        return $this;
246
    }
247
248
    /**
249
     * @param NodeInterface|NodeInterface[] $nodeOrNodes
250
     * @param mixed                         $key
251
     * @param NodeInterface                 $parent
252
     * @return array|null
253
     */
254
    protected function visitNodeOrNodes($nodeOrNodes, $key, NodeInterface $parent): ?array
255
    {
256
        $this->addAncestor($parent);
257
258
        $ast = \is_array($nodeOrNodes)
259
            ? $this->visitNodes($nodeOrNodes, $key)
260
            : $this->visitNode($nodeOrNodes, $key, $parent);
261
262
        $this->removeAncestor();
263
264
        return $ast;
265
    }
266
267
    /**
268
     * @param NodeInterface[]    $nodes
269
     * @param string|int         $key
270
     * @param NodeInterface|null $parent
271
     * @return array
272
     */
273
    protected function visitNodes(array $nodes, $key): array
274
    {
275
        $this->addOneToPath($key);
276
277
        $ast   = [];
278
        $index = 0;
279
280
        foreach ($nodes as $node) {
281
            $nodeAst = $this->visitNode($node, $index, null);
282
283
            if (null !== $nodeAst) {
284
                $ast[$index] = $nodeAst;
285
                $index++;
286
            }
287
        }
288
289
        $this->removeOneFromPath();
290
291
        return $ast;
292
    }
293
294
    /**
295
     * @param NodeInterface|AcceptsVisitorsTrait $node
296
     * @param string|int                         $key
297
     * @param NodeInterface|null                 $parent
298
     * @return array|null
299
     */
300
    protected function visitNode($node, $key, ?NodeInterface $parent): ?array
301
    {
302
        $this->addOneToPath($key);
303
304
        /** @var NodeInterface|AcceptsVisitorsTrait $newNode */
305
        $newNode = $node->acceptVisitor($this->visitor, $key, $parent, $this->path, $this->ancestors);
0 ignored issues
show
Bug introduced by
The method acceptVisitor() does not exist on Digia\GraphQL\Language\Node\NodeInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Digia\GraphQL\Language\N...mExtensionNodeInterface or Digia\GraphQL\Language\Node\SelectionNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\Node\FragmentNodeInterface or Digia\GraphQL\Language\Node\ValueNodeInterface or Digia\GraphQL\Language\Node\TypeNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\Node\NamedTypeNodeInterface. 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

305
        /** @scrutinizer ignore-call */ 
306
        $newNode = $node->acceptVisitor($this->visitor, $key, $parent, $this->path, $this->ancestors);
Loading history...
306
307
        // If the node was edited, we need to revisit it to produce the expected result.
308
        if (null !== $newNode && $newNode->isEdited()) {
0 ignored issues
show
Bug introduced by
The method isEdited() does not exist on Digia\GraphQL\Language\Node\NodeInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Digia\GraphQL\Language\N...mExtensionNodeInterface or Digia\GraphQL\Language\Node\SelectionNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\Node\FragmentNodeInterface or Digia\GraphQL\Language\Node\ValueNodeInterface or Digia\GraphQL\Language\Node\TypeNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\Node\NamedTypeNodeInterface. 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

308
        if (null !== $newNode && $newNode->/** @scrutinizer ignore-call */ isEdited()) {
Loading history...
309
            $newNode = $newNode->acceptVisitor($this->visitor, $key, $parent, $this->path, $this->ancestors);
310
        }
311
312
        $this->removeOneFromPath();
313
314
        return null !== $newNode ? $newNode->toAST() : null;
0 ignored issues
show
Bug introduced by
It seems like toAST() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

314
        return null !== $newNode ? $newNode->/** @scrutinizer ignore-call */ toAST() : null;
Loading history...
315
    }
316
317
    /**
318
     * @param NodeInterface $other
319
     * @return bool
320
     */
321
    protected function compareNode(NodeInterface $other)
322
    {
323
        // TODO: Figure out a better way to solve this.
324
        return $this->toJSON() === $other->toJSON();
0 ignored issues
show
Bug introduced by
It seems like toJSON() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

324
        return $this->/** @scrutinizer ignore-call */ toJSON() === $other->toJSON();
Loading history...
325
    }
326
327
    /**
328
     * Appends a key to the path.
329
     * @param string $key
330
     */
331
    protected function addOneToPath(string $key)
332
    {
333
        $this->path[] = $key;
334
    }
335
336
    /**
337
     * Removes the last item from the path.
338
     */
339
    protected function removeOneFromPath()
340
    {
341
        $this->path = \array_slice($this->path, 0, -1);
342
    }
343
344
    /**
345
     * Adds an ancestor.
346
     * @param NodeInterface $node
347
     */
348
    protected function addAncestor(NodeInterface $node)
349
    {
350
        $this->ancestors[] = $node;
351
    }
352
353
    /**
354
     *  Removes the last ancestor.
355
     */
356
    protected function removeAncestor()
357
    {
358
        $this->ancestors = \array_slice($this->ancestors, 0, -1);
359
    }
360
361
    /**
362
     * @return NodeBuilderInterface
363
     */
364
    protected function getNodeBuilder(): NodeBuilderInterface
365
    {
366
        if (null === self::$nodeBuilder) {
367
            self::$nodeBuilder = new NodeBuilder();
368
        }
369
370
        return self::$nodeBuilder;
371
    }
372
373
    /**
374
     * @var array
375
     */
376
    protected static $kindToNodesToVisitMap = [
377
        'Name' => [],
378
379
        'Document'            => ['definitions'],
380
        'OperationDefinition' => [
381
            'name',
382
            'variableDefinitions',
383
            'directives',
384
            'selectionSet',
385
        ],
386
        'VariableDefinition'  => ['variable', 'type', 'defaultValue'],
387
        'Variable'            => ['name'],
388
        'SelectionSet'        => ['selections'],
389
        'Field'               => ['alias', 'name', 'arguments', 'directives', 'selectionSet'],
390
        'Argument'            => ['name', 'value'],
391
392
        'FragmentSpread'     => ['name', 'directives'],
393
        'InlineFragment'     => ['typeCondition', 'directives', 'selectionSet'],
394
        'FragmentDefinition' => [
395
            'name',
396
            // Note: fragment variable definitions are experimental and may be changed or removed in the future.
397
            'variableDefinitions',
398
            'typeCondition',
399
            'directives',
400
            'selectionSet',
401
        ],
402
403
        'IntValue'     => [],
404
        'FloatValue'   => [],
405
        'StringValue'  => [],
406
        'BooleanValue' => [],
407
        'NullValue'    => [],
408
        'EnumValue'    => [],
409
        'ListValue'    => ['values'],
410
        'ObjectValue'  => ['fields'],
411
        'ObjectField'  => ['name', 'value'],
412
413
        'Directive' => ['name', 'arguments'],
414
415
        'NamedType'   => ['name'],
416
        'ListType'    => ['type'],
417
        'NonNullType' => ['type'],
418
419
        'SchemaDefinition'        => ['directives', 'operationTypes'],
420
        'OperationTypeDefinition' => ['type'],
421
422
        'ScalarTypeDefinition'      => ['description', 'name', 'directives'],
423
        'ObjectTypeDefinition'      => [
424
            'description',
425
            'name',
426
            'interfaces',
427
            'directives',
428
            'fields',
429
        ],
430
        'FieldDefinition'           => ['description', 'name', 'arguments', 'type', 'directives'],
431
        'InputValueDefinition'      => [
432
            'description',
433
            'name',
434
            'type',
435
            'defaultValue',
436
            'directives',
437
        ],
438
        'InterfaceTypeDefinition'   => ['description', 'name', 'directives', 'fields'],
439
        'UnionTypeDefinition'       => ['description', 'name', 'directives', 'types'],
440
        'EnumTypeDefinition'        => ['description', 'name', 'directives', 'values'],
441
        'EnumValueDefinition'       => ['description', 'name', 'directives'],
442
        'InputObjectTypeDefinition' => ['description', 'name', 'directives', 'fields'],
443
444
        'DirectiveDefinition' => ['description', 'name', 'arguments', 'locations'],
445
446
        'SchemaExtension' => ['directives', 'operationTypes'],
447
448
        'ScalarTypeExtension'      => ['name', 'directives'],
449
        'ObjectTypeExtension'      => ['name', 'interfaces', 'directives', 'fields'],
450
        'InterfaceTypeExtension'   => ['name', 'directives', 'fields'],
451
        'UnionTypeExtension'       => ['name', 'directives', 'types'],
452
        'EnumTypeExtension'        => ['name', 'directives', 'values'],
453
        'InputObjectTypeExtension' => ['name', 'directives', 'fields'],
454
    ];
455
}
456