Completed
Push — master ( 308454...cfcf30 )
by Lars
02:18
created

SimpleHtmlDom::isRemoved()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 1
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\helper;
6
7
use BadMethodCallException;
8
use DOMElement;
9
use DOMNode;
10
use RuntimeException;
11
12
/**
13
 * @property string      $outertext
14
 *                            <p>Get dom node's outer html (alias for "outerHtml").</p>
15
 * @property string      $outerhtml
16
 *                            <p>Get dom node's outer html.</p>
17
 * @property string      $innertext
18
 *                            <p>Get dom node's inner html (alias for "innerHtml").</p>
19
 * @property string      $innerhtml
20
 *                            <p>Get dom node's inner html.</p>
21
 * @property string      $plaintext
22
 *                            <p>Get dom node's plain text.</p>
23
 *
24
 * @property-read string $tag
25
 *                           <p>Get dom node name.</p>
26
 * @property-read string $attr
27
 *                            <p>Get dom node attributes.</p>
28
 * @property-read string $text
29
 *                            <p>Get dom node name.</p>
30
 * @property-read string $html
31
 *                            <p>Get dom node's outer html.</p>
32
 *
33
 * @method SimpleHtmlDom|SimpleHtmlDom[]|SimpleHtmlDomNode|null children() children($idx = -1)
34
 *                                                                                             <p>Returns children of
35
 *                                                                                             node.</p>
36
 * @method SimpleHtmlDom|null first_child()
37
 *                                          <p>Returns the first child of node.</p>
38
 * @method SimpleHtmlDom|null last_child()
39
 *                                         <p>Returns the last child of node.</p>
40
 * @method SimpleHtmlDom|null next_sibling()
41
 *                                           <p>Returns the next sibling of node.</p>
42
 * @method SimpleHtmlDom|null prev_sibling()
43
 *                                           <p>Returns the previous sibling of node.</p>
44
 * @method SimpleHtmlDom|null parent()
45
 *                                     <p>Returns the parent of node.</p>
46
 * @method string outerText()
47
 *                            <p>Get dom node's outer html (alias for "outerHtml()").</p>
48
 * @method string outerHtml()
49
 *                            <p>Get dom node's outer html.</p>
50
 * @method string innerText()
51
 *                            <p>Get dom node's inner html (alias for "innerHtml()").</p>
52
 */
53
class SimpleHtmlDom implements \IteratorAggregate
54
{
55
    /**
56
     * @var array
57
     */
58
    protected static $functionAliases = [
59
        'children'     => 'childNodes',
60
        'first_child'  => 'firstChild',
61
        'last_child'   => 'lastChild',
62
        'next_sibling' => 'nextSibling',
63
        'prev_sibling' => 'previousSibling',
64
        'parent'       => 'parentNode',
65
        'outertext'    => 'html',
66
        'outerhtml'    => 'html',
67
        'innertext'    => 'innerHtml',
68
        'innerhtml'    => 'innerHtml',
69
    ];
70
71
    /**
72
     * @var DOMElement|DOMNode
73
     */
74
    protected $node;
75
76
    /**
77
     * @param DOMElement|DOMNode $node
78
     */
79 103
    public function __construct(DOMNode $node)
80
    {
81 103
        $this->node = $node;
82 103
    }
83
84
    /**
85
     * @param string $name
86
     * @param array  $arguments
87
     *
88
     * @return SimpleHtmlDom|string|null
89
     * @throws \BadMethodCallException
90
     *
91
     */
92 9 View Code Duplication
    public function __call($name, $arguments)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
93
    {
94 9
        $name = \strtolower($name);
95
96 9
        if (isset(self::$functionAliases[$name])) {
97 9
            return \call_user_func_array([$this, self::$functionAliases[$name]], $arguments);
98
        }
99
100
        throw new BadMethodCallException('Method does not exist');
101
    }
102
103
    /**
104
     * @param string $name
105
     *
106
     * @return array|string|null
107
     */
108 47
    public function __get($name)
109
    {
110 47
        $nameOrig = $name;
111 47
        $name = \strtolower($name);
112
113
        switch ($name) {
114 47
            case 'outerhtml':
115 43
            case 'outertext':
116 36
            case 'html':
117 20
                return $this->html();
118 36
            case 'innerhtml':
119 30
            case 'innertext':
120 11
                return $this->innerHtml();
121 27
            case 'text':
122 22
            case 'plaintext':
123 17
                return $this->text();
124 12
            case 'tag':
125 5
                return $this->node->nodeName;
126 10
            case 'attr':
127
                return $this->getAllAttributes();
128 View Code Duplication
            default:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
129 10
                if (\property_exists($this->node, $nameOrig)) {
130 1
                    return $this->node->{$nameOrig};
131
                }
132
133 10
                return $this->getAttribute($name);
134
        }
135
    }
136
137
    /**
138
     * @param string $selector
139
     * @param int    $idx
140
     *
141
     * @return SimpleHtmlDom|SimpleHtmlDom[]|SimpleHtmlDomNodeInterface
142
     */
143 12
    public function __invoke($selector, $idx = null)
144
    {
145 12
        return $this->find($selector, $idx);
146
    }
147
148
    /**
149
     * @param string $name
150
     *
151
     * @return bool
152
     */
153 1
    public function __isset($name)
154
    {
155 1
        $nameOrig = $name;
156 1
        $name = \strtolower($name);
157
158
        switch ($name) {
159 1
            case 'outertext':
160 1
            case 'outerhtml':
161 1
            case 'innertext':
162 1
            case 'innerhtml':
163 1
            case 'plaintext':
164 1
            case 'text':
165 1
            case 'tag':
166
                return true;
167 View Code Duplication
            default:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
168 1
                if (\property_exists($this->node, $nameOrig)) {
169
                    return isset($this->node->{$nameOrig});
170
                }
171
172 1
                return $this->hasAttribute($name);
173
        }
174
    }
175
176
    /**
177
     * @param string $name
178
     * @param mixed  $value
179
     *
180
     * @return null|SimpleHtmlDom
181
     */
182 16
    public function __set($name, $value)
183
    {
184 16
        $nameOrig = $name;
185 16
        $name = \strtolower($name);
186
187
        switch ($name) {
188 16
            case 'outerhtml':
189 14
            case 'outertext':
190 4
                return $this->replaceNodeWithString($value);
191 12
            case 'innertext':
192 10
            case 'innerhtml':
193 7
                return $this->replaceChildWithString($value);
194 9
            case 'plaintext':
195 1
                return $this->replaceTextWithString($value);
196 View Code Duplication
            default:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
197 8
                if (\property_exists($this->node, $nameOrig)) {
198
                    return $this->node->{$nameOrig} = $value;
199
                }
200
201 8
                return $this->setAttribute($name, $value);
202
        }
203
    }
204
205
    /**
206
     * @return string
207
     */
208 2
    public function __toString()
209
    {
210 2
        return $this->html();
211
    }
212
213
    /**
214
     * @param string $name
215
     *
216
     * @return void
217
     */
218
    public function __unset($name)
219
    {
220
        $this->removeAttribute($name);
221
    }
222
223
    /**
224
     * Returns children of node.
225
     *
226
     * @param int $idx
227
     *
228
     * @return SimpleHtmlDom|SimpleHtmlDom[]|SimpleHtmlDomNodeInterface|null
229
     */
230 2
    public function childNodes(int $idx = -1)
231
    {
232 2
        $nodeList = $this->getIterator();
233
234 2
        if ($idx === -1) {
235 2
            return $nodeList;
236
        }
237
238 2
        return $nodeList[$idx] ?? null;
239
    }
240
241
    /**
242
     * Find list of nodes with a CSS selector.
243
     *
244
     * @param string   $selector
245
     * @param int|null $idx
246
     *
247
     * @return SimpleHtmlDom|SimpleHtmlDom[]|SimpleHtmlDomNodeInterface
248
     */
249 26
    public function find(string $selector, $idx = null)
250
    {
251 26
        return $this->getHtmlDomParser()->find($selector, $idx);
252
    }
253
254
    /**
255
     * Find one node with a CSS selector.
256
     *
257
     * @param string $selector
258
     *
259
     * @return SimpleHtmlDom
260
     */
261
    public function findOne(string $selector): SimpleHtmlDom
262
    {
263
        return $this->find($selector, 0);
264
    }
265
266
    /**
267
     * Returns the first child of node.
268
     *
269
     * @return SimpleHtmlDom|null
270
     */
271 4
    public function firstChild()
272
    {
273
        /** @var null|DOMNode $node */
274 4
        $node = $this->node->firstChild;
275
276 4
        if ($node === null) {
277 1
            return null;
278
        }
279
280 4
        return new self($node);
281
    }
282
283
    /**
284
     * Returns an array of attributes.
285
     *
286
     * @return array|null
287
     */
288 2
    public function getAllAttributes()
289
    {
290 2
        if ($this->node->hasAttributes()) {
291 2
            $attributes = [];
292 2
            foreach ($this->node->attributes as $attr) {
293 2
                $attributes[$attr->name] = HtmlDomParser::putReplacedBackToPreserveHtmlEntities($attr->value);
294
            }
295
296 2
            return $attributes;
297
        }
298
299 1
        return null;
300
    }
301
302
    /**
303
     * Return attribute value.
304
     *
305
     * @param string $name
306
     *
307
     * @return string
308
     */
309 14
    public function getAttribute(string $name): string
310
    {
311 14
        if ($this->node instanceof DOMElement) {
312 14
            return HtmlDomParser::putReplacedBackToPreserveHtmlEntities(
313 14
                $this->node->getAttribute($name)
314
            );
315
        }
316
317
        return '';
318
    }
319
320
    /**
321
     * Return element by #id.
322
     *
323
     * @param string $id
324
     *
325
     * @return SimpleHtmlDom
326
     */
327 1
    public function getElementById(string $id): SimpleHtmlDom
328
    {
329 1
        return $this->find("#${id}", 0);
330
    }
331
332
    /**
333
     * Return element by tag name.
334
     *
335
     * @param string $name
336
     *
337
     * @return SimpleHtmlDom|null
338
     */
339
    public function getElementByTagName(string $name)
340
    {
341 1
        if ($this->node instanceof DOMElement) {
342 1
            $node = $this->node->getElementsByTagName($name)->item(0);
343
        } else {
344
            $node = null;
345
        }
346
347 1
        if ($node === null) {
348
            return null;
349
        }
350
351 1
        return new self($node);
352
    }
353
354
    /**
355
     * Returns elements by #id.
356
     *
357
     * @param string   $id
358
     * @param int|null $idx
359
     *
360
     * @return SimpleHtmlDom|SimpleHtmlDom[]|SimpleHtmlDomNodeInterface
361
     */
362
    public function getElementsById(string $id, $idx = null)
363
    {
364
        return $this->find("#${id}", $idx);
365
    }
366
367
    /**
368
     * Returns elements by tag name.
369
     *
370
     * @param string   $name
371
     * @param int|null $idx
372
     *
373
     * @return SimpleHtmlDom|SimpleHtmlDom[]|SimpleHtmlDomNodeInterface
374
     */
375
    public function getElementsByTagName(string $name, $idx = null)
376
    {
377 1
        if ($this->node instanceof DOMElement) {
378 1
            $nodesList = $this->node->getElementsByTagName($name);
379
        } else {
380
            $nodesList = [];
381
        }
382
383 1
        $elements = new SimpleHtmlDomNode();
384
385 1
        foreach ($nodesList as $node) {
386 1
            $elements[] = new self($node);
387
        }
388
389
        // return all elements
390 1
        if ($idx === null) {
391 1
            return $elements;
392
        }
393
394
        // handle negative values
395
        if ($idx < 0) {
396
            $idx = \count($elements) + $idx;
397
        }
398
399
        // return one element
400
        return $elements[$idx] ?? new SimpleHtmlDom(new DOMNode());
401
    }
402
403
    /**
404
     * Create a new "HtmlDomParser"-object from the current context.
405
     *
406
     * @return HtmlDomParser
407
     */
408
    public function getHtmlDomParser(): HtmlDomParser
409
    {
410 67
        return new HtmlDomParser($this);
411
    }
412
413
    /**
414
     * Retrieve an external iterator.
415
     *
416
     * @see  http://php.net/manual/en/iteratoraggregate.getiterator.php
417
     *
418
     * @return SimpleHtmlDomNode
419
     *                           <p>
420
     *                              An instance of an object implementing <b>Iterator</b> or
421
     *                              <b>Traversable</b>
422
     *                           </p>
423
     */
424
    public function getIterator(): SimpleHtmlDomNode
425
    {
426 2
        $elements = new SimpleHtmlDomNode();
427 2
        if ($this->node->hasChildNodes()) {
428 2
            foreach ($this->node->childNodes as $node) {
429 2
                $elements[] = new self($node);
430
            }
431
        }
432
433 2
        return $elements;
434
    }
435
436
    /**
437
     * @return DOMNode
438
     */
439
    public function getNode(): DOMNode
440
    {
441 68
        return $this->node;
442
    }
443
444
    /**
445
     * Determine if an attribute exists on the element.
446
     *
447
     * @param string $name
448
     *
449
     * @return bool
450
     */
451
    public function hasAttribute(string $name): bool
452
    {
453 2
        if (!$this->node instanceof DOMElement) {
454
            return false;
455
        }
456
457 2
        return $this->node->hasAttribute($name);
458
    }
459
460
    /**
461
     * Get dom node's outer html.
462
     *
463
     * @param bool $multiDecodeNewHtmlEntity
464
     *
465
     * @return string
466
     */
467
    public function html(bool $multiDecodeNewHtmlEntity = false): string
468
    {
469 23
        return $this->getHtmlDomParser()->html($multiDecodeNewHtmlEntity);
470
    }
471
472
    /**
473
     * Get dom node's inner html.
474
     *
475
     * @param bool $multiDecodeNewHtmlEntity
476
     *
477
     * @return string
478
     */
479
    public function innerHtml(bool $multiDecodeNewHtmlEntity = false): string
480
    {
481 11
        return $this->getHtmlDomParser()->innerHtml($multiDecodeNewHtmlEntity);
482
    }
483
484
    /**
485
     * Returns the last child of node.
486
     *
487
     * @return SimpleHtmlDom|null
488
     */
489
    public function lastChild()
490
    {
491
        /** @var null|DOMNode $node */
492 4
        $node = $this->node->lastChild;
493
494 4
        if ($node === null) {
495 1
            return null;
496
        }
497
498 4
        return new self($node);
499
    }
500
501
    /**
502
     * Returns the next sibling of node.
503
     *
504
     * @return SimpleHtmlDom|null
505
     */
506
    public function nextSibling()
507
    {
508
        /** @var null|DOMNode $node */
509 1
        $node = $this->node->nextSibling;
510
511 1
        if ($node === null) {
512 1
            return null;
513
        }
514
515 1
        return new self($node);
516
    }
517
518
    /**
519
     * Returns the parent of node.
520
     *
521
     * @return SimpleHtmlDom
522
     */
523
    public function parentNode(): self
524
    {
525 1
        return new self($this->node->parentNode);
526
    }
527
528
    /**
529
     * Nodes can get partially destroyed in which they're still an
530
     * actual DOM node (such as \DOMElement) but almost their entire
531
     * body is gone, including the `nodeType` attribute.
532
     *
533
     * @return bool true if node has been destroyed
534
     */
535
    public function isRemoved(): bool
536
    {
537
        return !isset($this->node->nodeType);
538
    }
539
540
    /**
541
     * Returns the previous sibling of node.
542
     *
543
     * @return SimpleHtmlDom|null
544
     */
545
    public function previousSibling()
546
    {
547
        /** @var null|DOMNode $node */
548 1
        $node = $this->node->previousSibling;
549
550 1
        if ($node === null) {
551 1
            return null;
552
        }
553
554 1
        return new self($node);
555
    }
556
557
    /**
558
     * Replace child node.
559
     *
560
     * @param string $string
561
     *
562
     * @return SimpleHtmlDom
563
     *
564
     */
565
    protected function replaceChildWithString(string $string): self
566
    {
567 7
        if (!empty($string)) {
568 6
            $newDocument = new HtmlDomParser($string);
569
570 6
            $tmpDomString = $this->normalizeStringForComparision($newDocument);
571 6
            $tmpStr = $this->normalizeStringForComparision($string);
572 6
            if ($tmpDomString !== $tmpStr) {
573
                throw new RuntimeException(
574
                    'Not valid HTML fragment!' . "\n" .
575
                    $tmpDomString . "\n" .
576
                    $tmpStr
577
                );
578
            }
579
        }
580
581
        /** @noinspection PhpParamsInspection */
582 7
        if (\count($this->node->childNodes) > 0) {
583 7
            foreach ($this->node->childNodes as $node) {
584 7
                $this->node->removeChild($node);
585
            }
586
        }
587
588 7
        if (!empty($newDocument)) {
589 6
            $newDocument = $this->cleanHtmlWrapper($newDocument);
590 6
            $ownerDocument = $this->node->ownerDocument;
591 6
            if ($ownerDocument !== null) {
592 6
                $newNode = $ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
593
                /** @noinspection UnusedFunctionResultInspection */
594 6
                $this->node->appendChild($newNode);
595
            }
596
        }
597
598 7
        return $this;
599
    }
600
601
    /**
602
     * Replace this node with text
603
     *
604
     * @param string $string
605
     *
606
     * @return SimpleHtmlDom
607
     */
608
    protected function replaceTextWithString($string): self
609
    {
610 1
        if (empty($string)) {
611 1
            $this->node->parentNode->removeChild($this->node);
612
613 1
            return $this;
614
        }
615
616 1
        $ownerDocument = $this->node->ownerDocument;
617 1
        if ($ownerDocument !== null) {
618 1
            $newElement = $ownerDocument->createTextNode($string);
619 1
            $newNode = $ownerDocument->importNode($newElement, true);
620 1
            $this->node->parentNode->replaceChild($newNode, $this->node);
621 1
            $this->node = $newNode;
622
        }
623
624 1
        return $this;
625
    }
626
627
    /**
628
     * Replace this node.
629
     *
630
     * @param string $string
631
     *
632
     * @return SimpleHtmlDom
633
     *
634
     */
635
    protected function replaceNodeWithString(string $string): self
636
    {
637 4
        if (empty($string)) {
638 2
            $this->node->parentNode->removeChild($this->node);
639
640 2
            return $this;
641
        }
642
643 3
        $newDocument = new HtmlDomParser($string);
644
645 3
        $tmpDomOuterTextString = $this->normalizeStringForComparision($newDocument);
646 3
        $tmpStr = $this->normalizeStringForComparision($string);
647 3
        if ($tmpDomOuterTextString !== $tmpStr) {
648
            throw new RuntimeException(
649
                'Not valid HTML fragment!' . "\n"
650
                . $tmpDomOuterTextString . "\n" .
651
                $tmpStr
652
            );
653
        }
654
655 3
        $newDocument = $this->cleanHtmlWrapper($newDocument, true);
656 3
        $ownerDocument = $this->node->ownerDocument;
657 3
        if ($ownerDocument === null) {
658
            return $this;
659
        }
660
661 3
        $newNode = $ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
662
663 3
        $this->node->parentNode->replaceChild($newNode, $this->node);
664 3
        $this->node = $newNode;
665
666
        // Remove head element, preserving child nodes. (again)
667 3 View Code Duplication
        if ($newDocument->getIsDOMDocumentCreatedWithoutHeadWrapper()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
668 3
            $html = $this->node->parentNode->getElementsByTagName('head')[0];
669 3
            if ($this->node->parentNode->ownerDocument !== null) {
670 2
                $fragment = $this->node->parentNode->ownerDocument->createDocumentFragment();
671 2
                if ($html !== null) {
672
                    /** @var DOMNode $html */
673 1
                    while ($html->childNodes->length > 0) {
674
                        /** @noinspection UnusedFunctionResultInspection */
675 1
                        $fragment->appendChild($html->childNodes->item(0));
676
                    }
677
                    /** @noinspection UnusedFunctionResultInspection */
678 1
                    $html->parentNode->replaceChild($fragment, $html);
679
                }
680
            }
681
        }
682
683 3
        return $this;
684
    }
685
686
    /**
687
     * Normalize the given input for comparision.
688
     *
689
     * @param HtmlDomParser|string $input
690
     *
691
     * @return string
692
     */
693
    private function normalizeStringForComparision($input): string
694
    {
695 9
        if ($input instanceof HtmlDomParser) {
696 9
            $string = $input->outerText();
697
698 9
            if ($input->getIsDOMDocumentCreatedWithoutHeadWrapper()) {
699
                /** @noinspection HtmlRequiredTitleElement */
700 9
                $string = \str_replace(['<head>', '</head>'], '', $string);
701
            }
702
        } else {
703 9
            $string = (string) $input;
704
        }
705
706
        return
707 9
            \urlencode(
708 9
                \urldecode(
709 9
                    \trim(
710 9
                        \str_replace(
711
                            [
712 9
                                ' ',
713
                                "\n",
714
                                "\r",
715
                                '/>',
716
                            ],
717
                            [
718 9
                                '',
719
                                '',
720
                                '',
721
                                '>',
722
                            ],
723 9
                            \strtolower($string)
724
                        )
725
                    )
726
                )
727
            );
728
    }
729
730
    /**
731
     * @param HtmlDomParser $newDocument
732
     * @param bool          $removeExtraHeadTag
733
     *
734
     * @return HtmlDomParser
735
     */
736
    protected function cleanHtmlWrapper(HtmlDomParser $newDocument, $removeExtraHeadTag = false): HtmlDomParser
737
    {
738
        if (
739 9
            $newDocument->getIsDOMDocumentCreatedWithoutHtml()
740
            ||
741 9
            $newDocument->getIsDOMDocumentCreatedWithoutHtmlWrapper()
742
        ) {
743
744
            // Remove doc-type node.
745 9
            if ($newDocument->getDocument()->doctype !== null) {
746
                /** @noinspection UnusedFunctionResultInspection */
747
                $newDocument->getDocument()->doctype->parentNode->removeChild($newDocument->getDocument()->doctype);
748
            }
749
750
            // Remove html element, preserving child nodes.
751 9
            $html = $newDocument->getDocument()->getElementsByTagName('html')->item(0);
752 9
            $fragment = $newDocument->getDocument()->createDocumentFragment();
753 9
            if ($html !== null) {
754 6
                while ($html->childNodes->length > 0) {
755
                    /** @noinspection UnusedFunctionResultInspection */
756 6
                    $fragment->appendChild($html->childNodes->item(0));
757
                }
758
                /** @noinspection UnusedFunctionResultInspection */
759 6
                $html->parentNode->replaceChild($fragment, $html);
760
            }
761
762
            // Remove body element, preserving child nodes.
763 9
            $body = $newDocument->getDocument()->getElementsByTagName('body')->item(0);
764 9
            $fragment = $newDocument->getDocument()->createDocumentFragment();
765 9
            if ($body instanceof \DOMElement) {
766 4
                while ($body->childNodes->length > 0) {
767
                    /** @noinspection UnusedFunctionResultInspection */
768 4
                    $fragment->appendChild($body->childNodes->item(0));
769
                }
770
                /** @noinspection UnusedFunctionResultInspection */
771 4
                $body->parentNode->replaceChild($fragment, $body);
772
773
                // At this point DOMDocument still added a "<p>"-wrapper around our string,
774
                // so we replace it with "<simpleHtmlDomP>" and delete this at the ending ...
775 4
                $item = $newDocument->getDocument()->getElementsByTagName('p')->item(0);
776 4
                if ($item !== null) {
777
                    /** @noinspection UnusedFunctionResultInspection */
778 4
                    $this->changeElementName($item, 'simpleHtmlDomP');
779
                }
780
            }
781
        }
782
783
        // Remove head element, preserving child nodes.
784 View Code Duplication
        if (
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
785 9
            $removeExtraHeadTag
786
            &&
787 9
            $newDocument->getIsDOMDocumentCreatedWithoutHeadWrapper()
788
        ) {
789 3
            $html = $this->node->parentNode->getElementsByTagName('head')[0];
790 3
            if ($this->node->parentNode->ownerDocument !== null) {
791 2
                $fragment = $this->node->parentNode->ownerDocument->createDocumentFragment();
792 2
                if ($html !== null) {
793
                    /** @var DOMNode $html */
794
                    while ($html->childNodes->length > 0) {
795
                        /** @noinspection UnusedFunctionResultInspection */
796
                        $fragment->appendChild($html->childNodes->item(0));
797
                    }
798
                    /** @noinspection UnusedFunctionResultInspection */
799
                    $html->parentNode->replaceChild($fragment, $html);
800
                }
801
            }
802
        }
803
804 9
        return $newDocument;
805
    }
806
807
    /**
808
     * Change the name of a tag in a "DOMNode".
809
     *
810
     * @param DOMNode $node
811
     * @param string  $name
812
     *
813
     * @return false|DOMElement
814
     *                          <p>DOMElement a new instance of class DOMElement or false
815
     *                          if an error occured.</p>
816
     */
817
    protected function changeElementName(\DOMNode $node, string $name)
818
    {
819 4
        $ownerDocument = $node->ownerDocument;
820 4
        if ($ownerDocument) {
821 4
            $newNode = $ownerDocument->createElement($name);
822
        } else {
823
            return false;
824
        }
825
826 4
        foreach ($node->childNodes as $child) {
827 4
            $child = $ownerDocument->importNode($child, true);
828
            /** @noinspection UnusedFunctionResultInspection */
829 4
            $newNode->appendChild($child);
830
        }
831
832 4
        foreach ($node->attributes as $attrName => $attrNode) {
833
            /** @noinspection UnusedFunctionResultInspection */
834
            $newNode->setAttribute($attrName, $attrNode);
835
        }
836
837
        /** @noinspection UnusedFunctionResultInspection */
838 4
        $newNode->ownerDocument->replaceChild($newNode, $node);
839
840 4
        return $newNode;
841
    }
842
843
    /**
844
     * Set attribute value.
845
     *
846
     * @param string      $name       <p>The name of the html-attribute.</p>
847
     * @param string|null $value      <p>Set to NULL or empty string, to remove the attribute.</p>
848
     * @param bool        $strict     </p>
849
     *                                $value must be NULL, to remove the attribute,
850
     *                                so that you can set an empty string as attribute-value e.g. autofocus=""
851
     *                                </p>
852
     *
853
     * @return SimpleHtmlDom
854
     */
855
    public function setAttribute(string $name, $value = null, bool $strict = false): self
856
    {
857
        if (
858 10
            ($strict && $value === null)
859
            ||
860 10
            (!$strict && empty($value))
861
        ) {
862 2
            $this->node->removeAttribute($name);
0 ignored issues
show
Bug introduced by
The method removeAttribute does only exist in DOMElement, but not in DOMNode.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
863
        } else {
864
            /** @noinspection UnusedFunctionResultInspection */
865 10
            $this->node->setAttribute($name, $value);
0 ignored issues
show
Bug introduced by
The method setAttribute does only exist in DOMElement, but not in DOMNode.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
866
        }
867
868 10
        return $this;
869
    }
870
871
    /**
872
     * @param string|string[]|null $value <p>
873
     *                                    null === get the current input value
874
     *                                    text === set a new input value
875
     *                                    </p>
876
     *
877
     * @return string|string[]|null
878
     */
879
    public function val($value = null)
880
    {
881 1
        if ($value === null) {
882
            if (
883 1
                $this->tag === 'input'
884
                &&
885
                (
886 1
                    $this->getAttribute('type') === 'text'
887
                    ||
888 1
                    !$this->hasAttribute('type')
889
                )
890
            ) {
891 1
                return $this->getAttribute('value');
892
            }
893
894
            if (
895 1
                $this->hasAttribute('checked')
896
                &&
897 1
                \in_array($this->getAttribute('type'), ['checkbox', 'radio'], true)
898
            ) {
899 1
                return $this->getAttribute('value');
900
            }
901
902 1
            if ($this->node->nodeName === 'select') {
903
                $valuesFromDom = [];
904
                foreach ($this->getElementsByTagName('option') as $option) {
0 ignored issues
show
Bug introduced by
The expression $this->getElementsByTagName('option') of type object<voku\helper\Simpl...leHtmlDomNodeInterface> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
905
                    if ($this->hasAttribute('checked')) {
906
                        /** @noinspection UnnecessaryCastingInspection */
907
                        $valuesFromDom[] = (string) $option->getAttribute('value');
908
                    }
909
                }
910
911
                if (\count($valuesFromDom) === 0) {
912
                    return null;
913
                }
914
915
                return $valuesFromDom;
916
            }
917
918 1
            if ($this->node->nodeName === 'textarea') {
919 1
                return $this->node->nodeValue;
920
            }
921
922
        } else {
923
924 1
            if (\in_array($this->getAttribute('type'), ['checkbox', 'radio'], true)) {
925 1
                if ($value === $this->getAttribute('value')) {
926
                    /** @noinspection UnusedFunctionResultInspection */
927 1
                    $this->setAttribute('checked', 'checked');
928
                } else {
929 1
                    $this->removeAttribute('checked');
930
                }
931 1
            } elseif ($this->node->nodeName === 'select') {
932
                foreach ($this->node->getElementsByTagName('option') as $option) {
933
                    /** @var DOMElement $option */
934
                    if ($value === $option->getAttribute('value')) {
935
                        /** @noinspection UnusedFunctionResultInspection */
936
                        $option->setAttribute('selected', 'selected');
937
                    } else {
938
                        $option->removeAttribute('selected');
939
                    }
940
                }
941 1
            } elseif ($this->node->nodeName === 'input') {
942
                // Set value for input elements
943
                /** @noinspection UnusedFunctionResultInspection */
944 1
                $this->setAttribute('value', (string) $value);
945 1
            } elseif ($this->node->nodeName === 'textarea') {
946 1
                $this->node->nodeValue = (string) $value;
947
            }
948
        }
949
950 1
        return null;
951
    }
952
953
    /**
954
     * Remove attribute.
955
     *
956
     * @param string $name <p>The name of the html-attribute.</p>
957
     *
958
     * @return mixed
959
     */
960
    public function removeAttribute(string $name)
961
    {
962
        $this->node->removeAttribute($name);
0 ignored issues
show
Bug introduced by
The method removeAttribute does only exist in DOMElement, but not in DOMNode.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
963
964
        return $this;
965
    }
966
967
    /**
968
     * Get dom node's plain text.
969
     *
970
     * @return string
971
     */
972
    public function text(): string
973
    {
974 17
        return $this->getHtmlDomParser()->fixHtmlOutput($this->node->textContent);
975
    }
976
}
977