Passed
Pull Request — master (#36)
by
unknown
01:36
created

TraversalTrait   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 449
Duplicated Lines 0 %

Test Coverage

Coverage 79.59%

Importance

Changes 19
Bugs 4 Features 1
Metric Value
wmc 58
eloc 123
c 19
b 4
f 1
dl 0
loc 449
ccs 117
cts 147
cp 0.7959
rs 4.5599

27 Methods

Rating   Name   Duplication   Size   Complexity  
A newNodeList() 0 11 3
A getNodesMatchingInput() 0 32 6
A followingUntil() 0 2 1
A has() 0 31 6
A parentsUntil() 0 2 1
A siblings() 0 10 1
A findXPath() 0 16 3
A _uniqueNodes() 0 16 4
A closest() 0 4 1
A not() 0 2 1
A contents() 0 12 2
A add() 0 8 1
A intersect() 0 18 2
A children() 0 8 1
A preceding() 0 2 1
A is() 0 2 1
B _buildNodeListUntil() 0 29 11
A precedingUntil() 0 2 1
A parents() 0 2 1
A eq() 0 6 2
A following() 0 2 1
A filter() 0 2 1
A find() 0 8 2
A parent() 0 4 1
A _walkPathUntil() 0 8 1
A precedingAll() 0 2 1
A followingAll() 0 2 1

How to fix   Complexity   

Complex Class

Complex classes like TraversalTrait often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TraversalTrait, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
namespace DOMWrap\Traits;
4
5
use DOMWrap\{
6
    Element,
7
    NodeList
8
};
9
use Symfony\Component\CssSelector\CssSelectorConverter;
10
11
/**
12
 * Traversal Trait
13
 *
14
 * @package DOMWrap\Traits
15
 * @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
16
 */
17
trait TraversalTrait
18
{
19
    /**
20
     * @param iterable $nodes
21
     *
22
     * @return NodeList
23
     */
24 138
    public function newNodeList(iterable $nodes = null): NodeList {
25
26 138
        if (!is_iterable($nodes)) {
27 134
            if (!is_null($nodes)) {
28
                $nodes = [$nodes];
29
            } else {
30 134
                $nodes = [];
31
            }
32
        }
33
34 138
        return new NodeList($this->document(), $nodes);
0 ignored issues
show
Bug introduced by
It seems like document() 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

34
        return new NodeList($this->/** @scrutinizer ignore-call */ document(), $nodes);
Loading history...
35
    }
36
37
    /**
38
     * @param string $selector
39
     * @param string $prefix
40
     *
41
     * @return NodeList
42
     */
43 130
    public function find(string $selector, string $prefix = 'descendant::'): NodeList {
44 130
        static $converter;
45
46 130
        if (!isset($converter)) {
47
            $converter = new CssSelectorConverter();
48
        }
49
50
        return $this->findXPath($converter->toXPath($selector, $prefix));
51
    }
52
53
    /**
54 130
     * @param string $xpath
55 130
     *
56
     * @return NodeList
57 130
     */
58
    public function findXPath(string $xpath): NodeList {
59
        $results = $this->newNodeList();
60
61 130
        if ($this->isRemoved()) {
0 ignored issues
show
Bug introduced by
It seems like isRemoved() 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

61
        if ($this->/** @scrutinizer ignore-call */ isRemoved()) {
Loading history...
62
            return $results;
63 130
        }
64 130
65 130
        $domxpath = new \DOMXPath($this->document());
66
67
        foreach ($this->collection() as $node) {
0 ignored issues
show
Bug introduced by
It seems like collection() 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

67
        foreach ($this->/** @scrutinizer ignore-call */ collection() as $node) {
Loading history...
68
            $results = $results->merge(
69 130
                $node->newNodeList($domxpath->query($xpath, $node))
70
            );
71
        }
72
73
        return $results;
74
    }
75
76
    /**
77
     * @param string|NodeList|\DOMNode|callable $input
78 18
     * @param bool $matchType
79 18
     *
80 8
     * @return NodeList
81
     */
82 8
    protected function getNodesMatchingInput($input, bool $matchType = true): NodeList {
83 8
        if ($input instanceof NodeList || $input instanceof \DOMNode) {
84 8
            $inputNodes = $this->inputAsNodeList($input, false);
0 ignored issues
show
Bug introduced by
It seems like inputAsNodeList() 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

84
            /** @scrutinizer ignore-call */ 
85
            $inputNodes = $this->inputAsNodeList($input, false);
Loading history...
85
86
            $fn = function($node) use ($inputNodes) {
87 11
                return $inputNodes->exists($node);
88
            };
89
90 2
91
        } elseif (is_callable($input)) {
92 2
            // Since we're at the behest of the input callable, the 'matched'
93
            //  return value is always true.
94 10
            $matchType = true;
95 10
96 10
            $fn = $input;
97 10
98
        } elseif (is_string($input)) {
99
            $fn = function($node) use ($input) {
100
                return $node->find($input, 'self::')->count() != 0;
101
            };
102
103
        } else {
104 18
            throw new \InvalidArgumentException('Unexpected input value of type "' . gettype($input) . '"');
105 18
        }
106 17
107
        // Build a list of matching nodes.
108
        return $this->collection()->map(function($node) use ($fn, $matchType) {
109 18
            if ($fn($node) !== $matchType) {
110 18
                return null;
111
            }
112
113
            return $node;
114
        });
115
    }
116
117
    /**
118 15
     * @param string|NodeList|\DOMNode|callable $input
119 15
     *
120
     * @return bool
121
     */
122
    public function is($input): bool {
123
        return $this->getNodesMatchingInput($input)->count() != 0;
124
    }
125
126
    /**
127 1
     * @param string|NodeList|\DOMNode|callable $input
128 1
     *
129
     * @return NodeList
130
     */
131
    public function not($input): NodeList {
132
        return $this->getNodesMatchingInput($input, false);
133
    }
134
135
    /**
136 1
     * @param string|NodeList|\DOMNode|callable $input
137 1
     *
138
     * @return NodeList
139
     */
140
    public function filter($input): NodeList {
141
        return $this->getNodesMatchingInput($input);
142
    }
143
144
    /**
145 1
     * @param string|NodeList|\DOMNode|callable $input
146 1
     *
147 1
     * @return NodeList
148
     */
149 1
    public function has($input): NodeList {
150 1
        if ($input instanceof NodeList || $input instanceof \DOMNode) {
151
            $inputNodes = $this->inputAsNodeList($input, false);
152
153 1
            $fn = function($node) use ($inputNodes) {
154
                $descendantNodes = $node->find('*', 'descendant::');
155 1
156 1
                // Determine if we have a descendant match.
157
                return $inputNodes->reduce(function($carry, $inputNode) use ($descendantNodes) {
158
                    // Match descendant nodes against input nodes.
159
                    if ($descendantNodes->exists($inputNode)) {
160 1
                        return true;
161 1
                    }
162
163 1
                    return $carry;
164 1
                }, false);
165 1
            };
166 1
167
        } elseif (is_string($input)) {
168
            $fn = function($node) use ($input) {
169
                return $node->find($input, 'descendant::')->count() != 0;
170
            };
171
172
        } elseif (is_callable($input)) {
173
            $fn = $input;
174
175 1
        } else {
176
            throw new \InvalidArgumentException('Unexpected input value of type "' . gettype($input) . '"');
177
        }
178
179
        return $this->getNodesMatchingInput($fn);
180
    }
181
182
    /**
183
     * @param string|NodeList|\DOMNode|callable $selector
184
     *
185
     * @return \DOMNode|null
186
     */
187
    public function preceding($selector = null): ?\DOMNode {
188
        return $this->precedingUntil(null, $selector)->first();
189
    }
190
191
    /**
192 21
     * @param string|NodeList|\DOMNode|callable $selector
193 21
     *
194
     * @return NodeList
195
     */
196
    public function precedingAll($selector = null): NodeList {
197
        return $this->precedingUntil(null, $selector);
198
    }
199
200
    /**
201
     * @param string|NodeList|\DOMNode|callable $input
202 32
     * @param string|NodeList|\DOMNode|callable $selector
203 32
     *
204
     * @return NodeList
205
     */
206
    public function precedingUntil($input = null, $selector = null): NodeList {
207
        return $this->_walkPathUntil('previousSibling', $input, $selector);
208
    }
209
210
    /**
211 12
     * @param string|NodeList|\DOMNode|callable $selector
212 12
     *
213
     * @return \DOMNode|null
214
     */
215
    public function following($selector = null): ?\DOMNode {
216
        return $this->followingUntil(null, $selector)->first();
217
    }
218
219
    /**
220 21
     * @param string|NodeList|\DOMNode|callable $selector
221 21
     *
222
     * @return NodeList
223
     */
224
    public function followingAll($selector = null): NodeList {
225
        return $this->followingUntil(null, $selector);
226
    }
227
228
    /**
229
     * @param string|NodeList|\DOMNode|callable $input
230 25
     * @param string|NodeList|\DOMNode|callable $selector
231 25
     *
232
     * @return NodeList
233
     */
234
    public function followingUntil($input = null, $selector = null): NodeList {
235
        return $this->_walkPathUntil('nextSibling', $input, $selector);
236
    }
237
238
    /**
239
     * @param string|NodeList|\DOMNode|callable $selector
240 21
     *
241 21
     * @return NodeList
242 21
     */
243 21
    public function siblings($selector = null): NodeList {
244
        $results = $this->collection()->reduce(function($carry, $node) use ($selector) {
245
            return $carry->merge(
246 21
                $node->precedingAll($selector)->merge(
247
                    $node->followingAll($selector)
248 21
                )
249
            );
250
        }, $this->newNodeList());
251
252
        return $results;
253
    }
254
255
    /**
256
     * NodeList is only array like. Removing items using foreach() has undesired results.
257 93
     *
258 93
     * @return NodeList
259 93
     */
260
    public function children(): NodeList {
261 93
        $results = $this->collection()->reduce(function($carry, $node) {
262
            return $carry->merge(
263 93
                $node->findXPath('child::*')
264
            );
265
        }, $this->newNodeList());
266
267
        return $results;
268
    }
269
270
    /**
271 51
     * @param string|NodeList|\DOMNode|callable $selector
272 51
     *
273
     * @return Element|NodeList|null
274 51
     */
275
    public function parent($selector = null) {
276
        $results = $this->_walkPathUntil('parentNode', null, $selector, self::$MATCH_TYPE_FIRST);
277
278
        return $this->result($results);
0 ignored issues
show
Bug introduced by
It seems like result() 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

278
        return $this->/** @scrutinizer ignore-call */ result($results);
Loading history...
279
    }
280
281
    /**
282 2
     * @param int $index
283 2
     *
284 1
     * @return \DOMNode|null
285
     */
286
    public function eq(int $index): ?\DOMNode {
287 2
        if ($index < 0) {
288
            $index = $this->collection()->count() + $index;
289
        }
290
291
        return $this->collection()->offsetGet($index);
292
    }
293
294
    /**
295
     * @param string $selector
296
     *
297
     * @return NodeList
298
     */
299
    public function parents(string $selector = null): NodeList {
300
        return $this->parentsUntil(null, $selector);
301
    }
302
303
    /**
304
     * @param string|NodeList|\DOMNode|callable $input
305
     * @param string|NodeList|\DOMNode|callable $selector
306
     *
307
     * @return NodeList
308
     */
309
    public function parentsUntil($input = null, $selector = null): NodeList {
310
        return $this->_walkPathUntil('parentNode', $input, $selector);
311
    }
312
313
    /**
314
     * @return \DOMNode
315
     */
316
    public function intersect(): \DOMNode {
317
        if ($this->collection()->count() < 2) {
318
            return $this->collection()->first();
319
        }
320
321
        $nodeParents = [];
322
323
        // Build a multi-dimensional array of the collection nodes parent elements
324
        $this->collection()->each(function($node) use(&$nodeParents) {
325
            $nodeParents[] = $node->parents()->unshift($node)->toArray();
326
        });
327
328
        // Find the common parent
329
        $diff = call_user_func_array('array_uintersect', array_merge($nodeParents, [function($a, $b) {
330
            return strcmp(spl_object_hash($a), spl_object_hash($b));
331
        }]));
332
333
        return array_shift($diff);
334
    }
335
336
    /**
337 3
     * @param string|NodeList|\DOMNode|callable $input
338 3
     *
339
     * @return Element|NodeList|null
340 3
     */
341
    public function closest($input) {
342
        $results = $this->_walkPathUntil('parentNode', $input, null, self::$MATCH_TYPE_LAST);
343
344
        return $this->result($results);
345
    }
346
347
    /**
348
     * NodeList is only array like. Removing items using foreach() has undesired results.
349 26
     *
350 26
     * @return NodeList
351
     */
352 26
    public function contents(): NodeList {
353 26
        $results = $this->collection()->reduce(function($carry, $node) {
354
            if ($node->isRemoved()) {
355 26
                return $carry;
356
            }
357 26
358
            return $carry->merge(
359
                $node->newNodeList($node->childNodes)
360
            );
361
        }, $this->newNodeList());
362
363
        return $results;
364
    }
365
366
    /**
367
     * @param string|NodeList|\DOMNode $input
368
     *
369
     * @return NodeList
370
     */
371
    public function add($input): NodeList {
372
        $nodes = $this->inputAsNodeList($input);
373
374
        $results = $this->collection()->merge(
375
            $nodes
376
        );
377
378
        return $results;
379
    }
380
381
    /** @var int */
382
    private static $MATCH_TYPE_FIRST = 1;
383
384
    /** @var int */
385
    private static $MATCH_TYPE_LAST = 2;
386
387
    /**
388
     * @param \DOMNode $baseNode
389
     * @param string $property
390 65
     * @param string|NodeList|\DOMNode|callable $input
391 65
     * @param string|NodeList|\DOMNode|callable $selector
392
     * @param int $matchType
393
     *
394 65
     * @return NodeList
395
     */
396
    protected function _buildNodeListUntil(\DOMNode $baseNode, string $property, $input = null, $selector = null, int $matchType = null): NodeList {
397 65
        $resultNodes = $this->newNodeList();
398
399 64
        // Get our first node
400 61
        $node = $baseNode->$property;
401
402
        // Keep looping until we are out of nodes.
403
        // Allow either FIRST to reach \DOMDocument. Others that return multiple should ignore it.
404 64
        while ($node instanceof \DOMNode && ($matchType === self::$MATCH_TYPE_FIRST || !($node instanceof \DOMDocument))) {
405
            // Filter nodes if not matching last
406 60
            if ($matchType != self::$MATCH_TYPE_LAST && (is_null($selector) || $node->is($selector))) {
407 2
                $resultNodes[] = $node;
408
            }
409
410 60
            // 'Until' check or first match only
411
            if ($matchType == self::$MATCH_TYPE_FIRST || (!is_null($input) && $node->is($input))) {
412
                // Set last match
413
                if ($matchType == self::$MATCH_TYPE_LAST) {
414 21
                    $resultNodes[] = $node;
415
                }
416
417 65
                break;
418
            }
419
420
            // Find the next node
421
            $node = $node->{$property};
422
        }
423
424
        return $resultNodes;
425 65
    }
426 65
427
    /**
428
     * @param iterable $nodeLists
429 65
     *
430
     * @return NodeList
431 65
     */
432
    protected function _uniqueNodes(iterable $nodeLists): NodeList {
433 63
        $resultNodes = $this->newNodeList();
434 65
435
        // Loop through our array of NodeLists
436
        foreach ($nodeLists as $nodeList) {
437
            // Each node in the NodeList
438
            foreach ($nodeList as $node) {
439
                // We're only interested in unique nodes
440 65
                if (!$resultNodes->exists($node)) {
441
                    $resultNodes[] = $node;
442
                }
443
            }
444
        }
445
446
        // Sort resulting NodeList: outer-most => inner-most.
447
        return $resultNodes->reverse();
448
    }
449
450
    /**
451 65
     * @param string $property
452 65
     * @param string|NodeList|\DOMNode|callable $input
453
     * @param string|NodeList|\DOMNode|callable $selector
454 65
     * @param int $matchType
455 65
     *
456 65
     * @return NodeList
457
     */
458 65
    protected function _walkPathUntil(string $property, $input = null, $selector = null, int $matchType = null): NodeList {
459
        $nodeLists = [];
460
461
        $this->collection()->each(function($node) use($property, $input, $selector, $matchType, &$nodeLists) {
462
            $nodeLists[] = $this->_buildNodeListUntil($node, $property, $input, $selector, $matchType);
463
        });
464
465
        return $this->_uniqueNodes($nodeLists);
466
    }
467
}
468