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

SimpleXmlDom::replaceNodeWithString()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 38

Duplication

Lines 7
Ratio 18.42 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
cc 6
nc 5
nop 1
dl 7
loc 38
ccs 0
cts 20
cp 0
crap 42
rs 8.6897
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\helper;
6
7
/**
8
 * @noinspection PhpHierarchyChecksInspection
9
 *
10
 * {@inheritdoc}
11
 *
12
 * @implements \IteratorAggregate<int, \DOMNode>
13
 */
14
class SimpleXmlDom extends AbstractSimpleXmlDom implements \IteratorAggregate, SimpleXmlDomInterface
15
{
16
    /**
17
     * @param \DOMElement|\DOMNode $node
18
     */
19 1
    public function __construct(\DOMNode $node)
20
    {
21 1
        $this->node = $node;
22 1
    }
23
24
    /**
25
     * @param string $name
26
     * @param array  $arguments
27
     *
28
     * @throws \BadMethodCallException
29
     *
30
     * @return SimpleXmlDomInterface|string|null
31
     */
32
    public function __call($name, $arguments)
33
    {
34
        $name = \strtolower($name);
35
36
        if (isset(self::$functionAliases[$name])) {
37
            return \call_user_func_array([$this, self::$functionAliases[$name]], $arguments);
38
        }
39
40
        throw new \BadMethodCallException('Method does not exist');
41
    }
42
43
    /**
44
     * Find list of nodes with a CSS selector.
45
     *
46
     * @param string   $selector
47
     * @param int|null $idx
48
     *
49
     * @return SimpleXmlDomInterface|SimpleXmlDomInterface[]|SimpleXmlDomNodeInterface<SimpleXmlDomInterface>
0 ignored issues
show
Documentation introduced by
The doc-type SimpleXmlDomInterface|Si...<SimpleXmlDomInterface> could not be parsed: Expected "|" or "end of type", but got "<" at position 71. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
50
     */
51
    public function find(string $selector, $idx = null)
52
    {
53
        return $this->getXmlDomParser()->find($selector, $idx);
54
    }
55
56
    /**
57
     * Returns an array of attributes.
58
     *
59
     * @return string[]|null
60
     */
61 View Code Duplication
    public function getAllAttributes()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
62
    {
63
        if (
64
            $this->node
65
            &&
66
            $this->node->hasAttributes()
67
        ) {
68
            $attributes = [];
69
            foreach ($this->node->attributes ?? [] as $attr) {
70
                $attributes[$attr->name] = XmlDomParser::putReplacedBackToPreserveHtmlEntities($attr->value);
71
            }
72
73
            return $attributes;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $attributes; (array) is incompatible with the return type declared by the interface voku\helper\SimpleXmlDom...rface::getAllAttributes of type string[]|null.

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

Loading history...
74
        }
75
76
        return null;
77
    }
78
79
    /**
80
     * @return bool
81
     */
82
    public function hasAttributes(): bool
83
    {
84
        return $this->node->hasAttributes();
85
    }
86
87
    /**
88
     * Return attribute value.
89
     *
90
     * @param string $name
91
     *
92
     * @return string
93
     */
94
    public function getAttribute(string $name): string
95
    {
96
        if ($this->node instanceof \DOMElement) {
97
            return XmlDomParser::putReplacedBackToPreserveHtmlEntities(
98
                $this->node->getAttribute($name)
99
            );
100
        }
101
102
        return '';
103
    }
104
105
    /**
106
     * Determine if an attribute exists on the element.
107
     *
108
     * @param string $name
109
     *
110
     * @return bool
111
     */
112
    public function hasAttribute(string $name): bool
113
    {
114
        if (!$this->node instanceof \DOMElement) {
115
            return false;
116
        }
117
118
        return $this->node->hasAttribute($name);
119
    }
120
121
    /**
122
     * Get dom node's inner html.
123
     *
124
     * @param bool $multiDecodeNewHtmlEntity
125
     *
126
     * @return string
127
     */
128
    public function innerXml(bool $multiDecodeNewHtmlEntity = false): string
129
    {
130
        return $this->getXmlDomParser()->innerXml($multiDecodeNewHtmlEntity);
131
    }
132
133
    /**
134
     * Remove attribute.
135
     *
136
     * @param string $name <p>The name of the html-attribute.</p>
137
     *
138
     * @return SimpleXmlDomInterface
139
     */
140
    public function removeAttribute(string $name): SimpleXmlDomInterface
141
    {
142
        if (\method_exists($this->node, 'removeAttribute')) {
143
            $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...
144
        }
145
146
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (voku\helper\SimpleXmlDom) is incompatible with the return type declared by the interface voku\helper\SimpleXmlDomInterface::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...
147
    }
148
149
    /**
150
     * Replace child node.
151
     *
152
     * @param string $string
153
     *
154
     * @return SimpleXmlDomInterface
155
     */
156 View Code Duplication
    protected function replaceChildWithString(string $string): SimpleXmlDomInterface
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...
157
    {
158
        if (!empty($string)) {
159
            $newDocument = new XmlDomParser($string);
160
161
            $tmpDomString = $this->normalizeStringForComparision($newDocument);
162
            $tmpStr = $this->normalizeStringForComparision($string);
163
            if ($tmpDomString !== $tmpStr) {
164
                throw new \RuntimeException(
165
                    'Not valid XML fragment!' . "\n" .
166
                    $tmpDomString . "\n" .
167
                    $tmpStr
168
                );
169
            }
170
        }
171
172
        /** @var \DOMNode[] $remove_nodes */
173
        $remove_nodes = [];
174
        if ($this->node->childNodes->length > 0) {
175
            // INFO: We need to fetch the nodes first, before we can delete them, because of missing references in the dom,
176
            // if we delete the elements on the fly.
177
            foreach ($this->node->childNodes as $node) {
178
                $remove_nodes[] = $node;
179
            }
180
        }
181
        foreach ($remove_nodes as $remove_node) {
182
            $this->node->removeChild($remove_node);
183
        }
184
185
        if (!empty($newDocument)) {
186
            $ownerDocument = $this->node->ownerDocument;
187
            if (
188
                $ownerDocument
189
                &&
190
                $newDocument->getDocument()->documentElement
191
            ) {
192
                $newNode = $ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
193
                /** @noinspection UnusedFunctionResultInspection */
194
                $this->node->appendChild($newNode);
195
            }
196
        }
197
198
        return $this;
199
    }
200
201
    /**
202
     * Replace this node.
203
     *
204
     * @param string $string
205
     *
206
     * @return SimpleXmlDomInterface
207
     */
208
    protected function replaceNodeWithString(string $string): SimpleXmlDomInterface
209
    {
210
        if (empty($string)) {
211
            if ($this->node->parentNode) {
212
                $this->node->parentNode->removeChild($this->node);
213
            }
214
215
            return $this;
216
        }
217
218
        $newDocument = new XmlDomParser($string);
219
220
        $tmpDomOuterTextString = $this->normalizeStringForComparision($newDocument);
221
        $tmpStr = $this->normalizeStringForComparision($string);
222 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...
223
            throw new \RuntimeException(
224
                'Not valid XML fragment!' . "\n"
225
                . $tmpDomOuterTextString . "\n" .
226
                $tmpStr
227
            );
228
        }
229
230
        $ownerDocument = $this->node->ownerDocument;
231
        if (
232
            $ownerDocument === null
233
            ||
234
            $newDocument->getDocument()->documentElement === null
235
        ) {
236
            return $this;
237
        }
238
239
        $newNode = $ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
240
241
        $this->node->parentNode->replaceChild($newNode, $this->node);
242
        $this->node = $newNode;
243
244
        return $this;
245
    }
246
247
    /**
248
     * Replace this node with text
249
     *
250
     * @param string $string
251
     *
252
     * @return SimpleXmlDomInterface
253
     */
254 View Code Duplication
    protected function replaceTextWithString($string): SimpleXmlDomInterface
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...
255
    {
256
        if (empty($string)) {
257
            if ($this->node->parentNode) {
258
                $this->node->parentNode->removeChild($this->node);
259
            }
260
261
            return $this;
262
        }
263
264
        $ownerDocument = $this->node->ownerDocument;
265
        if ($ownerDocument) {
266
            $newElement = $ownerDocument->createTextNode($string);
267
            $newNode = $ownerDocument->importNode($newElement, true);
268
            $this->node->parentNode->replaceChild($newNode, $this->node);
269
            $this->node = $newNode;
270
        }
271
272
        return $this;
273
    }
274
275
    /**
276
     * Set attribute value.
277
     *
278
     * @param string      $name       <p>The name of the html-attribute.</p>
279
     * @param string|null $value      <p>Set to NULL or empty string, to remove the attribute.</p>
280
     * @param bool        $strict     </p>
281
     *                                $value must be NULL, to remove the attribute,
282
     *                                so that you can set an empty string as attribute-value e.g. autofocus=""
283
     *                                </p>
284
     *
285
     * @return SimpleXmlDomInterface
286
     */
287 View Code Duplication
    public function setAttribute(string $name, $value = null, bool $strict = false): SimpleXmlDomInterface
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...
288
    {
289
        if (
290
            ($strict && $value === null)
291
            ||
292
            (!$strict && empty($value))
293
        ) {
294
            /** @noinspection UnusedFunctionResultInspection */
295
            $this->removeAttribute($name);
296
        } elseif (\method_exists($this->node, 'setAttribute')) {
297
            /** @noinspection UnusedFunctionResultInspection */
298
            $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...
299
        }
300
301
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (voku\helper\SimpleXmlDom) is incompatible with the return type declared by the interface voku\helper\SimpleXmlDomInterface::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...
302
    }
303
304
    /**
305
     * Get dom node's plain text.
306
     *
307
     * @return string
308
     */
309
    public function text(): string
310
    {
311
        return $this->getXmlDomParser()->fixHtmlOutput($this->node->textContent);
312
    }
313
314
    /**
315
     * Get dom node's outer html.
316
     *
317
     * @param bool $multiDecodeNewHtmlEntity
318
     *
319
     * @return string
320
     */
321
    public function xml(bool $multiDecodeNewHtmlEntity = false): string
322
    {
323
        return $this->getXmlDomParser()->xml($multiDecodeNewHtmlEntity, false);
324
    }
325
326
    /**
327
     * Change the name of a tag in a "DOMNode".
328
     *
329
     * @param \DOMNode $node
330
     * @param string   $name
331
     *
332
     * @return \DOMElement|false
333
     *                          <p>DOMElement a new instance of class DOMElement or false
334
     *                          if an error occured.</p>
335
     */
336 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...
337
    {
338
        $ownerDocument = $node->ownerDocument;
339
        if (!$ownerDocument) {
340
            return false;
341
        }
342
343
        $newNode = $ownerDocument->createElement($name);
344
345
        foreach ($node->childNodes as $child) {
346
            $child = $ownerDocument->importNode($child, true);
347
            $newNode->appendChild($child);
348
        }
349
350
        foreach ($node->attributes ?? [] as $attrName => $attrNode) {
351
            /** @noinspection UnusedFunctionResultInspection */
352
            $newNode->setAttribute($attrName, $attrNode);
353
        }
354
355
        if ($newNode->ownerDocument) {
356
            /** @noinspection UnusedFunctionResultInspection */
357
            $newNode->ownerDocument->replaceChild($newNode, $node);
358
        }
359
360
        return $newNode;
361
    }
362
363
    /**
364
     * Returns children of node.
365
     *
366
     * @param int $idx
367
     *
368
     * @return SimpleXmlDomInterface|SimpleXmlDomInterface[]|SimpleXmlDomNodeInterface<SimpleXmlDomInterface>|null
0 ignored issues
show
Documentation introduced by
The doc-type SimpleXmlDomInterface|Si...leXmlDomInterface>|null could not be parsed: Expected "|" or "end of type", but got "<" at position 71. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
369
     */
370 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...
371
    {
372
        $nodeList = $this->getIterator();
373
374
        if ($idx === -1) {
375
            return $nodeList;
376
        }
377
378
        return $nodeList[$idx] ?? null;
379
    }
380
381
    /**
382
     * Find nodes with a CSS selector.
383
     *
384
     * @param string $selector
385
     *
386
     * @return SimpleXmlDomInterface[]|SimpleXmlDomNodeInterface<SimpleXmlDomInterface>
0 ignored issues
show
Documentation introduced by
The doc-type SimpleXmlDomInterface[]|...<SimpleXmlDomInterface> could not be parsed: Expected "|" or "end of type", but got "<" at position 49. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
387
     */
388
    public function findMulti(string $selector): SimpleXmlDomNodeInterface
389
    {
390
        return $this->getXmlDomParser()->findMulti($selector);
391
    }
392
393
    /**
394
     * Find nodes with a CSS selector.
395
     *
396
     * @param string $selector
397
     *
398
     * @return false|SimpleXmlDomInterface[]|SimpleXmlDomNodeInterface<SimpleXmlDomInterface>
0 ignored issues
show
Documentation introduced by
The doc-type false|SimpleXmlDomInterf...<SimpleXmlDomInterface> could not be parsed: Expected "|" or "end of type", but got "<" at position 55. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
399
     */
400
    public function findMultiOrFalse(string $selector)
401
    {
402
        return $this->getXmlDomParser()->findMultiOrFalse($selector);
403
    }
404
405
    /**
406
     * Find one node with a CSS selector.
407
     *
408
     * @param string $selector
409
     *
410
     * @return SimpleXmlDomInterface
411
     */
412
    public function findOne(string $selector): SimpleXmlDomInterface
413
    {
414
        return $this->getXmlDomParser()->findOne($selector);
415
    }
416
417
    /**
418
     * Find one node with a CSS selector or false, if no element is found.
419
     *
420
     * @param string $selector
421
     *
422
     * @return false|SimpleXmlDomInterface
423
     */
424
    public function findOneOrFalse(string $selector)
425
    {
426
        return $this->getXmlDomParser()->findOneOrFalse($selector);
427
    }
428
429
    /**
430
     * Returns the first child of node.
431
     *
432
     * @return SimpleXmlDomInterface|null
433
     */
434 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...
435
    {
436
        /** @var \DOMNode|null $node */
437
        $node = $this->node->firstChild;
438
439
        if ($node === null) {
440
            return null;
441
        }
442
443
        return new static($node);
444
    }
445
446
    /**
447
     * Return elements by ".class".
448
     *
449
     * @param string $class
450
     *
451
     * @return SimpleXmlDomInterface[]|SimpleXmlDomNodeInterface<SimpleXmlDomInterface>
0 ignored issues
show
Documentation introduced by
The doc-type SimpleXmlDomInterface[]|...<SimpleXmlDomInterface> could not be parsed: Expected "|" or "end of type", but got "<" at position 49. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
452
     */
453
    public function getElementByClass(string $class): SimpleXmlDomNodeInterface
454
    {
455
        return $this->findMulti(".${class}");
456
    }
457
458
    /**
459
     * Return element by #id.
460
     *
461
     * @param string $id
462
     *
463
     * @return SimpleXmlDomInterface
464
     */
465
    public function getElementById(string $id): SimpleXmlDomInterface
466
    {
467
        return $this->findOne("#${id}");
468
    }
469
470
    /**
471
     * Return element by tag name.
472
     *
473
     * @param string $name
474
     *
475
     * @return SimpleXmlDomInterface
476
     */
477 View Code Duplication
    public function getElementByTagName(string $name): SimpleXmlDomInterface
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...
478
    {
479
        if ($this->node instanceof \DOMElement) {
480
            $node = $this->node->getElementsByTagName($name)->item(0);
481
        } else {
482
            $node = null;
483
        }
484
485
        if ($node === null) {
486
            return new SimpleXmlDomBlank();
487
        }
488
489
        return new static($node);
490
    }
491
492
    /**
493
     * Returns elements by "#id".
494
     *
495
     * @param string   $id
496
     * @param int|null $idx
497
     *
498
     * @return SimpleXmlDomInterface|SimpleXmlDomInterface[]|SimpleXmlDomNodeInterface<SimpleXmlDomInterface>
0 ignored issues
show
Documentation introduced by
The doc-type SimpleXmlDomInterface|Si...<SimpleXmlDomInterface> could not be parsed: Expected "|" or "end of type", but got "<" at position 71. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
499
     */
500
    public function getElementsById(string $id, $idx = null)
501
    {
502
        return $this->find("#${id}", $idx);
503
    }
504
505
    /**
506
     * Returns elements by tag name.
507
     *
508
     * @param string   $name
509
     * @param int|null $idx
510
     *
511
     * @return SimpleXmlDomInterface|SimpleXmlDomInterface[]|SimpleXmlDomNodeInterface<SimpleXmlDomInterface>
0 ignored issues
show
Documentation introduced by
The doc-type SimpleXmlDomInterface|Si...<SimpleXmlDomInterface> could not be parsed: Expected "|" or "end of type", but got "<" at position 71. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
512
     */
513 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...
514
    {
515
        if ($this->node instanceof \DOMElement) {
516
            $nodesList = $this->node->getElementsByTagName($name);
517
        } else {
518
            $nodesList = [];
519
        }
520
521
        $elements = new SimpleXmlDomNode();
522
523
        foreach ($nodesList as $node) {
524
            $elements[] = new static($node);
525
        }
526
527
        // return all elements
528
        if ($idx === null) {
529
            if (\count($elements) === 0) {
530
                return new SimpleXmlDomNodeBlank();
531
            }
532
533
            return $elements;
534
        }
535
536
        // handle negative values
537
        if ($idx < 0) {
538
            $idx = \count($elements) + $idx;
539
        }
540
541
        // return one element
542
        return $elements[$idx] ?? new SimpleXmlDomBlank();
543
    }
544
545
    /**
546
     * @return \DOMNode
547
     */
548 1
    public function getNode(): \DOMNode
549
    {
550 1
        return $this->node;
551
    }
552
553
    /**
554
     * Create a new "XmlDomParser"-object from the current context.
555
     *
556
     * @return XmlDomParser
557
     */
558
    public function getXmlDomParser(): XmlDomParser
559
    {
560
        return new XmlDomParser($this);
561
    }
562
563
    /**
564
     * Get dom node's inner html.
565
     *
566
     * @param bool $multiDecodeNewHtmlEntity
567
     *
568
     * @return string
569
     */
570
    public function innerHtml(bool $multiDecodeNewHtmlEntity = false): string
571
    {
572
        return $this->getXmlDomParser()->innerHtml($multiDecodeNewHtmlEntity);
573
    }
574
575
    /**
576
     * Nodes can get partially destroyed in which they're still an
577
     * actual DOM node (such as \DOMElement) but almost their entire
578
     * body is gone, including the `nodeType` attribute.
579
     *
580
     * @return bool true if node has been destroyed
581
     */
582
    public function isRemoved(): bool
583
    {
584
        return !isset($this->node->nodeType);
585
    }
586
587
    /**
588
     * Returns the last child of node.
589
     *
590
     * @return SimpleXmlDomInterface|null
591
     */
592 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...
593
    {
594
        /** @var \DOMNode|null $node */
595
        $node = $this->node->lastChild;
596
597
        if ($node === null) {
598
            return null;
599
        }
600
601
        return new static($node);
602
    }
603
604
    /**
605
     * Returns the next sibling of node.
606
     *
607
     * @return SimpleXmlDomInterface|null
608
     */
609 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...
610
    {
611
        /** @var \DOMNode|null $node */
612
        $node = $this->node->nextSibling;
613
614
        if ($node === null) {
615
            return null;
616
        }
617
618
        return new static($node);
619
    }
620
621
    /**
622
     * Returns the next sibling of node.
623
     *
624
     * @return SimpleXmlDomInterface|null
625
     */
626 View Code Duplication
    public function nextNonWhitespaceSibling()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
627
    {
628
        /** @var \DOMNode|null $node */
629
        $node = $this->node->nextSibling;
630
631
        if ($node === null) {
632
            return null;
633
        }
634
635
        while ($node && !\trim($node->textContent)) {
636
            /** @var \DOMNode|null $node */
637
            $node = $node->nextSibling;
638
        }
639
640
        return new static($node);
0 ignored issues
show
Bug introduced by
It seems like $node can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
641
    }
642
643
    /**
644
     * Returns the parent of node.
645
     *
646
     * @return SimpleXmlDomInterface
647
     */
648
    public function parentNode(): SimpleXmlDomInterface
649
    {
650
        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\SimpleXmlDom) is incompatible with the return type declared by the interface voku\helper\SimpleXmlDomInterface::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...
651
    }
652
653
    /**
654
     * Returns the previous sibling of node.
655
     *
656
     * @return SimpleXmlDomInterface|null
657
     */
658 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...
659
    {
660
        /** @var \DOMNode|null $node */
661
        $node = $this->node->previousSibling;
662
663
        if ($node === null) {
664
            return null;
665
        }
666
667
        return new static($node);
668
    }
669
670
    /**
671
     * @param string|string[]|null $value <p>
672
     *                                    null === get the current input value
673
     *                                    text === set a new input value
674
     *                                    </p>
675
     *
676
     * @return string|string[]|null
677
     */
678 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...
679
    {
680
        if ($value === null) {
681
            if (
682
                $this->tag === 'input'
0 ignored issues
show
Documentation introduced by
The property tag does not exist on object<voku\helper\SimpleXmlDom>. 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...
683
                &&
684
                (
685
                    $this->getAttribute('type') === 'hidden'
686
                    ||
687
                    $this->getAttribute('type') === 'text'
688
                    ||
689
                    !$this->hasAttribute('type')
690
                )
691
            ) {
692
                return $this->getAttribute('value');
693
            }
694
695
            if (
696
                $this->hasAttribute('checked')
697
                &&
698
                \in_array($this->getAttribute('type'), ['checkbox', 'radio'], true)
699
            ) {
700
                return $this->getAttribute('value');
701
            }
702
703
            if ($this->node->nodeName === 'select') {
704
                $valuesFromDom = [];
705
                $options = $this->getElementsByTagName('option');
706
                if ($options instanceof SimpleXmlDomNode) {
707
                    foreach ($options as $option) {
708
                        if ($this->hasAttribute('checked')) {
709
                            /** @noinspection UnnecessaryCastingInspection */
710
                            $valuesFromDom[] = (string) $option->getAttribute('value');
711
                        }
712
                    }
713
                }
714
715
                if (\count($valuesFromDom) === 0) {
716
                    return null;
717
                }
718
719
                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\SimpleXmlDomInterface::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...
720
            }
721
722
            if ($this->node->nodeName === 'textarea') {
723
                return $this->node->nodeValue;
724
            }
725
        } else {
726
            /** @noinspection NestedPositiveIfStatementsInspection */
727
            if (\in_array($this->getAttribute('type'), ['checkbox', 'radio'], true)) {
728
                if ($value === $this->getAttribute('value')) {
729
                    /** @noinspection UnusedFunctionResultInspection */
730
                    $this->setAttribute('checked', 'checked');
731
                } else {
732
                    /** @noinspection UnusedFunctionResultInspection */
733
                    $this->removeAttribute('checked');
734
                }
735
            } elseif ($this->node instanceof \DOMElement && $this->node->nodeName === 'select') {
736
                foreach ($this->node->getElementsByTagName('option') as $option) {
737
                    /** @var \DOMElement $option */
738
                    if ($value === $option->getAttribute('value')) {
739
                        /** @noinspection UnusedFunctionResultInspection */
740
                        $option->setAttribute('selected', 'selected');
741
                    } else {
742
                        $option->removeAttribute('selected');
743
                    }
744
                }
745
            } elseif ($this->node->nodeName === 'input' && \is_string($value)) {
746
                // Set value for input elements
747
                /** @noinspection UnusedFunctionResultInspection */
748
                $this->setAttribute('value', $value);
749
            } elseif ($this->node->nodeName === 'textarea' && \is_string($value)) {
750
                $this->node->nodeValue = $value;
751
            }
752
        }
753
754
        return null;
755
    }
756
757
    /**
758
     * Retrieve an external iterator.
759
     *
760
     * @see  http://php.net/manual/en/iteratoraggregate.getiterator.php
761
     *
762
     * @return SimpleXmlDomNode
763
     *                           <p>
764
     *                              An instance of an object implementing <b>Iterator</b> or
765
     *                              <b>Traversable</b>
766
     *                           </p>
767
     */
768 View Code Duplication
    public function getIterator(): SimpleXmlDomNodeInterface
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...
769
    {
770
        $elements = new SimpleXmlDomNode();
771
        if ($this->node->hasChildNodes()) {
772
            foreach ($this->node->childNodes as $node) {
773
                $elements[] = new static($node);
774
            }
775
        }
776
777
        return $elements;
778
    }
779
780
    /**
781
     * Normalize the given input for comparision.
782
     *
783
     * @param string|XmlDomParser $input
784
     *
785
     * @return string
786
     */
787
    private function normalizeStringForComparision($input): string
788
    {
789
        if ($input instanceof XmlDomParser) {
790
            $string = $input->plaintext;
791
        } else {
792
            $string = (string) $input;
793
        }
794
795
        return
796
            \urlencode(
797
                \urldecode(
798
                    \trim(
799
                        \str_replace(
800
                            [
801
                                ' ',
802
                                "\n",
803
                                "\r",
804
                                '/>',
805
                            ],
806
                            [
807
                                '',
808
                                '',
809
                                '',
810
                                '>',
811
                            ],
812
                            \strtolower($string)
813
                        )
814
                    )
815
                )
816
            );
817
    }
818
}
819