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

AbstractNode::setParent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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