Completed
Pull Request — master (#55)
by Volodymyr
02:41
created

SimpleXmlDom::text()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\helper;
6
7
/**
8
 * @noinspection PhpHierarchyChecksInspection
9
 *
10
 * {@inheritdoc}
11
 *
12
 * @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 HTML 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 !== null
189
                &&
190
                $newDocument->getDocument()->documentElement !== null
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
            $this->node->parentNode->removeChild($this->node);
212
213
            return $this;
214
        }
215
216
        $newDocument = new XmlDomParser($string);
217
218
        $tmpDomOuterTextString = $this->normalizeStringForComparision($newDocument);
219
        $tmpStr = $this->normalizeStringForComparision($string);
220 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...
221
            throw new \RuntimeException(
222
                'Not valid HTML fragment!' . "\n"
223
                . $tmpDomOuterTextString . "\n" .
224
                $tmpStr
225
            );
226
        }
227
228
        $ownerDocument = $this->node->ownerDocument;
229
        if (
230
            $ownerDocument === null
231
            ||
232
            $newDocument->getDocument()->documentElement === null
233
        ) {
234
            return $this;
235
        }
236
237
        $newNode = $ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
238
239
        $this->node->parentNode->replaceChild($newNode, $this->node);
240
        $this->node = $newNode;
241
242
        return $this;
243
    }
244
245
    /**
246
     * Replace this node with text
247
     *
248
     * @param string $string
249
     *
250
     * @return SimpleXmlDomInterface
251
     */
252 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...
253
    {
254
        if (empty($string)) {
255
            $this->node->parentNode->removeChild($this->node);
256
257
            return $this;
258
        }
259
260
        $ownerDocument = $this->node->ownerDocument;
261
        if ($ownerDocument !== null) {
262
            $newElement = $ownerDocument->createTextNode($string);
263
            $newNode = $ownerDocument->importNode($newElement, true);
264
            $this->node->parentNode->replaceChild($newNode, $this->node);
265
            $this->node = $newNode;
266
        }
267
268
        return $this;
269
    }
270
271
    /**
272
     * Set attribute value.
273
     *
274
     * @param string      $name       <p>The name of the html-attribute.</p>
275
     * @param string|null $value      <p>Set to NULL or empty string, to remove the attribute.</p>
276
     * @param bool        $strict     </p>
277
     *                                $value must be NULL, to remove the attribute,
278
     *                                so that you can set an empty string as attribute-value e.g. autofocus=""
279
     *                                </p>
280
     *
281
     * @return SimpleXmlDomInterface
282
     */
283 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...
284
    {
285
        if (
286
            ($strict && $value === null)
287
            ||
288
            (!$strict && empty($value))
289
        ) {
290
            /** @noinspection UnusedFunctionResultInspection */
291
            $this->removeAttribute($name);
292
        } elseif (\method_exists($this->node, 'setAttribute')) {
293
            /** @noinspection UnusedFunctionResultInspection */
294
            $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...
295
        }
296
297
        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...
298
    }
299
300
    /**
301
     * Get dom node's plain text.
302
     *
303
     * @return string
304
     */
305
    public function text(): string
306
    {
307
        return $this->getXmlDomParser()->fixHtmlOutput($this->node->textContent);
308
    }
309
310
    /**
311
     * Get dom node's outer html.
312
     *
313
     * @param bool $multiDecodeNewHtmlEntity
314
     *
315
     * @return string
316
     */
317
    public function xml(bool $multiDecodeNewHtmlEntity = false): string
318
    {
319
        return $this->getXmlDomParser()->xml($multiDecodeNewHtmlEntity, false);
320
    }
321
322
    /**
323
     * Change the name of a tag in a "DOMNode".
324
     *
325
     * @param \DOMNode $node
326
     * @param string   $name
327
     *
328
     * @return \DOMElement|false
329
     *                          <p>DOMElement a new instance of class DOMElement or false
330
     *                          if an error occured.</p>
331
     */
332 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...
333
    {
334
        $ownerDocument = $node->ownerDocument;
335
        if ($ownerDocument) {
336
            $newNode = $ownerDocument->createElement($name);
337
        } else {
338
            return false;
339
        }
340
341
        foreach ($node->childNodes as $child) {
342
            $child = $ownerDocument->importNode($child, true);
343
            /** @noinspection UnusedFunctionResultInspection */
344
            $newNode->appendChild($child);
345
        }
346
347
        foreach ($node->attributes ?? [] as $attrName => $attrNode) {
348
            /** @noinspection UnusedFunctionResultInspection */
349
            $newNode->setAttribute($attrName, $attrNode);
350
        }
351
352
        if ($newNode->ownerDocument !== null) {
353
            /** @noinspection UnusedFunctionResultInspection */
354
            $newNode->ownerDocument->replaceChild($newNode, $node);
355
        }
356
357
        return $newNode;
358
    }
359
360
    /**
361
     * Returns children of node.
362
     *
363
     * @param int $idx
364
     *
365
     * @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...
366
     */
367 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...
368
    {
369
        $nodeList = $this->getIterator();
370
371
        if ($idx === -1) {
372
            return $nodeList;
373
        }
374
375
        return $nodeList[$idx] ?? null;
376
    }
377
378
    /**
379
     * Find nodes with a CSS selector.
380
     *
381
     * @param string $selector
382
     *
383
     * @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...
384
     */
385
    public function findMulti(string $selector): SimpleXmlDomNodeInterface
386
    {
387
        return $this->getXmlDomParser()->findMulti($selector);
388
    }
389
390
    /**
391
     * Find nodes with a CSS selector.
392
     *
393
     * @param string $selector
394
     *
395
     * @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...
396
     */
397
    public function findMultiOrFalse(string $selector)
398
    {
399
        return $this->getXmlDomParser()->findMultiOrFalse($selector);
400
    }
401
402
    /**
403
     * Find one node with a CSS selector.
404
     *
405
     * @param string $selector
406
     *
407
     * @return SimpleXmlDomInterface
408
     */
409
    public function findOne(string $selector): SimpleXmlDomInterface
410
    {
411
        return $this->getXmlDomParser()->findOne($selector);
412
    }
413
414
    /**
415
     * Find one node with a CSS selector or false, if no element is found.
416
     *
417
     * @param string $selector
418
     *
419
     * @return false|SimpleXmlDomInterface
420
     */
421
    public function findOneOrFalse(string $selector)
422
    {
423
        return $this->getXmlDomParser()->findOneOrFalse($selector);
424
    }
425
426
    /**
427
     * Returns the first child of node.
428
     *
429
     * @return SimpleXmlDomInterface|null
430
     */
431 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...
432
    {
433
        /** @var \DOMNode|null $node */
434
        $node = $this->node->firstChild;
435
436
        if ($node === null) {
437
            return null;
438
        }
439
440
        return new static($node);
441
    }
442
443
    /**
444
     * Return elements by ".class".
445
     *
446
     * @param string $class
447
     *
448
     * @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...
449
     */
450
    public function getElementByClass(string $class): SimpleXmlDomNodeInterface
451
    {
452
        return $this->findMulti(".${class}");
453
    }
454
455
    /**
456
     * Return element by #id.
457
     *
458
     * @param string $id
459
     *
460
     * @return SimpleXmlDomInterface
461
     */
462
    public function getElementById(string $id): SimpleXmlDomInterface
463
    {
464
        return $this->findOne("#${id}");
465
    }
466
467
    /**
468
     * Return element by tag name.
469
     *
470
     * @param string $name
471
     *
472
     * @return SimpleXmlDomInterface
473
     */
474 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...
475
    {
476
        if ($this->node instanceof \DOMElement) {
477
            $node = $this->node->getElementsByTagName($name)->item(0);
478
        } else {
479
            $node = null;
480
        }
481
482
        if ($node === null) {
483
            return new SimpleXmlDomBlank();
484
        }
485
486
        return new static($node);
487
    }
488
489
    /**
490
     * Returns elements by "#id".
491
     *
492
     * @param string   $id
493
     * @param int|null $idx
494
     *
495
     * @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...
496
     */
497
    public function getElementsById(string $id, $idx = null)
498
    {
499
        return $this->find("#${id}", $idx);
500
    }
501
502
    /**
503
     * Returns elements by tag name.
504
     *
505
     * @param string   $name
506
     * @param int|null $idx
507
     *
508
     * @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...
509
     */
510 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...
511
    {
512
        if ($this->node instanceof \DOMElement) {
513
            $nodesList = $this->node->getElementsByTagName($name);
514
        } else {
515
            $nodesList = [];
516
        }
517
518
        $elements = new SimpleXmlDomNode();
519
520
        foreach ($nodesList as $node) {
521
            $elements[] = new static($node);
522
        }
523
524
        // return all elements
525
        if ($idx === null) {
526
            if (\count($elements) === 0) {
527
                return new SimpleXmlDomNodeBlank();
528
            }
529
530
            return $elements;
531
        }
532
533
        // handle negative values
534
        if ($idx < 0) {
535
            $idx = \count($elements) + $idx;
536
        }
537
538
        // return one element
539
        return $elements[$idx] ?? new SimpleXmlDomBlank();
540
    }
541
542
    /**
543
     * @return \DOMNode
544
     */
545 1
    public function getNode(): \DOMNode
546
    {
547 1
        return $this->node;
548
    }
549
550
    /**
551
     * Create a new "XmlDomParser"-object from the current context.
552
     *
553
     * @return XmlDomParser
554
     */
555
    public function getXmlDomParser(): XmlDomParser
556
    {
557
        return new XmlDomParser($this);
558
    }
559
560
    /**
561
     * Get dom node's inner html.
562
     *
563
     * @param bool $multiDecodeNewHtmlEntity
564
     *
565
     * @return string
566
     */
567
    public function innerHtml(bool $multiDecodeNewHtmlEntity = false): string
568
    {
569
        return $this->getXmlDomParser()->innerHtml($multiDecodeNewHtmlEntity);
570
    }
571
572
    /**
573
     * Nodes can get partially destroyed in which they're still an
574
     * actual DOM node (such as \DOMElement) but almost their entire
575
     * body is gone, including the `nodeType` attribute.
576
     *
577
     * @return bool true if node has been destroyed
578
     */
579
    public function isRemoved(): bool
580
    {
581
        return !isset($this->node->nodeType);
582
    }
583
584
    /**
585
     * Returns the last child of node.
586
     *
587
     * @return SimpleXmlDomInterface|null
588
     */
589 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...
590
    {
591
        /** @var \DOMNode|null $node */
592
        $node = $this->node->lastChild;
593
594
        if ($node === null) {
595
            return null;
596
        }
597
598
        return new static($node);
599
    }
600
601
    /**
602
     * Returns the next sibling of node.
603
     *
604
     * @return SimpleXmlDomInterface|null
605
     */
606 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...
607
    {
608
        /** @var \DOMNode|null $node */
609
        $node = $this->node->nextSibling;
610
611
        if ($node === null) {
612
            return null;
613
        }
614
615
        return new static($node);
616
    }
617
618
    /**
619
     * Returns the next sibling of node.
620
     *
621
     * @return SimpleXmlDomInterface|null
622
     */
623 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...
624
    {
625
        /** @var \DOMNode|null $node */
626
        $node = $this->node->nextSibling;
627
628
        if ($node === null) {
629
            return null;
630
        }
631
632
        while ($node && !\trim($node->textContent)) {
633
            /** @var \DOMNode|null $node */
634
            $node = $node->nextSibling;
635
        }
636
637
        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...
638
    }
639
640
    /**
641
     * Returns the parent of node.
642
     *
643
     * @return SimpleXmlDomInterface
644
     */
645
    public function parentNode(): SimpleXmlDomInterface
646
    {
647
        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...
648
    }
649
650
    /**
651
     * Returns the previous sibling of node.
652
     *
653
     * @return SimpleXmlDomInterface|null
654
     */
655 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...
656
    {
657
        /** @var \DOMNode|null $node */
658
        $node = $this->node->previousSibling;
659
660
        if ($node === null) {
661
            return null;
662
        }
663
664
        return new static($node);
665
    }
666
667
    /**
668
     * @param string|string[]|null $value <p>
669
     *                                    null === get the current input value
670
     *                                    text === set a new input value
671
     *                                    </p>
672
     *
673
     * @return string|string[]|null
674
     */
675 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...
676
    {
677
        if ($value === null) {
678
            if (
679
                $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...
680
                &&
681
                (
682
                    $this->getAttribute('type') === 'hidden'
683
                    ||
684
                    $this->getAttribute('type') === 'text'
685
                    ||
686
                    !$this->hasAttribute('type')
687
                )
688
            ) {
689
                return $this->getAttribute('value');
690
            }
691
692
            if (
693
                $this->hasAttribute('checked')
694
                &&
695
                \in_array($this->getAttribute('type'), ['checkbox', 'radio'], true)
696
            ) {
697
                return $this->getAttribute('value');
698
            }
699
700
            if ($this->node->nodeName === 'select') {
701
                $valuesFromDom = [];
702
                $options = $this->getElementsByTagName('option');
703
                if ($options instanceof SimpleXmlDomNode) {
704
                    foreach ($options as $option) {
705
                        if ($this->hasAttribute('checked')) {
706
                            /** @noinspection UnnecessaryCastingInspection */
707
                            $valuesFromDom[] = (string) $option->getAttribute('value');
708
                        }
709
                    }
710
                }
711
712
                if (\count($valuesFromDom) === 0) {
713
                    return null;
714
                }
715
716
                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...
717
            }
718
719
            if ($this->node->nodeName === 'textarea') {
720
                return $this->node->nodeValue;
721
            }
722
        } else {
723
            /** @noinspection NestedPositiveIfStatementsInspection */
724
            if (\in_array($this->getAttribute('type'), ['checkbox', 'radio'], true)) {
725
                if ($value === $this->getAttribute('value')) {
726
                    /** @noinspection UnusedFunctionResultInspection */
727
                    $this->setAttribute('checked', 'checked');
728
                } else {
729
                    /** @noinspection UnusedFunctionResultInspection */
730
                    $this->removeAttribute('checked');
731
                }
732
            } elseif ($this->node instanceof \DOMElement && $this->node->nodeName === 'select') {
733
                foreach ($this->node->getElementsByTagName('option') as $option) {
734
                    /** @var \DOMElement $option */
735
                    if ($value === $option->getAttribute('value')) {
736
                        /** @noinspection UnusedFunctionResultInspection */
737
                        $option->setAttribute('selected', 'selected');
738
                    } else {
739
                        $option->removeAttribute('selected');
740
                    }
741
                }
742
            } elseif ($this->node->nodeName === 'input' && \is_string($value)) {
743
                // Set value for input elements
744
                /** @noinspection UnusedFunctionResultInspection */
745
                $this->setAttribute('value', $value);
746
            } elseif ($this->node->nodeName === 'textarea' && \is_string($value)) {
747
                $this->node->nodeValue = $value;
748
            }
749
        }
750
751
        return null;
752
    }
753
754
    /**
755
     * Retrieve an external iterator.
756
     *
757
     * @see  http://php.net/manual/en/iteratoraggregate.getiterator.php
758
     *
759
     * @return SimpleXmlDomNode
760
     *                           <p>
761
     *                              An instance of an object implementing <b>Iterator</b> or
762
     *                              <b>Traversable</b>
763
     *                           </p>
764
     */
765 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...
766
    {
767
        $elements = new SimpleXmlDomNode();
768
        if ($this->node->hasChildNodes()) {
769
            foreach ($this->node->childNodes as $node) {
770
                $elements[] = new static($node);
771
            }
772
        }
773
774
        return $elements;
775
    }
776
777
    /**
778
     * Normalize the given input for comparision.
779
     *
780
     * @param string|XmlDomParser $input
781
     *
782
     * @return string
783
     */
784
    private function normalizeStringForComparision($input): string
785
    {
786
        if ($input instanceof XmlDomParser) {
787
            $string = $input->plaintext;
788
        } else {
789
            $string = (string) $input;
790
        }
791
792
        return
793
            \urlencode(
794
                \urldecode(
795
                    \trim(
796
                        \str_replace(
797
                            [
798
                                ' ',
799
                                "\n",
800
                                "\r",
801
                                '/>',
802
                            ],
803
                            [
804
                                '',
805
                                '',
806
                                '',
807
                                '>',
808
                            ],
809
                            \strtolower($string)
810
                        )
811
                    )
812
                )
813
            );
814
    }
815
}
816