Completed
Push — master ( a941a6...feea10 )
by Lars
21s queued 13s
created

SimpleHtmlDom::previousNonWhitespaceSibling()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16

Duplication

Lines 16
Ratio 100 %

Code Coverage

Tests 6
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
nc 4
nop 0
dl 16
loc 16
ccs 6
cts 6
cp 1
crap 4
rs 9.7333
c 0
b 0
f 0
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 164
    public function __construct(\DOMNode $node)
20
    {
21 164
        $this->node = $node;
22 164
    }
23
24
    /**
25
     * @param string $name
26
     * @param array  $arguments
27
     *
28
     * @throws \BadMethodCallException
29
     *
30
     * @return SimpleHtmlDomInterface|string|null
31
     */
32 11
    public function __call($name, $arguments)
33
    {
34 11
        $name = \strtolower($name);
35
36 11
        if (isset(self::$functionAliases[$name])) {
37 11
            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 26
    public function getAttribute(string $name): string
95
    {
96 26
        if ($this->node instanceof \DOMElement) {
97 26
            return HtmlDomParser::putReplacedBackToPreserveHtmlEntities(
98 26
                $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 36
    public function html(bool $multiDecodeNewHtmlEntity = false): string
129
    {
130 36
        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 24
    public function innerHtml(bool $multiDecodeNewHtmlEntity = false): string
141
    {
142 24
        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
202
                &&
203 8
                $newDocument->getDocument()->documentElement
204
            ) {
205 8
                $newNode = $ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
206 8
                $this->node->appendChild($newNode);
207
            }
208
        }
209
210 9
        return $this;
211
    }
212
213
    /**
214
     * Replace this node.
215
     *
216
     * @param string $string
217
     *
218
     * @return SimpleHtmlDomInterface
219
     */
220 8
    protected function replaceNodeWithString(string $string): SimpleHtmlDomInterface
221
    {
222 8
        if (empty($string)) {
223 4
            if ($this->node->parentNode) {
224 4
                $this->node->parentNode->removeChild($this->node);
225
            }
226 4
            $this->node = new \DOMText();
227
228 4
            return $this;
229
        }
230
231 6
        $newDocument = new HtmlDomParser($string);
232
233 6
        $tmpDomOuterTextString = $this->normalizeStringForComparision($newDocument);
234 6
        $tmpStr = $this->normalizeStringForComparision($string);
235 6 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...
236
            throw new \RuntimeException(
237
                'Not valid HTML fragment!' . "\n"
238
                . $tmpDomOuterTextString . "\n" .
239
                $tmpStr
240
            );
241
        }
242
243 6
        $newDocument = $this->cleanHtmlWrapper($newDocument, true);
244 6
        $ownerDocument = $this->node->ownerDocument;
245
        if (
246 6
            $ownerDocument === null
247
            ||
248 6
            $newDocument->getDocument()->documentElement === null
249
        ) {
250
            return $this;
251
        }
252
253 6
        $newNode = $ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
254
255 6
        $this->node->parentNode->replaceChild($newNode, $this->node);
256 6
        $this->node = $newNode;
257
258
        // Remove head element, preserving child nodes. (again)
259 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...
260 6
            $this->node->parentNode instanceof \DOMElement
261
            &&
262 6
            $newDocument->getIsDOMDocumentCreatedWithoutHeadWrapper()
263
        ) {
264 3
            $html = $this->node->parentNode->getElementsByTagName('head')[0];
265
266
            if (
267 3
                $html !== null
268
                &&
269 3
                $this->node->parentNode->ownerDocument
270
            ) {
271 1
                $fragment = $this->node->parentNode->ownerDocument->createDocumentFragment();
272
                /** @var \DOMNode $html */
273 1
                while ($html->childNodes->length > 0) {
274 1
                    $tmpNode = $html->childNodes->item(0);
275 1
                    if ($tmpNode !== null) {
276
                        /** @noinspection UnusedFunctionResultInspection */
277 1
                        $fragment->appendChild($tmpNode);
278
                    }
279
                }
280 1
                $html->parentNode->replaceChild($fragment, $html);
281
            }
282
        }
283
284 6
        return $this;
285
    }
286
287
    /**
288
     * Replace this node with text
289
     *
290
     * @param string $string
291
     *
292
     * @return SimpleHtmlDomInterface
293
     */
294 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...
295
    {
296 1
        if (empty($string)) {
297 1
            if ($this->node->parentNode) {
298 1
                $this->node->parentNode->removeChild($this->node);
299
            }
300 1
            $this->node = new \DOMText();
301
302 1
            return $this;
303
        }
304
305 1
        $ownerDocument = $this->node->ownerDocument;
306 1
        if ($ownerDocument) {
307 1
            $newElement = $ownerDocument->createTextNode($string);
308 1
            $newNode = $ownerDocument->importNode($newElement, true);
309 1
            $this->node->parentNode->replaceChild($newNode, $this->node);
310 1
            $this->node = $newNode;
311
        }
312
313 1
        return $this;
314
    }
315
316
    /**
317
     * Set attribute value.
318
     *
319
     * @param string      $name                     <p>The name of the html-attribute.</p>
320
     * @param string|null $value                    <p>Set to NULL or empty string, to remove the attribute.</p>
321
     * @param bool        $strictEmptyValueCheck <p>
322
     *                                $value must be NULL, to remove the attribute,
323
     *                                so that you can set an empty string as attribute-value e.g. autofocus=""
324
     *                                </p>
325
     *
326
     * @return SimpleHtmlDomInterface
327
     */
328 16 View Code Duplication
    public function setAttribute(string $name, $value = null, bool $strictEmptyValueCheck = 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...
329
    {
330
        if (
331 16
            ($strictEmptyValueCheck && $value === null)
332
            ||
333 16
            (!$strictEmptyValueCheck && empty($value))
334
        ) {
335
            /** @noinspection UnusedFunctionResultInspection */
336 2
            $this->removeAttribute($name);
337 16
        } elseif (\method_exists($this->node, 'setAttribute')) {
338
            /** @noinspection UnusedFunctionResultInspection */
339 16
            $this->node->setAttribute($name, HtmlDomParser::replaceToPreserveHtmlEntities((string) $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...
340
        }
341
342 16
        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...
343
    }
344
345
    /**
346
     * Get dom node's plain text.
347
     *
348
     * @return string
349
     */
350 32
    public function text(): string
351
    {
352 32
        return $this->getHtmlDomParser()->fixHtmlOutput($this->node->textContent);
353
    }
354
355
    /**
356
     * Change the name of a tag in a "DOMNode".
357
     *
358
     * @param \DOMNode $node
359
     * @param string   $name
360
     *
361
     * @return \DOMElement|false
362
     *                          <p>DOMElement a new instance of class DOMElement or false
363
     *                          if an error occured.</p>
364
     */
365 11 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...
366
    {
367 11
        $ownerDocument = $node->ownerDocument;
368 11
        if (!$ownerDocument) {
369
            return false;
370
        }
371
372 11
        $newNode = $ownerDocument->createElement($name);
373
374 11
        foreach ($node->childNodes as $child) {
375 11
            $child = $ownerDocument->importNode($child, true);
376 11
            $newNode->appendChild($child);
377
        }
378
379 11
        foreach ($node->attributes ?? [] as $attrName => $attrNode) {
380
            /** @noinspection UnusedFunctionResultInspection */
381
            $newNode->setAttribute($attrName, $attrNode);
382
        }
383
384 11
        if ($newNode->ownerDocument) {
385
            /** @noinspection UnusedFunctionResultInspection */
386 11
            $newNode->ownerDocument->replaceChild($newNode, $node);
387
        }
388
389 11
        return $newNode;
390
    }
391
392
    /**
393
     * Returns children of node.
394
     *
395
     * @param int $idx
396
     *
397
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface|null
398
     */
399 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...
400
    {
401 2
        $nodeList = $this->getIterator();
402
403 2
        if ($idx === -1) {
404 2
            return $nodeList;
405
        }
406
407 2
        return $nodeList[$idx] ?? null;
408
    }
409
410
    /**
411
     * Find nodes with a CSS selector.
412
     *
413
     * @param string $selector
414
     *
415
     * @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...
416
     */
417 1
    public function findMulti(string $selector): SimpleHtmlDomNodeInterface
418
    {
419 1
        return $this->getHtmlDomParser()->findMulti($selector);
420
    }
421
422
    /**
423
     * Find nodes with a CSS selector or false, if no element is found.
424
     *
425
     * @param string $selector
426
     *
427
     * @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...
428
     */
429 1
    public function findMultiOrFalse(string $selector)
430
    {
431 1
        return $this->getHtmlDomParser()->findMultiOrFalse($selector);
432
    }
433
434
    /**
435
     * Find one node with a CSS selector.
436
     *
437
     * @param string $selector
438
     *
439
     * @return SimpleHtmlDomInterface
440
     */
441 3
    public function findOne(string $selector): SimpleHtmlDomInterface
442
    {
443 3
        return $this->getHtmlDomParser()->findOne($selector);
444
    }
445
446
    /**
447
     * Find one node with a CSS selector or false, if no element is found.
448
     *
449
     * @param string $selector
450
     *
451
     * @return false|SimpleHtmlDomInterface
452
     */
453 1
    public function findOneOrFalse(string $selector)
454
    {
455 1
        return $this->getHtmlDomParser()->findOneOrFalse($selector);
456
    }
457
458
    /**
459
     * Returns the first child of node.
460
     *
461
     * @return SimpleHtmlDomInterface|null
462
     */
463 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...
464
    {
465
        /** @var \DOMNode|null $node */
466 4
        $node = $this->node->firstChild;
467
468 4
        if ($node === null) {
469 1
            return null;
470
        }
471
472 4
        return new static($node);
473
    }
474
475
    /**
476
     * Return elements by ".class".
477
     *
478
     * @param string $class
479
     *
480
     * @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...
481
     */
482
    public function getElementByClass(string $class): SimpleHtmlDomNodeInterface
483
    {
484
        return $this->findMulti(".${class}");
485
    }
486
487
    /**
488
     * Return element by #id.
489
     *
490
     * @param string $id
491
     *
492
     * @return SimpleHtmlDomInterface
493
     */
494 1
    public function getElementById(string $id): SimpleHtmlDomInterface
495
    {
496 1
        return $this->findOne("#${id}");
497
    }
498
499
    /**
500
     * Return element by tag name.
501
     *
502
     * @param string $name
503
     *
504
     * @return SimpleHtmlDomInterface
505
     */
506 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...
507
    {
508 1
        if ($this->node instanceof \DOMElement) {
509 1
            $node = $this->node->getElementsByTagName($name)->item(0);
510
        } else {
511
            $node = null;
512
        }
513
514 1
        if ($node === null) {
515
            return new SimpleHtmlDomBlank();
516
        }
517
518 1
        return new static($node);
519
    }
520
521
    /**
522
     * Returns elements by "#id".
523
     *
524
     * @param string   $id
525
     * @param int|null $idx
526
     *
527
     * @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...
528
     */
529
    public function getElementsById(string $id, $idx = null)
530
    {
531
        return $this->find("#${id}", $idx);
532
    }
533
534
    /**
535
     * Returns elements by tag name.
536
     *
537
     * @param string   $name
538
     * @param int|null $idx
539
     *
540
     * @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...
541
     */
542 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...
543
    {
544 1
        if ($this->node instanceof \DOMElement) {
545 1
            $nodesList = $this->node->getElementsByTagName($name);
546
        } else {
547
            $nodesList = [];
548
        }
549
550 1
        $elements = new SimpleHtmlDomNode();
551
552 1
        foreach ($nodesList as $node) {
553 1
            $elements[] = new static($node);
554
        }
555
556
        // return all elements
557 1
        if ($idx === null) {
558 1
            if (\count($elements) === 0) {
559
                return new SimpleHtmlDomNodeBlank();
560
            }
561
562 1
            return $elements;
563
        }
564
565
        // handle negative values
566
        if ($idx < 0) {
567
            $idx = \count($elements) + $idx;
568
        }
569
570
        // return one element
571
        return $elements[$idx] ?? new SimpleHtmlDomBlank();
572
    }
573
574
    /**
575
     * Create a new "HtmlDomParser"-object from the current context.
576
     *
577
     * @return HtmlDomParser
578
     */
579 105
    public function getHtmlDomParser(): HtmlDomParser
580
    {
581 105
        return new HtmlDomParser($this);
582
    }
583
584
    /**
585
     * @return \DOMNode
586
     */
587 106
    public function getNode(): \DOMNode
588
    {
589 106
        return $this->node;
590
    }
591
592
    /**
593
     * Nodes can get partially destroyed in which they're still an
594
     * actual DOM node (such as \DOMElement) but almost their entire
595
     * body is gone, including the `nodeType` attribute.
596
     *
597
     * @return bool true if node has been destroyed
598
     */
599
    public function isRemoved(): bool
600
    {
601
        return !isset($this->node->nodeType);
602
    }
603
604
    /**
605
     * Returns the last child of node.
606
     *
607
     * @return SimpleHtmlDomInterface|null
608
     */
609 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...
610
    {
611
        /** @var \DOMNode|null $node */
612 4
        $node = $this->node->lastChild;
613
614 4
        if ($node === null) {
615 1
            return null;
616
        }
617
618 4
        return new static($node);
619
    }
620
621
    /**
622
     * Returns the next sibling of node.
623
     *
624
     * @return SimpleHtmlDomInterface|null
625
     */
626 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...
627
    {
628
        /** @var \DOMNode|null $node */
629 1
        $node = $this->node->nextSibling;
630
631 1
        if ($node === null) {
632 1
            return null;
633
        }
634
635 1
        return new static($node);
636
    }
637
638
    /**
639
     * Returns the next sibling of node.
640
     *
641
     * @return SimpleHtmlDomInterface|null
642
     */
643 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...
644
    {
645
        /** @var \DOMNode|null $node */
646 1
        $node = $this->node->nextSibling;
647
648 1
        while ($node && !\trim($node->textContent)) {
649
            /** @var \DOMNode|null $node */
650 1
            $node = $node->nextSibling;
651
        }
652
653 1
        if ($node === null) {
654
            return null;
655
        }
656
657 1
        return new static($node);
658
    }
659
660
    /**
661
     * Returns the parent of node.
662
     *
663
     * @return SimpleHtmlDomInterface
664
     */
665 2
    public function parentNode(): SimpleHtmlDomInterface
666
    {
667 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...
668
    }
669
670
    /**
671
     * Returns the previous sibling of node.
672
     *
673
     * @return SimpleHtmlDomInterface|null
674
     */
675 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...
676
    {
677
        /** @var \DOMNode|null $node */
678 1
        $node = $this->node->previousSibling;
679
680 1
        if ($node === null) {
681 1
            return null;
682
        }
683
684 1
        return new static($node);
685
    }
686
687
688
    /**
689
     * Returns the previous sibling of node.
690
     *
691
     * @return SimpleHtmlDomInterface|null
692
     */
693 View Code Duplication
    public function previousNonWhitespaceSibling()
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...
694
    {
695 1
        /** @var \DOMNode|null $node */
696
        $node = $this->node->previousSibling;
697 1
698
        while ($node && !\trim($node->textContent)) {
699 1
            /** @var \DOMNode|null $node */
700
            $node = $node->previousSibling;
701
        }
702 1
703
        if ($node === null) {
704 1
            return null;
705
        }
706 1
707
        return new static($node);
708
    }
709 1
710
    /**
711
     * @param string|string[]|null $value <p>
712
     *                                    null === get the current input value
713 1
     *                                    text === set a new input value
714
     *                                    </p>
715 1
     *
716
     * @return string|string[]|null
717 1
     */
718 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...
719
    {
720 1
        if ($value === null) {
721
            if (
722
                $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...
723
                &&
724
                (
725
                    $this->getAttribute('type') === 'hidden'
726
                    ||
727
                    $this->getAttribute('type') === 'text'
728
                    ||
729
                    !$this->hasAttribute('type')
730
                )
731
            ) {
732
                return $this->getAttribute('value');
733
            }
734
735
            if (
736
                $this->hasAttribute('checked')
737
                &&
738 1
                \in_array($this->getAttribute('type'), ['checkbox', 'radio'], true)
739 1
            ) {
740
                return $this->getAttribute('value');
741
            }
742
743 1
            if ($this->node->nodeName === 'select') {
744 1
                $valuesFromDom = [];
745
                $options = $this->getElementsByTagName('option');
746 1
                if ($options instanceof SimpleHtmlDomNode) {
747
                    foreach ($options as $option) {
748
                        if ($this->hasAttribute('checked')) {
749 1
                            $valuesFromDom[] = (string) $option->getAttribute('value');
750
                        }
751 1
                    }
752
                }
753
754
                if (\count($valuesFromDom) === 0) {
755
                    return null;
756
                }
757
758
                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...
759
            }
760
761 1
            if ($this->node->nodeName === 'textarea') {
762
                return $this->node->nodeValue;
763
            }
764 1
        } else {
765 1
            /** @noinspection NestedPositiveIfStatementsInspection */
766 1
            if (\in_array($this->getAttribute('type'), ['checkbox', 'radio'], true)) {
767
                if ($value === $this->getAttribute('value')) {
768
                    /** @noinspection UnusedFunctionResultInspection */
769
                    $this->setAttribute('checked', 'checked');
770 1
                } else {
771
                    /** @noinspection UnusedFunctionResultInspection */
772
                    $this->removeAttribute('checked');
773
                }
774
            } elseif ($this->node instanceof \DOMElement && $this->node->nodeName === 'select') {
775
                foreach ($this->node->getElementsByTagName('option') as $option) {
776
                    /** @var \DOMElement $option */
777
                    if ($value === $option->getAttribute('value')) {
778
                        /** @noinspection UnusedFunctionResultInspection */
779 14
                        $option->setAttribute('selected', 'selected');
780
                    } else {
781
                        $option->removeAttribute('selected');
782
                    }
783
                }
784 14
            } elseif ($this->node->nodeName === 'input' && \is_string($value)) {
785
                // Set value for input elements
786 14
                /** @noinspection UnusedFunctionResultInspection */
787
                $this->setAttribute('value', $value);
788
            } elseif ($this->node->nodeName === 'textarea' && \is_string($value)) {
789
                $this->node->nodeValue = $value;
790 14
            }
791
        }
792
793
        return null;
794
    }
795
796 14
    /**
797 14
     * @param HtmlDomParser $newDocument
798
     * @param bool          $removeExtraHeadTag
799 11
     *
800
     * @return HtmlDomParser
801
     */
802 14
    protected function cleanHtmlWrapper(
803
        HtmlDomParser $newDocument,
804 13
        $removeExtraHeadTag = false
805 13
    ): HtmlDomParser {
806 8
        if (
807
            $newDocument->getIsDOMDocumentCreatedWithoutHtml()
808 8
            ||
809 8
            $newDocument->getIsDOMDocumentCreatedWithoutHtmlWrapper()
810 8
        ) {
811
812 8
            // Remove doc-type node.
813
            if ($newDocument->getDocument()->doctype !== null) {
814
                $newDocument->getDocument()->doctype->parentNode->removeChild($newDocument->getDocument()->doctype);
815
            }
816 8
817 8
            // Replace html element, preserving child nodes -> but keep the html wrapper, otherwise we got other problems ...
818
            // so we replace it with "<simpleHtmlDomHtml>" and delete this at the ending.
819
            $item = $newDocument->getDocument()->getElementsByTagName('html')->item(0);
820
            if ($item !== null) {
821
                /** @noinspection UnusedFunctionResultInspection */
822
                $this->changeElementName($item, 'simpleHtmlDomHtml');
823 14
            }
824 14
825 9
            if ($newDocument->getIsDOMDocumentCreatedWithoutPTagWrapper()) {
826
                // Remove <p>-element, preserving child nodes.
827 9
                $pElement = $newDocument->getDocument()->getElementsByTagName('p')->item(0);
828 9 View Code Duplication
                if ($pElement instanceof \DOMElement) {
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...
829 9
                    $fragment = $newDocument->getDocument()->createDocumentFragment();
830
831 9
                    while ($pElement->childNodes->length > 0) {
832
                        $tmpNode = $pElement->childNodes->item(0);
833
                        if ($tmpNode !== null) {
834
                            /** @noinspection UnusedFunctionResultInspection */
835 9
                            $fragment->appendChild($tmpNode);
836 9
                        }
837
                    }
838
839
                    if ($pElement->parentNode !== null) {
840
                        $pElement->parentNode->replaceChild($fragment, $pElement);
841
                    }
842
                }
843 14
            }
844
845 14
            // Remove <body>-element, preserving child nodes.
846
            $body = $newDocument->getDocument()->getElementsByTagName('body')->item(0);
847 14 View Code Duplication
            if ($body instanceof \DOMElement) {
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...
848
                $fragment = $newDocument->getDocument()->createDocumentFragment();
849 3
850
                while ($body->childNodes->length > 0) {
851
                    $tmpNode = $body->childNodes->item(0);
852 3
                    if ($tmpNode !== null) {
853
                        /** @noinspection UnusedFunctionResultInspection */
854 3
                        $fragment->appendChild($tmpNode);
855
                    }
856
                }
857
858
                if ($body->parentNode !== null) {
859
                    $body->parentNode->replaceChild($fragment, $body);
860
                }
861
            }
862
        }
863
864
        // Remove head element, preserving child nodes.
865 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...
866
            $removeExtraHeadTag
867
            &&
868
            $this->node->parentNode instanceof \DOMElement
869
            &&
870
            $newDocument->getIsDOMDocumentCreatedWithoutHeadWrapper()
871 14
        ) {
872
            $html = $this->node->parentNode->getElementsByTagName('head')[0] ?? null;
873
874
            if (
875
                $html !== null
876
                &&
877
                $this->node->parentNode->ownerDocument
878
            ) {
879
                $fragment = $this->node->parentNode->ownerDocument->createDocumentFragment();
880
881
                /** @var \DOMNode $html */
882
                while ($html->childNodes->length > 0) {
883
                    $tmpNode = $html->childNodes->item(0);
884
                    if ($tmpNode !== null) {
885 3
                        /** @noinspection UnusedFunctionResultInspection */
886
                        $fragment->appendChild($tmpNode);
887 3
                    }
888 3
                }
889 3
890 3
                $html->parentNode->replaceChild($fragment, $html);
891
            }
892
        }
893
894 3
        return $newDocument;
895
    }
896
897
    /**
898
     * Retrieve an external iterator.
899
     *
900
     * @see  http://php.net/manual/en/iteratoraggregate.getiterator.php
901
     *
902
     * @return SimpleHtmlDomNode
903
     *                           <p>
904
     *                              An instance of an object implementing <b>Iterator</b> or
905
     *                              <b>Traversable</b>
906
     *                           </p>
907
     */
908 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...
909
    {
910
        $elements = new SimpleHtmlDomNode();
911
        if ($this->node->hasChildNodes()) {
912
            foreach ($this->node->childNodes as $node) {
913
                $elements[] = new static($node);
914
            }
915
        }
916 14
917
        return $elements;
918 14
    }
919 14
920
    /**
921 14
     * Get dom node's inner html.
922
     *
923 14
     * @param bool $multiDecodeNewHtmlEntity
924
     *
925
     * @return string
926 14
     */
927
    public function innerXml(bool $multiDecodeNewHtmlEntity = false): string
928
    {
929
        return $this->getHtmlDomParser()->innerXml($multiDecodeNewHtmlEntity);
930 14
    }
931 14
932 14
    /**
933 14
     * Normalize the given input for comparision.
934
     *
935 14
     * @param HtmlDomParser|string $input
936
     *
937
     * @return string
938
     */
939
    private function normalizeStringForComparision($input): string
940
    {
941 14
        if ($input instanceof HtmlDomParser) {
942
            $string = $input->outerText();
943
944
            if ($input->getIsDOMDocumentCreatedWithoutHeadWrapper()) {
945
                /** @noinspection HtmlRequiredTitleElement */
946 14
                $string = \str_replace(['<head>', '</head>'], '', $string);
947
            }
948
        } else {
949
            $string = (string) $input;
950
        }
951
952
        return
953
            \urlencode(
954
                \urldecode(
955
                    \trim(
956
                        \str_replace(
957
                            [
958
                                ' ',
959
                                "\n",
960
                                "\r",
961
                                '/>',
962
                            ],
963
                            [
964
                                '',
965
                                '',
966
                                '',
967
                                '>',
968
                            ],
969
                            \strtolower($string)
970
                        )
971
                    )
972
                )
973
            );
974
    }
975
}
976