Completed
Pull Request — master (#55)
by Volodymyr
01:53
created

SimpleXmlDom::replaceTextWithString()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 18

Duplication

Lines 18
Ratio 100 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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