Completed
Push — master ( d7231d...c3f5ad )
by Lars
01:56
created

SimpleHtmlDom::changeElementName()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 25

Duplication

Lines 25
Ratio 100 %

Code Coverage

Tests 10
CRAP Score 4.074

Importance

Changes 0
Metric Value
dl 25
loc 25
ccs 10
cts 12
cp 0.8333
rs 9.52
c 0
b 0
f 0
cc 4
nc 5
nop 2
crap 4.074
1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\helper;
6
7
/** @noinspection PhpHierarchyChecksInspection */
8
class SimpleHtmlDom extends AbstractSimpleHtmlDom implements \IteratorAggregate, SimpleHtmlDomInterface
9
{
10
    /**
11
     * @param \DOMElement|\DOMNode $node
12
     */
13 110
    public function __construct(\DOMNode $node)
14
    {
15 110
        $this->node = $node;
16 110
    }
17
18
    /**
19
     * @param string $name
20
     * @param array  $arguments
21
     *
22
     * @throws \BadMethodCallException
23
     *
24
     * @return SimpleHtmlDomInterface|string|null
25
     */
26 9
    public function __call($name, $arguments)
27
    {
28 9
        $name = \strtolower($name);
29
30 9
        if (isset(self::$functionAliases[$name])) {
31 9
            return \call_user_func_array([$this, self::$functionAliases[$name]], $arguments);
32
        }
33
34
        throw new \BadMethodCallException('Method does not exist');
35
    }
36
37
    /**
38
     * Find list of nodes with a CSS selector.
39
     *
40
     * @param string   $selector
41
     * @param int|null $idx
42
     *
43
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface
44
     */
45 25
    public function find(string $selector, $idx = null)
46
    {
47 25
        return $this->getHtmlDomParser()->find($selector, $idx);
48
    }
49
50
    /**
51
     * Returns an array of attributes.
52
     *
53
     * @return array|null
54
     */
55 2 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...
56
    {
57 2
        if ($this->node->hasAttributes()) {
58 2
            $attributes = [];
59 2
            foreach ($this->node->attributes as $attr) {
60 2
                $attributes[$attr->name] = HtmlDomParser::putReplacedBackToPreserveHtmlEntities($attr->value);
61
            }
62
63 2
            return $attributes;
64
        }
65
66 1
        return null;
67
    }
68
69
    /**
70
     * Return attribute value.
71
     *
72
     * @param string $name
73
     *
74
     * @return string
75
     */
76 14
    public function getAttribute(string $name): string
77
    {
78 14
        if ($this->node instanceof \DOMElement) {
79 14
            return HtmlDomParser::putReplacedBackToPreserveHtmlEntities(
80 14
                $this->node->getAttribute($name)
81
            );
82
        }
83
84
        return '';
85
    }
86
87
    /**
88
     * Determine if an attribute exists on the element.
89
     *
90
     * @param string $name
91
     *
92
     * @return bool
93
     */
94 2
    public function hasAttribute(string $name): bool
95
    {
96 2
        if (!$this->node instanceof \DOMElement) {
97
            return false;
98
        }
99
100 2
        return $this->node->hasAttribute($name);
101
    }
102
103
    /**
104
     * Get dom node's outer html.
105
     *
106
     * @param bool $multiDecodeNewHtmlEntity
107
     *
108
     * @return string
109
     */
110 27
    public function html(bool $multiDecodeNewHtmlEntity = false): string
111
    {
112 27
        return $this->getHtmlDomParser()->html($multiDecodeNewHtmlEntity);
113
    }
114
115
    /**
116
     * Get dom node's inner html.
117
     *
118
     * @param bool $multiDecodeNewHtmlEntity
119
     *
120
     * @return string
121
     */
122 13
    public function innerHtml(bool $multiDecodeNewHtmlEntity = false): string
123
    {
124 13
        return $this->getHtmlDomParser()->innerHtml($multiDecodeNewHtmlEntity);
125
    }
126
127
    /**
128
     * Remove attribute.
129
     *
130
     * @param string $name <p>The name of the html-attribute.</p>
131
     *
132
     * @return SimpleHtmlDomInterface
133
     */
134 2
    public function removeAttribute(string $name): SimpleHtmlDomInterface
135
    {
136 2
        if (\method_exists($this->node, 'removeAttribute')) {
137 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...
138
        }
139
140 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...
141
    }
142
143
    /**
144
     * Replace child node.
145
     *
146
     * @param string $string
147
     *
148
     * @return SimpleHtmlDomInterface
149
     */
150 7 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...
151
    {
152 7
        if (!empty($string)) {
153 6
            $newDocument = new HtmlDomParser($string);
154
155 6
            $tmpDomString = $this->normalizeStringForComparision($newDocument);
156 6
            $tmpStr = $this->normalizeStringForComparision($string);
157 6
            if ($tmpDomString !== $tmpStr) {
158
                throw new \RuntimeException(
159
                    'Not valid HTML fragment!' . "\n" .
160
                    $tmpDomString . "\n" .
161
                    $tmpStr
162
                );
163
            }
164
        }
165
166 7
        if (\count($this->node->childNodes) > 0) {
167 7
            foreach ($this->node->childNodes as $node) {
168 7
                $this->node->removeChild($node);
169
            }
170
        }
171
172 7
        if (!empty($newDocument)) {
173 6
            $newDocument = $this->cleanHtmlWrapper($newDocument);
174 6
            $ownerDocument = $this->node->ownerDocument;
175
            if (
176 6
                $ownerDocument !== null
177
                &&
178 6
                $newDocument->getDocument()->documentElement !== null
179
            ) {
180 6
                $newNode = $ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
181
                /** @noinspection UnusedFunctionResultInspection */
182 6
                $this->node->appendChild($newNode);
183
            }
184
        }
185
186 7
        return $this;
187
    }
188
189
    /**
190
     * Replace this node.
191
     *
192
     * @param string $string
193
     *
194
     * @return SimpleHtmlDomInterface
195
     */
196 4
    protected function replaceNodeWithString(string $string): SimpleHtmlDomInterface
197
    {
198 4
        if (empty($string)) {
199 2
            $this->node->parentNode->removeChild($this->node);
200
201 2
            return $this;
202
        }
203
204 3
        $newDocument = new HtmlDomParser($string);
205
206 3
        $tmpDomOuterTextString = $this->normalizeStringForComparision($newDocument);
207 3
        $tmpStr = $this->normalizeStringForComparision($string);
208 3 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...
209
            throw new \RuntimeException(
210
                'Not valid HTML fragment!' . "\n"
211
                . $tmpDomOuterTextString . "\n" .
212
                $tmpStr
213
            );
214
        }
215
216 3
        $newDocument = $this->cleanHtmlWrapper($newDocument, true);
217 3
        $ownerDocument = $this->node->ownerDocument;
218
        if (
219 3
            $ownerDocument === null
220
            ||
221 3
            $newDocument->getDocument()->documentElement === null
222
        ) {
223
            return $this;
224
        }
225
226 3
        $newNode = $ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
227
228 3
        $this->node->parentNode->replaceChild($newNode, $this->node);
229 3
        $this->node = $newNode;
230
231
        // Remove head element, preserving child nodes. (again)
232 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...
233 3
            $this->node->parentNode instanceof \DOMElement
234
            &&
235 3
            $newDocument->getIsDOMDocumentCreatedWithoutHeadWrapper()
236
        ) {
237 2
            $html = $this->node->parentNode->getElementsByTagName('head')[0];
238 2
            if ($this->node->parentNode->ownerDocument !== null) {
239 2
                $fragment = $this->node->parentNode->ownerDocument->createDocumentFragment();
240 2
                if ($html !== null) {
241
                    /** @var \DOMNode $html */
242 1
                    while ($html->childNodes->length > 0) {
243 1
                        $tmpNode = $html->childNodes->item(0);
244 1
                        if ($tmpNode !== null) {
245
                            /** @noinspection UnusedFunctionResultInspection */
246 1
                            $fragment->appendChild($tmpNode);
247
                        }
248
                    }
249
                    /** @noinspection UnusedFunctionResultInspection */
250 1
                    $html->parentNode->replaceChild($fragment, $html);
251
                }
252
            }
253
        }
254
255 3
        return $this;
256
    }
257
258
    /**
259
     * Replace this node with text
260
     *
261
     * @param string $string
262
     *
263
     * @return SimpleHtmlDomInterface
264
     */
265 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...
266
    {
267 1
        if (empty($string)) {
268 1
            $this->node->parentNode->removeChild($this->node);
269
270 1
            return $this;
271
        }
272
273 1
        $ownerDocument = $this->node->ownerDocument;
274 1
        if ($ownerDocument !== null) {
275 1
            $newElement = $ownerDocument->createTextNode($string);
276 1
            $newNode = $ownerDocument->importNode($newElement, true);
277 1
            $this->node->parentNode->replaceChild($newNode, $this->node);
278 1
            $this->node = $newNode;
279
        }
280
281 1
        return $this;
282
    }
283
284
    /**
285
     * Set attribute value.
286
     *
287
     * @param string      $name       <p>The name of the html-attribute.</p>
288
     * @param string|null $value      <p>Set to NULL or empty string, to remove the attribute.</p>
289
     * @param bool        $strict     </p>
290
     *                                $value must be NULL, to remove the attribute,
291
     *                                so that you can set an empty string as attribute-value e.g. autofocus=""
292
     *                                </p>
293
     *
294
     * @return SimpleHtmlDomInterface
295
     */
296 10 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...
297
    {
298
        if (
299 10
            ($strict && $value === null)
300
            ||
301 10
            (!$strict && empty($value))
302
        ) {
303
            /** @noinspection UnusedFunctionResultInspection */
304 2
            $this->removeAttribute($name);
305 10
        } elseif (\method_exists($this->node, 'setAttribute')) {
306
            /** @noinspection UnusedFunctionResultInspection */
307 10
            $this->node->setAttribute($name, $value);
0 ignored issues
show
Bug introduced by
The method setAttribute does only exist in DOMElement, but not in DOMNode.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
308
        }
309
310 10
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (voku\helper\SimpleHtmlDom) is incompatible with the return type declared by the interface voku\helper\SimpleHtmlDomInterface::setAttribute of type self.

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

Loading history...
311
    }
312
313
    /**
314
     * Get dom node's plain text.
315
     *
316
     * @return string
317
     */
318 17
    public function text(): string
319
    {
320 17
        return $this->getHtmlDomParser()->fixHtmlOutput($this->node->textContent);
321
    }
322
323
    /**
324
     * Change the name of a tag in a "DOMNode".
325
     *
326
     * @param \DOMNode $node
327
     * @param string   $name
328
     *
329
     * @return \DOMElement|false
330
     *                          <p>DOMElement a new instance of class DOMElement or false
331
     *                          if an error occured.</p>
332
     */
333 4 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...
334
    {
335 4
        $ownerDocument = $node->ownerDocument;
336 4
        if ($ownerDocument) {
337 4
            $newNode = $ownerDocument->createElement($name);
338
        } else {
339
            return false;
340
        }
341
342 4
        foreach ($node->childNodes as $child) {
343 4
            $child = $ownerDocument->importNode($child, true);
344
            /** @noinspection UnusedFunctionResultInspection */
345 4
            $newNode->appendChild($child);
346
        }
347
348 4
        foreach ($node->attributes as $attrName => $attrNode) {
349
            /** @noinspection UnusedFunctionResultInspection */
350
            $newNode->setAttribute($attrName, $attrNode);
351
        }
352
353
        /** @noinspection UnusedFunctionResultInspection */
354 4
        $newNode->ownerDocument->replaceChild($newNode, $node);
355
356 4
        return $newNode;
357
    }
358
359
    /**
360
     * Returns children of node.
361
     *
362
     * @param int $idx
363
     *
364
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface|null
365
     */
366 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...
367
    {
368 2
        $nodeList = $this->getIterator();
369
370 2
        if ($idx === -1) {
371 2
            return $nodeList;
372
        }
373
374 2
        return $nodeList[$idx] ?? null;
375
    }
376
377
    /**
378
     * Find nodes with a CSS selector.
379
     *
380
     * @param string $selector
381
     *
382
     * @return SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface
383
     */
384
    public function findMulti(string $selector): SimpleHtmlDomNodeInterface
385
    {
386
        return $this->getHtmlDomParser()->findMulti($selector);
387
    }
388
389
    /**
390
     * Find nodes with a CSS selector or false, if no element is found.
391
     *
392
     * @param string $selector
393
     *
394
     * @return false|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface
395
     */
396
    public function findMultiOrFalse(string $selector)
397
    {
398
        return $this->getHtmlDomParser()->findMultiOrFalse($selector);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->getHtmlDomParser(...ultiOrFalse($selector); of type false|voku\helper\Simple...pleHtmlDomNodeInterface adds the type voku\helper\SimpleHtmlDomInterface to the return on line 398 which is incompatible with the return type declared by the interface voku\helper\SimpleHtmlDo...rface::findMultiOrFalse of type false|voku\helper\Simple...pleHtmlDomNodeInterface.
Loading history...
399
    }
400
401
    /**
402
     * Find one node with a CSS selector.
403
     *
404
     * @param string $selector
405
     *
406
     * @return SimpleHtmlDomInterface
407
     */
408 1
    public function findOne(string $selector): SimpleHtmlDomInterface
409
    {
410 1
        return $this->getHtmlDomParser()->findOne($selector);
411
    }
412
413
    /**
414
     * Find one node with a CSS selector or false, if no element is found.
415
     *
416
     * @param string $selector
417
     *
418
     * @return false|SimpleHtmlDomInterface
419
     */
420
    public function findOneOrFalse(string $selector)
421
    {
422
        return $this->getHtmlDomParser()->findOneOrFalse($selector);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->getHtmlDom...dOneOrFalse($selector); (false|voku\helper\Simple...pleHtmlDomNodeInterface) is incompatible with the return type declared by the interface voku\helper\SimpleHtmlDomInterface::findOneOrFalse of type false|voku\helper\SimpleHtmlDomInterface.

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...
423
    }
424
425
    /**
426
     * Returns the first child of node.
427
     *
428
     * @return SimpleHtmlDomInterface|null
429
     */
430 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...
431
    {
432
        /** @var \DOMNode|null $node */
433 4
        $node = $this->node->firstChild;
434
435 4
        if ($node === null) {
436 1
            return null;
437
        }
438
439 4
        return new static($node);
440
    }
441
442
    /**
443
     * Return elements by .class.
444
     *
445
     * @param string $class
446
     *
447
     * @return SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface
448
     */
449
    public function getElementByClass(string $class): SimpleHtmlDomNodeInterface
450
    {
451
        return $this->findMulti(".${class}");
452
    }
453
454
    /**
455
     * Return element by #id.
456
     *
457
     * @param string $id
458
     *
459
     * @return SimpleHtmlDomInterface
460
     */
461 1
    public function getElementById(string $id): SimpleHtmlDomInterface
462
    {
463 1
        return $this->findOne("#${id}");
464
    }
465
466
    /**
467
     * Return element by tag name.
468
     *
469
     * @param string $name
470
     *
471
     * @return SimpleHtmlDomInterface
472
     */
473 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...
474
    {
475 1
        if ($this->node instanceof \DOMElement) {
476 1
            $node = $this->node->getElementsByTagName($name)->item(0);
477
        } else {
478
            $node = null;
479
        }
480
481 1
        if ($node === null) {
482
            return new SimpleHtmlDomBlank();
483
        }
484
485 1
        return new static($node);
486
    }
487
488
    /**
489
     * Returns elements by #id.
490
     *
491
     * @param string   $id
492
     * @param int|null $idx
493
     *
494
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface
495
     */
496
    public function getElementsById(string $id, $idx = null)
497
    {
498
        return $this->find("#${id}", $idx);
499
    }
500
501
    /**
502
     * Returns elements by tag name.
503
     *
504
     * @param string   $name
505
     * @param int|null $idx
506
     *
507
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface
508
     */
509 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...
510
    {
511 1
        if ($this->node instanceof \DOMElement) {
512 1
            $nodesList = $this->node->getElementsByTagName($name);
513
        } else {
514
            $nodesList = [];
515
        }
516
517 1
        $elements = new SimpleHtmlDomNode();
518
519 1
        foreach ($nodesList as $node) {
520 1
            $elements[] = new static($node);
521
        }
522
523
        // return all elements
524 1
        if ($idx === null) {
525 1
            if (\count($elements) === 0) {
526
                return new SimpleHtmlDomNodeBlank();
527
            }
528
529 1
            return $elements;
530
        }
531
532
        // handle negative values
533
        if ($idx < 0) {
534
            $idx = \count($elements) + $idx;
535
        }
536
537
        // return one element
538
        return $elements[$idx] ?? new SimpleHtmlDomBlank();
539
    }
540
541
    /**
542
     * Create a new "HtmlDomParser"-object from the current context.
543
     *
544
     * @return HtmlDomParser
545
     */
546 73
    public function getHtmlDomParser(): HtmlDomParser
547
    {
548 73
        return new HtmlDomParser($this);
549
    }
550
551
    /**
552
     * @return \DOMNode
553
     */
554 74
    public function getNode(): \DOMNode
555
    {
556 74
        return $this->node;
557
    }
558
559
    /**
560
     * Nodes can get partially destroyed in which they're still an
561
     * actual DOM node (such as \DOMElement) but almost their entire
562
     * body is gone, including the `nodeType` attribute.
563
     *
564
     * @return bool true if node has been destroyed
565
     */
566
    public function isRemoved(): bool
567
    {
568
        return !isset($this->node->nodeType);
569
    }
570
571
    /**
572
     * Returns the last child of node.
573
     *
574
     * @return SimpleHtmlDomInterface|null
575
     */
576 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...
577
    {
578
        /** @var \DOMNode|null $node */
579 4
        $node = $this->node->lastChild;
580
581 4
        if ($node === null) {
582 1
            return null;
583
        }
584
585 4
        return new static($node);
586
    }
587
588
    /**
589
     * Returns the next sibling of node.
590
     *
591
     * @return SimpleHtmlDomInterface|null
592
     */
593 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...
594
    {
595
        /** @var \DOMNode|null $node */
596 1
        $node = $this->node->nextSibling;
597
598 1
        if ($node === null) {
599 1
            return null;
600
        }
601
602 1
        return new static($node);
603
    }
604
605
    /**
606
     * Returns the parent of node.
607
     *
608
     * @return SimpleHtmlDomInterface
609
     */
610 1
    public function parentNode(): SimpleHtmlDomInterface
611
    {
612 1
        return new static($this->node->parentNode);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new static($this->node->parentNode); (voku\helper\SimpleHtmlDom) is incompatible with the return type declared by the interface voku\helper\SimpleHtmlDomInterface::parentNode of type self.

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

Loading history...
613
    }
614
615
    /**
616
     * Returns the previous sibling of node.
617
     *
618
     * @return SimpleHtmlDomInterface|null
619
     */
620 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...
621
    {
622
        /** @var \DOMNode|null $node */
623 1
        $node = $this->node->previousSibling;
624
625 1
        if ($node === null) {
626 1
            return null;
627
        }
628
629 1
        return new static($node);
630
    }
631
632
    /**
633
     * @param string|string[]|null $value <p>
634
     *                                    null === get the current input value
635
     *                                    text === set a new input value
636
     *                                    </p>
637
     *
638
     * @return string|string[]|null
639
     */
640 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...
641
    {
642 1
        if ($value === null) {
643
            if (
644 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...
645
                &&
646
                (
647 1
                    $this->getAttribute('type') === 'text'
648
                    ||
649 1
                    !$this->hasAttribute('type')
650
                )
651
            ) {
652 1
                return $this->getAttribute('value');
653
            }
654
655
            if (
656 1
                $this->hasAttribute('checked')
657
                &&
658 1
                \in_array($this->getAttribute('type'), ['checkbox', 'radio'], true)
659
            ) {
660 1
                return $this->getAttribute('value');
661
            }
662
663 1
            if ($this->node->nodeName === 'select') {
664
                $valuesFromDom = [];
665
                $options = $this->getElementsByTagName('option');
666
                if ($options instanceof SimpleHtmlDomNode) {
667
                    foreach ($options as $option) {
668
                        if ($this->hasAttribute('checked')) {
669
                            /** @noinspection UnnecessaryCastingInspection */
670
                            $valuesFromDom[] = (string) $option->getAttribute('value');
671
                        }
672
                    }
673
                }
674
675
                if (\count($valuesFromDom) === 0) {
676
                    return null;
677
                }
678
679
                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...
680
            }
681
682 1
            if ($this->node->nodeName === 'textarea') {
683 1
                return $this->node->nodeValue;
684
            }
685
        } else {
686
            /** @noinspection NestedPositiveIfStatementsInspection */
687 1
            if (\in_array($this->getAttribute('type'), ['checkbox', 'radio'], true)) {
688 1
                if ($value === $this->getAttribute('value')) {
689
                    /** @noinspection UnusedFunctionResultInspection */
690 1
                    $this->setAttribute('checked', 'checked');
691
                } else {
692
                    /** @noinspection UnusedFunctionResultInspection */
693 1
                    $this->removeAttribute('checked');
694
                }
695 1
            } elseif ($this->node instanceof \DOMElement && $this->node->nodeName === 'select') {
696
                foreach ($this->node->getElementsByTagName('option') as $option) {
697
                    /** @var \DOMElement $option */
698
                    if ($value === $option->getAttribute('value')) {
699
                        /** @noinspection UnusedFunctionResultInspection */
700
                        $option->setAttribute('selected', 'selected');
701
                    } else {
702
                        $option->removeAttribute('selected');
703
                    }
704
                }
705 1
            } elseif ($this->node->nodeName === 'input' && \is_string($value)) {
706
                // Set value for input elements
707
                /** @noinspection UnusedFunctionResultInspection */
708 1
                $this->setAttribute('value', $value);
709 1
            } elseif ($this->node->nodeName === 'textarea' && \is_string($value)) {
710 1
                $this->node->nodeValue = $value;
711
            }
712
        }
713
714 1
        return null;
715
    }
716
717
    /**
718
     * @param HtmlDomParser $newDocument
719
     * @param bool          $removeExtraHeadTag
720
     *
721
     * @return HtmlDomParser
722
     */
723 9
    protected function cleanHtmlWrapper(HtmlDomParser $newDocument, $removeExtraHeadTag = false): HtmlDomParser
724
    {
725
        if (
726 9
            $newDocument->getIsDOMDocumentCreatedWithoutHtml()
727
            ||
728 9
            $newDocument->getIsDOMDocumentCreatedWithoutHtmlWrapper()
729
        ) {
730
731
            // Remove doc-type node.
732 9
            if ($newDocument->getDocument()->doctype !== null) {
733
                /** @noinspection UnusedFunctionResultInspection */
734
                $newDocument->getDocument()->doctype->parentNode->removeChild($newDocument->getDocument()->doctype);
735
            }
736
737
            // Remove html element, preserving child nodes.
738 9
            $html = $newDocument->getDocument()->getElementsByTagName('html')->item(0);
739 9
            $fragment = $newDocument->getDocument()->createDocumentFragment();
740 9
            if ($html !== null) {
741 6
                while ($html->childNodes->length > 0) {
742 6
                    $tmpNode = $html->childNodes->item(0);
743 6
                    if ($tmpNode !== null) {
744
                        /** @noinspection UnusedFunctionResultInspection */
745 6
                        $fragment->appendChild($tmpNode);
746
                    }
747
                }
748
                /** @noinspection UnusedFunctionResultInspection */
749 6
                $html->parentNode->replaceChild($fragment, $html);
750
            }
751
752
            // Remove body element, preserving child nodes.
753 9
            $body = $newDocument->getDocument()->getElementsByTagName('body')->item(0);
754 9
            $fragment = $newDocument->getDocument()->createDocumentFragment();
755 9
            if ($body instanceof \DOMElement) {
756 4
                while ($body->childNodes->length > 0) {
757 4
                    $tmpNode = $body->childNodes->item(0);
758 4
                    if ($tmpNode !== null) {
759
                        /** @noinspection UnusedFunctionResultInspection */
760 4
                        $fragment->appendChild($tmpNode);
761
                    }
762
                }
763
                /** @noinspection UnusedFunctionResultInspection */
764 4
                $body->parentNode->replaceChild($fragment, $body);
765
766
                // At this point DOMDocument still added a "<p>"-wrapper around our string,
767
                // so we replace it with "<simpleHtmlDomP>" and delete this at the ending ...
768 4
                $item = $newDocument->getDocument()->getElementsByTagName('p')->item(0);
769 4
                if ($item !== null) {
770
                    /** @noinspection UnusedFunctionResultInspection */
771 4
                    $this->changeElementName($item, 'simpleHtmlDomP');
772
                }
773
            }
774
        }
775
776
        // Remove head element, preserving child nodes.
777 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...
778 9
            $removeExtraHeadTag
779
            &&
780 9
            $this->node->parentNode instanceof \DOMElement
781
            &&
782 9
            $newDocument->getIsDOMDocumentCreatedWithoutHeadWrapper()
783
        ) {
784 2
            $html = $this->node->parentNode->getElementsByTagName('head')[0];
785 2
            if ($this->node->parentNode->ownerDocument !== null) {
786 2
                $fragment = $this->node->parentNode->ownerDocument->createDocumentFragment();
787 2
                if ($html !== null) {
788
                    /** @var \DOMNode $html */
789
                    while ($html->childNodes->length > 0) {
790
                        $tmpNode = $html->childNodes->item(0);
791
                        if ($tmpNode !== null) {
792
                            /** @noinspection UnusedFunctionResultInspection */
793
                            $fragment->appendChild($tmpNode);
794
                        }
795
                    }
796
                    /** @noinspection UnusedFunctionResultInspection */
797
                    $html->parentNode->replaceChild($fragment, $html);
798
                }
799
            }
800
        }
801
802 9
        return $newDocument;
803
    }
804
805
    /**
806
     * Retrieve an external iterator.
807
     *
808
     * @see  http://php.net/manual/en/iteratoraggregate.getiterator.php
809
     *
810
     * @return SimpleHtmlDomNode
811
     *                           <p>
812
     *                              An instance of an object implementing <b>Iterator</b> or
813
     *                              <b>Traversable</b>
814
     *                           </p>
815
     */
816 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...
817
    {
818 3
        $elements = new SimpleHtmlDomNode();
819 3
        if ($this->node->hasChildNodes()) {
820 3
            foreach ($this->node->childNodes as $node) {
821 3
                $elements[] = new static($node);
822
            }
823
        }
824
825 3
        return $elements;
826
    }
827
828
    /**
829
     * Get dom node's inner html.
830
     *
831
     * @param bool $multiDecodeNewHtmlEntity
832
     *
833
     * @return string
834
     */
835
    public function innerXml(bool $multiDecodeNewHtmlEntity = false): string
836
    {
837
        return $this->getHtmlDomParser()->innerXml($multiDecodeNewHtmlEntity);
838
    }
839
840
    /**
841
     * Normalize the given input for comparision.
842
     *
843
     * @param HtmlDomParser|string $input
844
     *
845
     * @return string
846
     */
847 9
    private function normalizeStringForComparision($input): string
848
    {
849 9
        if ($input instanceof HtmlDomParser) {
850 9
            $string = $input->outerText();
851
852 9
            if ($input->getIsDOMDocumentCreatedWithoutHeadWrapper()) {
853
                /** @noinspection HtmlRequiredTitleElement */
854 9
                $string = \str_replace(['<head>', '</head>'], '', $string);
855
            }
856
        } else {
857 9
            $string = (string) $input;
858
        }
859
860
        return
861 9
            \urlencode(
862 9
                \urldecode(
863 9
                    \trim(
864 9
                        \str_replace(
865
                            [
866 9
                                ' ',
867
                                "\n",
868
                                "\r",
869
                                '/>',
870
                            ],
871
                            [
872 9
                                '',
873
                                '',
874
                                '',
875
                                '>',
876
                            ],
877 9
                            \strtolower($string)
878
                        )
879
                    )
880
                )
881
            );
882
    }
883
}
884