Passed
Branch master (8d332b)
by Andrew
02:53
created

ManipulationTrait::_empty()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
c 0
b 0
f 0
ccs 4
cts 4
cp 1
rs 9.4285
cc 1
eloc 3
nc 1
nop 0
crap 1
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
     * @param string|NodeList|\DOMNode $input
37
     *
38
     * @return iterable
39
     */
40 61
    protected function inputPrepareAsTraversable($input): iterable {
41 61
        if ($input instanceof \DOMNode) {
42 34
            $nodes = [$input];
43 50
        } else if (is_string($input)) {
44 45
            $nodes = $this->nodesFromHtml($input);
45 13
        } else if (is_iterable($input)) {
46 13
            $nodes = $input;
47
        } else {
48
            throw new \InvalidArgumentException();
49
        }
50
51 61
        return $nodes;
52
    }
53
54
    /**
55
     * @param string|NodeList|\DOMNode $input
56
     *
57
     * @return NodeList
58
     */
59 61
    protected function inputAsNodeList($input): NodeList {
60 61
        $nodes = $this->inputPrepareAsTraversable($input);
61
62 61
        $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

62
        /** @scrutinizer ignore-call */ 
63
        $newNodes = $this->newNodeList();
Loading history...
63
64 61
        foreach ($nodes as $node) {
65 60
            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

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

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

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

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