Completed
Push — master ( 061358...056c4c )
by Lars
01:53
created

SimpleHtmlDom::getNode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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