Completed
Push — master ( c8a569...162761 )
by Lars
02:04 queued 35s
created

SimpleHtmlDom::changeElementName()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4.0961

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 9
cts 11
cp 0.8182
rs 9.52
c 0
b 0
f 0
cc 4
nc 5
nop 2
crap 4.0961
1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\helper;
6
7
/** @noinspection PhpHierarchyChecksInspection */
8
class SimpleHtmlDom extends AbstractSimpleHtmlDom implements \IteratorAggregate, SimpleHtmlDomInterface
9
{
10
    /**
11
     * @var \DOMElement|\DOMNode
12
     */
13
    protected $node;
14
15
    /**
16
     * @param \DOMElement|\DOMNode $node
17
     */
18 107
    public function __construct(\DOMNode $node)
19
    {
20 107
        $this->node = $node;
21 107
    }
22
23
    /**
24
     * @param string $name
25
     * @param array  $arguments
26
     *
27
     * @throws \BadMethodCallException
28
     *
29
     * @return SimpleHtmlDomInterface|string|null
30
     */
31 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...
32
    {
33 9
        $name = \strtolower($name);
34
35 9
        if (isset(self::$functionAliases[$name])) {
36 9
            return \call_user_func_array([$this, self::$functionAliases[$name]], $arguments);
37
        }
38
39
        throw new \BadMethodCallException('Method does not exist');
40
    }
41
42
    /**
43
     * Returns children of node.
44
     *
45
     * @param int $idx
46
     *
47
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface|null
48
     */
49 2
    public function childNodes(int $idx = -1)
50
    {
51 2
        $nodeList = $this->getIterator();
52
53 2
        if ($idx === -1) {
54 2
            return $nodeList;
55
        }
56
57 2
        return $nodeList[$idx] ?? null;
58
    }
59
60
    /**
61
     * Find list of nodes with a CSS selector.
62
     *
63
     * @param string   $selector
64
     * @param int|null $idx
65
     *
66
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface
67
     */
68 26
    public function find(string $selector, $idx = null)
69
    {
70 26
        return $this->getHtmlDomParser()->find($selector, $idx);
71
    }
72
73
    /**
74
     * Find one node with a CSS selector.
75
     *
76
     * @param string $selector
77
     *
78
     * @return SimpleHtmlDomInterface
79
     */
80 1
    public function findOne(string $selector): SimpleHtmlDomInterface
81
    {
82 1
        return $this->find($selector, 0);
83
    }
84
85
    /**
86
     * Find nodes with a CSS selector.
87
     *
88
     * @param string $selector
89
     *
90
     * @return SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface
91
     */
92
    public function findMulti(string $selector): SimpleHtmlDomNodeInterface
93
    {
94
        return $this->find($selector, null);
95
    }
96
97
    /**
98
     * Returns the first child of node.
99
     *
100
     * @return SimpleHtmlDomInterface|null
101
     */
102 4
    public function firstChild()
103
    {
104
        /** @var \DOMNode|null $node */
105 4
        $node = $this->node->firstChild;
106
107 4
        if ($node === null) {
108 1
            return null;
109
        }
110
111 4
        return new static($node);
112
    }
113
114
    /**
115
     * Returns an array of attributes.
116
     *
117
     * @return array|null
118
     */
119 2
    public function getAllAttributes()
120
    {
121 2
        if ($this->node->hasAttributes()) {
122 2
            $attributes = [];
123 2
            foreach ($this->node->attributes as $attr) {
124 2
                $attributes[$attr->name] = HtmlDomParser::putReplacedBackToPreserveHtmlEntities($attr->value);
125
            }
126
127 2
            return $attributes;
128
        }
129
130 1
        return null;
131
    }
132
133
    /**
134
     * Return attribute value.
135
     *
136
     * @param string $name
137
     *
138
     * @return string
139
     */
140 14
    public function getAttribute(string $name): string
141
    {
142 14
        if ($this->node instanceof \DOMElement) {
143 14
            return HtmlDomParser::putReplacedBackToPreserveHtmlEntities(
144 14
                $this->node->getAttribute($name)
145
            );
146
        }
147
148
        return '';
149
    }
150
151
    /**
152
     * Return element by #id.
153
     *
154
     * @param string $id
155
     *
156
     * @return SimpleHtmlDomInterface
157
     */
158 1
    public function getElementById(string $id): SimpleHtmlDomInterface
159
    {
160 1
        return $this->findOne("#${id}");
161
    }
162
163
    /**
164
     * Returns elements by #id.
165
     *
166
     * @param string   $id
167
     * @param int|null $idx
168
     *
169
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface
170
     */
171
    public function getElementsById(string $id, $idx = null)
172
    {
173
        return $this->find("#${id}", $idx);
174
    }
175
176
    /**
177
     * Return elements by .class.
178
     *
179
     * @param string $class
180
     *
181
     * @return SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface
182
     */
183
    public function getElementByClass(string $class): SimpleHtmlDomNodeInterface
184
    {
185
        return $this->findMulti(".${class}");
186
    }
187
188
    /**
189
     * Return element by tag name.
190
     *
191
     * @param string $name
192
     *
193
     * @return SimpleHtmlDomInterface
194
     */
195
    public function getElementByTagName(string $name): SimpleHtmlDomInterface
196
    {
197 1
        if ($this->node instanceof \DOMElement) {
198 1
            $node = $this->node->getElementsByTagName($name)->item(0);
199
        } else {
200
            $node = null;
201
        }
202
203 1
        if ($node === null) {
204
            return new SimpleHtmlDomBlank();
205
        }
206
207 1
        return new static($node);
208
    }
209
210
    /**
211
     * Returns elements by tag name.
212
     *
213
     * @param string   $name
214
     * @param int|null $idx
215
     *
216
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface
217
     */
218 View Code Duplication
    public function getElementsByTagName(string $name, $idx = null)
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...
219
    {
220 1
        if ($this->node instanceof \DOMElement) {
221 1
            $nodesList = $this->node->getElementsByTagName($name);
222
        } else {
223
            $nodesList = [];
224
        }
225
226 1
        $elements = new SimpleHtmlDomNode();
227
228 1
        foreach ($nodesList as $node) {
229 1
            $elements[] = new static($node);
230
        }
231
232
        // return all elements
233 1
        if ($idx === null) {
234 1
            if (\count($elements) === 0) {
235
                return new SimpleHtmlDomNodeBlank();
236
            }
237
238 1
            return $elements;
239
        }
240
241
        // handle negative values
242
        if ($idx < 0) {
243
            $idx = \count($elements) + $idx;
244
        }
245
246
        // return one element
247
        return $elements[$idx] ?? new SimpleHtmlDomBlank();
248
    }
249
250
    /**
251
     * Create a new "HtmlDomParser"-object from the current context.
252
     *
253
     * @return HtmlDomParser
254
     */
255
    public function getHtmlDomParser(): HtmlDomParser
256
    {
257 72
        return new HtmlDomParser($this);
258
    }
259
260
    /**
261
     * Retrieve an external iterator.
262
     *
263
     * @see  http://php.net/manual/en/iteratoraggregate.getiterator.php
264
     *
265
     * @return SimpleHtmlDomNode
266
     *                           <p>
267
     *                              An instance of an object implementing <b>Iterator</b> or
268
     *                              <b>Traversable</b>
269
     *                           </p>
270
     */
271
    public function getIterator(): SimpleHtmlDomNodeInterface
272
    {
273 3
        $elements = new SimpleHtmlDomNode();
274 3
        if ($this->node->hasChildNodes()) {
275 3
            foreach ($this->node->childNodes as $node) {
276 3
                $elements[] = new static($node);
277
            }
278
        }
279
280 3
        return $elements;
281
    }
282
283
    /**
284
     * @return \DOMNode
285
     */
286
    public function getNode(): \DOMNode
287
    {
288 73
        return $this->node;
289
    }
290
291
    /**
292
     * Determine if an attribute exists on the element.
293
     *
294
     * @param string $name
295
     *
296
     * @return bool
297
     */
298
    public function hasAttribute(string $name): bool
299
    {
300 2
        if (!$this->node instanceof \DOMElement) {
301
            return false;
302
        }
303
304 2
        return $this->node->hasAttribute($name);
305
    }
306
307
    /**
308
     * Get dom node's outer html.
309
     *
310
     * @param bool $multiDecodeNewHtmlEntity
311
     *
312
     * @return string
313
     */
314
    public function html(bool $multiDecodeNewHtmlEntity = false): string
315
    {
316 27
        return $this->getHtmlDomParser()->html($multiDecodeNewHtmlEntity);
317
    }
318
319
    /**
320
     * Get dom node's inner html.
321
     *
322
     * @param bool $multiDecodeNewHtmlEntity
323
     *
324
     * @return string
325
     */
326
    public function innerHtml(bool $multiDecodeNewHtmlEntity = false): string
327
    {
328 12
        return $this->getHtmlDomParser()->innerHtml($multiDecodeNewHtmlEntity);
329
    }
330
331
    /**
332
     * Returns the last child of node.
333
     *
334
     * @return SimpleHtmlDomInterface|null
335
     */
336
    public function lastChild()
337
    {
338
        /** @var \DOMNode|null $node */
339 4
        $node = $this->node->lastChild;
340
341 4
        if ($node === null) {
342 1
            return null;
343
        }
344
345 4
        return new static($node);
346
    }
347
348
    /**
349
     * Returns the next sibling of node.
350
     *
351
     * @return SimpleHtmlDomInterface|null
352
     */
353
    public function nextSibling()
354
    {
355
        /** @var \DOMNode|null $node */
356 1
        $node = $this->node->nextSibling;
357
358 1
        if ($node === null) {
359 1
            return null;
360
        }
361
362 1
        return new static($node);
363
    }
364
365
    /**
366
     * Returns the parent of node.
367
     *
368
     * @return SimpleHtmlDomInterface
369
     */
370
    public function parentNode(): SimpleHtmlDomInterface
371
    {
372 1
        return new static($this->node->parentNode);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new static($this->node->parentNode); (voku\helper\SimpleHtmlDom) is incompatible with the return type declared by the interface voku\helper\SimpleHtmlDomInterface::parentNode of type self.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
373
    }
374
375
    /**
376
     * Nodes can get partially destroyed in which they're still an
377
     * actual DOM node (such as \DOMElement) but almost their entire
378
     * body is gone, including the `nodeType` attribute.
379
     *
380
     * @return bool true if node has been destroyed
381
     */
382
    public function isRemoved(): bool
383
    {
384
        return !isset($this->node->nodeType);
385
    }
386
387
    /**
388
     * Returns the previous sibling of node.
389
     *
390
     * @return SimpleHtmlDomInterface|null
391
     */
392
    public function previousSibling()
393
    {
394
        /** @var \DOMNode|null $node */
395 1
        $node = $this->node->previousSibling;
396
397 1
        if ($node === null) {
398 1
            return null;
399
        }
400
401 1
        return new static($node);
402
    }
403
404
    /**
405
     * Replace child node.
406
     *
407
     * @param string $string
408
     *
409
     * @return SimpleHtmlDomInterface
410
     */
411
    protected function replaceChildWithString(string $string): SimpleHtmlDomInterface
412
    {
413 7
        if (!empty($string)) {
414 6
            $newDocument = new HtmlDomParser($string);
415
416 6
            $tmpDomString = $this->normalizeStringForComparision($newDocument);
417 6
            $tmpStr = $this->normalizeStringForComparision($string);
418 6
            if ($tmpDomString !== $tmpStr) {
419
                throw new \RuntimeException(
420
                    'Not valid HTML fragment!' . "\n" .
421
                    $tmpDomString . "\n" .
422
                    $tmpStr
423
                );
424
            }
425
        }
426
427 7
        if (\count($this->node->childNodes) > 0) {
428 7
            foreach ($this->node->childNodes as $node) {
429 7
                $this->node->removeChild($node);
430
            }
431
        }
432
433 7
        if (!empty($newDocument)) {
434 6
            $newDocument = $this->cleanHtmlWrapper($newDocument);
435 6
            $ownerDocument = $this->node->ownerDocument;
436
            if (
437 6
                $ownerDocument !== null
438
                &&
439 6
                $newDocument->getDocument()->documentElement !== null
440
            ) {
441 6
                $newNode = $ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
442
                /** @noinspection UnusedFunctionResultInspection */
443 6
                $this->node->appendChild($newNode);
444
            }
445
        }
446
447 7
        return $this;
448
    }
449
450
    /**
451
     * Replace this node with text
452
     *
453
     * @param string $string
454
     *
455
     * @return SimpleHtmlDomInterface
456
     */
457
    protected function replaceTextWithString($string): SimpleHtmlDomInterface
458
    {
459 1
        if (empty($string)) {
460 1
            $this->node->parentNode->removeChild($this->node);
461
462 1
            return $this;
463
        }
464
465 1
        $ownerDocument = $this->node->ownerDocument;
466 1
        if ($ownerDocument !== null) {
467 1
            $newElement = $ownerDocument->createTextNode($string);
468 1
            $newNode = $ownerDocument->importNode($newElement, true);
469 1
            $this->node->parentNode->replaceChild($newNode, $this->node);
470 1
            $this->node = $newNode;
471
        }
472
473 1
        return $this;
474
    }
475
476
    /**
477
     * Replace this node.
478
     *
479
     * @param string $string
480
     *
481
     * @return SimpleHtmlDomInterface
482
     */
483
    protected function replaceNodeWithString(string $string): SimpleHtmlDomInterface
484
    {
485 4
        if (empty($string)) {
486 2
            $this->node->parentNode->removeChild($this->node);
487
488 2
            return $this;
489
        }
490
491 3
        $newDocument = new HtmlDomParser($string);
492
493 3
        $tmpDomOuterTextString = $this->normalizeStringForComparision($newDocument);
494 3
        $tmpStr = $this->normalizeStringForComparision($string);
495 3
        if ($tmpDomOuterTextString !== $tmpStr) {
496
            throw new \RuntimeException(
497
                'Not valid HTML fragment!' . "\n"
498
                . $tmpDomOuterTextString . "\n" .
499
                $tmpStr
500
            );
501
        }
502
503 3
        $newDocument = $this->cleanHtmlWrapper($newDocument, true);
504 3
        $ownerDocument = $this->node->ownerDocument;
505
        if (
506 3
            $ownerDocument === null
507
            ||
508 3
            $newDocument->getDocument()->documentElement === null
509
        ) {
510
            return $this;
511
        }
512
513 3
        $newNode = $ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
514
515 3
        $this->node->parentNode->replaceChild($newNode, $this->node);
516 3
        $this->node = $newNode;
517
518
        // Remove head element, preserving child nodes. (again)
519 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...
520 3
            $this->node->parentNode instanceof \DOMElement
521
            &&
522 3
            $newDocument->getIsDOMDocumentCreatedWithoutHeadWrapper()
523
        ) {
524 2
            $html = $this->node->parentNode->getElementsByTagName('head')[0];
525 2
            if ($this->node->parentNode->ownerDocument !== null) {
526 2
                $fragment = $this->node->parentNode->ownerDocument->createDocumentFragment();
527 2
                if ($html !== null) {
528
                    /** @var \DOMNode $html */
529 1
                    while ($html->childNodes->length > 0) {
530 1
                        $tmpNode = $html->childNodes->item(0);
531 1
                        if ($tmpNode !== null) {
532
                            /** @noinspection UnusedFunctionResultInspection */
533 1
                            $fragment->appendChild($tmpNode);
534
                        }
535
                    }
536
                    /** @noinspection UnusedFunctionResultInspection */
537 1
                    $html->parentNode->replaceChild($fragment, $html);
538
                }
539
            }
540
        }
541
542 3
        return $this;
543
    }
544
545
    /**
546
     * Normalize the given input for comparision.
547
     *
548
     * @param HtmlDomParser|string $input
549
     *
550
     * @return string
551
     */
552
    private function normalizeStringForComparision($input): string
553
    {
554 9
        if ($input instanceof HtmlDomParser) {
555 9
            $string = $input->outerText();
556
557 9
            if ($input->getIsDOMDocumentCreatedWithoutHeadWrapper()) {
558
                /** @noinspection HtmlRequiredTitleElement */
559 9
                $string = \str_replace(['<head>', '</head>'], '', $string);
560
            }
561
        } else {
562 9
            $string = (string) $input;
563
        }
564
565
        return
566 9
            \urlencode(
567 9
                \urldecode(
568 9
                    \trim(
569 9
                        \str_replace(
570
                            [
571 9
                                ' ',
572
                                "\n",
573
                                "\r",
574
                                '/>',
575
                            ],
576
                            [
577 9
                                '',
578
                                '',
579
                                '',
580
                                '>',
581
                            ],
582 9
                            \strtolower($string)
583
                        )
584
                    )
585
                )
586
            );
587
    }
588
589
    /**
590
     * @param HtmlDomParser $newDocument
591
     * @param bool          $removeExtraHeadTag
592
     *
593
     * @return HtmlDomParser
594
     */
595
    protected function cleanHtmlWrapper(HtmlDomParser $newDocument, $removeExtraHeadTag = false): HtmlDomParser
596
    {
597
        if (
598 9
            $newDocument->getIsDOMDocumentCreatedWithoutHtml()
599
            ||
600 9
            $newDocument->getIsDOMDocumentCreatedWithoutHtmlWrapper()
601
        ) {
602
603
            // Remove doc-type node.
604 9
            if ($newDocument->getDocument()->doctype !== null) {
605
                /** @noinspection UnusedFunctionResultInspection */
606
                $newDocument->getDocument()->doctype->parentNode->removeChild($newDocument->getDocument()->doctype);
607
            }
608
609
            // Remove html element, preserving child nodes.
610 9
            $html = $newDocument->getDocument()->getElementsByTagName('html')->item(0);
611 9
            $fragment = $newDocument->getDocument()->createDocumentFragment();
612 9
            if ($html !== null) {
613 6
                while ($html->childNodes->length > 0) {
614 6
                    $tmpNode = $html->childNodes->item(0);
615 6
                    if ($tmpNode !== null) {
616
                        /** @noinspection UnusedFunctionResultInspection */
617 6
                        $fragment->appendChild($tmpNode);
618
                    }
619
                }
620
                /** @noinspection UnusedFunctionResultInspection */
621 6
                $html->parentNode->replaceChild($fragment, $html);
622
            }
623
624
            // Remove body element, preserving child nodes.
625 9
            $body = $newDocument->getDocument()->getElementsByTagName('body')->item(0);
626 9
            $fragment = $newDocument->getDocument()->createDocumentFragment();
627 9
            if ($body instanceof \DOMElement) {
628 4
                while ($body->childNodes->length > 0) {
629 4
                    $tmpNode = $body->childNodes->item(0);
630 4
                    if ($tmpNode !== null) {
631
                        /** @noinspection UnusedFunctionResultInspection */
632 4
                        $fragment->appendChild($tmpNode);
633
                    }
634
                }
635
                /** @noinspection UnusedFunctionResultInspection */
636 4
                $body->parentNode->replaceChild($fragment, $body);
637
638
                // At this point DOMDocument still added a "<p>"-wrapper around our string,
639
                // so we replace it with "<simpleHtmlDomP>" and delete this at the ending ...
640 4
                $item = $newDocument->getDocument()->getElementsByTagName('p')->item(0);
641 4
                if ($item !== null) {
642
                    /** @noinspection UnusedFunctionResultInspection */
643 4
                    $this->changeElementName($item, 'simpleHtmlDomP');
644
                }
645
            }
646
        }
647
648
        // Remove head element, preserving child nodes.
649 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...
650 9
            $removeExtraHeadTag
651
            &&
652 9
            $this->node->parentNode instanceof \DOMElement
653
            &&
654 9
            $newDocument->getIsDOMDocumentCreatedWithoutHeadWrapper()
655
        ) {
656 2
            $html = $this->node->parentNode->getElementsByTagName('head')[0];
657 2
            if ($this->node->parentNode->ownerDocument !== null) {
658 2
                $fragment = $this->node->parentNode->ownerDocument->createDocumentFragment();
659 2
                if ($html !== null) {
660
                    /** @var \DOMNode $html */
661
                    while ($html->childNodes->length > 0) {
662
                        $tmpNode = $html->childNodes->item(0);
663
                        if ($tmpNode !== null) {
664
                            /** @noinspection UnusedFunctionResultInspection */
665
                            $fragment->appendChild($tmpNode);
666
                        }
667
                    }
668
                    /** @noinspection UnusedFunctionResultInspection */
669
                    $html->parentNode->replaceChild($fragment, $html);
670
                }
671
            }
672
        }
673
674 9
        return $newDocument;
675
    }
676
677
    /**
678
     * Change the name of a tag in a "DOMNode".
679
     *
680
     * @param \DOMNode $node
681
     * @param string   $name
682
     *
683
     * @return \DOMElement|false
684
     *                          <p>DOMElement a new instance of class DOMElement or false
685
     *                          if an error occured.</p>
686
     */
687
    protected function changeElementName(\DOMNode $node, string $name)
688
    {
689 4
        $ownerDocument = $node->ownerDocument;
690 4
        if ($ownerDocument) {
691 4
            $newNode = $ownerDocument->createElement($name);
692
        } else {
693
            return false;
694
        }
695
696 4
        foreach ($node->childNodes as $child) {
697 4
            $child = $ownerDocument->importNode($child, true);
698
            /** @noinspection UnusedFunctionResultInspection */
699 4
            $newNode->appendChild($child);
700
        }
701
702 4
        foreach ($node->attributes as $attrName => $attrNode) {
703
            /** @noinspection UnusedFunctionResultInspection */
704
            $newNode->setAttribute($attrName, $attrNode);
705
        }
706
707
        /** @noinspection UnusedFunctionResultInspection */
708 4
        $newNode->ownerDocument->replaceChild($newNode, $node);
709
710 4
        return $newNode;
711
    }
712
713
    /**
714
     * Set attribute value.
715
     *
716
     * @param string      $name       <p>The name of the html-attribute.</p>
717
     * @param string|null $value      <p>Set to NULL or empty string, to remove the attribute.</p>
718
     * @param bool        $strict     </p>
719
     *                                $value must be NULL, to remove the attribute,
720
     *                                so that you can set an empty string as attribute-value e.g. autofocus=""
721
     *                                </p>
722
     *
723
     * @return SimpleHtmlDomInterface
724
     */
725
    public function setAttribute(string $name, $value = null, bool $strict = false): SimpleHtmlDomInterface
726
    {
727
        if (
728 10
            ($strict && $value === null)
729
            ||
730 10
            (!$strict && empty($value))
731
        ) {
732
            /** @noinspection UnusedFunctionResultInspection */
733 2
            $this->removeAttribute($name);
734 10
        } elseif (\method_exists($this->node, 'setAttribute')) {
735
            /** @noinspection UnusedFunctionResultInspection */
736 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...
737
        }
738
739 10
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (voku\helper\SimpleHtmlDom) is incompatible with the return type declared by the interface voku\helper\SimpleHtmlDomInterface::setAttribute of type self.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
740
    }
741
742
    /**
743
     * @param string|string[]|null $value <p>
744
     *                                    null === get the current input value
745
     *                                    text === set a new input value
746
     *                                    </p>
747
     *
748
     * @return string|string[]|null
749
     */
750
    public function val($value = null)
751
    {
752 1
        if ($value === null) {
753
            if (
754 1
                $this->tag === 'input'
0 ignored issues
show
Documentation introduced by
The property tag does not exist on object<voku\helper\SimpleHtmlDom>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
755
                &&
756
                (
757 1
                    $this->getAttribute('type') === 'text'
758
                    ||
759 1
                    !$this->hasAttribute('type')
760
                )
761
            ) {
762 1
                return $this->getAttribute('value');
763
            }
764
765
            if (
766 1
                $this->hasAttribute('checked')
767
                &&
768 1
                \in_array($this->getAttribute('type'), ['checkbox', 'radio'], true)
769
            ) {
770 1
                return $this->getAttribute('value');
771
            }
772
773 1
            if ($this->node->nodeName === 'select') {
774
                $valuesFromDom = [];
775
                $options = $this->getElementsByTagName('option');
776
                if ($options instanceof SimpleHtmlDomNode) {
777
                    foreach ($options as $option) {
778
                        if ($this->hasAttribute('checked')) {
779
                            /** @noinspection UnnecessaryCastingInspection */
780
                            $valuesFromDom[] = (string) $option->getAttribute('value');
781
                        }
782
                    }
783
                }
784
785
                if (\count($valuesFromDom) === 0) {
786
                    return null;
787
                }
788
789
                return $valuesFromDom;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $valuesFromDom; (array) is incompatible with the return type declared by the interface voku\helper\SimpleHtmlDomInterface::val of type string|string[]|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
790
            }
791
792 1
            if ($this->node->nodeName === 'textarea') {
793 1
                return $this->node->nodeValue;
794
            }
795
        } else {
796
            /** @noinspection NestedPositiveIfStatementsInspection */
797 1
            if (\in_array($this->getAttribute('type'), ['checkbox', 'radio'], true)) {
798 1
                if ($value === $this->getAttribute('value')) {
799
                    /** @noinspection UnusedFunctionResultInspection */
800 1
                    $this->setAttribute('checked', 'checked');
801
                } else {
802
                    /** @noinspection UnusedFunctionResultInspection */
803 1
                    $this->removeAttribute('checked');
804
                }
805 1
            } elseif ($this->node instanceof \DOMElement && $this->node->nodeName === 'select') {
806
                foreach ($this->node->getElementsByTagName('option') as $option) {
807
                    /** @var \DOMElement $option */
808
                    if ($value === $option->getAttribute('value')) {
809
                        /** @noinspection UnusedFunctionResultInspection */
810
                        $option->setAttribute('selected', 'selected');
811
                    } else {
812
                        $option->removeAttribute('selected');
813
                    }
814
                }
815 1
            } elseif ($this->node->nodeName === 'input' && \is_string($value)) {
816
                // Set value for input elements
817
                /** @noinspection UnusedFunctionResultInspection */
818 1
                $this->setAttribute('value', $value);
819 1
            } elseif ($this->node->nodeName === 'textarea' && \is_string($value)) {
820 1
                $this->node->nodeValue = $value;
821
            }
822
        }
823
824 1
        return null;
825
    }
826
827
    /**
828
     * Remove attribute.
829
     *
830
     * @param string $name <p>The name of the html-attribute.</p>
831
     *
832
     * @return SimpleHtmlDomInterface
833
     */
834
    public function removeAttribute(string $name): SimpleHtmlDomInterface
835
    {
836 2
        if (\method_exists($this->node, 'removeAttribute')) {
837 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...
838
        }
839
840 2
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (voku\helper\SimpleHtmlDom) is incompatible with the return type declared by the interface voku\helper\SimpleHtmlDo...erface::removeAttribute of type self.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
841
    }
842
843
    /**
844
     * Get dom node's plain text.
845
     *
846
     * @return string
847
     */
848
    public function text(): string
849
    {
850 17
        return $this->getHtmlDomParser()->fixHtmlOutput($this->node->textContent);
851
    }
852
}
853