Completed
Push — master ( 236b50...7a7214 )
by Lars
02:07
created

SimpleHtmlDom::hasAttributes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\helper;
6
7
/**
8
 * @noinspection PhpHierarchyChecksInspection
9
 *
10
 * {@inheritDoc}
11
 */
12
class SimpleHtmlDom extends AbstractSimpleHtmlDom implements \IteratorAggregate, SimpleHtmlDomInterface
13
{
14
    /**
15
     * @param \DOMElement|\DOMNode $node
16
     */
17 135
    public function __construct(\DOMNode $node)
18
    {
19 135
        $this->node = $node;
20 135
    }
21
22
    /**
23
     * @param string $name
24
     * @param array  $arguments
25
     *
26
     * @throws \BadMethodCallException
27
     *
28
     * @return SimpleHtmlDomInterface|string|null
29
     */
30 10
    public function __call($name, $arguments)
31
    {
32 10
        $name = \strtolower($name);
33
34 10
        if (isset(self::$functionAliases[$name])) {
35 10
            return \call_user_func_array([$this, self::$functionAliases[$name]], $arguments);
36
        }
37
38
        throw new \BadMethodCallException('Method does not exist');
39
    }
40
41
    /**
42
     * Find list of nodes with a CSS selector.
43
     *
44
     * @param string   $selector
45
     * @param int|null $idx
46
     *
47
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface
48
     */
49 26
    public function find(string $selector, $idx = null)
50
    {
51 26
        return $this->getHtmlDomParser()->find($selector, $idx);
52
    }
53
54
    /**
55
     * Returns an array of attributes.
56
     *
57
     * @return array|null
58
     */
59 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...
60
    {
61 3
        if ($this->node->hasAttributes()) {
62 3
            $attributes = [];
63 3
            foreach ($this->node->attributes as $attr) {
64 3
                $attributes[$attr->name] = HtmlDomParser::putReplacedBackToPreserveHtmlEntities($attr->value);
65
            }
66
67 3
            return $attributes;
68
        }
69
70 1
        return null;
71
    }
72
73
    /**
74
     * @return bool
75
     */
76
    public function hasAttributes(): bool {
77
        return $this->node->hasAttributes();
78
    }
79
80
    /**
81
     * Return attribute value.
82
     *
83
     * @param string $name
84
     *
85
     * @return string
86
     */
87 24
    public function getAttribute(string $name): string
88
    {
89 24
        if ($this->node instanceof \DOMElement) {
90 24
            return HtmlDomParser::putReplacedBackToPreserveHtmlEntities(
91 24
                $this->node->getAttribute($name)
92
            );
93
        }
94
95
        return '';
96
    }
97
98
    /**
99
     * Determine if an attribute exists on the element.
100
     *
101
     * @param string $name
102
     *
103
     * @return bool
104
     */
105 2
    public function hasAttribute(string $name): bool
106
    {
107 2
        if (!$this->node instanceof \DOMElement) {
108
            return false;
109
        }
110
111 2
        return $this->node->hasAttribute($name);
112
    }
113
114
    /**
115
     * Get dom node's outer html.
116
     *
117
     * @param bool $multiDecodeNewHtmlEntity
118
     *
119
     * @return string
120
     */
121 30
    public function html(bool $multiDecodeNewHtmlEntity = false): string
122
    {
123 30
        return $this->getHtmlDomParser()->html($multiDecodeNewHtmlEntity);
124
    }
125
126
    /**
127
     * Get dom node's inner html.
128
     *
129
     * @param bool $multiDecodeNewHtmlEntity
130
     *
131
     * @return string
132
     */
133 19
    public function innerHtml(bool $multiDecodeNewHtmlEntity = false): string
134
    {
135 19
        return $this->getHtmlDomParser()->innerHtml($multiDecodeNewHtmlEntity);
136
    }
137
138
    /**
139
     * Remove attribute.
140
     *
141
     * @param string $name <p>The name of the html-attribute.</p>
142
     *
143
     * @return SimpleHtmlDomInterface
144
     */
145 2
    public function removeAttribute(string $name): SimpleHtmlDomInterface
146
    {
147 2
        if (\method_exists($this->node, 'removeAttribute')) {
148 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...
149
        }
150
151 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...
152
    }
153
154
    /**
155
     * Replace child node.
156
     *
157
     * @param string $string
158
     *
159
     * @return SimpleHtmlDomInterface
160
     */
161 8 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...
162
    {
163 8
        if (!empty($string)) {
164 7
            $newDocument = new HtmlDomParser($string);
165
166 7
            $tmpDomString = $this->normalizeStringForComparision($newDocument);
167 7
            $tmpStr = $this->normalizeStringForComparision($string);
168 7
            if ($tmpDomString !== $tmpStr) {
169
                throw new \RuntimeException(
170
                    'Not valid HTML fragment!' . "\n" .
171
                    $tmpDomString . "\n" .
172
                    $tmpStr
173
                );
174
            }
175
        }
176
177 8
        if (\count($this->node->childNodes) > 0) {
178 8
            foreach ($this->node->childNodes as $node) {
179 8
                $this->node->removeChild($node);
180
            }
181
        }
182
183 8
        if (!empty($newDocument)) {
184 7
            $newDocument = $this->cleanHtmlWrapper($newDocument);
185 7
            $ownerDocument = $this->node->ownerDocument;
186
            if (
187 7
                $ownerDocument !== null
188
                &&
189 7
                $newDocument->getDocument()->documentElement !== null
190
            ) {
191 7
                $newNode = $ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
192
                /** @noinspection UnusedFunctionResultInspection */
193 7
                $this->node->appendChild($newNode);
194
            }
195
        }
196
197 8
        return $this;
198
    }
199
200
    /**
201
     * Replace this node.
202
     *
203
     * @param string $string
204
     *
205
     * @return SimpleHtmlDomInterface
206
     */
207 5
    protected function replaceNodeWithString(string $string): SimpleHtmlDomInterface
208
    {
209 5
        if (empty($string)) {
210 2
            $this->node->parentNode->removeChild($this->node);
211
212 2
            return $this;
213
        }
214
215 4
        $newDocument = new HtmlDomParser($string);
216
217 4
        $tmpDomOuterTextString = $this->normalizeStringForComparision($newDocument);
218 4
        $tmpStr = $this->normalizeStringForComparision($string);
219 4 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...
220
            throw new \RuntimeException(
221
                'Not valid HTML fragment!' . "\n"
222
                . $tmpDomOuterTextString . "\n" .
223
                $tmpStr
224
            );
225
        }
226
227 4
        $newDocument = $this->cleanHtmlWrapper($newDocument, true);
228 4
        $ownerDocument = $this->node->ownerDocument;
229
        if (
230 4
            $ownerDocument === null
231
            ||
232 4
            $newDocument->getDocument()->documentElement === null
233
        ) {
234
            return $this;
235
        }
236
237 4
        $newNode = $ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
238
239 4
        $this->node->parentNode->replaceChild($newNode, $this->node);
240 4
        $this->node = $newNode;
241
242
        // Remove head element, preserving child nodes. (again)
243 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...
244 4
            $this->node->parentNode instanceof \DOMElement
245
            &&
246 4
            $newDocument->getIsDOMDocumentCreatedWithoutHeadWrapper()
247
        ) {
248 2
            $html = $this->node->parentNode->getElementsByTagName('head')[0];
249 2
            if ($this->node->parentNode->ownerDocument !== null) {
250 2
                $fragment = $this->node->parentNode->ownerDocument->createDocumentFragment();
251 2
                if ($html !== null) {
252
                    /** @var \DOMNode $html */
253 1
                    while ($html->childNodes->length > 0) {
254 1
                        $tmpNode = $html->childNodes->item(0);
255 1
                        if ($tmpNode !== null) {
256
                            /** @noinspection UnusedFunctionResultInspection */
257 1
                            $fragment->appendChild($tmpNode);
258
                        }
259
                    }
260
                    /** @noinspection UnusedFunctionResultInspection */
261 1
                    $html->parentNode->replaceChild($fragment, $html);
262
                }
263
            }
264
        }
265
266 4
        return $this;
267
    }
268
269
    /**
270
     * Replace this node with text
271
     *
272
     * @param string $string
273
     *
274
     * @return SimpleHtmlDomInterface
275
     */
276 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...
277
    {
278 1
        if (empty($string)) {
279 1
            $this->node->parentNode->removeChild($this->node);
280
281 1
            return $this;
282
        }
283
284 1
        $ownerDocument = $this->node->ownerDocument;
285 1
        if ($ownerDocument !== null) {
286 1
            $newElement = $ownerDocument->createTextNode($string);
287 1
            $newNode = $ownerDocument->importNode($newElement, true);
288 1
            $this->node->parentNode->replaceChild($newNode, $this->node);
289 1
            $this->node = $newNode;
290
        }
291
292 1
        return $this;
293
    }
294
295
    /**
296
     * Set attribute value.
297
     *
298
     * @param string      $name       <p>The name of the html-attribute.</p>
299
     * @param string|null $value      <p>Set to NULL or empty string, to remove the attribute.</p>
300
     * @param bool        $strict     </p>
301
     *                                $value must be NULL, to remove the attribute,
302
     *                                so that you can set an empty string as attribute-value e.g. autofocus=""
303
     *                                </p>
304
     *
305
     * @return SimpleHtmlDomInterface
306
     */
307 14 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...
308
    {
309
        if (
310 14
            ($strict && $value === null)
311
            ||
312 14
            (!$strict && empty($value))
313
        ) {
314
            /** @noinspection UnusedFunctionResultInspection */
315 2
            $this->removeAttribute($name);
316 14
        } elseif (\method_exists($this->node, 'setAttribute')) {
317
            /** @noinspection UnusedFunctionResultInspection */
318 14
            $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...
319
        }
320
321 14
        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...
322
    }
323
324
    /**
325
     * Get dom node's plain text.
326
     *
327
     * @return string
328
     */
329 18
    public function text(): string
330
    {
331 18
        return $this->getHtmlDomParser()->fixHtmlOutput($this->node->textContent);
332
    }
333
334
    /**
335
     * Change the name of a tag in a "DOMNode".
336
     *
337
     * @param \DOMNode $node
338
     * @param string   $name
339
     *
340
     * @return \DOMElement|false
341
     *                          <p>DOMElement a new instance of class DOMElement or false
342
     *                          if an error occured.</p>
343
     */
344 6 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...
345
    {
346 6
        $ownerDocument = $node->ownerDocument;
347 6
        if ($ownerDocument) {
348 6
            $newNode = $ownerDocument->createElement($name);
349
        } else {
350
            return false;
351
        }
352
353 6
        foreach ($node->childNodes as $child) {
354 6
            $child = $ownerDocument->importNode($child, true);
355
            /** @noinspection UnusedFunctionResultInspection */
356 6
            $newNode->appendChild($child);
357
        }
358
359 6
        foreach ($node->attributes as $attrName => $attrNode) {
360
            /** @noinspection UnusedFunctionResultInspection */
361
            $newNode->setAttribute($attrName, $attrNode);
362
        }
363
364
        /** @noinspection UnusedFunctionResultInspection */
365 6
        $newNode->ownerDocument->replaceChild($newNode, $node);
366
367 6
        return $newNode;
368
    }
369
370
    /**
371
     * Returns children of node.
372
     *
373
     * @param int $idx
374
     *
375
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface|null
376
     */
377 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...
378
    {
379 2
        $nodeList = $this->getIterator();
380
381 2
        if ($idx === -1) {
382 2
            return $nodeList;
383
        }
384
385 2
        return $nodeList[$idx] ?? null;
386
    }
387
388
    /**
389
     * Find nodes with a CSS selector.
390
     *
391
     * @param string $selector
392
     *
393
     * @return SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface
394
     */
395 1
    public function findMulti(string $selector): SimpleHtmlDomNodeInterface
396
    {
397 1
        return $this->getHtmlDomParser()->findMulti($selector);
398
    }
399
400
    /**
401
     * Find nodes with a CSS selector or false, if no element is found.
402
     *
403
     * @param string $selector
404
     *
405
     * @return false|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface
406
     */
407 1
    public function findMultiOrFalse(string $selector)
408
    {
409 1
        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 409 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...
410
    }
411
412
    /**
413
     * Find one node with a CSS selector.
414
     *
415
     * @param string $selector
416
     *
417
     * @return SimpleHtmlDomInterface
418
     */
419 2
    public function findOne(string $selector): SimpleHtmlDomInterface
420
    {
421 2
        return $this->getHtmlDomParser()->findOne($selector);
422
    }
423
424
    /**
425
     * Find one node with a CSS selector or false, if no element is found.
426
     *
427
     * @param string $selector
428
     *
429
     * @return false|SimpleHtmlDomInterface
430
     */
431 1
    public function findOneOrFalse(string $selector)
432
    {
433 1
        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...
434
    }
435
436
    /**
437
     * Returns the first child of node.
438
     *
439
     * @return SimpleHtmlDomInterface|null
440
     */
441 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...
442
    {
443
        /** @var \DOMNode|null $node */
444 4
        $node = $this->node->firstChild;
445
446 4
        if ($node === null) {
447 1
            return null;
448
        }
449
450 4
        return new static($node);
451
    }
452
453
    /**
454
     * Return elements by ".class".
455
     *
456
     * @param string $class
457
     *
458
     * @return SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface
459
     */
460
    public function getElementByClass(string $class): SimpleHtmlDomNodeInterface
461
    {
462
        return $this->findMulti(".${class}");
463
    }
464
465
    /**
466
     * Return element by #id.
467
     *
468
     * @param string $id
469
     *
470
     * @return SimpleHtmlDomInterface
471
     */
472 1
    public function getElementById(string $id): SimpleHtmlDomInterface
473
    {
474 1
        return $this->findOne("#${id}");
475
    }
476
477
    /**
478
     * Return element by tag name.
479
     *
480
     * @param string $name
481
     *
482
     * @return SimpleHtmlDomInterface
483
     */
484 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...
485
    {
486 1
        if ($this->node instanceof \DOMElement) {
487 1
            $node = $this->node->getElementsByTagName($name)->item(0);
488
        } else {
489
            $node = null;
490
        }
491
492 1
        if ($node === null) {
493
            return new SimpleHtmlDomBlank();
494
        }
495
496 1
        return new static($node);
497
    }
498
499
    /**
500
     * Returns elements by "#id".
501
     *
502
     * @param string   $id
503
     * @param int|null $idx
504
     *
505
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface
506
     */
507
    public function getElementsById(string $id, $idx = null)
508
    {
509
        return $this->find("#${id}", $idx);
510
    }
511
512
    /**
513
     * Returns elements by tag name.
514
     *
515
     * @param string   $name
516
     * @param int|null $idx
517
     *
518
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface
519
     */
520 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...
521
    {
522 1
        if ($this->node instanceof \DOMElement) {
523 1
            $nodesList = $this->node->getElementsByTagName($name);
524
        } else {
525
            $nodesList = [];
526
        }
527
528 1
        $elements = new SimpleHtmlDomNode();
529
530 1
        foreach ($nodesList as $node) {
531 1
            $elements[] = new static($node);
532
        }
533
534
        // return all elements
535 1
        if ($idx === null) {
536 1
            if (\count($elements) === 0) {
537
                return new SimpleHtmlDomNodeBlank();
538
            }
539
540 1
            return $elements;
541
        }
542
543
        // handle negative values
544
        if ($idx < 0) {
545
            $idx = \count($elements) + $idx;
546
        }
547
548
        // return one element
549
        return $elements[$idx] ?? new SimpleHtmlDomBlank();
550
    }
551
552
    /**
553
     * Create a new "HtmlDomParser"-object from the current context.
554
     *
555
     * @return HtmlDomParser
556
     */
557 82
    public function getHtmlDomParser(): HtmlDomParser
558
    {
559 82
        return new HtmlDomParser($this);
560
    }
561
562
    /**
563
     * @return \DOMNode
564
     */
565 83
    public function getNode(): \DOMNode
566
    {
567 83
        return $this->node;
568
    }
569
570
    /**
571
     * Nodes can get partially destroyed in which they're still an
572
     * actual DOM node (such as \DOMElement) but almost their entire
573
     * body is gone, including the `nodeType` attribute.
574
     *
575
     * @return bool true if node has been destroyed
576
     */
577
    public function isRemoved(): bool
578
    {
579
        return !isset($this->node->nodeType);
580
    }
581
582
    /**
583
     * Returns the last child of node.
584
     *
585
     * @return SimpleHtmlDomInterface|null
586
     */
587 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...
588
    {
589
        /** @var \DOMNode|null $node */
590 4
        $node = $this->node->lastChild;
591
592 4
        if ($node === null) {
593 1
            return null;
594
        }
595
596 4
        return new static($node);
597
    }
598
599
    /**
600
     * Returns the next sibling of node.
601
     *
602
     * @return SimpleHtmlDomInterface|null
603
     */
604 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...
605
    {
606
        /** @var \DOMNode|null $node */
607 1
        $node = $this->node->nextSibling;
608
609 1
        if ($node === null) {
610 1
            return null;
611
        }
612
613 1
        return new static($node);
614
    }
615
616
    /**
617
     * Returns the parent of node.
618
     *
619
     * @return SimpleHtmlDomInterface
620
     */
621 2
    public function parentNode(): SimpleHtmlDomInterface
622
    {
623 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...
624
    }
625
626
    /**
627
     * Returns the previous sibling of node.
628
     *
629
     * @return SimpleHtmlDomInterface|null
630
     */
631 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...
632
    {
633
        /** @var \DOMNode|null $node */
634 1
        $node = $this->node->previousSibling;
635
636 1
        if ($node === null) {
637 1
            return null;
638
        }
639
640 1
        return new static($node);
641
    }
642
643
    /**
644
     * @param string|string[]|null $value <p>
645
     *                                    null === get the current input value
646
     *                                    text === set a new input value
647
     *                                    </p>
648
     *
649
     * @return string|string[]|null
650
     */
651 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...
652
    {
653 1
        if ($value === null) {
654
            if (
655 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...
656
                &&
657
                (
658 1
                    $this->getAttribute('type') === 'text'
659
                    ||
660 1
                    !$this->hasAttribute('type')
661
                )
662
            ) {
663 1
                return $this->getAttribute('value');
664
            }
665
666
            if (
667 1
                $this->hasAttribute('checked')
668
                &&
669 1
                \in_array($this->getAttribute('type'), ['checkbox', 'radio'], true)
670
            ) {
671 1
                return $this->getAttribute('value');
672
            }
673
674 1
            if ($this->node->nodeName === 'select') {
675
                $valuesFromDom = [];
676
                $options = $this->getElementsByTagName('option');
677
                if ($options instanceof SimpleHtmlDomNode) {
678
                    foreach ($options as $option) {
679
                        if ($this->hasAttribute('checked')) {
680
                            /** @noinspection UnnecessaryCastingInspection */
681
                            $valuesFromDom[] = (string) $option->getAttribute('value');
682
                        }
683
                    }
684
                }
685
686
                if (\count($valuesFromDom) === 0) {
687
                    return null;
688
                }
689
690
                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...
691
            }
692
693 1
            if ($this->node->nodeName === 'textarea') {
694 1
                return $this->node->nodeValue;
695
            }
696
        } else {
697
            /** @noinspection NestedPositiveIfStatementsInspection */
698 1
            if (\in_array($this->getAttribute('type'), ['checkbox', 'radio'], true)) {
699 1
                if ($value === $this->getAttribute('value')) {
700
                    /** @noinspection UnusedFunctionResultInspection */
701 1
                    $this->setAttribute('checked', 'checked');
702
                } else {
703
                    /** @noinspection UnusedFunctionResultInspection */
704 1
                    $this->removeAttribute('checked');
705
                }
706 1
            } elseif ($this->node instanceof \DOMElement && $this->node->nodeName === 'select') {
707
                foreach ($this->node->getElementsByTagName('option') as $option) {
708
                    /** @var \DOMElement $option */
709
                    if ($value === $option->getAttribute('value')) {
710
                        /** @noinspection UnusedFunctionResultInspection */
711
                        $option->setAttribute('selected', 'selected');
712
                    } else {
713
                        $option->removeAttribute('selected');
714
                    }
715
                }
716 1
            } elseif ($this->node->nodeName === 'input' && \is_string($value)) {
717
                // Set value for input elements
718
                /** @noinspection UnusedFunctionResultInspection */
719 1
                $this->setAttribute('value', $value);
720 1
            } elseif ($this->node->nodeName === 'textarea' && \is_string($value)) {
721 1
                $this->node->nodeValue = $value;
722
            }
723
        }
724
725 1
        return null;
726
    }
727
728
    /**
729
     * @param HtmlDomParser $newDocument
730
     * @param bool          $removeExtraHeadTag
731
     *
732
     * @return HtmlDomParser
733
     */
734 11
    protected function cleanHtmlWrapper(
735
        HtmlDomParser $newDocument,
736
        $removeExtraHeadTag = false
737
    ): HtmlDomParser
738
    {
739
        if (
740 11
            $newDocument->getIsDOMDocumentCreatedWithoutHtml()
741
            ||
742 11
            $newDocument->getIsDOMDocumentCreatedWithoutHtmlWrapper()
743
        ) {
744
745
            // Remove doc-type node.
746 11
            if ($newDocument->getDocument()->doctype !== null) {
747
                /** @noinspection UnusedFunctionResultInspection */
748
                $newDocument->getDocument()->doctype->parentNode->removeChild($newDocument->getDocument()->doctype);
749
            }
750
751
            // Remove html element, preserving child nodes.
752 11
            $html = $newDocument->getDocument()->getElementsByTagName('html')->item(0);
753 11
            $fragment = $newDocument->getDocument()->createDocumentFragment();
754 11
            if ($html !== null) {
755 8
                while ($html->childNodes->length > 0) {
756 8
                    $tmpNode = $html->childNodes->item(0);
757 8
                    if ($tmpNode !== null) {
758
                        /** @noinspection UnusedFunctionResultInspection */
759 8
                        $fragment->appendChild($tmpNode);
760
                    }
761
                }
762
                /** @noinspection UnusedFunctionResultInspection */
763 8
                $html->parentNode->replaceChild($fragment, $html);
764
            }
765
766
            // Remove body element, preserving child nodes.
767 11
            $body = $newDocument->getDocument()->getElementsByTagName('body')->item(0);
768 11
            $fragment = $newDocument->getDocument()->createDocumentFragment();
769 11
            if ($body instanceof \DOMElement) {
770 6
                while ($body->childNodes->length > 0) {
771 6
                    $tmpNode = $body->childNodes->item(0);
772 6
                    if ($tmpNode !== null) {
773
                        /** @noinspection UnusedFunctionResultInspection */
774 6
                        $fragment->appendChild($tmpNode);
775
                    }
776
                }
777
                /** @noinspection UnusedFunctionResultInspection */
778 6
                $body->parentNode->replaceChild($fragment, $body);
779
780
                // At this point DOMDocument still added a "<p>"-wrapper around our string,
781
                // so we replace it with "<simpleHtmlDomP>" and delete this at the ending ...
782 6
                $item = $newDocument->getDocument()->getElementsByTagName('p')->item(0);
783 6
                if ($item !== null) {
784
                    /** @noinspection UnusedFunctionResultInspection */
785 6
                    $this->changeElementName($item, 'simpleHtmlDomP');
786
                }
787
            }
788
        }
789
790
        // Remove head element, preserving child nodes.
791 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...
792 11
            $removeExtraHeadTag
793
            &&
794 11
            $this->node->parentNode instanceof \DOMElement
795
            &&
796 11
            $newDocument->getIsDOMDocumentCreatedWithoutHeadWrapper()
797
        ) {
798 2
            $html = $this->node->parentNode->getElementsByTagName('head')[0];
799 2
            if ($this->node->parentNode->ownerDocument !== null) {
800 2
                $fragment = $this->node->parentNode->ownerDocument->createDocumentFragment();
801 2
                if ($html !== null) {
802
                    /** @var \DOMNode $html */
803
                    while ($html->childNodes->length > 0) {
804
                        $tmpNode = $html->childNodes->item(0);
805
                        if ($tmpNode !== null) {
806
                            /** @noinspection UnusedFunctionResultInspection */
807
                            $fragment->appendChild($tmpNode);
808
                        }
809
                    }
810
                    /** @noinspection UnusedFunctionResultInspection */
811
                    $html->parentNode->replaceChild($fragment, $html);
812
                }
813
            }
814
        }
815
816 11
        return $newDocument;
817
    }
818
819
    /**
820
     * Retrieve an external iterator.
821
     *
822
     * @see  http://php.net/manual/en/iteratoraggregate.getiterator.php
823
     *
824
     * @return SimpleHtmlDomNode
825
     *                           <p>
826
     *                              An instance of an object implementing <b>Iterator</b> or
827
     *                              <b>Traversable</b>
828
     *                           </p>
829
     */
830 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...
831
    {
832 3
        $elements = new SimpleHtmlDomNode();
833 3
        if ($this->node->hasChildNodes()) {
834 3
            foreach ($this->node->childNodes as $node) {
835 3
                $elements[] = new static($node);
836
            }
837
        }
838
839 3
        return $elements;
840
    }
841
842
    /**
843
     * Get dom node's inner html.
844
     *
845
     * @param bool $multiDecodeNewHtmlEntity
846
     *
847
     * @return string
848
     */
849
    public function innerXml(bool $multiDecodeNewHtmlEntity = false): string
850
    {
851
        return $this->getHtmlDomParser()->innerXml($multiDecodeNewHtmlEntity);
852
    }
853
854
    /**
855
     * Normalize the given input for comparision.
856
     *
857
     * @param HtmlDomParser|string $input
858
     *
859
     * @return string
860
     */
861 11
    private function normalizeStringForComparision($input): string
862
    {
863 11
        if ($input instanceof HtmlDomParser) {
864 11
            $string = $input->outerText();
865
866 11
            if ($input->getIsDOMDocumentCreatedWithoutHeadWrapper()) {
867
                /** @noinspection HtmlRequiredTitleElement */
868 11
                $string = \str_replace(['<head>', '</head>'], '', $string);
869
            }
870
        } else {
871 11
            $string = (string) $input;
872
        }
873
874
        return
875 11
            \urlencode(
876 11
                \urldecode(
877 11
                    \trim(
878 11
                        \str_replace(
879
                            [
880 11
                                ' ',
881
                                "\n",
882
                                "\r",
883
                                '/>',
884
                            ],
885
                            [
886 11
                                '',
887
                                '',
888
                                '',
889
                                '>',
890
                            ],
891 11
                            \strtolower($string)
892
                        )
893
                    )
894
                )
895
            );
896
    }
897
}
898