Completed
Push — master ( 5dbfda...a73b65 )
by Kacper
03:37
created

XmlElement   D

Complexity

Total Complexity 80

Size/Duplication

Total Lines 577
Duplicated Lines 1.56 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
dl 9
loc 577
ccs 161
cts 161
cp 1
rs 4.8717
c 0
b 0
f 0
wmc 80
lcom 1
cbo 4

40 Methods

Rating   Name   Duplication   Size   Complexity  
A init() 0 11 2
A __construct() 0 6 1
A plain() 0 8 1
A getInnerXml() 0 12 3
A setInnerXml() 0 6 1
A getContent() 0 4 1
A setContent() 0 5 1
B xml() 0 21 6
A lookupPrefix() 0 4 2
A lookupUri() 0 4 2
A getNamespaces() 0 12 3
A setAttribute() 0 11 2
A getAttribute() 0 4 1
A hasAttribute() 0 4 1
A getParents() 0 4 2
A getParent() 0 4 1
A setParent() 0 12 4
A append() 0 13 3
B remove() 0 14 5
A appendChild() 0 16 4
A getNamespace() 0 8 2
A setNamespace() 0 8 2
A getFullName() 0 4 2
A getChildren() 0 4 1
A getPrefix() 0 4 1
A getLocalName() 0 4 1
A getAttributes() 0 4 1
A setAttributes() 0 8 2
A element() 0 4 1
A elements() 9 9 2
A all() 0 4 1
A get() 0 11 3
A has() 0 4 1
A query() 0 4 1
A attributes() 0 12 2
A _prefix() 0 12 3
A __toString() 0 4 1
A resolve() 0 10 2
A cast() 0 10 2
A __clone() 0 10 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like XmlElement often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use XmlElement, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Nucleus - XMPP Library for PHP
4
 *
5
 * Copyright (C) 2016, Some rights reserved.
6
 *
7
 * @author Kacper "Kadet" Donat <[email protected]>
8
 *
9
 * Contact with author:
10
 * Xmpp: [email protected]
11
 * E-mail: [email protected]
12
 *
13
 * From Kadet with love.
14
 */
15
16
namespace Kadet\Xmpp\Xml;
17
18
use Interop\Container\ContainerInterface;
19
use Kadet\Xmpp\Exception\InvalidArgumentException;
20
use Kadet\Xmpp\Utils\Accessors;
21
use Kadet\Xmpp\Utils\filter;
22
use Kadet\Xmpp\Utils\helper;
23
use function Kadet\Xmpp\Utils\filter\not;
24
25
/**
26
 * Class XmlElement
27
 * @package Kadet\Xmpp\Xml
28
 *
29
 * @property string          $localName  Tag name without prefix
30
 * @property string          $namespace  XML Namespace URI
31
 * @property string          $prefix     Tag prefix
32
 * @property string          $fullName   Full tag name prefix:local-name
33
 *
34
 * @property XmlElement|null $parent     Element's parent or null if root node.
35
 * @property XmlElement[]    parents     All element's parents in chronological order (from youngest to oldest)
36
 * @property XmlElement[]    $children   All element's child nodes
37
 *
38
 * @property array           $attributes Element's attributes, without xmlns definitions
39
 * @property array           $namespaces Element's namespaces
40
 *
41
 * @property string          $innerXml   Inner XML content
42
 */
43
class XmlElement implements ContainerInterface
44
{
45
    use Accessors;
46
    const XMLNS = 'http://www.w3.org/2000/xmlns/';
47
    const XML = 'http://www.w3.org/XML/1998/namespace';
48
49
    /**
50
     * Settings for tiding up XML output
51
     *
52
     * @var array
53
     */
54
    public static $tidy = [
55
        'indent'           => true,
56
        'input-xml'        => true,
57
        'output-xml'       => true,
58
        'drop-empty-paras' => false,
59
        'wrap'             => 0
60
    ];
61
62
    /** @var string */
63
    private $_localName;
64
    /** @var null|string|false */
65
    private $_prefix = null;
66
67
    /** @var array */
68
    private $_namespaces = [];
69
    /** @var array */
70
    private $_attributes = [];
71
72
    /**
73
     * @var XmlElement
74
     */
75
    private $_parent;
76
77
    /**
78
     * @var XmlElement[]
79
     */
80
    private $_children = [];
81
82
    /**
83
     * Initializes element with given name and URI
84
     *
85
     * @param string $name Element name, including prefix if needed
86
     * @param string $uri  Namespace URI of element
87
     */
88 39
    protected function init(string $name, string $uri = null)
89
    {
90 39
        list($name, $prefix) = self::resolve($name);
91
92 39
        $this->_localName = $name;
93 39
        $this->_prefix    = $prefix;
94
95 39
        if ($uri !== null) {
96 22
            $this->namespace = $uri;
97
        }
98 39
    }
99
100
    /**
101
     * XmlElement constructor
102
     *
103
     * @param string $name    Element name, including prefix if needed
104
     * @param string $uri     Namespace URI of element
105
     * @param array  $options {
106
     *     @var mixed  $content    Content of element
107
     *     @var array  $attributes Element attributes
108
     * }
109
     */
110 35
    public function __construct(string $name, string $uri = null, array $options = [])
111
    {
112 35
        $this->init($name, $uri);
113
114 35
        $this->applyOptions($options);
115 35
    }
116
117
    /**
118
     * Elements named constructor, same for every subclass.
119
     * It's used for factory creation.
120
     *
121
     * @param string $name Element name, including prefix if needed
122
     * @param string $uri  Namespace URI of element
123
     *
124
     * @return static
125
     */
126 5
    public static function plain(string $name, string $uri = null)
127
    {
128
        /** @var XmlElement $element */
129 5
        $element = (new \ReflectionClass(static::class))->newInstanceWithoutConstructor();
130 5
        $element->init($name, $uri);
131
132 5
        return $element;
133
    }
134
135
    /**
136
     * @see $innerXml
137
     * @return string
138
     */
139 6
    public function getInnerXml()
140
    {
141
        return implode('', array_map(function ($element) {
142 6
            if (is_string($element)) {
143 5
                return htmlspecialchars($element);
144
            } elseif ($element instanceof XmlElement) {
145 1
                return $element->xml(false);
146
            }
147
148
            return (string)$element;
149 6
        }, $this->_children));
150
    }
151
152
    public function setInnerXml($value)
153
    {
154
        $this->_children = [];
155
156
        $this->append($value);
157
    }
158
159
    public function getContent()
160
    {
161
        return $this->children;
162
    }
163
164 5
    public function setContent($value)
165
    {
166 5
        $this->_children = [];
167 5
        $this->append($value);
168 5
    }
169
170
    /**
171
     * Returns XML representation of element
172
     *
173
     * @param bool $clean Result will be cleaned if set to true
174
     *
175
     * @return string
176
     */
177 4
    public function xml(bool $clean = true): string
178
    {
179 4
        if ($this->namespace && $this->_prefix === null) {
180 1
            $this->_prefix = $this->lookupPrefix($this->namespace);
181
        }
182
183 4
        $attributes = $this->attributes();
184
185 4
        $result = "<{$this->fullName}";
186
        $result .= ' ' . implode(' ', array_map(function ($key, $value) {
187 2
            return $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"';
188 4
        }, array_keys($attributes), array_values($attributes)));
189
190 4
        if (!empty($this->_children)) {
191 1
            $result .= ">{$this->innerXml}</{$this->fullName}>";
192
        } else {
193 4
            $result .= "/>";
194
        }
195
196 4
        return $clean && function_exists('tidy_repair_string') ? tidy_repair_string($result, self::$tidy) : $result;
197
    }
198
199
    /**
200
     * Looks up prefix associated with given URI
201
     *
202
     * @param string|null $uri
203
     * @return string|false
204
     */
205 22
    public function lookupPrefix(string $uri = null)
206
    {
207 22
        return $this->getNamespaces()[ $uri ] ?? array_search($uri, XmlParser::$predefined) ?: false;
208
    }
209
210
    /**
211
     * Looks up URI associated with given prefix
212
     *
213
     * @param string|null $prefix
214
     * @return string|false
215
     */
216 25
    public function lookupUri(string $prefix = null)
217
    {
218 25
        return array_search($prefix, $this->getNamespaces()) ?: XmlParser::$predefined[$prefix] ?? false;
219
    }
220
221
    /**
222
     * Returns element's namespaces
223
     *
224
     * @param bool $parent Include namespaces from parent?
225
     * @return array
226
     */
227 30
    public function getNamespaces($parent = true): array
228
    {
229 30
        if (!$this->_parent) {
230 30
            return $this->_namespaces;
231
        }
232
233 18
        if ($parent) {
234 18
            return array_merge($this->_namespaces, $this->_parent->getNamespaces());
235
        } else {
236 2
            return array_diff_assoc($this->_namespaces, $this->_parent->getNamespaces());
237
        }
238
    }
239
240
    /**
241
     * Sets XML attribute of element
242
     *
243
     * @param string      $attribute Attribute name, optionally with prefix
244
     * @param mixed       $value     Attribute value
245
     * @param string|null $uri       XML Namespace URI of attribute, prefix will be automatically looked up
246
     */
247 20
    public function setAttribute(string $attribute, $value, string $uri = null)
248
    {
249 20
        $attribute = $this->_prefix($attribute, $uri);
250 19
        if ($value === null) {
251 5
            unset($this->_attributes[ $attribute ]);
252
253 5
            return;
254
        }
255
256 18
        $this->_attributes[ $attribute ] = $value;
257 18
    }
258
259
    /**
260
     * Returns value of specified attribute.
261
     *
262
     * @param string      $attribute Attribute name, optionally with prefix
263
     * @param string|null $uri       XML Namespace URI of attribute, prefix will be automatically looked up
264
     * @return bool|mixed
265
     */
266 13
    public function getAttribute(string $attribute, string $uri = null)
267
    {
268 13
        return $this->_attributes[ $this->_prefix($attribute, $uri) ] ?? false;
269
    }
270
271
    /**
272
     * Checks if attribute exists
273
     *
274
     * @param string      $attribute Attribute name, optionally with prefix
275
     * @param string|null $uri       XML Namespace URI of attribute, prefix will be automatically looked up
276
     *
277
     * @return bool
278
     */
279 5
    public function hasAttribute(string $attribute, string $uri = null)
280
    {
281 5
        return isset($this->_attributes[ $this->_prefix($attribute, $uri) ]);
282
    }
283
284
    /**
285
     * Returns all element's parents in order, oldest ancestor is the last element in returned array.
286
     * @return XmlElement[]
287
     */
288
    public function getParents()
289
    {
290
        return $this->_parent ? array_merge([ $this->_parent ], $this->_parent->getParents()) : [];
291
    }
292
293
    /**
294
     * Returns element's parent
295
     * @return XmlElement|null
296
     */
297 3
    public function getParent()
298
    {
299 3
        return $this->_parent;
300
    }
301
302
    /**
303
     * Sets element's parent
304
     * @param XmlElement $parent
305
     */
306 18
    protected function setParent(XmlElement $parent)
307
    {
308 18
        if (!$this->_prefix && ($prefix = $parent->lookupPrefix($this->namespace)) !== false) {
309 1
            $this->_namespaces[ $this->namespace ] = $prefix;
310 1
            $this->_prefix                         = $prefix;
311
        }
312
313 18
        $this->_parent = $parent;
314 18
        if ($this->namespace === false) {
315 7
            $this->namespace = $parent->namespace;
316
        }
317 18
    }
318
319
    /**
320
     * Appends child to element
321
     *
322
     * @param XmlElement|string $element
323
     *
324
     * @return XmlElement|string Same as $element
325
     */
326 23
    public function append($element)
327
    {
328 23
        if (empty($element)) {
329 1
            return false;
330
        }
331
332 22
        if(is_array($element)) {
333 2
            array_walk($element, [$this, 'appendChild']);
334 2
            return $element;
335
        }
336
337 20
        return $this->appendChild($element);
338
    }
339
340 4
    public function remove($element)
341
    {
342 4
        if(!$element instanceof \Closure) {
343 4
            $element = is_array($element) ? filter\in($element) : filter\same($element);
344
        }
345 4
        $old = $this->_children;
346 4
        $this->_children = array_filter($this->_children, not($element));
347
348 4
        foreach (array_diff($old, $this->_children) as $removed) {
349 1
            if($removed instanceof XmlElement) {
350 1
                $removed->_parent = null;
351
            }
352
        }
353 4
    }
354
355 22
    protected function appendChild($element) {
356 22
        if (!is_string($element) && !$element instanceof XmlElement) {
357 1
            throw new InvalidArgumentException(helper\format(
358 1
                '$element should be either string or object of {class} class. or array of given types, {type} given', [
359 1
                    'class' => XmlElement::class,
360 1
                    'type'  => helper\typeof($element)
361
                ]
362
            ));
363
        }
364
365 21
        if ($element instanceof XmlElement) {
366 18
            $element->parent = $this;
367
        }
368
369 21
        return $this->_children[] = $element;
370
    }
371
372
    /**
373
     * Returns namespace URI associated with element or specified prefix
374
     *
375
     * @param string|bool|null $prefix
376
     * @return false|string
377
     */
378 25
    public function getNamespace($prefix = false)
379
    {
380 25
        if ($prefix === false) {
381 25
            $prefix = $this->prefix;
382
        }
383
384 25
        return $this->lookupUri($prefix);
0 ignored issues
show
Bug introduced by
It seems like $prefix defined by parameter $prefix on line 378 can also be of type boolean; however, Kadet\Xmpp\Xml\XmlElement::lookupUri() does only seem to accept null|string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
385
    }
386
387
    /**
388
     * Adds namespace to element, and associates it with prefix.
389
     *
390
     * @param string           $uri    Namespace URI
391
     * @param string|bool|null $prefix Prefix which will be used for namespace, false for using element's prefix
392
     *                                 and null for no prefix
393
     */
394 28
    public function setNamespace(string $uri, $prefix = false)
395
    {
396 28
        if ($prefix === false) {
397 26
            $prefix = $this->_prefix;
398
        }
399
400 28
        $this->_namespaces[ $uri ] = $prefix;
401 28
    }
402
403 9
    public function getFullName()
404
    {
405 9
        return ($this->_prefix ? $this->prefix . ':' : null) . $this->localName;
406
    }
407
408 12
    public function getChildren()
409
    {
410 12
        return $this->_children;
411
    }
412
413 25
    public function getPrefix()
414
    {
415 25
        return $this->_prefix;
416
    }
417
418 20
    public function getLocalName()
419
    {
420 20
        return $this->_localName;
421
    }
422
423 7
    public function getAttributes()
424
    {
425 7
        return $this->_attributes;
426
    }
427
428 2
    protected function setAttributes(array $attributes)
429
    {
430 2
        $this->_attributes = [];
431
432 2
        foreach ($attributes as $attribute => $value) {
433 2
            $this->setAttribute($attribute, $value);
434
        }
435 2
    }
436
437
    /**
438
     * Returns one element at specified index (for default the first one).
439
     *
440
     * @param string $name  Requested element tag name
441
     * @param string $uri   Requested element namespace
442
     * @param int    $index Index of element to retrieve
443
     *
444
     * @return XmlElement|false Retrieved element
445
     */
446 1
    public function element(string $name, string $uri = null, int $index = 0)
447
    {
448 1
        return array_values($this->elements($name, $uri))[ $index ] ?? false;
449
    }
450
451
    /**
452
     * Retrieves array of matching elements
453
     *
454
     * @param string      $name Requested element tag name
455
     * @param string|null $uri  Requested element namespace
456
     *
457
     * @return XmlElement[] Found Elements
458
     */
459 2 View Code Duplication
    public function elements($name, $uri = null) : array
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...
460
    {
461 2
        $predicate = filter\element\name($name);
462 2
        if ($uri !== null) {
463 1
            $predicate = filter\all($predicate, filter\element\xmlns($uri));
464
        }
465
466 2
        return $this->all($predicate);
467
    }
468
469
    /**
470
     * Filters element with given predicate
471
     *
472
     * @param callable|string $predicate Predicate or class name
473
     *
474
     * @return XmlElement[]
475
     */
476 2
    public function all($predicate)
477
    {
478 2
        return array_values(array_filter($this->_children, filter\predicate($predicate)));
479
    }
480
481
    /**
482
     * Iterates over matching elements
483
     *
484
     * @param callable|string $predicate Predicate or class name
485
     *
486
     * @return XmlElement|false
487
     */
488 8
    public function get($predicate)
489
    {
490 8
        $predicate = filter\predicate($predicate);
491 8
        foreach ($this->_children as $index => $child) {
492 7
            if ($predicate($child)) {
493 7
                return $child;
494
            }
495
        }
496
497 7
        return null;
498
    }
499
500
    /**
501
     * Checks if any element matching predicate exists
502
     *
503
     * @param callable|string $predicate Predicate or class name
504
     *
505
     * @return bool
506
     */
507 1
    public function has($predicate)
508
    {
509 1
        return $this->get($predicate) !== null;
510
    }
511
512
    /**
513
     * @param string|null $query
514
     * @return XPathQuery
515
     */
516 1
    public function query(string $query = null)
517
    {
518 1
        return new XPathQuery($query, $this);
519
    }
520
521
    /**
522
     * Helper for retrieving all arguments (including namespaces)
523
     *
524
     * @return array
525
     */
526 4
    private function attributes(): array
527
    {
528 4
        $namespaces = $this->getNamespaces(false);
529 4
        $namespaces = array_map(function ($prefix, $uri) {
530 2
            return [$prefix ? "xmlns:{$prefix}" : 'xmlns', $uri];
531 4
        }, array_values($namespaces), array_keys($namespaces));
532
533 4
        return array_merge(
534 4
            $this->_attributes,
535 4
            array_combine(array_column($namespaces, 0), array_column($namespaces, 1))
536
        );
537
    }
538
539
    /**
540
     * Prefixes $name with attribute associated with $uri
541
     *
542
     * @param string $name Name to prefix
543
     * @param string $uri  Namespace URI
544
     *
545
     * @return string
546
     */
547 20
    protected function _prefix(string $name, string $uri = null): string
548
    {
549 20
        if ($uri === null) {
550 16
            return $name;
551
        }
552
553 8
        if (($prefix = $this->lookupPrefix($uri)) === false) {
554 1
            throw new InvalidArgumentException(helper\format('URI "{uri}" is not a registered namespace', ['uri' => $uri]));
555
        }
556
557 7
        return "{$prefix}:{$name}";
558
    }
559
560 3
    public function __toString()
561
    {
562 3
        return trim($this->xml(true));
563
    }
564
565
    /**
566
     * Splits name into local-name and prefix
567
     *
568
     * @param $name
569
     * @return array [$name, $prefix]
570
     */
571 39
    public static function resolve($name)
572
    {
573 39
        $prefix = null;
574 39
        if (($pos = strpos($name, ':')) !== false) {
575 2
            $prefix = substr($name, 0, $pos);
576 2
            $name   = substr($name, $pos + 1);
577
        }
578
579 39
        return [$name, $prefix];
580
    }
581
582
    /**
583
     * Casts XML Element object to another class, it's not recommended but should work, as child classes should
584
     * only decorate parent with additional getters and setters for accessing data.
585
     *
586
     * @param XmlElement $element
587
     * @return static
588
     */
589 1
    public static function cast(XmlElement $element)
590
    {
591
        /** @var static $return */
592 1
        $return = (new \ReflectionClass(static::class))->newInstanceWithoutConstructor();
593 1
        foreach (get_object_vars($element) as $property => $value) {
594 1
            $return->$property = $value;
595
        }
596
597 1
        return $return;
598
    }
599
600
    /**
601
     * When an object is cloned, PHP 5 will perform a shallow copy of all of the object's properties.
602
     * Any properties that are references to other variables, will remain references.
603
     * Once the cloning is complete, if a __clone() method is defined,
604
     * then the newly created object's __clone() method will be called, to allow any necessary properties that need to be changed.
605
     * NOT CALLABLE DIRECTLY.
606
     *
607
     * @link http://php.net/manual/en/language.oop5.cloning.php
608
     */
609
    public function __clone()
610
    {
611
        $children = $this->_children;
612
        $this->_children = [];
613
        $this->_parent = null;
614
615
        foreach ($children as $child) {
616
            $this->append(is_object($child) ? clone $child : $child);
617
        }
618
    }
619
}
620