Passed
Pull Request — master (#228)
by Christoffer
02:43
created

AcceptsVisitorsTrait::removeOneFromPath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
1
<?php
2
3
namespace Digia\GraphQL\Language\Visitor;
4
5
use Digia\GraphQL\Language\Node\NodeInterface;
6
use Digia\GraphQL\Util\SerializationInterface;
7
8
trait AcceptsVisitorsTrait
9
{
10
11
    /**
12
     * @var VisitorInterface
13
     */
14
    protected $visitor;
15
16
    /**
17
     * @var string|int|null
18
     */
19
    protected $key;
20
21
    /**
22
     * @var NodeInterface|null
23
     */
24
    protected $parent;
25
26
    /**
27
     * @var array
28
     */
29
    protected $path;
30
31
    /**
32
     * @var array
33
     */
34
    protected $ancestors = [];
35
36
    /**
37
     * @var boolean
38
     */
39
    protected $isEdited = false;
40
41
    /**
42
     * @param VisitorInterface   $visitor
43
     * @param mixed              $key
44
     * @param NodeInterface|null $parent
45
     * @param string[]           $path
46
     * @param NodeInterface[]    $ancestors
47
     * @return NodeInterface|SerializationInterface|null
48
     */
49
    public function acceptVisitor(
50
        VisitorInterface $visitor,
51
        $key = null,
52
        ?NodeInterface $parent = null,
53
        array $path = [],
54
        array $ancestors = []
55
    ): ?NodeInterface {
56
        $this->visitor   = $visitor;
57
        $this->key       = $key;
58
        $this->parent    = $parent;
59
        $this->path      = $path;
60
        $this->ancestors = $ancestors;
61
62
        /** @var NodeInterface $newNode */
63
        $newNode = clone $this; // TODO: Benchmark cloning
64
65
        // If the result was null, it means that we should not traverse this branch.
66
        if (null === ($newNode = $visitor->enterNode($newNode))) {
67
            return null;
68
        }
69
70
        // If the node was edited, we want to return early
71
        // to avoid visiting its sub-tree completely.
72
        if ($newNode instanceof AcceptsVisitorsInterface && $newNode->determineIsEdited($this)) {
73
            return $newNode;
74
        }
75
76
        foreach (self::$kindToNodesToVisitMap[$this->kind] as $property) {
77
            $nodeOrNodes = $this->{$property};
78
79
            if (empty($nodeOrNodes)) {
80
                continue;
81
            }
82
83
            $newNodeOrNodes = $this->visitNodeOrNodes($nodeOrNodes, $property, $newNode);
84
85
            if (empty($newNodeOrNodes)) {
86
                continue;
87
            }
88
89
            $setter = 'set' . \ucfirst($property);
90
91
            if (\method_exists($newNode, $setter)) {
92
                $newNode->{$setter}($newNodeOrNodes);
93
            }
94
        }
95
96
        return $visitor->leaveNode($newNode);
97
    }
98
99
    /**
100
     * @inheritdoc
101
     */
102
    public function determineIsEdited($node): bool
103
    {
104
        $this->isEdited = $this->isEdited || !$this->compareNode($node);
105
        return $this->isEdited;
106
    }
107
108
    /**
109
     * @return bool
110
     */
111
    public function isEdited(): bool
112
    {
113
        return $this->isEdited;
114
    }
115
116
    /**
117
     * @inheritdoc
118
     */
119
    public function getAncestor(int $depth = 1): ?NodeInterface
120
    {
121
        if (empty($this->ancestors)) {
122
            return null;
123
        }
124
125
        $index = \count($this->ancestors) - $depth;
126
127
        return $this->ancestors[$index] ?? null;
128
    }
129
130
    /**
131
     * @inheritdoc
132
     */
133
    public function getAncestors(): array
134
    {
135
        return $this->ancestors;
136
    }
137
138
    /**
139
     * @inheritdoc
140
     */
141
    public function getKey()
142
    {
143
        return $this->key;
144
    }
145
146
    /**
147
     * @inheritdoc
148
     */
149
    public function getParent(): ?NodeInterface
150
    {
151
        return $this->parent;
152
    }
153
154
    /**
155
     * @inheritdoc
156
     */
157
    public function getPath(): array
158
    {
159
        return $this->path;
160
    }
161
162
    /**
163
     * @param NodeInterface|NodeInterface[] $nodeOrNodes
164
     * @param mixed                         $key
165
     * @param NodeInterface                 $parent
166
     * @return NodeInterface|NodeInterface[]|null
167
     */
168
    protected function visitNodeOrNodes($nodeOrNodes, $key, NodeInterface $parent)
169
    {
170
        $this->addAncestor($parent);
171
172
        $newNodeOrNodes = \is_array($nodeOrNodes)
173
            ? $this->visitNodes($nodeOrNodes, $key)
174
            : $this->visitNode($nodeOrNodes, $key, $parent);
175
176
        $this->removeAncestor();
177
178
        return $newNodeOrNodes;
179
    }
180
181
    /**
182
     * @param NodeInterface[]    $nodes
183
     * @param string|int         $key
184
     * @param NodeInterface|null $parent
185
     * @return NodeInterface[]
186
     */
187
    protected function visitNodes(array $nodes, $key): array
188
    {
189
        $this->addOneToPath($key);
190
191
        $index    = 0;
192
        $newNodes = [];
193
194
        foreach ($nodes as $node) {
195
            $newNode = $this->visitNode($node, $index, null);
196
197
            if (null !== $newNode) {
198
                $newNodes[$index] = $newNode;
199
                $index++;
200
            }
201
        }
202
203
        $this->removeOneFromPath();
204
205
        return $newNodes;
206
    }
207
208
    /**
209
     * @param NodeInterface|AcceptsVisitorsTrait $node
210
     * @param string|int                         $key
211
     * @param NodeInterface|null                 $parent
212
     * @return NodeInterface|null
213
     */
214
    protected function visitNode($node, $key, ?NodeInterface $parent): ?NodeInterface
215
    {
216
        $this->addOneToPath($key);
217
218
        $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\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...eExtensionNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\N...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

218
        /** @scrutinizer ignore-call */ 
219
        $newNode = $node->acceptVisitor($this->visitor, $key, $parent, $this->path, $this->ancestors);
Loading history...
219
220
        // If the node was edited, we need to revisit it
221
        // to produce the expected result.
222
        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\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...eExtensionNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\N...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

222
        if (null !== $newNode && $newNode->/** @scrutinizer ignore-call */ isEdited()) {
Loading history...
223
            $newNode = $newNode->acceptVisitor($this->visitor, $key, $parent, $this->path, $this->ancestors);
224
        }
225
226
        $this->removeOneFromPath();
227
228
        return $newNode;
229
    }
230
231
    /**
232
     * @param NodeInterface $other
233
     * @return bool
234
     */
235
    protected function compareNode(NodeInterface $other)
236
    {
237
        // TODO: Figure out a better way to solve this.
238
        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

238
        return $this->/** @scrutinizer ignore-call */ toJSON() === $other->toJSON();
Loading history...
239
    }
240
241
    /**
242
     * Appends a key to the path.
243
     * @param string $key
244
     */
245
    protected function addOneToPath(string $key)
246
    {
247
        $this->path[] = $key;
248
    }
249
250
    /**
251
     * Removes the last item from the path.
252
     */
253
    protected function removeOneFromPath()
254
    {
255
        $this->path = \array_slice($this->path, 0, -1);
256
    }
257
258
    /**
259
     * Adds an ancestor.
260
     * @param NodeInterface $node
261
     */
262
    protected function addAncestor(NodeInterface $node)
263
    {
264
        $this->ancestors[] = $node;
265
    }
266
267
    /**
268
     *  Removes the last ancestor.
269
     */
270
    protected function removeAncestor()
271
    {
272
        $this->ancestors = \array_slice($this->ancestors, 0, -1);
273
    }
274
275
    /**
276
     * @var array
277
     */
278
    protected static $kindToNodesToVisitMap = [
279
        'Name' => [],
280
281
        'Document'            => ['definitions'],
282
        'OperationDefinition' => [
283
            'name',
284
            'variableDefinitions',
285
            'directives',
286
            'selectionSet',
287
        ],
288
        'VariableDefinition'  => ['variable', 'type', 'defaultValue'],
289
        'Variable'            => ['name'],
290
        'SelectionSet'        => ['selections'],
291
        'Field'               => ['alias', 'name', 'arguments', 'directives', 'selectionSet'],
292
        'Argument'            => ['name', 'value'],
293
294
        'FragmentSpread'     => ['name', 'directives'],
295
        'InlineFragment'     => ['typeCondition', 'directives', 'selectionSet'],
296
        'FragmentDefinition' => [
297
            'name',
298
            // Note: fragment variable definitions are experimental and may be changed or removed in the future.
299
            'variableDefinitions',
300
            'typeCondition',
301
            'directives',
302
            'selectionSet',
303
        ],
304
305
        'IntValue'     => [],
306
        'FloatValue'   => [],
307
        'StringValue'  => [],
308
        'BooleanValue' => [],
309
        'NullValue'    => [],
310
        'EnumValue'    => [],
311
        'ListValue'    => ['values'],
312
        'ObjectValue'  => ['fields'],
313
        'ObjectField'  => ['name', 'value'],
314
315
        'Directive' => ['name', 'arguments'],
316
317
        'NamedType'   => ['name'],
318
        'ListType'    => ['type'],
319
        'NonNullType' => ['type'],
320
321
        'SchemaDefinition'        => ['directives', 'operationTypes'],
322
        'OperationTypeDefinition' => ['type'],
323
324
        'ScalarTypeDefinition'      => ['description', 'name', 'directives'],
325
        'ObjectTypeDefinition'      => [
326
            'description',
327
            'name',
328
            'interfaces',
329
            'directives',
330
            'fields',
331
        ],
332
        'FieldDefinition'           => ['description', 'name', 'arguments', 'type', 'directives'],
333
        'InputValueDefinition'      => [
334
            'description',
335
            'name',
336
            'type',
337
            'defaultValue',
338
            'directives',
339
        ],
340
        'InterfaceTypeDefinition'   => ['description', 'name', 'directives', 'fields'],
341
        'UnionTypeDefinition'       => ['description', 'name', 'directives', 'types'],
342
        'EnumTypeDefinition'        => ['description', 'name', 'directives', 'values'],
343
        'EnumValueDefinition'       => ['description', 'name', 'directives'],
344
        'InputObjectTypeDefinition' => ['description', 'name', 'directives', 'fields'],
345
346
        'ScalarTypeExtension'      => ['name', 'directives'],
347
        'ObjectTypeExtension'      => ['name', 'interfaces', 'directives', 'fields'],
348
        'InterfaceTypeExtension'   => ['name', 'directives', 'fields'],
349
        'UnionTypeExtension'       => ['name', 'directives', 'types'],
350
        'EnumTypeExtension'        => ['name', 'directives', 'values'],
351
        'InputObjectTypeExtension' => ['name', 'directives', 'fields'],
352
353
        'DirectiveDefinition' => ['description', 'name', 'arguments', 'locations'],
354
    ];
355
}
356