Passed
Pull Request — master (#274)
by Christoffer
02:38
created

AbstractNode::visitNodeOrNodes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 3
dl 0
loc 11
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Digia\GraphQL\Language\Node;
4
5
use Digia\GraphQL\GraphQL;
6
use Digia\GraphQL\Language\Location;
7
use Digia\GraphQL\Language\NodeBuilderInterface;
8
use Digia\GraphQL\Language\Visitor\VisitorInterface;
9
use Digia\GraphQL\Util\ArrayToJsonTrait;
10
use Digia\GraphQL\Util\SerializationInterface;
11
12
abstract class AbstractNode implements NodeInterface, SerializationInterface
13
{
14
    use ArrayToJsonTrait;
15
16
    /**
17
     * @var string
18
     */
19
    protected $kind;
20
21
    /**
22
     * @var Location|null
23
     */
24
    protected $location;
25
26
    /**
27
     * @var VisitorInterface
28
     */
29
    protected $visitor;
30
31
    /**
32
     * @var string|int|null
33
     */
34
    protected $key;
35
36
    /**
37
     * @var NodeInterface|null
38
     */
39
    protected $parent;
40
41
    /**
42
     * @var array
43
     */
44
    protected $path;
45
46
    /**
47
     * @var array
48
     */
49
    protected $ancestors = [];
50
51
    /**
52
     * @var bool
53
     */
54
    protected $isEdited = false;
55
56
    /**
57
     * @var NodeBuilderInterface
58
     */
59
    private static $nodeBuilder;
60
61
    /**
62
     * @return array
63
     */
64
    abstract public function toAST(): array;
65
66
    /**
67
     * AbstractNode constructor.
68
     *
69
     * @param string        $kind
70
     * @param Location|null $location
71
     */
72
    public function __construct(string $kind, ?Location $location)
73
    {
74
        $this->kind     = $kind;
75
        $this->location = $location;
76
    }
77
78
    /**
79
     * @return string
80
     */
81
    public function getKind(): string
82
    {
83
        return $this->kind;
84
    }
85
86
    /**
87
     * @return bool
88
     */
89
    public function hasLocation(): bool
90
    {
91
        return null !== $this->location;
92
    }
93
94
    /**
95
     * @return Location|null
96
     */
97
    public function getLocation(): ?Location
98
    {
99
        return $this->location;
100
    }
101
102
    /**
103
     * @return array|null
104
     */
105
    public function getLocationAST(): ?array
106
    {
107
        return null !== $this->location
108
            ? $this->location->toArray()
109
            : null;
110
    }
111
112
    /**
113
     * @return array
114
     */
115
    public function toArray(): array
116
    {
117
        return $this->toAST();
118
    }
119
120
    /**
121
     * @return string
122
     */
123
    public function __toString(): string
124
    {
125
        return $this->toJSON();
126
    }
127
128
    /**
129
     * @param VisitorInterface   $visitor
130
     * @param mixed              $key
131
     * @param NodeInterface|null $parent
132
     * @param string[]           $path
133
     * @param NodeInterface[]    $ancestors
134
     * @return NodeInterface|null
135
     */
136
    public function acceptVisitor(
137
        VisitorInterface $visitor,
138
        $key = null,
139
        ?NodeInterface $parent = null,
140
        array $path = [],
141
        array $ancestors = []
142
    ): ?NodeInterface {
143
        $this->visitor   = $visitor;
144
        $this->key       = $key;
145
        $this->parent    = $parent;
146
        $this->path      = $path;
147
        $this->ancestors = $ancestors;
148
149
        // If the result was null, it means that we should not traverse this branch.
150
        if (null === ($newNode = $visitor->enterNode($this))) {
151
            return null;
152
        }
153
154
        // If the node was edited, we want to return early to avoid visiting its sub-tree completely.
155
        if ($newNode->determineIsEdited($this)) {
156
            return $newNode;
157
        }
158
159
        $newAst = $newNode->toAST();
160
161
        // We have to manually copy the source, because it's not included in the AST.
162
        // The reason for this is because then it would also be included when the node is serialized.
163
        $newAst['loc']['source'] = $newNode->hasLocation()
164
            ? $newNode->getLocation()->getSource()
165
            : null;
166
167
        foreach (self::$kindToNodesToVisitMap[$this->kind] as $propertyName) {
168
            $nodeOrNodes = $this->{$propertyName};
169
170
            if (empty($nodeOrNodes)) {
171
                continue;
172
            }
173
174
            $propertyAst = $this->visitNodeOrNodes($nodeOrNodes, $propertyName, $newNode);
175
176
            if (empty($propertyAst)) {
177
                continue;
178
            }
179
180
            $newAst[$propertyName] = $propertyAst;
181
        }
182
183
        $newNode = $this->createNode($newAst);
184
185
        return $visitor->leaveNode($newNode);
186
    }
187
188
    /**
189
     * @param array $ast
190
     * @return NodeInterface
191
     */
192
    protected function createNode(array $ast): NodeInterface
193
    {
194
        /** @var NodeInterface $newNode */
195
        $newNode = $this->getNodeBuilder()->build($ast);
196
197
        $newNode->setVisitorProps([
198
            'visitor'   => $this->visitor,
199
            'key'       => $this->key,
200
            'parent'    => $this->parent,
201
            'path'      => $this->path,
202
            'ancestors' => $this->ancestors,
203
            'isEdited'  => $this->isEdited,
204
        ]);
205
206
        return $newNode;
207
    }
208
209
    /**
210
     * @param array $properties
211
     */
212
    public function setVisitorProps(array $properties)
213
    {
214
        foreach ($properties as $name => $value) {
215
            $this->{$name} = $value;
216
        }
217
    }
218
219
    /**
220
     * @inheritdoc
221
     */
222
    public function determineIsEdited(NodeInterface $node): bool
223
    {
224
        $this->isEdited = $this->isEdited || !$this->compareNode($node);
225
        return $this->isEdited;
226
    }
227
228
    /**
229
     * @return bool
230
     */
231
    public function isEdited(): bool
232
    {
233
        return $this->isEdited;
234
    }
235
236
    /**
237
     * @inheritdoc
238
     */
239
    public function getAncestor(int $depth = 1): ?NodeInterface
240
    {
241
        if (empty($this->ancestors)) {
242
            return null;
243
        }
244
245
        $index = \count($this->ancestors) - $depth;
246
247
        return $this->ancestors[$index] ?? null;
248
    }
249
250
    /**
251
     * @inheritdoc
252
     */
253
    public function getAncestors(): array
254
    {
255
        return $this->ancestors;
256
    }
257
258
    /**
259
     * @inheritdoc
260
     */
261
    public function getKey()
262
    {
263
        return $this->key;
264
    }
265
266
    /**
267
     * @inheritdoc
268
     */
269
    public function getParent(): ?NodeInterface
270
    {
271
        return $this->parent;
272
    }
273
274
    /**
275
     * @inheritdoc
276
     */
277
    public function getPath(): array
278
    {
279
        return $this->path;
280
    }
281
282
    /**
283
     * @param VisitorInterface $visitor
284
     * @return $this
285
     */
286
    public function setVisitor(VisitorInterface $visitor)
287
    {
288
        $this->visitor = $visitor;
289
        return $this;
290
    }
291
292
    /**
293
     * @param mixed $key
294
     * @return $this
295
     */
296
    public function setKey($key)
297
    {
298
        $this->key = $key;
299
        return $this;
300
    }
301
302
    /**
303
     * @param NodeInterface|null $parent
304
     * @return $this
305
     */
306
    public function setParent(?NodeInterface $parent)
307
    {
308
        $this->parent = $parent;
309
        return $this;
310
    }
311
312
    /**
313
     * @param array $path
314
     * @return $this
315
     */
316
    public function setPath(array $path)
317
    {
318
        $this->path = $path;
319
        return $this;
320
    }
321
322
    /**
323
     * @param array $ancestors
324
     * @return $this
325
     */
326
    public function setAncestors(array $ancestors)
327
    {
328
        $this->ancestors = $ancestors;
329
        return $this;
330
    }
331
332
    /**
333
     * @param bool $isEdited
334
     * @return $this
335
     */
336
    public function setIsEdited(bool $isEdited)
337
    {
338
        $this->isEdited = $isEdited;
339
        return $this;
340
    }
341
342
    /**
343
     * @param NodeInterface|NodeInterface[] $nodeOrNodes
344
     * @param mixed                         $key
345
     * @param NodeInterface                 $parent
346
     * @return array|null
347
     */
348
    protected function visitNodeOrNodes($nodeOrNodes, $key, NodeInterface $parent): ?array
349
    {
350
        $this->addAncestor($parent);
351
352
        $ast = \is_array($nodeOrNodes)
353
            ? $this->visitNodes($nodeOrNodes, $key)
354
            : $this->visitNode($nodeOrNodes, $key, $parent);
355
356
        $this->removeAncestor();
357
358
        return $ast;
359
    }
360
361
    /**
362
     * @param NodeInterface[] $nodes
363
     * @param string|int      $key
364
     * @return array
365
     */
366
    protected function visitNodes(array $nodes, $key): array
367
    {
368
        $this->addOneToPath($key);
369
370
        $ast   = [];
371
        $index = 0;
372
373
        foreach ($nodes as $node) {
374
            $nodeAst = $this->visitNode($node, $index, null);
375
376
            if (null !== $nodeAst) {
377
                $ast[$index] = $nodeAst;
378
                $index++;
379
            }
380
        }
381
382
        $this->removeOneFromPath();
383
384
        return $ast;
385
    }
386
387
    /**
388
     * @param NodeInterface      $node
389
     * @param string|int         $key
390
     * @param NodeInterface|null $parent
391
     * @return array|null
392
     */
393
    protected function visitNode(NodeInterface $node, $key, ?NodeInterface $parent): ?array
394
    {
395
        $this->addOneToPath($key);
396
397
        $newNode = $node->acceptVisitor($this->visitor, $key, $parent, $this->path, $this->ancestors);
398
399
        // If the node was edited, we need to revisit it to produce the expected result.
400
        if (null !== $newNode && $newNode->isEdited()) {
401
            $newNode = $newNode->acceptVisitor($this->visitor, $key, $parent, $this->path, $this->ancestors);
402
        }
403
404
        $this->removeOneFromPath();
405
406
        return null !== $newNode ? $newNode->toAST() : null;
407
    }
408
409
    /**
410
     * @param NodeInterface $other
411
     * @return bool
412
     */
413
    protected function compareNode(NodeInterface $other)
414
    {
415
        return $this->toJSON() === $other->toJSON();
416
    }
417
418
    /**
419
     * Appends a key to the path.
420
     * @param string $key
421
     */
422
    protected function addOneToPath(string $key)
423
    {
424
        $this->path[] = $key;
425
    }
426
427
    /**
428
     * Removes the last item from the path.
429
     */
430
    protected function removeOneFromPath()
431
    {
432
        $this->path = \array_slice($this->path, 0, -1);
433
    }
434
435
    /**
436
     * Adds an ancestor.
437
     * @param NodeInterface $node
438
     */
439
    protected function addAncestor(NodeInterface $node)
440
    {
441
        $this->ancestors[] = $node;
442
    }
443
444
    /**
445
     *  Removes the last ancestor.
446
     */
447
    protected function removeAncestor()
448
    {
449
        $this->ancestors = \array_slice($this->ancestors, 0, -1);
450
    }
451
452
    /**
453
     * @return NodeBuilderInterface
454
     */
455
    protected function getNodeBuilder(): NodeBuilderInterface
456
    {
457
        if (null === self::$nodeBuilder) {
458
            self::$nodeBuilder = GraphQL::make(NodeBuilderInterface::class);
459
        }
460
461
        return self::$nodeBuilder;
462
    }
463
464
    /**
465
     * @var array
466
     */
467
    protected static $kindToNodesToVisitMap = [
468
        'Name' => [],
469
470
        'Document'            => ['definitions'],
471
        'OperationDefinition' => [
472
            'name',
473
            'variableDefinitions',
474
            'directives',
475
            'selectionSet',
476
        ],
477
        'VariableDefinition'  => ['variable', 'type', 'defaultValue'],
478
        'Variable'            => ['name'],
479
        'SelectionSet'        => ['selections'],
480
        'Field'               => ['alias', 'name', 'arguments', 'directives', 'selectionSet'],
481
        'Argument'            => ['name', 'value'],
482
483
        'FragmentSpread'     => ['name', 'directives'],
484
        'InlineFragment'     => ['typeCondition', 'directives', 'selectionSet'],
485
        'FragmentDefinition' => [
486
            'name',
487
            'variableDefinitions',
488
            'typeCondition',
489
            'directives',
490
            'selectionSet',
491
        ],
492
493
        'IntValue'     => [],
494
        'FloatValue'   => [],
495
        'StringValue'  => [],
496
        'BooleanValue' => [],
497
        'NullValue'    => [],
498
        'EnumValue'    => [],
499
        'ListValue'    => ['values'],
500
        'ObjectValue'  => ['fields'],
501
        'ObjectField'  => ['name', 'value'],
502
503
        'Directive' => ['name', 'arguments'],
504
505
        'NamedType'   => ['name'],
506
        'ListType'    => ['type'],
507
        'NonNullType' => ['type'],
508
509
        'SchemaDefinition'        => ['directives', 'operationTypes'],
510
        'OperationTypeDefinition' => ['type'],
511
512
        'ScalarTypeDefinition'      => ['description', 'name', 'directives'],
513
        'ObjectTypeDefinition'      => [
514
            'description',
515
            'name',
516
            'interfaces',
517
            'directives',
518
            'fields',
519
        ],
520
        'FieldDefinition'           => ['description', 'name', 'arguments', 'type', 'directives'],
521
        'InputValueDefinition'      => [
522
            'description',
523
            'name',
524
            'type',
525
            'defaultValue',
526
            'directives',
527
        ],
528
        'InterfaceTypeDefinition'   => ['description', 'name', 'directives', 'fields'],
529
        'UnionTypeDefinition'       => ['description', 'name', 'directives', 'types'],
530
        'EnumTypeDefinition'        => ['description', 'name', 'directives', 'values'],
531
        'EnumValueDefinition'       => ['description', 'name', 'directives'],
532
        'InputObjectTypeDefinition' => ['description', 'name', 'directives', 'fields'],
533
534
        'DirectiveDefinition' => ['description', 'name', 'arguments', 'locations'],
535
536
        'SchemaExtension' => ['directives', 'operationTypes'],
537
538
        'ScalarTypeExtension'      => ['name', 'directives'],
539
        'ObjectTypeExtension'      => ['name', 'interfaces', 'directives', 'fields'],
540
        'InterfaceTypeExtension'   => ['name', 'directives', 'fields'],
541
        'UnionTypeExtension'       => ['name', 'directives', 'types'],
542
        'EnumTypeExtension'        => ['name', 'directives', 'values'],
543
        'InputObjectTypeExtension' => ['name', 'directives', 'fields'],
544
    ];
545
}
546