Completed
Push — master ( bccd28...ee3bb4 )
by Andrew
05:18
created

TraversalTrait::newNodeList()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.1755

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 7
cts 9
cp 0.7778
rs 9.2
c 0
b 0
f 0
cc 4
eloc 7
nc 3
nop 1
crap 4.1755
1
<?php
2
3
namespace DOMWrap\Traits;
4
5
use DOMWrap\Element;
6
use DOMWrap\NodeList;
7
use Symfony\Component\CssSelector\CssSelectorConverter;
8
9
/**
10
 * Traversal Trait
11
 *
12
 * @package DOMWrap\Traits
13
 * @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
14
 */
15
trait TraversalTrait
16
{
17
    /** @see CommonTrait::collection() */
18
    abstract public function collection();
19
20
    /** @see CommonTrait::document() */
21
    abstract public function document();
22
23
    /** @see CommonTrait::result() */
24
    abstract public function result($nodeList);
25
26
    /** @see ManipulationTrait::inputAsNodeList() */
27
    abstract public function inputAsNodeList($input);
28
29
    /**
30
     * @param Traversable|array $nodes
31
     *
32
     * @return NodeList
33
     */
34 132
    public function newNodeList($nodes = null) {
35 132
        if (!is_array($nodes) && !($nodes instanceof \Traversable)) {
36 130
            if (!is_null($nodes)) {
37
                $nodes = [$nodes];
38
            } else {
39 130
                $nodes = [];
40
            }
41 130
        }
42
43 132
        return new NodeList($this->document(), $nodes);
44 1
    }
45
46
    /**
47
     * @param string $selector
48
     * @param string $prefix
49
     *
50
     * @return NodeList
51
     */
52 130
    public function find($selector, $prefix = 'descendant::') {
53 130
        $converter = new CssSelectorConverter();
54
55 130
        return $this->findXPath($converter->toXPath($selector, $prefix));
56
    }
57
58
    /**
59
     * @param string $xpath
60
     *
61
     * @return NodeList
62
     */
63 130
    public function findXPath($xpath) {
64 130
        $results = $this->newNodeList();
65
66 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?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
67
            return $results;
68
        }
69
70 130
        $domxpath = new \DOMXPath($this->document());
71
72 130
        foreach ($this->collection() as $node) {
73 130
            $results = $results->merge(
74 130
                $node->newNodeList($domxpath->query($xpath, $node))
75 130
            );
76 130
        }
77
78 130
        return $results;
79
    }
80
81
    /**
82
     * @param string|NodeList|\DOMNode|\Closure $input
83
     * @param bool $matchType
84
     *
85
     * @return NodeList
86
     */
87 18
    protected function getNodesMatchingInput($input, $matchType = true) {
88 18
        if ($input instanceof NodeList || $input instanceof \DOMNode) {
89 8
            $inputNodes = $this->inputAsNodeList($input);
90
91
            $fn = function($node) use ($inputNodes) {
92 8
                return $inputNodes->exists($node);
93 8
            };
94
95 18
        } elseif ($input instanceof \Closure) {
96
            // Since we're at the behest of the input \Closure, the 'matched'
97
            //  return value is always true.
98 2
            $matchType = true;
99
100 2
            $fn = $input;
101
102 11
        } elseif (is_string($input)) {
103
            $fn = function($node) use ($input) {
104 10
                return $node->find($input, 'self::')->count() != 0;
105 10
            };
106
107 10
        } else {
108
            throw new \InvalidArgumentException('Unexpected input value of type "' . gettype($input) . '"');
109
        }
110
111
        // Build a list of matching nodes.
112
        return $this->collection()->map(function($node) use ($fn, $matchType) {
113 18
            if ($fn($node) !== $matchType) {
114 17
                return null;
115
            }
116
117 18
            return $node;
118 18
        });
119
    }
120
121
    /**
122
     * @param string|NodeList|\DOMNode|\Closure $input
123
     *
124
     * @return bool
125
     */
126 15
    public function is($input) {
127 15
        return $this->getNodesMatchingInput($input)->count() != 0;
128
    }
129
130
    /**
131
     * @param string|NodeList|\DOMNode|\Closure $input
132
     *
133
     * @return NodeList
134
     */
135 1
    public function not($input) {
136 1
        return $this->getNodesMatchingInput($input, false);
137
    }
138
139
    /**
140
     * @param string|NodeList|\DOMNode|\Closure $input
141
     *
142
     * @return NodeList
143
     */
144 1
    public function filter($input) {
145 1
        return $this->getNodesMatchingInput($input);
146
    }
147
148
    /**
149
     * @param string|NodeList|\DOMNode|\Closure $input
150
     *
151
     * @return NodeList
152
     */
153 1
    public function has($input) {
154 1
        if ($input instanceof NodeList || $input instanceof \DOMNode) {
155 1
            $inputNodes = $this->inputAsNodeList($input);
156
157
            $fn = function($node) use ($inputNodes) {
158 1
                $descendantNodes = $node->find('*', 'descendant::');
159
160
                // Determine if we have a descendant match.
161
                return $inputNodes->reduce(function($carry, $inputNode) use ($descendantNodes) {
162
                    // Match descendant nodes against input nodes.
163 1
                    if ($descendantNodes->exists($inputNode)) {
164 1
                        return true;
165
                    }
166
167
                    return $carry;
168 1
                }, false);
169 1
            };
170
171 1
        } elseif (is_string($input)) {
172
            $fn = function($node) use ($input) {
173 1
                return $node->find($input, 'descendant::')->count() != 0;
174 1
            };
175
176 1
        } elseif ($input instanceof \Closure) {
177
            $fn = $input;
178
179
        } else {
180
            throw new \InvalidArgumentException('Unexpected input value of type "' . gettype($input) . '"');
181
        }
182
183 1
        return $this->getNodesMatchingInput($fn);
184
    }
185
186
    /**
187
     * @param string|NodeList|\DOMNode|\Closure $selector
188
     *
189
     * @return \DOMNode|null
190
     */
191
    public function preceding($selector = null) {
192
        return $this->precedingUntil(null, $selector)->first();
193
    }
194
195
    /**
196
     * @param string|NodeList|\DOMNode|\Closure $selector
197
     *
198
     * @return NodeList
199
     */
200 21
    public function precedingAll($selector = null) {
201 21
        return $this->precedingUntil(null, $selector);
202
    }
203
204
    /**
205
     * @param string|NodeList|\DOMNode|\Closure $input
206
     * @param string|NodeList|\DOMNode|\Closure $selector
207
     *
208
     * @return NodeList
209
     */
210 32
    public function precedingUntil($input = null, $selector = null) {
211 32
        return $this->_walkPathUntil('previousSibling', $input, $selector);
212
    }
213
214
    /**
215
     * @param string|NodeList|\DOMNode|\Closure $selector 
216
     *
217
     * @return \DOMNode|null
218
     */
219 12
    public function following($selector = null) {
220 12
        return $this->followingUntil(null, $selector)->first();
221
    }
222
223
    /**
224
     * @param string|NodeList|\DOMNode|\Closure $selector 
225
     *
226
     * @return NodeList
227
     */
228 21
    public function followingAll($selector = null) {
229 21
        return $this->followingUntil(null, $selector);
230
    }
231
232
    /**
233
     * @param string|NodeList|\DOMNode|\Closure $input
234
     * @param string|NodeList|\DOMNode|\Closure $selector
235
     *
236
     * @return NodeList
237
     */
238 25
    public function followingUntil($input = null, $selector = null) {
239 25
        return $this->_walkPathUntil('nextSibling', $input, $selector);
240
    }
241
242
    /**
243
     * @param string|NodeList|\DOMNode|\Closure $selector 
244
     *
245
     * @return NodeList
246
     */
247 21 View Code Duplication
    public function siblings($selector = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
248
        $results = $this->collection()->reduce(function($carry, $node) use ($selector) {
249 21
            return $carry->merge(
250 21
                $node->precedingAll($selector)->merge(
251 21
                    $node->followingAll($selector)
252 21
                )
253 21
            );
254 21
        }, $this->newNodeList());
255
256 21
        return $results;
257
    }
258
259
    /**
260
     * NodeList is only array like. Removing items using foreach() has undesired results.
261
     *
262
     * @return NodeList
263
     */
264 93
    public function children() {
265
        $results = $this->collection()->reduce(function($carry, $node) {
266 93
            return $carry->merge(
267 93
                $node->findXPath('child::*')
268 93
            );
269 93
        }, $this->newNodeList());
270
271 93
        return $results;
272
    }
273
274
    /**
275
     * @param string|NodeList|\DOMNode|\Closure $selector
276
     *
277
     * @return Element|NodeList|null
278
     */
279 51
    public function parent($selector = null) {
280 51
        $results = $this->_walkPathUntil('parentNode', null, $selector, self::$MATCH_TYPE_FIRST);
281
282 51
        return $this->result($results);
283
    }
284
285
    /**
286
     * @param int $index
287
     *
288
     * @return \DOMNode|null
289
     */
290 2
    public function eq($index) {
291 2
        if ($index < 0) {
292 1
            $index = $this->collection()->count() + $index;
293 1
        }
294
295 2
        return $this->collection()->offsetGet($index);
296
    }
297
298
    /**
299
     * @param string $selector
300
     *
301
     * @return NodeList
302
     */
303
    public function parents($selector = null) {
304
        return $this->parentsUntil(null, $selector);
305
    }
306
307
    /**
308
     * @param string|NodeList|\DOMNode|\Closure $input
309
     * @param string|NodeList|\DOMNode|\Closure $selector
310
     *
311
     * @return NodeList
312
     */
313
    public function parentsUntil($input = null, $selector = null) {
314
        return $this->_walkPathUntil('parentNode', $input, $selector);
315
    }
316
317
    /**
318
     * @return \DOMNode
319
     */
320
    public function intersect() {
321
        if ($this->collection()->count() < 2) {
322
            return $this->collection()->first();
323
        }
324
325
        $nodeParents = [];
326
327
        // Build a multi-dimensional array of the collection nodes parent elements
328
        $this->collection()->each(function($node) use(&$nodeParents) {
329
            $nodeParents[] = $node->parents()->unshift($node)->toArray();
330
        });
331
332
        // Find the common parent
333
        $diff = call_user_func_array('array_uintersect', array_merge($nodeParents, [function($a, $b) {
334
            return strcmp(spl_object_hash($a), spl_object_hash($b));
335
        }]));
336
337
        return array_shift($diff);
338
    }
339
340
    /**
341
     * @param string|NodeList|\DOMNode|\Closure $input
342
     *
343
     * @return Element|NodeList|null
344
     */
345 2
    public function closest($input) {
346 2
        $results = $this->_walkPathUntil('parentNode', $input, null, self::$MATCH_TYPE_LAST);
347
348 2
        return $this->result($results);
349
    }
350
351
    /**
352
     * NodeList is only array like. Removing items using foreach() has undesired results.
353
     *
354
     * @return NodeList
355
     */
356 26 View Code Duplication
    public function contents() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
357
        $results = $this->collection()->reduce(function($carry, $node) {
358 26
            if($node->isRemoved())
359 26
                return $this->newNodeList();
360 26
            return $carry->merge(
361 26
                $node->newNodeList($node->childNodes)
362 26
            );
363 26
        }, $this->newNodeList());
364
365 26
        return $results;
366
    }
367
368
    /**
369
     * @param string|NodeList|\DOMNode $input
370
     *
371
     * @return NodeList
372
     */
373
    public function add($input) {
374
        $nodes = $this->inputAsNodeList($input);
375
376
        $results = $this->collection()->merge(
377
            $nodes
378
        );
379
380
        return $results;
381
    }
382
383
    /** @var int */
384
    private static $MATCH_TYPE_FIRST = 1;
385
386
    /** @var int */
387
    private static $MATCH_TYPE_LAST = 2;
388
389
    /**
390
     * @param \DOMNode $baseNode
391
     * @param string $property
392
     * @param string|NodeList|\DOMNode|\Closure $input
393
     * @param string|NodeList|\DOMNode|\Closure $selector
394
     * @param int $matchType
395
     *
396
     * @return NodeList
397
     */
398 64
    protected function _buildNodeListUntil($baseNode, $property, $input = null, $selector = null, $matchType = null) {
399 64
        $resultNodes = $this->newNodeList();
400
401
        // Get our first node
402 64
        $node = $baseNode->$property;
403
404
        // Keep looping until we're either out of nodes, or at the root of the DOM.
405 64
        while ($node instanceof \DOMNode &&
406 64
               !($node instanceof \DOMDocument))
407
        {
408
            // Filter nodes if not matching last
409 64
            if ($matchType != self::$MATCH_TYPE_LAST && (is_null($selector) || $node->is($selector))) {
410 61
                $resultNodes[] = $node;
411 61
            }
412
413
            // 'Until' check or first match only
414 64
            if ($matchType == self::$MATCH_TYPE_FIRST || (!is_null($input) && $node->is($input))) {
415
                // Set last match
416 60
                if ($matchType == self::$MATCH_TYPE_LAST) {
417 2
                    $resultNodes[] = $node;
418 2
                }
419
420 60
                break;
421
            }
422
423
            // Find the next node
424 21
            $node = $node->$property;
425 21
        }
426
427 64
        return $resultNodes;
428
    }
429
430
    /**
431
     * @param array $nodeLists
432
     *
433
     * @return NodeList
434
     */
435 64
    protected function _uniqueNodes($nodeLists) {
436 64
        $resultNodes = $this->newNodeList();
437
438
        // Loop through our array of NodeLists
439 64
        foreach ($nodeLists as $nodeList) {
440
            // Each node in the NodeList
441 64
            foreach ($nodeList as $node) {
442
                // We're only interested in unique nodes
443 63
                if (!$resultNodes->exists($node)) {
444 63
                    $resultNodes[] = $node;
445 63
                }
446 64
            }
447 64
        }
448
449
        // Sort resulting NodeList: outer-most => inner-most.
450 64
        return $resultNodes->reverse();
451
    }
452
453
    /**
454
     * @param string $property
455
     * @param string|NodeList|\DOMNode|\Closure $input
456
     * @param string|NodeList|\DOMNode|\Closure $selector
457
     * @param int $matchType
458
     *
459
     * @return NodeList
460
     */
461 64
    protected function _walkPathUntil($property, $input = null, $selector = null, $matchType = null) {
462 64
        $nodeLists = [];
463
464 64
        $this->collection()->each(function($node) use($property, $input, $selector, $matchType, &$nodeLists) {
465 64
            $nodeLists[] = $this->_buildNodeListUntil($node, $property, $input, $selector, $matchType);
466 64
        });
467
468 64
        return $this->_uniqueNodes($nodeLists);
469
    }
470
}