Completed
Pull Request — master (#80)
by Christoffer
02:18
created

AcceptsVisitorsTrait::acceptVisitor()   C

Complexity

Conditions 9
Paths 7

Size

Total Lines 48
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 48
rs 5.5102
c 0
b 0
f 0
cc 9
eloc 21
nc 7
nop 5
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 string|int|null       $key
44
     * @param NodeInterface|null    $parent
45
     * @param array|string[]        $path
46
     * @param array|NodeInterface[] $ancestors
47
     * @return NodeInterface|AcceptsVisitorsTrait|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|AcceptsVisitorsTrait|null $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))) {
0 ignored issues
show
Bug introduced by
It seems like $newNode can also be of type null and Digia\GraphQL\Language\V...or\AcceptsVisitorsTrait; however, parameter $node of Digia\GraphQL\Language\V...rInterface::enterNode() does only seem to accept Digia\GraphQL\Language\Node\NodeInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

66
        if (null === ($newNode = $visitor->enterNode(/** @scrutinizer ignore-type */ $newNode))) {
Loading history...
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->determineIsEdited($this)) {
0 ignored issues
show
Bug introduced by
The method determineIsEdited() 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\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

72
        if ($newNode->/** @scrutinizer ignore-call */ determineIsEdited($this)) {
Loading history...
73
            return $newNode;
74
        }
75
76
        foreach (self::$kindToNodesToVisitMap[$this->kind] as $name) {
0 ignored issues
show
Bug Best Practice introduced by
The property kind does not exist on Digia\GraphQL\Language\V...or\AcceptsVisitorsTrait. Did you maybe forget to declare it?
Loading history...
77
            $nodeOrNodes = $this->getNodeOrNodes($name);
78
79
            if (null === $nodeOrNodes || empty($nodeOrNodes)) {
80
                continue;
81
            }
82
83
            $newNodeOrNodes = $this->visitNodeOrNodes($nodeOrNodes, $name);
84
85
            if (null === $newNodeOrNodes || empty($newNodeOrNodes)) {
86
                continue;
87
            }
88
89
            $setter = 'set' . ucfirst($name);
90
91
            if (method_exists($newNode, $setter)) {
92
                $newNode->{$setter}($newNodeOrNodes);
93
            }
94
        }
95
96
        return $visitor->leaveNode($newNode);
97
    }
98
99
    /**
100
     * @return bool
101
     */
102
    public function isEdited(): bool
103
    {
104
        return $this->isEdited;
105
    }
106
107
    /**
108
     * @param bool $isEdited
109
     * @return $this
110
     */
111
    public function setIsEdited(bool $isEdited)
112
    {
113
        $this->isEdited = $isEdited;
114
        return $this;
115
    }
116
117
    /**
118
     * @param int $depth
119
     * @return NodeInterface|null
120
     */
121
    public function getAncestor(int $depth = 1): ?NodeInterface
122
    {
123
        return !empty($this->ancestors) ? $this->ancestors[\count($this->ancestors) - $depth] : null;
124
    }
125
126
    /**
127
     * @return NodeInterface[]
128
     */
129
    public function getAncestors(): array
130
    {
131
        return $this->ancestors;
132
    }
133
134
    /**
135
     * @return int|null|string
136
     */
137
    public function getKey()
138
    {
139
        return $this->key;
140
    }
141
142
    /**
143
     * @return NodeInterface|null
144
     */
145
    public function getParent(): ?NodeInterface
146
    {
147
        return $this->parent;
148
    }
149
150
    /**
151
     * @return array
152
     */
153
    public function getPath(): array
154
    {
155
        return $this->path;
156
    }
157
158
    /**
159
     * @param string|int $key
160
     * @return array|NodeInterface|NodeInterface[]|null
161
     */
162
    protected function getNodeOrNodes($key)
163
    {
164
        return $this->{$key};
165
    }
166
167
    /**
168
     * @param            $nodeOrNodes
169
     * @param string|int $key
170
     * @return array|NodeInterface|NodeInterface[]|null
171
     */
172
    protected function visitNodeOrNodes($nodeOrNodes, $key)
173
    {
174
        $this->addAncestor($this);
0 ignored issues
show
Bug introduced by
$this of type Digia\GraphQL\Language\V...or\AcceptsVisitorsTrait is incompatible with the type Digia\GraphQL\Language\Node\NodeInterface expected by parameter $node of Digia\GraphQL\Language\V...orsTrait::addAncestor(). ( Ignorable by Annotation )

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

174
        $this->addAncestor(/** @scrutinizer ignore-type */ $this);
Loading history...
175
176
        $newNodeOrNodes = \is_array($nodeOrNodes)
177
            ? $this->visitNodes($nodeOrNodes, $key)
178
            : $this->visitNode($nodeOrNodes, $key, $this);
0 ignored issues
show
Bug introduced by
$this of type Digia\GraphQL\Language\V...or\AcceptsVisitorsTrait is incompatible with the type null|Digia\GraphQL\Language\Node\NodeInterface expected by parameter $parent of Digia\GraphQL\Language\V...itorsTrait::visitNode(). ( Ignorable by Annotation )

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

178
            : $this->visitNode($nodeOrNodes, $key, /** @scrutinizer ignore-type */ $this);
Loading history...
179
180
        $this->removeAncestor();
181
182
        return $newNodeOrNodes;
183
    }
184
185
    /**
186
     * @param NodeInterface[]    $nodes
187
     * @param string|int         $key
188
     * @param NodeInterface|null $parent
189
     * @return NodeInterface[]
190
     */
191
    protected function visitNodes(array $nodes, $key): array
192
    {
193
        $this->addOneToPath($key);
194
195
        $index    = 0;
196
        $newNodes = [];
197
198
        foreach ($nodes as $node) {
199
            $newNode = $this->visitNode($node, $index, null);
200
201
            if (null !== $newNode) {
202
                $newNodes[$index] = $newNode;
203
                $index++;
204
            }
205
        }
206
207
        $this->removeOneFromPath();
208
209
        return $newNodes;
210
    }
211
212
    /**
213
     * @param NodeInterface|AcceptsVisitorsTrait $node
214
     * @param string|int                         $key
215
     * @param NodeInterface|null                 $parent
216
     * @return NodeInterface|null
217
     */
218
    protected function visitNode(NodeInterface $node, $key, ?NodeInterface $parent): ?NodeInterface
219
    {
220
        $this->addOneToPath($key);
221
222
        $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\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
        /** @scrutinizer ignore-call */ 
223
        $newNode = $node->acceptVisitor($this->visitor, $key, $parent, $this->path, $this->ancestors);
Loading history...
223
224
        // If the node was edited, we need to revisit it
225
        // to produce the expected result.
226
        if (null !== $newNode && $newNode->isEdited()) {
227
            $newNode = $newNode->acceptVisitor($this->visitor, $key, $parent, $this->path, $this->ancestors);
228
        }
229
230
        $this->removeOneFromPath();
231
232
        return $newNode;
233
    }
234
235
    /**
236
     * @param NodeInterface|AcceptsVisitorsTrait $node
237
     * @return bool
238
     */
239
    protected function determineIsEdited(NodeInterface $node): bool
240
    {
241
        $this->isEdited = $this->isEdited || !$this->compareNode($node);
242
        return $this->isEdited;
243
    }
244
245
    /**
246
     * @param NodeInterface $other
247
     * @return bool
248
     */
249
    protected function compareNode(NodeInterface $other)
250
    {
251
        // TODO: Figure out a better way to solve this.
252
        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

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