Passed
Push — master ( 7cfddd...4e3f7f )
by Andrew
02:36
created

ManipulationTrait::create()   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 1
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 ($input instanceof \DOMNode) {
117
                if ($input->parentNode !== null) {
118 53
                    $html = $input->cloneNode(true);
119
                }
120
            } elseif (is_callable($input)) {
121
                $html = $input($node, $index);
122
            }
123
124
            $newNodes = $this->inputAsNodeList($html);
125
126 40
            $callback($node, $newNodes);
127 40
        });
128 1
129
        return $this;
130 39
    }
131
132
    /**
133 40
     * @param string|null $selector
134
     *
135 40
     * @return NodeList
136 36
     */
137 36
    public function detach(string $selector = null): NodeList {
138
        if (!is_null($selector)) {
139 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

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

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