Completed
Push — master ( 722454...5b9105 )
by Lars
02:01 queued 12s
created

SimpleHtmlDom::firstChild()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11

Duplication

Lines 11
Ratio 100 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 11
loc 11
ccs 5
cts 5
cp 1
rs 9.9
c 0
b 0
f 0
cc 2
nc 2
nop 0
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\helper;
6
7
/**
8
 * @noinspection PhpHierarchyChecksInspection
9
 *
10
 * {@inheritdoc}
11
 *
12
 * @implements \IteratorAggregate<int, \DOMNode>
13
 */
14
class SimpleHtmlDom extends AbstractSimpleHtmlDom implements \IteratorAggregate, SimpleHtmlDomInterface
15
{
16
    /**
17
     * @param \DOMElement|\DOMNode $node
18
     */
19 156
    public function __construct(\DOMNode $node)
20
    {
21 156
        $this->node = $node;
22 156
    }
23
24
    /**
25
     * @param string $name
26
     * @param array  $arguments
27
     *
28
     * @throws \BadMethodCallException
29
     *
30
     * @return SimpleHtmlDomInterface|string|null
31
     */
32 10
    public function __call($name, $arguments)
33
    {
34 10
        $name = \strtolower($name);
35
36 10
        if (isset(self::$functionAliases[$name])) {
37 10
            return \call_user_func_array([$this, self::$functionAliases[$name]], $arguments);
38
        }
39
40
        throw new \BadMethodCallException('Method does not exist');
41
    }
42
43
    /**
44
     * Find list of nodes with a CSS selector.
45
     *
46
     * @param string   $selector
47
     * @param int|null $idx
48
     *
49
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface<SimpleHtmlDomInterface>
0 ignored issues
show
Documentation introduced by
The doc-type SimpleHtmlDomInterface|S...SimpleHtmlDomInterface> could not be parsed: Expected "|" or "end of type", but got "<" at position 74. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
50
     */
51 27
    public function find(string $selector, $idx = null)
52
    {
53 27
        return $this->getHtmlDomParser()->find($selector, $idx);
54
    }
55
56
    /**
57
     * Returns an array of attributes.
58
     *
59
     * @return string[]|null
60
     */
61 3 View Code Duplication
    public function getAllAttributes()
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...
62
    {
63
        if (
64 3
            $this->node
65
            &&
66 3
            $this->node->hasAttributes()
67
        ) {
68 3
            $attributes = [];
69 3
            foreach ($this->node->attributes ?? [] as $attr) {
70 3
                $attributes[$attr->name] = HtmlDomParser::putReplacedBackToPreserveHtmlEntities($attr->value);
71
            }
72
73 3
            return $attributes;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $attributes; (array) is incompatible with the return type declared by the interface voku\helper\SimpleHtmlDo...rface::getAllAttributes of type 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...
74
        }
75
76 1
        return null;
77
    }
78
79
    /**
80
     * @return bool
81
     */
82
    public function hasAttributes(): bool
83
    {
84
        return $this->node && $this->node->hasAttributes();
85
    }
86
87
    /**
88
     * Return attribute value.
89
     *
90
     * @param string $name
91
     *
92
     * @return string
93
     */
94 25
    public function getAttribute(string $name): string
95
    {
96 25
        if ($this->node instanceof \DOMElement) {
97 25
            return HtmlDomParser::putReplacedBackToPreserveHtmlEntities(
98 25
                $this->node->getAttribute($name)
99
            );
100
        }
101
102
        return '';
103
    }
104
105
    /**
106
     * Determine if an attribute exists on the element.
107
     *
108
     * @param string $name
109
     *
110
     * @return bool
111
     */
112 2
    public function hasAttribute(string $name): bool
113
    {
114 2
        if (!$this->node instanceof \DOMElement) {
115
            return false;
116
        }
117
118 2
        return $this->node->hasAttribute($name);
119
    }
120
121
    /**
122
     * Get dom node's outer html.
123
     *
124
     * @param bool $multiDecodeNewHtmlEntity
125
     *
126
     * @return string
127
     */
128 34
    public function html(bool $multiDecodeNewHtmlEntity = false): string
129
    {
130 34
        return $this->getHtmlDomParser()->html($multiDecodeNewHtmlEntity);
131
    }
132
133
    /**
134
     * Get dom node's inner html.
135
     *
136
     * @param bool $multiDecodeNewHtmlEntity
137
     *
138
     * @return string
139
     */
140 23
    public function innerHtml(bool $multiDecodeNewHtmlEntity = false): string
141
    {
142 23
        return $this->getHtmlDomParser()->innerHtml($multiDecodeNewHtmlEntity);
143
    }
144
145
    /**
146
     * Remove attribute.
147
     *
148
     * @param string $name <p>The name of the html-attribute.</p>
149
     *
150
     * @return SimpleHtmlDomInterface
151
     */
152 2
    public function removeAttribute(string $name): SimpleHtmlDomInterface
153
    {
154 2
        if (\method_exists($this->node, 'removeAttribute')) {
155 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...
156
        }
157
158 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...
159
    }
160
161
    /**
162
     * Replace child node.
163
     *
164
     * @param string $string
165
     *
166
     * @return SimpleHtmlDomInterface
167
     */
168 9 View Code Duplication
    protected function replaceChildWithString(string $string): SimpleHtmlDomInterface
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...
169
    {
170 9
        if (!empty($string)) {
171 8
            $newDocument = new HtmlDomParser($string);
172
173 8
            $tmpDomString = $this->normalizeStringForComparision($newDocument);
174 8
            $tmpStr = $this->normalizeStringForComparision($string);
175 8
            if ($tmpDomString !== $tmpStr) {
176
                throw new \RuntimeException(
177
                    'Not valid HTML fragment!' . "\n" .
178
                    $tmpDomString . "\n" .
179
                    $tmpStr
180
                );
181
            }
182
        }
183
184
        /** @var \DOMNode[] $remove_nodes */
185 9
        $remove_nodes = [];
186 9
        if ($this->node->childNodes->length > 0) {
187
            // INFO: We need to fetch the nodes first, before we can delete them, because of missing references in the dom,
188
            // if we delete the elements on the fly.
189 9
            foreach ($this->node->childNodes as $node) {
190 9
                $remove_nodes[] = $node;
191
            }
192
        }
193 9
        foreach ($remove_nodes as $remove_node) {
194 9
            $this->node->removeChild($remove_node);
195
        }
196
197 9
        if (!empty($newDocument)) {
198 8
            $newDocument = $this->cleanHtmlWrapper($newDocument);
199 8
            $ownerDocument = $this->node->ownerDocument;
200
            if (
201 8
                $ownerDocument !== null
202
                &&
203 8
                $newDocument->getDocument()->documentElement !== null
204
            ) {
205 8
                $newNode = $ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
206
                /** @noinspection UnusedFunctionResultInspection */
207 8
                $this->node->appendChild($newNode);
208
            }
209
        }
210
211 9
        return $this;
212
    }
213
214
    /**
215
     * Replace this node.
216
     *
217
     * @param string $string
218
     *
219
     * @return SimpleHtmlDomInterface
220
     */
221 6
    protected function replaceNodeWithString(string $string): SimpleHtmlDomInterface
222
    {
223 6
        if (empty($string)) {
224 2
            $this->node->parentNode->removeChild($this->node);
225
226 2
            return $this;
227
        }
228
229 5
        $newDocument = new HtmlDomParser($string);
230
231 5
        $tmpDomOuterTextString = $this->normalizeStringForComparision($newDocument);
232 5
        $tmpStr = $this->normalizeStringForComparision($string);
233 5 View Code Duplication
        if ($tmpDomOuterTextString !== $tmpStr) {
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...
234
            throw new \RuntimeException(
235
                'Not valid HTML fragment!' . "\n"
236
                . $tmpDomOuterTextString . "\n" .
237
                $tmpStr
238
            );
239
        }
240
241 5
        $newDocument = $this->cleanHtmlWrapper($newDocument, true);
242 5
        $ownerDocument = $this->node->ownerDocument;
243
        if (
244 5
            $ownerDocument === null
245
            ||
246 5
            $newDocument->getDocument()->documentElement === null
247
        ) {
248
            return $this;
249
        }
250
251 5
        $newNode = $ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
252
253 5
        $this->node->parentNode->replaceChild($newNode, $this->node);
254 5
        $this->node = $newNode;
255
256
        // Remove head element, preserving child nodes. (again)
257 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...
258 5
            $this->node->parentNode instanceof \DOMElement
259
            &&
260 5
            $newDocument->getIsDOMDocumentCreatedWithoutHeadWrapper()
261
        ) {
262 3
            $html = $this->node->parentNode->getElementsByTagName('head')[0];
263
264 3
            if ($this->node->parentNode->ownerDocument !== null) {
265 3
                $fragment = $this->node->parentNode->ownerDocument->createDocumentFragment();
266 3
                if ($html !== null) {
267
                    /** @var \DOMNode $html */
268 1
                    while ($html->childNodes->length > 0) {
269 1
                        $tmpNode = $html->childNodes->item(0);
270 1
                        if ($tmpNode !== null) {
271
                            /** @noinspection UnusedFunctionResultInspection */
272 1
                            $fragment->appendChild($tmpNode);
273
                        }
274
                    }
275
                    /** @noinspection UnusedFunctionResultInspection */
276 1
                    $html->parentNode->replaceChild($fragment, $html);
277
                }
278
            }
279
        }
280
281 5
        return $this;
282
    }
283
284
    /**
285
     * Replace this node with text
286
     *
287
     * @param string $string
288
     *
289
     * @return SimpleHtmlDomInterface
290
     */
291 1 View Code Duplication
    protected function replaceTextWithString($string): SimpleHtmlDomInterface
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...
292
    {
293 1
        if (empty($string)) {
294 1
            $this->node->parentNode->removeChild($this->node);
295
296 1
            return $this;
297
        }
298
299 1
        $ownerDocument = $this->node->ownerDocument;
300 1
        if ($ownerDocument !== null) {
301 1
            $newElement = $ownerDocument->createTextNode($string);
302 1
            $newNode = $ownerDocument->importNode($newElement, true);
303 1
            $this->node->parentNode->replaceChild($newNode, $this->node);
304 1
            $this->node = $newNode;
305
        }
306
307 1
        return $this;
308
    }
309
310
    /**
311
     * Set attribute value.
312
     *
313
     * @param string      $name       <p>The name of the html-attribute.</p>
314
     * @param string|null $value      <p>Set to NULL or empty string, to remove the attribute.</p>
315
     * @param bool        $strict     </p>
316
     *                                $value must be NULL, to remove the attribute,
317
     *                                so that you can set an empty string as attribute-value e.g. autofocus=""
318
     *                                </p>
319
     *
320
     * @return SimpleHtmlDomInterface
321
     */
322 15 View Code Duplication
    public function setAttribute(string $name, $value = null, bool $strict = false): SimpleHtmlDomInterface
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...
323
    {
324
        if (
325 15
            ($strict && $value === null)
326
            ||
327 15
            (!$strict && empty($value))
328
        ) {
329
            /** @noinspection UnusedFunctionResultInspection */
330 2
            $this->removeAttribute($name);
331 15
        } elseif (\method_exists($this->node, 'setAttribute')) {
332
            /** @noinspection UnusedFunctionResultInspection */
333 15
            $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...
334
        }
335
336 15
        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...
337
    }
338
339
    /**
340
     * Get dom node's plain text.
341
     *
342
     * @return string
343
     */
344 28
    public function text(): string
345
    {
346 28
        return $this->getHtmlDomParser()->fixHtmlOutput($this->node->textContent);
347
    }
348
349
    /**
350
     * Change the name of a tag in a "DOMNode".
351
     *
352
     * @param \DOMNode $node
353
     * @param string   $name
354
     *
355
     * @return \DOMElement|false
356
     *                          <p>DOMElement a new instance of class DOMElement or false
357
     *                          if an error occured.</p>
358
     */
359 10 View Code Duplication
    protected function changeElementName(\DOMNode $node, string $name)
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...
360
    {
361 10
        $ownerDocument = $node->ownerDocument;
362 10
        if ($ownerDocument) {
363 10
            $newNode = $ownerDocument->createElement($name);
364
        } else {
365
            return false;
366
        }
367
368 10
        foreach ($node->childNodes as $child) {
369 10
            $child = $ownerDocument->importNode($child, true);
370
            /** @noinspection UnusedFunctionResultInspection */
371 10
            $newNode->appendChild($child);
372
        }
373
374 10
        foreach ($node->attributes ?? [] as $attrName => $attrNode) {
375
            /** @noinspection UnusedFunctionResultInspection */
376
            $newNode->setAttribute($attrName, $attrNode);
377
        }
378
379 10
        if ($newNode->ownerDocument !== null) {
380
            /** @noinspection UnusedFunctionResultInspection */
381 10
            $newNode->ownerDocument->replaceChild($newNode, $node);
382
        }
383
384 10
        return $newNode;
385
    }
386
387
    /**
388
     * Returns children of node.
389
     *
390
     * @param int $idx
391
     *
392
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface|null
393
     */
394 2 View Code Duplication
    public function childNodes(int $idx = -1)
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...
395
    {
396 2
        $nodeList = $this->getIterator();
397
398 2
        if ($idx === -1) {
399 2
            return $nodeList;
400
        }
401
402 2
        return $nodeList[$idx] ?? null;
403
    }
404
405
    /**
406
     * Find nodes with a CSS selector.
407
     *
408
     * @param string $selector
409
     *
410
     * @return SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface<SimpleHtmlDomInterface>
0 ignored issues
show
Documentation introduced by
The doc-type SimpleHtmlDomInterface[]...SimpleHtmlDomInterface> could not be parsed: Expected "|" or "end of type", but got "<" at position 51. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
411
     */
412 1
    public function findMulti(string $selector): SimpleHtmlDomNodeInterface
413
    {
414 1
        return $this->getHtmlDomParser()->findMulti($selector);
415
    }
416
417
    /**
418
     * Find nodes with a CSS selector or false, if no element is found.
419
     *
420
     * @param string $selector
421
     *
422
     * @return false|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface<SimpleHtmlDomInterface>
0 ignored issues
show
Documentation introduced by
The doc-type false|SimpleHtmlDomInter...SimpleHtmlDomInterface> could not be parsed: Expected "|" or "end of type", but got "<" at position 57. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
423
     */
424 1
    public function findMultiOrFalse(string $selector)
425
    {
426 1
        return $this->getHtmlDomParser()->findMultiOrFalse($selector);
427
    }
428
429
    /**
430
     * Find one node with a CSS selector.
431
     *
432
     * @param string $selector
433
     *
434
     * @return SimpleHtmlDomInterface
435
     */
436 3
    public function findOne(string $selector): SimpleHtmlDomInterface
437
    {
438 3
        return $this->getHtmlDomParser()->findOne($selector);
439
    }
440
441
    /**
442
     * Find one node with a CSS selector or false, if no element is found.
443
     *
444
     * @param string $selector
445
     *
446
     * @return false|SimpleHtmlDomInterface
447
     */
448 1
    public function findOneOrFalse(string $selector)
449
    {
450 1
        return $this->getHtmlDomParser()->findOneOrFalse($selector);
451
    }
452
453
    /**
454
     * Returns the first child of node.
455
     *
456
     * @return SimpleHtmlDomInterface|null
457
     */
458 4 View Code Duplication
    public function firstChild()
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...
459
    {
460
        /** @var \DOMNode|null $node */
461 4
        $node = $this->node->firstChild;
462
463 4
        if ($node === null) {
464 1
            return null;
465
        }
466
467 4
        return new static($node);
468
    }
469
470
    /**
471
     * Return elements by ".class".
472
     *
473
     * @param string $class
474
     *
475
     * @return SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface<SimpleHtmlDomInterface>
0 ignored issues
show
Documentation introduced by
The doc-type SimpleHtmlDomInterface[]...SimpleHtmlDomInterface> could not be parsed: Expected "|" or "end of type", but got "<" at position 51. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
476
     */
477
    public function getElementByClass(string $class): SimpleHtmlDomNodeInterface
478
    {
479
        return $this->findMulti(".${class}");
480
    }
481
482
    /**
483
     * Return element by #id.
484
     *
485
     * @param string $id
486
     *
487
     * @return SimpleHtmlDomInterface
488
     */
489 1
    public function getElementById(string $id): SimpleHtmlDomInterface
490
    {
491 1
        return $this->findOne("#${id}");
492
    }
493
494
    /**
495
     * Return element by tag name.
496
     *
497
     * @param string $name
498
     *
499
     * @return SimpleHtmlDomInterface
500
     */
501 1 View Code Duplication
    public function getElementByTagName(string $name): SimpleHtmlDomInterface
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...
502
    {
503 1
        if ($this->node instanceof \DOMElement) {
504 1
            $node = $this->node->getElementsByTagName($name)->item(0);
505
        } else {
506
            $node = null;
507
        }
508
509 1
        if ($node === null) {
510
            return new SimpleHtmlDomBlank();
511
        }
512
513 1
        return new static($node);
514
    }
515
516
    /**
517
     * Returns elements by "#id".
518
     *
519
     * @param string   $id
520
     * @param int|null $idx
521
     *
522
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface<SimpleHtmlDomInterface>
0 ignored issues
show
Documentation introduced by
The doc-type SimpleHtmlDomInterface|S...SimpleHtmlDomInterface> could not be parsed: Expected "|" or "end of type", but got "<" at position 74. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
523
     */
524
    public function getElementsById(string $id, $idx = null)
525
    {
526
        return $this->find("#${id}", $idx);
527
    }
528
529
    /**
530
     * Returns elements by tag name.
531
     *
532
     * @param string   $name
533
     * @param int|null $idx
534
     *
535
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface<SimpleHtmlDomInterface>
0 ignored issues
show
Documentation introduced by
The doc-type SimpleHtmlDomInterface|S...SimpleHtmlDomInterface> could not be parsed: Expected "|" or "end of type", but got "<" at position 74. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
536
     */
537 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...
538
    {
539 1
        if ($this->node instanceof \DOMElement) {
540 1
            $nodesList = $this->node->getElementsByTagName($name);
541
        } else {
542
            $nodesList = [];
543
        }
544
545 1
        $elements = new SimpleHtmlDomNode();
546
547 1
        foreach ($nodesList as $node) {
548 1
            $elements[] = new static($node);
549
        }
550
551
        // return all elements
552 1
        if ($idx === null) {
553 1
            if (\count($elements) === 0) {
554
                return new SimpleHtmlDomNodeBlank();
555
            }
556
557 1
            return $elements;
558
        }
559
560
        // handle negative values
561
        if ($idx < 0) {
562
            $idx = \count($elements) + $idx;
563
        }
564
565
        // return one element
566
        return $elements[$idx] ?? new SimpleHtmlDomBlank();
567
    }
568
569
    /**
570
     * Create a new "HtmlDomParser"-object from the current context.
571
     *
572
     * @return HtmlDomParser
573
     */
574 98
    public function getHtmlDomParser(): HtmlDomParser
575
    {
576 98
        return new HtmlDomParser($this);
577
    }
578
579
    /**
580
     * @return \DOMNode
581
     */
582 99
    public function getNode(): \DOMNode
583
    {
584 99
        return $this->node;
585
    }
586
587
    /**
588
     * Nodes can get partially destroyed in which they're still an
589
     * actual DOM node (such as \DOMElement) but almost their entire
590
     * body is gone, including the `nodeType` attribute.
591
     *
592
     * @return bool true if node has been destroyed
593
     */
594
    public function isRemoved(): bool
595
    {
596
        return !isset($this->node->nodeType);
597
    }
598
599
    /**
600
     * Returns the last child of node.
601
     *
602
     * @return SimpleHtmlDomInterface|null
603
     */
604 4 View Code Duplication
    public function lastChild()
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...
605
    {
606
        /** @var \DOMNode|null $node */
607 4
        $node = $this->node->lastChild;
608
609 4
        if ($node === null) {
610 1
            return null;
611
        }
612
613 4
        return new static($node);
614
    }
615
616
    /**
617
     * Returns the next sibling of node.
618
     *
619
     * @return SimpleHtmlDomInterface|null
620
     */
621 1 View Code Duplication
    public function nextSibling()
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...
622
    {
623
        /** @var \DOMNode|null $node */
624 1
        $node = $this->node->nextSibling;
625
626 1
        if ($node === null) {
627 1
            return null;
628
        }
629
630 1
        return new static($node);
631
    }
632
633
    /**
634
     * Returns the next sibling of node.
635
     *
636
     * @return SimpleHtmlDomInterface|null
637
     */
638 1 View Code Duplication
    public function nextNonWhitespaceSibling()
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...
639
    {
640
        /** @var \DOMNode|null $node */
641 1
        $node = $this->node->nextSibling;
642
643 1
        while ($node && !\trim($node->textContent)) {
644
            /** @var \DOMNode|null $node */
645 1
            $node = $node->nextSibling;
646
        }
647
648 1
        if ($node === null) {
649
            return null;
650
        }
651
652 1
        return new static($node);
653
    }
654
655
    /**
656
     * Returns the parent of node.
657
     *
658
     * @return SimpleHtmlDomInterface
659
     */
660 2
    public function parentNode(): SimpleHtmlDomInterface
661
    {
662 2
        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...
663
    }
664
665
    /**
666
     * Returns the previous sibling of node.
667
     *
668
     * @return SimpleHtmlDomInterface|null
669
     */
670 1 View Code Duplication
    public function previousSibling()
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...
671
    {
672
        /** @var \DOMNode|null $node */
673 1
        $node = $this->node->previousSibling;
674
675 1
        if ($node === null) {
676 1
            return null;
677
        }
678
679 1
        return new static($node);
680
    }
681
682
    /**
683
     * @param string|string[]|null $value <p>
684
     *                                    null === get the current input value
685
     *                                    text === set a new input value
686
     *                                    </p>
687
     *
688
     * @return string|string[]|null
689
     */
690 1 View Code Duplication
    public function val($value = 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...
691
    {
692 1
        if ($value === null) {
693
            if (
694 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...
695
                &&
696
                (
697 1
                    $this->getAttribute('type') === 'hidden'
698
                    ||
699 1
                    $this->getAttribute('type') === 'text'
700
                    ||
701 1
                    !$this->hasAttribute('type')
702
                )
703
            ) {
704 1
                return $this->getAttribute('value');
705
            }
706
707
            if (
708 1
                $this->hasAttribute('checked')
709
                &&
710 1
                \in_array($this->getAttribute('type'), ['checkbox', 'radio'], true)
711
            ) {
712 1
                return $this->getAttribute('value');
713
            }
714
715 1
            if ($this->node->nodeName === 'select') {
716
                $valuesFromDom = [];
717
                $options = $this->getElementsByTagName('option');
718
                if ($options instanceof SimpleHtmlDomNode) {
719
                    foreach ($options as $option) {
720
                        if ($this->hasAttribute('checked')) {
721
                            /** @noinspection UnnecessaryCastingInspection */
722
                            $valuesFromDom[] = (string) $option->getAttribute('value');
723
                        }
724
                    }
725
                }
726
727
                if (\count($valuesFromDom) === 0) {
728
                    return null;
729
                }
730
731
                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...
732
            }
733
734 1
            if ($this->node->nodeName === 'textarea') {
735 1
                return $this->node->nodeValue;
736
            }
737
        } else {
738
            /** @noinspection NestedPositiveIfStatementsInspection */
739 1
            if (\in_array($this->getAttribute('type'), ['checkbox', 'radio'], true)) {
740 1
                if ($value === $this->getAttribute('value')) {
741
                    /** @noinspection UnusedFunctionResultInspection */
742 1
                    $this->setAttribute('checked', 'checked');
743
                } else {
744
                    /** @noinspection UnusedFunctionResultInspection */
745 1
                    $this->removeAttribute('checked');
746
                }
747 1
            } elseif ($this->node instanceof \DOMElement && $this->node->nodeName === 'select') {
748
                foreach ($this->node->getElementsByTagName('option') as $option) {
749
                    /** @var \DOMElement $option */
750
                    if ($value === $option->getAttribute('value')) {
751
                        /** @noinspection UnusedFunctionResultInspection */
752
                        $option->setAttribute('selected', 'selected');
753
                    } else {
754
                        $option->removeAttribute('selected');
755
                    }
756
                }
757 1
            } elseif ($this->node->nodeName === 'input' && \is_string($value)) {
758
                // Set value for input elements
759
                /** @noinspection UnusedFunctionResultInspection */
760 1
                $this->setAttribute('value', $value);
761 1
            } elseif ($this->node->nodeName === 'textarea' && \is_string($value)) {
762 1
                $this->node->nodeValue = $value;
763
            }
764
        }
765
766 1
        return null;
767
    }
768
769
    /**
770
     * @param HtmlDomParser $newDocument
771
     * @param bool          $removeExtraHeadTag
772
     *
773
     * @return HtmlDomParser
774
     */
775 13
    protected function cleanHtmlWrapper(
776
        HtmlDomParser $newDocument,
777
        $removeExtraHeadTag = false
778
    ): HtmlDomParser {
779
        if (
780 13
            $newDocument->getIsDOMDocumentCreatedWithoutHtml()
781
            ||
782 13
            $newDocument->getIsDOMDocumentCreatedWithoutHtmlWrapper()
783
        ) {
784
785
            // Remove doc-type node.
786 13
            if ($newDocument->getDocument()->doctype !== null) {
787
                /** @noinspection UnusedFunctionResultInspection */
788
                $newDocument->getDocument()->doctype->parentNode->removeChild($newDocument->getDocument()->doctype);
789
            }
790
791
            // Replace html element, preserving child nodes -> but keep the html wrapper, otherwise we got other problems ...
792
            // so we replace it with "<simpleHtmlDomHtml>" and delete this at the ending.
793 13
            $item = $newDocument->getDocument()->getElementsByTagName('html')->item(0);
794 13
            if ($item !== null) {
795
                /** @noinspection UnusedFunctionResultInspection */
796 10
                $this->changeElementName($item, 'simpleHtmlDomHtml');
797
            }
798
799
            // Remove body element, preserving child nodes.
800 13
            $body = $newDocument->getDocument()->getElementsByTagName('body')->item(0);
801 13
            if ($body instanceof \DOMElement) {
802 8
                $fragment = $newDocument->getDocument()->createDocumentFragment();
803
804 8
                while ($body->childNodes->length > 0) {
805 8
                    $tmpNode = $body->childNodes->item(0);
806 8
                    if ($tmpNode !== null) {
807
                        /** @noinspection UnusedFunctionResultInspection */
808 8
                        $fragment->appendChild($tmpNode);
809
                    }
810
                }
811
812 8
                if ($body->parentNode !== null) {
813
                    /** @noinspection UnusedFunctionResultInspection */
814 8
                    $body->parentNode->replaceChild($fragment, $body);
815
                }
816
            }
817
        }
818
819
        // Remove head element, preserving child nodes.
820 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...
821 13
            $removeExtraHeadTag
822
            &&
823 13
            $this->node->parentNode instanceof \DOMElement
824
            &&
825 13
            $newDocument->getIsDOMDocumentCreatedWithoutHeadWrapper()
826
        ) {
827 3
            $html = $this->node->parentNode->getElementsByTagName('head')[0] ?? null;
828
829 3
            if ($html !== null && $this->node->parentNode->ownerDocument !== null) {
830
                $fragment = $this->node->parentNode->ownerDocument->createDocumentFragment();
831
832
                /** @var \DOMNode $html */
833
                while ($html->childNodes->length > 0) {
834
                    $tmpNode = $html->childNodes->item(0);
835
                    if ($tmpNode !== null) {
836
                        /** @noinspection UnusedFunctionResultInspection */
837
                        $fragment->appendChild($tmpNode);
838
                    }
839
                }
840
841
                /** @noinspection UnusedFunctionResultInspection */
842
                $html->parentNode->replaceChild($fragment, $html);
843
            }
844
        }
845
846 13
        return $newDocument;
847
    }
848
849
    /**
850
     * Retrieve an external iterator.
851
     *
852
     * @see  http://php.net/manual/en/iteratoraggregate.getiterator.php
853
     *
854
     * @return SimpleHtmlDomNode
855
     *                           <p>
856
     *                              An instance of an object implementing <b>Iterator</b> or
857
     *                              <b>Traversable</b>
858
     *                           </p>
859
     */
860 3 View Code Duplication
    public function getIterator(): SimpleHtmlDomNodeInterface
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...
861
    {
862 3
        $elements = new SimpleHtmlDomNode();
863 3
        if ($this->node->hasChildNodes()) {
864 3
            foreach ($this->node->childNodes as $node) {
865 3
                $elements[] = new static($node);
866
            }
867
        }
868
869 3
        return $elements;
870
    }
871
872
    /**
873
     * Get dom node's inner html.
874
     *
875
     * @param bool $multiDecodeNewHtmlEntity
876
     *
877
     * @return string
878
     */
879
    public function innerXml(bool $multiDecodeNewHtmlEntity = false): string
880
    {
881
        return $this->getHtmlDomParser()->innerXml($multiDecodeNewHtmlEntity);
882
    }
883
884
    /**
885
     * Normalize the given input for comparision.
886
     *
887
     * @param HtmlDomParser|string $input
888
     *
889
     * @return string
890
     */
891 13
    private function normalizeStringForComparision($input): string
892
    {
893 13
        if ($input instanceof HtmlDomParser) {
894 13
            $string = $input->outerText();
895
896 13
            if ($input->getIsDOMDocumentCreatedWithoutHeadWrapper()) {
897
                /** @noinspection HtmlRequiredTitleElement */
898 13
                $string = \str_replace(['<head>', '</head>'], '', $string);
899
            }
900
        } else {
901 13
            $string = (string) $input;
902
        }
903
904
        return
905 13
            \urlencode(
906 13
                \urldecode(
907 13
                    \trim(
908 13
                        \str_replace(
909
                            [
910 13
                                ' ',
911
                                "\n",
912
                                "\r",
913
                                '/>',
914
                            ],
915
                            [
916 13
                                '',
917
                                '',
918
                                '',
919
                                '>',
920
                            ],
921 13
                            \strtolower($string)
922
                        )
923
                    )
924
                )
925
            );
926
    }
927
}
928