Completed
Push — master ( 0ce976...37c50a )
by Lars
01:41
created

SimpleHtmlDom::cleanHtmlWrapper()   F

Complexity

Conditions 21
Paths 147

Size

Total Lines 98

Duplication

Lines 61
Ratio 62.24 %

Code Coverage

Tests 33
CRAP Score 23.3634

Importance

Changes 0
Metric Value
cc 21
nc 147
nop 2
dl 61
loc 98
ccs 33
cts 40
cp 0.825
crap 23.3634
rs 3.775
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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