Completed
Push — master ( 05cc52...a48403 )
by Lars
01:56 queued 11s
created

SimpleHtmlDom::__call()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 10
Ratio 100 %

Code Coverage

Tests 4
CRAP Score 2.032

Importance

Changes 0
Metric Value
dl 10
loc 10
ccs 4
cts 5
cp 0.8
rs 9.9332
c 0
b 0
f 0
cc 2
nc 2
nop 2
crap 2.032
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 103
    public function __construct(\DOMNode $node)
19
    {
20 103
        $this->node = $node;
21 103
    }
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 1
    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 1 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 68
    public function getHtmlDomParser(): HtmlDomParser
256
    {
257 68
        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 3
    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 69
    public function getNode(): \DOMNode
287
    {
288 69
        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 2
    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 24
    public function html(bool $multiDecodeNewHtmlEntity = false): string
315
    {
316 24
        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 11
    public function innerHtml(bool $multiDecodeNewHtmlEntity = false): string
327
    {
328 11
        return $this->getHtmlDomParser()->innerHtml($multiDecodeNewHtmlEntity);
329
    }
330
331
    /**
332
     * Returns the last child of node.
333
     *
334
     * @return SimpleHtmlDomInterface|null
335
     */
336 4
    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 1
    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 1
    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 1
    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 7
    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 6
            if ($ownerDocument !== null) {
437 6
                $newNode = $ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
438
                /** @noinspection UnusedFunctionResultInspection */
439 6
                $this->node->appendChild($newNode);
440
            }
441
        }
442
443 7
        return $this;
444
    }
445
446
    /**
447
     * Replace this node with text
448
     *
449
     * @param string $string
450
     *
451
     * @return SimpleHtmlDomInterface
452
     */
453 1
    protected function replaceTextWithString($string): SimpleHtmlDomInterface
454
    {
455 1
        if (empty($string)) {
456 1
            $this->node->parentNode->removeChild($this->node);
457
458 1
            return $this;
459
        }
460
461 1
        $ownerDocument = $this->node->ownerDocument;
462 1
        if ($ownerDocument !== null) {
463 1
            $newElement = $ownerDocument->createTextNode($string);
464 1
            $newNode = $ownerDocument->importNode($newElement, true);
465 1
            $this->node->parentNode->replaceChild($newNode, $this->node);
466 1
            $this->node = $newNode;
467
        }
468
469 1
        return $this;
470
    }
471
472
    /**
473
     * Replace this node.
474
     *
475
     * @param string $string
476
     *
477
     * @return SimpleHtmlDomInterface
478
     */
479 4
    protected function replaceNodeWithString(string $string): SimpleHtmlDomInterface
480
    {
481 4
        if (empty($string)) {
482 2
            $this->node->parentNode->removeChild($this->node);
483
484 2
            return $this;
485
        }
486
487 3
        $newDocument = new HtmlDomParser($string);
488
489 3
        $tmpDomOuterTextString = $this->normalizeStringForComparision($newDocument);
490 3
        $tmpStr = $this->normalizeStringForComparision($string);
491 3
        if ($tmpDomOuterTextString !== $tmpStr) {
492
            throw new \RuntimeException(
493
                'Not valid HTML fragment!' . "\n"
494
                . $tmpDomOuterTextString . "\n" .
495
                $tmpStr
496
            );
497
        }
498
499 3
        $newDocument = $this->cleanHtmlWrapper($newDocument, true);
500 3
        $ownerDocument = $this->node->ownerDocument;
501 3
        if ($ownerDocument === null) {
502
            return $this;
503
        }
504
505 3
        $newNode = $ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
506
507 3
        $this->node->parentNode->replaceChild($newNode, $this->node);
508 3
        $this->node = $newNode;
509
510
        // Remove head element, preserving child nodes. (again)
511 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...
512 3
            $this->node->parentNode instanceof \DOMElement
513
            &&
514 3
            $newDocument->getIsDOMDocumentCreatedWithoutHeadWrapper()
515
        ) {
516 2
            $html = $this->node->parentNode->getElementsByTagName('head')[0];
517 2
            if ($this->node->parentNode->ownerDocument !== null) {
518 2
                $fragment = $this->node->parentNode->ownerDocument->createDocumentFragment();
519 2
                if ($html !== null) {
520
                    /** @var \DOMNode $html */
521 1
                    while ($html->childNodes->length > 0) {
522 1
                        $tmpNode = $html->childNodes->item(0);
523 1
                        if ($tmpNode !== null) {
524
                            /** @noinspection UnusedFunctionResultInspection */
525 1
                            $fragment->appendChild($tmpNode);
526
                        }
527
                    }
528
                    /** @noinspection UnusedFunctionResultInspection */
529 1
                    $html->parentNode->replaceChild($fragment, $html);
530
                }
531
            }
532
        }
533
534 3
        return $this;
535
    }
536
537
    /**
538
     * Normalize the given input for comparision.
539
     *
540
     * @param HtmlDomParser|string $input
541
     *
542
     * @return string
543
     */
544 9
    private function normalizeStringForComparision($input): string
545
    {
546 9
        if ($input instanceof HtmlDomParser) {
547 9
            $string = $input->outerText();
548
549 9
            if ($input->getIsDOMDocumentCreatedWithoutHeadWrapper()) {
550
                /** @noinspection HtmlRequiredTitleElement */
551 9
                $string = \str_replace(['<head>', '</head>'], '', $string);
552
            }
553
        } else {
554 9
            $string = (string) $input;
555
        }
556
557
        return
558 9
            \urlencode(
559 9
                \urldecode(
560 9
                    \trim(
561 9
                        \str_replace(
562
                            [
563 9
                                ' ',
564
                                "\n",
565
                                "\r",
566
                                '/>',
567
                            ],
568
                            [
569 9
                                '',
570
                                '',
571
                                '',
572
                                '>',
573
                            ],
574 9
                            \strtolower($string)
575
                        )
576
                    )
577
                )
578
            );
579
    }
580
581
    /**
582
     * @param HtmlDomParser $newDocument
583
     * @param bool          $removeExtraHeadTag
584
     *
585
     * @return HtmlDomParser
586
     */
587 9
    protected function cleanHtmlWrapper(HtmlDomParser $newDocument, $removeExtraHeadTag = false): HtmlDomParser
588
    {
589
        if (
590 9
            $newDocument->getIsDOMDocumentCreatedWithoutHtml()
591
            ||
592 9
            $newDocument->getIsDOMDocumentCreatedWithoutHtmlWrapper()
593
        ) {
594
595
            // Remove doc-type node.
596 9
            if ($newDocument->getDocument()->doctype !== null) {
597
                /** @noinspection UnusedFunctionResultInspection */
598
                $newDocument->getDocument()->doctype->parentNode->removeChild($newDocument->getDocument()->doctype);
599
            }
600
601
            // Remove html element, preserving child nodes.
602 9
            $html = $newDocument->getDocument()->getElementsByTagName('html')->item(0);
603 9
            $fragment = $newDocument->getDocument()->createDocumentFragment();
604 9
            if ($html !== null) {
605 6
                while ($html->childNodes->length > 0) {
606 6
                    $tmpNode = $html->childNodes->item(0);
607 6
                    if ($tmpNode !== null) {
608
                        /** @noinspection UnusedFunctionResultInspection */
609 6
                        $fragment->appendChild($tmpNode);
610
                    }
611
                }
612
                /** @noinspection UnusedFunctionResultInspection */
613 6
                $html->parentNode->replaceChild($fragment, $html);
614
            }
615
616
            // Remove body element, preserving child nodes.
617 9
            $body = $newDocument->getDocument()->getElementsByTagName('body')->item(0);
618 9
            $fragment = $newDocument->getDocument()->createDocumentFragment();
619 9
            if ($body instanceof \DOMElement) {
620 4
                while ($body->childNodes->length > 0) {
621 4
                    $tmpNode = $body->childNodes->item(0);
622 4
                    if ($tmpNode !== null) {
623
                        /** @noinspection UnusedFunctionResultInspection */
624 4
                        $fragment->appendChild($tmpNode);
625
                    }
626
                }
627
                /** @noinspection UnusedFunctionResultInspection */
628 4
                $body->parentNode->replaceChild($fragment, $body);
629
630
                // At this point DOMDocument still added a "<p>"-wrapper around our string,
631
                // so we replace it with "<simpleHtmlDomP>" and delete this at the ending ...
632 4
                $item = $newDocument->getDocument()->getElementsByTagName('p')->item(0);
633 4
                if ($item !== null) {
634
                    /** @noinspection UnusedFunctionResultInspection */
635 4
                    $this->changeElementName($item, 'simpleHtmlDomP');
636
                }
637
            }
638
        }
639
640
        // Remove head element, preserving child nodes.
641 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...
642 9
            $removeExtraHeadTag
643
            &&
644 9
            $this->node->parentNode instanceof \DOMElement
645
            &&
646 9
            $newDocument->getIsDOMDocumentCreatedWithoutHeadWrapper()
647
        ) {
648 2
            $html = $this->node->parentNode->getElementsByTagName('head')[0];
649 2
            if ($this->node->parentNode->ownerDocument !== null) {
650 2
                $fragment = $this->node->parentNode->ownerDocument->createDocumentFragment();
651 2
                if ($html !== null) {
652
                    /** @var \DOMNode $html */
653
                    while ($html->childNodes->length > 0) {
654
                        $tmpNode = $html->childNodes->item(0);
655
                        if ($tmpNode !== null) {
656
                            /** @noinspection UnusedFunctionResultInspection */
657
                            $fragment->appendChild($tmpNode);
658
                        }
659
                    }
660
                    /** @noinspection UnusedFunctionResultInspection */
661
                    $html->parentNode->replaceChild($fragment, $html);
662
                }
663
            }
664
        }
665
666 9
        return $newDocument;
667
    }
668
669
    /**
670
     * Change the name of a tag in a "DOMNode".
671
     *
672
     * @param \DOMNode $node
673
     * @param string   $name
674
     *
675
     * @return \DOMElement|false
676
     *                          <p>DOMElement a new instance of class DOMElement or false
677
     *                          if an error occured.</p>
678
     */
679 4
    protected function changeElementName(\DOMNode $node, string $name)
680
    {
681 4
        $ownerDocument = $node->ownerDocument;
682 4
        if ($ownerDocument) {
683 4
            $newNode = $ownerDocument->createElement($name);
684
        } else {
685
            return false;
686
        }
687
688 4
        foreach ($node->childNodes as $child) {
689 4
            $child = $ownerDocument->importNode($child, true);
690
            /** @noinspection UnusedFunctionResultInspection */
691 4
            $newNode->appendChild($child);
692
        }
693
694 4
        foreach ($node->attributes as $attrName => $attrNode) {
695
            /** @noinspection UnusedFunctionResultInspection */
696
            $newNode->setAttribute($attrName, $attrNode);
697
        }
698
699
        /** @noinspection UnusedFunctionResultInspection */
700 4
        $newNode->ownerDocument->replaceChild($newNode, $node);
701
702 4
        return $newNode;
703
    }
704
705
    /**
706
     * Set attribute value.
707
     *
708
     * @param string      $name       <p>The name of the html-attribute.</p>
709
     * @param string|null $value      <p>Set to NULL or empty string, to remove the attribute.</p>
710
     * @param bool        $strict     </p>
711
     *                                $value must be NULL, to remove the attribute,
712
     *                                so that you can set an empty string as attribute-value e.g. autofocus=""
713
     *                                </p>
714
     *
715
     * @return SimpleHtmlDomInterface
716
     */
717 10
    public function setAttribute(string $name, $value = null, bool $strict = false): SimpleHtmlDomInterface
718
    {
719
        if (
720 10
            ($strict && $value === null)
721
            ||
722 10
            (!$strict && empty($value))
723
        ) {
724
            /** @noinspection UnusedFunctionResultInspection */
725 2
            $this->removeAttribute($name);
726 10
        } elseif (\method_exists($this->node, 'setAttribute')) {
727
            /** @noinspection UnusedFunctionResultInspection */
728 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...
729
        }
730
731 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...
732
    }
733
734
    /**
735
     * @param string|string[]|null $value <p>
736
     *                                    null === get the current input value
737
     *                                    text === set a new input value
738
     *                                    </p>
739
     *
740
     * @return string|string[]|null
741
     */
742 1
    public function val($value = null)
743
    {
744 1
        if ($value === null) {
745
            if (
746 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...
747
                &&
748
                (
749 1
                    $this->getAttribute('type') === 'text'
750
                    ||
751 1
                    !$this->hasAttribute('type')
752
                )
753
            ) {
754 1
                return $this->getAttribute('value');
755
            }
756
757
            if (
758 1
                $this->hasAttribute('checked')
759
                &&
760 1
                \in_array($this->getAttribute('type'), ['checkbox', 'radio'], true)
761
            ) {
762 1
                return $this->getAttribute('value');
763
            }
764
765 1
            if ($this->node->nodeName === 'select') {
766
                $valuesFromDom = [];
767
                $options = $this->getElementsByTagName('option');
768
                if ($options instanceof SimpleHtmlDomNode) {
769
                    foreach ($options as $option) {
770
                        if ($this->hasAttribute('checked')) {
771
                            /** @noinspection UnnecessaryCastingInspection */
772
                            $valuesFromDom[] = (string) $option->getAttribute('value');
773
                        }
774
                    }
775
                }
776
777
                if (\count($valuesFromDom) === 0) {
778
                    return null;
779
                }
780
781
                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...
782
            }
783
784 1
            if ($this->node->nodeName === 'textarea') {
785 1
                return $this->node->nodeValue;
786
            }
787
        } else {
788
            /** @noinspection NestedPositiveIfStatementsInspection */
789 1
            if (\in_array($this->getAttribute('type'), ['checkbox', 'radio'], true)) {
790 1
                if ($value === $this->getAttribute('value')) {
791
                    /** @noinspection UnusedFunctionResultInspection */
792 1
                    $this->setAttribute('checked', 'checked');
793
                } else {
794
                    /** @noinspection UnusedFunctionResultInspection */
795 1
                    $this->removeAttribute('checked');
796
                }
797 1
            } elseif ($this->node instanceof \DOMElement && $this->node->nodeName === 'select') {
798
                foreach ($this->node->getElementsByTagName('option') as $option) {
799
                    /** @var \DOMElement $option */
800
                    if ($value === $option->getAttribute('value')) {
801
                        /** @noinspection UnusedFunctionResultInspection */
802
                        $option->setAttribute('selected', 'selected');
803
                    } else {
804
                        $option->removeAttribute('selected');
805
                    }
806
                }
807 1
            } elseif ($this->node->nodeName === 'input' && \is_string($value)) {
808
                // Set value for input elements
809
                /** @noinspection UnusedFunctionResultInspection */
810 1
                $this->setAttribute('value', $value);
811 1
            } elseif ($this->node->nodeName === 'textarea' && \is_string($value)) {
812 1
                $this->node->nodeValue = $value;
813
            }
814
        }
815
816 1
        return null;
817
    }
818
819
    /**
820
     * Remove attribute.
821
     *
822
     * @param string $name <p>The name of the html-attribute.</p>
823
     *
824
     * @return SimpleHtmlDomInterface
825
     */
826 2
    public function removeAttribute(string $name): SimpleHtmlDomInterface
827
    {
828 2
        if (\method_exists($this->node, 'removeAttribute')) {
829 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...
830
        }
831
832 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...
833
    }
834
835
    /**
836
     * Get dom node's plain text.
837
     *
838
     * @return string
839
     */
840 17
    public function text(): string
841
    {
842 17
        return $this->getHtmlDomParser()->fixHtmlOutput($this->node->textContent);
843
    }
844
}
845