Passed
Push — master ( 7d3727...7ca9c2 )
by Andrew
01:30
created

ManipulationTrait::prependTo()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 10
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 3
nc 2
nop 1
crap 3
1
<?php declare(strict_types=1);
2
3
namespace DOMWrap\Traits;
4
5
use DOMWrap\{
6
    Text,
7
    Element,
8
    NodeList
9
};
10
11
/**
12
 * Manipulation Trait
13
 *
14
 * @package DOMWrap\Traits
15
 * @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
16
 */
17
trait ManipulationTrait
18
{
19
    /**
20
     * Magic method - Trap function names using reserved keyword (empty, clone, etc..)
21
     *
22
     * @param string $name
23
     * @param array $arguments
24
     *
25
     * @return mixed
26
     */
27 8
    public function __call(string $name, array $arguments) {
28 8
        if (!method_exists($this, '_' . $name)) {
29 1
            throw new \BadMethodCallException("Call to undefined method " . get_class($this) . '::' . $name . "()");
30
        }
31
32 7
        return call_user_func_array([$this, '_' . $name], $arguments);
33
    }
34
35
    /**
36
     * @return string
37
     */
38
    public function __toString(): string {
39
        return $this->getOuterHtml();
40 61
    }
41 61
42 34
    /**
43 50
     * @param string|NodeList|\DOMNode $input
44 45
     *
45 13
     * @return iterable
46 13
     */
47
    protected function inputPrepareAsTraversable($input): iterable {
48
        if ($input instanceof \DOMNode) {
49
            // Handle raw \DOMNode elements and 'convert' them into their DOMWrap/* counterpart
50
            if (!method_exists($input, 'inputPrepareAsTraversable')) {
51 61
                $input = $this->document()->importNode($input, true);
0 ignored issues
show
Bug introduced by
The method document() does not exist on DOMWrap\Traits\ManipulationTrait. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

51
                $input = $this->/** @scrutinizer ignore-call */ document()->importNode($input, true);
Loading history...
52
            }
53
54
            $nodes = [$input];
55
        } else if (is_string($input)) {
56
            $nodes = $this->nodesFromHtml($input);
57
        } else if (is_iterable($input)) {
58
            $nodes = $input;
59 61
        } else {
60 61
            throw new \InvalidArgumentException();
61
        }
62 61
63
        return $nodes;
64 61
    }
65 60
66 49
    /**
67
     * @param string|NodeList|\DOMNode $input
68 60
     * @param bool $cloneForManipulate
69
     *
70
     * @return NodeList
71
     */
72 61
    protected function inputAsNodeList($input, $cloneForManipulate = true): NodeList {
73
        $nodes = $this->inputPrepareAsTraversable($input);
74
75
        $newNodes = $this->newNodeList();
0 ignored issues
show
Bug introduced by
The method newNodeList() does not exist on DOMWrap\Traits\ManipulationTrait. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

75
        /** @scrutinizer ignore-call */ 
76
        $newNodes = $this->newNodeList();
Loading history...
76
77
        foreach ($nodes as $node) {
78
            if ($node->document() !== $this->document()) {
79
                 $node = $this->document()->importNode($node, true);
80 22
            }
81 22
82
            if ($cloneForManipulate && $node->parentNode !== null) {
83 22
                $node = $node->cloneNode(true);
84
            }
85
86
            $newNodes[] = $node;
87
        }
88
89
        return $newNodes;
90
    }
91 45
92 45
    /**
93 45
     * @param string|NodeList|\DOMNode $input
94 45
     *
95
     * @return \DOMNode|null
96 45
     */
97
    protected function inputAsFirstNode($input): ?\DOMNode {
98
        $nodes = $this->inputAsNodeList($input);
99
100
        return $nodes->findXPath('self::*')->first();
101
    }
102
103
    /**
104
     * @param string $html
105
     *
106 53
     * @return NodeList
107 51
     */
108
    protected function nodesFromHtml($html): NodeList {
109 51
        $class = get_class($this->document());
110 6
        $doc = new $class();
111
        $nodes = $doc->html($html)->find('body > *');
112
113 51
        return $nodes;
114
    }
115 51
116 53
    /**
117
     * @param string|NodeList|\DOMNode|callable $input
118 53
     * @param callable $callback
119
     *
120
     * @return self
121
     */
122
    protected function manipulateNodesWithInput($input, callable $callback): self {
123
        $this->collection()->each(function($node, $index) use ($input, $callback) {
0 ignored issues
show
Bug introduced by
The method collection() does not exist on DOMWrap\Traits\ManipulationTrait. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

123
        $this->/** @scrutinizer ignore-call */ 
124
               collection()->each(function($node, $index) use ($input, $callback) {
Loading history...
124
            $html = $input;
125
126 40
            /*if ($input instanceof \DOMNode) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
56% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
127 40
                if ($input->parentNode !== null) {
128 1
                    $html = $input->cloneNode(true);
129
                }
130 39
            } else*/if (is_callable($input)) {
131
                $html = $input($node, $index);
132
            }
133 40
134
            $newNodes = $this->inputAsNodeList($html);
135 40
136 36
            $callback($node, $newNodes);
137 36
        });
138
139 40
        return $this;
140
    }
141 40
142
    /**
143 40
     * @param string|null $selector
144
     *
145
     * @return NodeList
146
     */
147
    public function detach(string $selector = null): NodeList {
148
        if (!is_null($selector)) {
149
            $nodes = $this->find($selector, 'self::');
0 ignored issues
show
Bug introduced by
The method find() does not exist on DOMWrap\Traits\ManipulationTrait. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

149
            /** @scrutinizer ignore-call */ 
150
            $nodes = $this->find($selector, 'self::');
Loading history...
150
        } else {
151 35
            $nodes = $this->collection();
152 35
        }
153
154 35
        $nodeList = $this->newNodeList();
155
156
        $nodes->each(function($node) use($nodeList) {
157
            if ($node->parent() instanceof \DOMNode) {
158
                $nodeList[] = $node->parent()->removeChild($node);
159
            }
160
        });
161
162
        $nodes->fromArray([]);
163 3
164 3
        return $nodeList;
165 3
    }
166
167 3
    /**
168
     * @param string|null $selector
169 3
     *
170
     * @return self
171
     */
172
    public function remove(string $selector = null): self {
173
        $this->detach($selector);
174
175
        return $this;
176
    }
177 14
178 14
    /**
179 9
     * @param string|NodeList|\DOMNode|callable $input
180
     *
181 5
     * @return self
182
     */
183
    public function replaceWith($input): self {
184
        $this->manipulateNodesWithInput($input, function($node, $newNodes) {
185
            foreach ($newNodes as $newNode) {
186
                $node->parent()->replaceChild($newNode, $node);
187
            }
188
        });
189 13
190 13
        return $this;
191 13
    }
192
193
    /**
194
     * @param string|NodeList|\DOMNode|callable $input
195
     *
196
     * @return string|self
197
     */
198
    public function text($input = null) {
199 5
        if (is_null($input)) {
200 5
            return $this->getText();
201 4
        } else {
202
            return $this->setText($input);
203
        }
204 5
    }
205
206 4
    /**
207
     * @return string
208
     */
209 4
    public function getText(): string {
210 5
        return (string)$this->collection()->reduce(function($carry, $node) {
211
            return $carry . $node->textContent;
212 5
        }, '');
213
    }
214
215
    /**
216
     * @param string|NodeList|\DOMNode|callable $input
217
     *
218
     * @return self
219
     */
220
    public function setText($input): self {
221 12
        if (is_string($input)) {
222 12
            $input = new Text($input);
223 12
        }
224
225 12
        $this->manipulateNodesWithInput($input, function($node, $newNodes) {
226
            // Remove old contents from the current node.
227 12
            $node->contents()->remove();
228
229
            // Add new contents in it's place.
230
            $node->append(new Text($newNodes->getText()));
231
        });
232
233
        return $this;
234
    }
235
236 12
    /**
237 12
     * @param string|NodeList|\DOMNode|callable $input
238 12
     *
239 12
     * @return self
240
     */
241 12
    public function before($input): self {
242
        $this->manipulateNodesWithInput($input, function($node, $newNodes) {
243
            foreach ($newNodes as $newNode) {
244 12
                $node->parent()->insertBefore($newNode, $node);
245
            }
246 12
        });
247
248
        return $this;
249
    }
250
251
    /**
252
     * @param string|NodeList|\DOMNode|callable $input
253
     *
254
     * @return self
255 4
     */
256 4
    public function after($input): self {
257 4
        $this->manipulateNodesWithInput($input, function($node, $newNodes) {
258
            foreach ($newNodes as $newNode) {
259 4
                if (is_null($node->following())) {
260
                    $node->parent()->appendChild($newNode);
261 4
                } else {
262
                    $node->parent()->insertBefore($newNode, $node->following());
263
                }
264
            }
265
        });
266
267
        return $this;
268
    }
269
270 33
    /**
271 33
     * @param string|NodeList|\DOMNode|callable $input
272 33
     *
273
     * @return self
274 33
     */
275
    public function prepend($input): self {
276 33
        $this->manipulateNodesWithInput($input, function($node, $newNodes) {
277
            foreach ($newNodes as $newNode) {
278
                $node->insertBefore($newNode, $node->contents()->first());
279
            }
280
        });
281
282
        return $this;
283 4
    }
284 3
285 4
    /**
286
     * @param string|NodeList|\DOMNode|callable $input
287 4
     *
288
     * @return self
289
     */
290
    public function append($input): self {
291
        $this->manipulateNodesWithInput($input, function($node, $newNodes) {
292
            foreach ($newNodes as $newNode) {
293 3
                $node->appendChild($newNode);
294 3
            }
295
        });
296 3
297 3
        return $this;
298 3
    }
299
300 3
    /**
301
     * @param string|NodeList|\DOMNode $selector
302
     *
303
     * @return self
304
     */
305
    public function prependTo($selector): self {
306
        if ($selector instanceof \DOMNode || $selector instanceof NodeList) {
307
            $nodes = $this->inputAsNodeList($selector);
308
        } else {
309 5
            $nodes = $this->document()->find($selector);
310 4
        }
311 4
312
        $nodes->prepend($this);
313 5
314
        return $this;
315 5
    }
316
317
    /**
318
     * @param string|NodeList|\DOMNode $selector
319
     *
320
     * @return self
321
     */
322
    public function appendTo($selector): self {
323
        if ($selector instanceof \DOMNode || $selector instanceof NodeList) {
324
            $nodes = $this->inputAsNodeList($selector);
325
        } else {
326
            $nodes = $this->document()->find($selector);
327
        }
328
329
        $nodes->append($this);
330
331
        return $this;
332
    }
333
334
    /**
335
     * @return self
336
     */
337
    public function _empty(): self {
338
        $this->collection()->each(function($node) {
339
            $node->contents()->remove();
340 18
        });
341 18
342
        return $this;
343 18
    }
344 1
345
    /**
346
     * @return NodeList|\DOMNode
347 17
     */
348
    public function _clone() {
349 17
        $clonedNodes = $this->newNodeList();
350 6
351
        $this->collection()->each(function($node) use($clonedNodes) {
352
            $clonedNodes[] = $node->cloneNode(true);
353 13
        });
354
355
        return $this->result($clonedNodes);
0 ignored issues
show
Bug introduced by
The method result() does not exist on DOMWrap\Traits\ManipulationTrait. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

355
        return $this->/** @scrutinizer ignore-call */ result($clonedNodes);
Loading history...
356
    }
357
358
    /**
359
     * @param string $name
360
     *
361
     * @return self
362
     */
363
    public function removeAttr(string $name): self {
364
        $this->collection()->each(function($node) use($name) {
365 5
            if ($node instanceof \DOMElement) {
366 3
                $node->removeAttribute($name);
367 3
            }
368
        });
369 5
370
        return $this;
371 5
    }
372
373
    /**
374
     * @param string $name
375
     *
376
     * @return bool
377
     */
378
    public function hasAttr(string $name): bool {
379
        return (bool)$this->collection()->reduce(function($carry, $node) use ($name) {
380 17
            if ($node->hasAttribute($name)) {
381 17
                return true;
382 12
            }
383
384 5
            return $carry;
385
        }, false);
386
    }
387
388
    /**
389
     * @internal
390
     *
391
     * @param string $name
392
     *
393
     * @return string
394
     */
395
    public function getAttr(string $name): string {
396 15
        $node = $this->collection()->first();
397 15
398 15
        if (!($node instanceof \DOMElement)) {
399
            return '';
400 15
        }
401 2
402
        $result = $node->getAttribute($name);
403
404
        if (empty($result)) {
405 15
            return '';
406 15
        }
407 10
408
        return $result;
409
    }
410 11
411 15
    /**
412
     * @internal
413
     *
414 15
     * @param string $name
415 7
     * @param mixed $value
416
     *
417
     * @return self
418
     */
419
    public function setAttr(string $name, $value): self {
420
        $this->collection()->each(function($node) use($name, $value) {
421
            if ($node instanceof \DOMElement) {
422 15
                $node->setAttribute($name, (string)$value);
423 13
            }
424
        });
425
426 15
        return $this;
427 15
    }
428
429
    /**
430
     * @param string $name
431
     * @param mixed $value
432
     *
433
     * @return self|string
434 7
     */
435 7
    public function attr(string $name, $value = null) {
436
        if (is_null($value)) {
437 7
            return $this->getAttr($name);
438
        } else {
439
            return $this->setAttr($name, $value);
440
        }
441
    }
442
443
    /**
444
     * @internal
445 8
     *
446 8
     * @param string $name
447
     * @param string|callable $value
448 8
     * @param bool $addValue
449
     */
450
    protected function _pushAttrValue(string $name, $value, bool $addValue = false): void {
451
        $this->collection()->each(function($node, $index) use($name, $value, $addValue) {
452
            if ($node instanceof \DOMElement) {
453
                $attr = $node->getAttribute($name);
454
455
                if (is_callable($value)) {
456
                    $value = $value($node, $index, $attr);
457 6
                }
458 6
459
                // Remove any existing instances of the value, or empty values.
460 6
                $values = array_filter(explode(' ', $attr), function($_value) use($value) {
461 6
                    if (strcasecmp($_value, $value) == 0 || empty($_value)) {
462 2
                        return false;
463
                    }
464
465 6
                    return true;
466 6
                });
467 6
468
                // If required add attr value to array
469
                if ($addValue) {
470
                    $values[] = $value;
471
                }
472
473
                // Set the attr if we either have values, or the attr already
474
                //  existed (we might be removing classes).
475 21
                //
476 21
                // Don't set the attr if it doesn't already exist.
477
                if (!empty($values) || $node->hasAttribute($name)) {
478
                    $node->setAttribute($name, implode(' ', $values));
479
                }
480 21
            }
481
        });
482
    }
483 21
484 21
    /**
485
     * @param string|callable $class
486
     *
487 21
     * @return self
488
     */
489
    public function addClass($class): self {
490
        $this->_pushAttrValue('class', $class, true);
491
492
        return $this;
493
    }
494
495 21
    /**
496
     * @param string|callable $class
497
     *
498 21
     * @return self
499
     */
500
    public function removeClass($class): self {
501 21
        $this->_pushAttrValue('class', $class);
502 21
503
        return $this;
504
    }
505 21
506
    /**
507
     * @param string $class
508
     *
509
     * @return bool
510
     */
511
    public function hasClass(string $class): bool {
512
        return (bool)$this->collection()->reduce(function($carry, $node) use ($class) {
513 18
            $attr = $node->getAttr('class');
514 16
515
            return array_reduce(explode(' ', (string)$attr), function($carry, $item) use ($class) {
516 16
                if (strcasecmp($item, $class) == 0) {
517
                    return true;
518
                }
519
520 16
                return $carry;
521
            }, false);
522 16
        }, false);
523
    }
524 16
525
    /**
526 16
     * @param Element $node
527
     *
528 18
     * @return \SplStack
529 18
     */
530
    protected function _getFirstChildWrapStack(Element $node): \SplStack {
531
        $stack = new \SplStack;
532
533
        do {
534
            // Push our current node onto the stack
535
            $stack->push($node);
536
537 9
            // Get the first element child node
538 8
            $node = $node->children()->first();
539
        } while ($node instanceof Element);
540 8
541
        // Get the top most node.
542
        return $stack;
543 8
    }
544
545
    /**
546
     * @param Element $node
547 8
     *
548 9
     * @return \SplStack
549
     */
550 9
    protected function _prepareWrapStack(Element $node): \SplStack {
551
        // Generate a stack (root to leaf) of the wrapper.
552
        // Includes only first element nodes / first element children.
553
        $stackNodes = $this->_getFirstChildWrapStack($node);
554
555
        // Only using the first element, remove any siblings.
556
        foreach ($stackNodes as $stackNode) {
557
            $stackNode->siblings()->remove();
558
        }
559 9
560
        return $stackNodes;
561 8
    }
562
563
    /**
564 8
     * @param string|NodeList|\DOMNode|callable $input
565
     * @param callable $callback
566
     */
567 8
    protected function wrapWithInputByCallback($input, callable $callback): void {
568 9
        $this->collection()->each(function($node, $index) use ($input, $callback) {
569
            $html = $input;
570 9
571
            if (is_callable($input)) {
572
                $html = $input($node, $index);
573
            }
574
575
            $inputNode = $this->inputAsFirstNode($html);
576
577
            if ($inputNode instanceof Element) {
578 7
                // Pre-process wrapper into a stack of first element nodes.
579 7
                $stackNodes = $this->_prepareWrapStack($inputNode);
580 1
581
                $callback($node, $stackNodes);
582
            }
583 6
        });
584
    }
585
586
    /**
587 6
     * @param string|NodeList|\DOMNode|callable $input
588
     *
589 6
     * @return self
590 1
     */
591
    public function wrapInner($input): self {
592
        $this->wrapWithInputByCallback($input, function($node, $stackNodes) {
593 5
            foreach ($node->contents() as $child) {
594
                // Remove child from the current node
595
                $oldChild = $child->detach()->first();
596 5
597
                // Add it back as a child of the top (leaf) node on the stack
598 5
                $stackNodes->top()->append($oldChild);
599
            }
600 5
601 5
            // Add the bottom (root) node on the stack
602
            $node->append($stackNodes->bottom());
603 5
        });
604
605
        return $this;
606
    }
607
608
    /**
609
     * @param string|NodeList|\DOMNode|callable $input
610 4
     *
611 3
     * @return self
612
     */
613
    public function wrap($input): self {
614 3
        $this->wrapWithInputByCallback($input, function($node, $stackNodes) {
615 3
            // Add the new bottom (root) node after the current node
616
            $node->after($stackNodes->bottom());
617 3
618 3
            // Remove the current node
619
            $oldNode = $node->detach()->first();
620 3
621 4
            // Add the 'current node' back inside the new top (leaf) node.
622
            $stackNodes->top()->append($oldNode);
623 4
        });
624
625
        return $this;
626
    }
627
628
    /**
629 2
     * @param string|NodeList|\DOMNode|callable $input
630 2
     *
631 2
     * @return self
632
     */
633
    public function wrapAll($input): self {
634
        if (!$this->collection()->count()) {
635
            return $this;
636
        }
637
638
        if (is_callable($input)) {
639 1
            $input = $input($this->collection()->first());
640 1
        }
641 1
642
        $inputNode = $this->inputAsFirstNode($input);
643
644
        if (!($inputNode instanceof Element)) {
645
            return $this;
646
        }
647
648
        $stackNodes = $this->_prepareWrapStack($inputNode);
649
650 5
        // Add the new bottom (root) node before the first matched node
651
        $this->collection()->first()->before($stackNodes->bottom());
652 4
653
        $this->collection()->each(function($node) use ($stackNodes) {
654
            // Detach and add node back inside the new wrappers top (leaf) node.
655 4
            $stackNodes->top()->append($node->detach());
656 5
        });
657
658 5
        return $this;
659
    }
660
661
    /**
662
     * @return self
663
     */
664
    public function unwrap(): self {
665
        $this->collection()->each(function($node) {
666 140
            $parent = $node->parent();
667 140
668 2
            // Replace parent node (the one we're unwrapping) with it's children.
669
            $parent->contents()->each(function($childNode) use($parent) {
670 140
                $oldChildNode = $childNode->detach()->first();
671
672
                $parent->before($oldChildNode);
673
            });
674
675
            $parent->remove();
676
        });
677
678
        return $this;
679
    }
680
681
    /**
682
     * @return string
683
     */
684
    public function getOuterHtml(): string {
685
        return $this->document()->saveHTML(
686
            $this->collection()->first()
687
        );
688
    }
689
690
    /**
691
     * @return string
692
     */
693
    public function getHtml(): string {
694
        return $this->collection()->first()->contents()->reduce(function($carry, $node) {
695
            return $carry . $this->document()->saveHTML($node);
696
        }, '');
697
    }
698
699
    /**
700
     * @param string|NodeList|\DOMNode|callable $input
701
     *
702
     * @return self
703
     */
704
    public function setHtml($input): self {
705
        $this->manipulateNodesWithInput($input, function($node, $newNodes) {
706
            // Remove old contents from the current node.
707
            $node->contents()->remove();
708
709
            // Add new contents in it's place.
710
            $node->append($newNodes);
711
        });
712
713
        return $this;
714
    }
715
716
    /**
717
     * @param string|NodeList|\DOMNode|callable $input
718
     *
719
     * @return string|self
720
     */
721
    public function html($input = null) {
722
        if (is_null($input)) {
723
            return $this->getHtml();
724
        } else {
725
            return $this->setHtml($input);
726
        }
727
    }
728
729
    /**
730
     * @param string|NodeList|\DOMNode $input
731
     *
732
     * @return NodeList
733
     */
734
    public function create($input): NodeList {
735
        return $this->inputAsNodeList($input);
736
    }
737
}