Passed
Pull Request — master (#275)
by Christoffer
02:19
created

AbstractNode::addOneToPath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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