Completed
Push — master ( 295db5...bad9f1 )
by Andrew
03:58
created

ManipulationTrait::__toString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 2
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
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
            $nodes = [$input];
50
        } else if (is_string($input)) {
51 61
            $nodes = $this->nodesFromHtml($input);
52
        } else if (is_iterable($input)) {
53
            $nodes = $input;
54
        } else {
55
            throw new \InvalidArgumentException();
56
        }
57
58
        return $nodes;
59 61
    }
60 61
61
    /**
62 61
     * @param string|NodeList|\DOMNode $input
63
     *
64 61
     * @return NodeList
65 60
     */
66 49
    protected function inputAsNodeList($input): NodeList {
67
        $nodes = $this->inputPrepareAsTraversable($input);
68 60
69
        $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

69
        /** @scrutinizer ignore-call */ 
70
        $newNodes = $this->newNodeList();
Loading history...
70
71
        foreach ($nodes as $node) {
72 61
            if ($node->document() !== $this->document()) {
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

72
            if ($node->document() !== $this->/** @scrutinizer ignore-call */ document()) {
Loading history...
73
                $newNodes[] = $this->document()->importNode($node, true);
74
            } else {
75
                $newNodes[] = $node;
76
            }
77
        }
78
79
        return $newNodes;
80 22
    }
81 22
82
    /**
83 22
     * @param string|NodeList|\DOMNode $input
84
     *
85
     * @return \DOMNode|null
86
     */
87
    protected function inputAsFirstNode($input): ?\DOMNode {
88
        $nodes = $this->inputAsNodeList($input);
89
90
        return $nodes->findXPath('self::*')->first();
91 45
    }
92 45
93 45
    /**
94 45
     * @param string $html
95
     *
96 45
     * @return NodeList
97
     */
98
    protected function nodesFromHtml($html): NodeList {
99
        $class = get_class($this->document());
100
        $doc = new $class();
101
        $nodes = $doc->html($html)->find('body > *');
102
103
        return $nodes;
104
    }
105
106 53
    /**
107 51
     * @param string|NodeList|\DOMNode|callable $input
108
     * @param callable $callback
109 51
     *
110 6
     * @return self
111
     */
112
    protected function manipulateNodesWithInput($input, callable $callback): self {
113 51
        $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

113
        $this->/** @scrutinizer ignore-call */ 
114
               collection()->each(function($node, $index) use ($input, $callback) {
Loading history...
114
            $html = $input;
115 51
116 53
            if (is_callable($input)) {
117
                $html = $input($node, $index);
118 53
            }
119
120
            $newNodes = $this->inputAsNodeList($html);
121
122
            $callback($node, $newNodes);
123
        });
124
125
        return $this;
126 40
    }
127 40
128 1
    /**
129
     * @param string|null $selector
130 39
     *
131
     * @return NodeList
132
     */
133 40
    public function detach(string $selector = null): NodeList {
134
        if (!is_null($selector)) {
135 40
            $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

135
            /** @scrutinizer ignore-call */ 
136
            $nodes = $this->find($selector, 'self::');
Loading history...
136 36
        } else {
137 36
            $nodes = $this->collection();
138
        }
139 40
140
        $nodeList = $this->newNodeList();
141 40
142
        $nodes->each(function($node) use($nodeList) {
143 40
            if ($node->parent() instanceof \DOMNode) {
144
                $nodeList[] = $node->parent()->removeChild($node);
145
            }
146
        });
147
148
        $nodes->fromArray([]);
149
150
        return $nodeList;
151 35
    }
152 35
153
    /**
154 35
     * @param string|null $selector
155
     *
156
     * @return self
157
     */
158
    public function remove(string $selector = null): self {
159
        $this->detach($selector);
160
161
        return $this;
162
    }
163 3
164 3
    /**
165 3
     * @param string|NodeList|\DOMNode|callable $input
166
     *
167 3
     * @return self
168
     */
169 3
    public function replaceWith($input): self {
170
        $this->manipulateNodesWithInput($input, function($node, $newNodes) {
171
            foreach ($newNodes as $newNode) {
172
                $node->parent()->replaceChild($newNode, $node);
173
            }
174
        });
175
176
        return $this;
177 14
    }
178 14
179 9
    /**
180
     * @param string|NodeList|\DOMNode|callable $input
181 5
     *
182
     * @return string|self
183
     */
184
    public function text($input = null) {
185
        if (is_null($input)) {
186
            return $this->getText();
187
        } else {
188
            return $this->setText($input);
189 13
        }
190 13
    }
191 13
192
    /**
193
     * @return string
194
     */
195
    public function getText(): string {
196
        return (string)$this->collection()->reduce(function($carry, $node) {
197
            return $carry . $node->textContent;
198
        }, '');
199 5
    }
200 5
201 4
    /**
202
     * @param string|NodeList|\DOMNode|callable $input
203
     *
204 5
     * @return self
205
     */
206 4
    public function setText($input): self {
207
        if (is_string($input)) {
208
            $input = new Text($input);
209 4
        }
210 5
211
        $this->manipulateNodesWithInput($input, function($node, $newNodes) {
212 5
            // Remove old contents from the current node.
213
            $node->contents()->remove();
214
215
            // Add new contents in it's place.
216
            $node->append(new Text($newNodes->getText()));
217
        });
218
219
        return $this;
220
    }
221 12
222 12
    /**
223 12
     * @param string|NodeList|\DOMNode|callable $input
224
     *
225 12
     * @return self
226
     */
227 12
    public function before($input): self {
228
        $this->manipulateNodesWithInput($input, function($node, $newNodes) {
229
            foreach ($newNodes as $newNode) {
230
                $node->parent()->insertBefore($newNode, $node);
231
            }
232
        });
233
234
        return $this;
235
    }
236 12
237 12
    /**
238 12
     * @param string|NodeList|\DOMNode|callable $input
239 12
     *
240
     * @return self
241 12
     */
242
    public function after($input): self {
243
        $this->manipulateNodesWithInput($input, function($node, $newNodes) {
244 12
            foreach ($newNodes as $newNode) {
245
                if (is_null($node->following())) {
246 12
                    $node->parent()->appendChild($newNode);
247
                } else {
248
                    $node->parent()->insertBefore($newNode, $node->following());
249
                }
250
            }
251
        });
252
253
        return $this;
254
    }
255 4
256 4
    /**
257 4
     * @param string|NodeList|\DOMNode|callable $input
258
     *
259 4
     * @return self
260
     */
261 4
    public function prepend($input): self {
262
        $this->manipulateNodesWithInput($input, function($node, $newNodes) {
263
            foreach ($newNodes as $newNode) {
264
                $node->insertBefore($newNode, $node->contents()->first());
265
            }
266
        });
267
268
        return $this;
269
    }
270 33
271 33
    /**
272 33
     * @param string|NodeList|\DOMNode|callable $input
273
     *
274 33
     * @return self
275
     */
276 33
    public function append($input): self {
277
        $this->manipulateNodesWithInput($input, function($node, $newNodes) {
278
            foreach ($newNodes as $newNode) {
279
                $node->appendChild($newNode);
280
            }
281
        });
282
283 4
        return $this;
284 3
    }
285 4
286
    /**
287 4
     * @return self
288
     */
289
    public function _empty(): self {
290
        $this->collection()->each(function($node) {
291
            $node->contents()->remove();
292
        });
293 3
294 3
        return $this;
295
    }
296 3
297 3
    /**
298 3
     * @return NodeList|\DOMNode
299
     */
300 3
    public function _clone() {
301
        $clonedNodes = $this->newNodeList();
302
303
        $this->collection()->each(function($node) use($clonedNodes) {
304
            $clonedNodes[] = $node->cloneNode(true);
305
        });
306
307
        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

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