Completed
Push — master ( 5eff09...92586b )
by Kacper
03:19
created

XmlElement   C

Complexity

Total Complexity 61

Size/Duplication

Total Lines 470
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
dl 0
loc 470
ccs 136
cts 136
cp 1
rs 6.018
c 2
b 0
f 0
wmc 61
lcom 1
cbo 3

31 Methods

Rating   Name   Duplication   Size   Complexity  
A init() 0 11 2
A plain() 0 8 1
A getInnerXml() 0 12 3
B xml() 0 21 6
A lookupPrefix() 0 4 1
A lookupUri() 0 4 2
A getNamespaces() 0 12 3
A getAttribute() 0 4 1
A hasAttribute() 0 4 1
A getParent() 0 4 1
A setParent() 0 12 4
A getNamespace() 0 4 1
A setNamespace() 0 8 2
A getName() 0 4 2
A getChildren() 0 4 1
A getPrefix() 0 4 1
A getLocalName() 0 4 1
A getAttributes() 0 4 1
A element() 0 4 1
A elements() 0 9 2
A all() 0 4 1
A get() 0 11 3
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 __construct() 0 8 2
A setAttribute() 0 11 2
B append() 0 26 6
A has() 0 4 1

How to fix   Complexity   

Complex Class

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
24
/**
25
 * Class XmlElement
26
 * @package Kadet\Xmpp\Xml
27
 *
28
 * @property string          $localName  Tag name without prefix
29
 * @property string          $namespace  XML Namespace URI
30
 * @property string          $prefix     Tag prefix
31
 * @property string          $name       Full tag name prefix:local-name
32
 *
33
 * @property XmlElement|null $parent     Element's parent or null if root node.
34
 * @property XmlElement[]    $children   All element's child nodes
35
 *
36
 * @property array           $attributes Element's attributes, without xmlns definitions
37
 * @property array           $namespaces Element's namespaces
38
 *
39
 * @property string          $innerXml   Inner XML content
40
 */
41
class XmlElement implements ContainerInterface
42
{
43
    use Accessors;
44
45
    /**
46
     * Settings for tiding up XML output
47
     *
48
     * @var array
49
     */
50
    public static $tidy = [
51
        'indent'           => true,
52
        'input-xml'        => true,
53
        'output-xml'       => true,
54
        'drop-empty-paras' => false,
55
        'wrap'             => 0
56
    ];
57
58
    /** @var string */
59
    private $_localName;
60
    /** @var null|string|false */
61
    private $_prefix = null;
62
63
    /** @var array */
64
    private $_namespaces = [];
65
    /** @var array */
66
    private $_attributes = [];
67
68
    /**
69
     * @var XmlElement
70
     */
71
    private $_parent;
72
73
    /**
74
     * @var XmlElement[]
75
     */
76
    private $_children = [];
77
78
    /**
79
     * Initializes element with given name and URI
80
     *
81
     * @param string $name Element name, including prefix if needed
82
     * @param string $uri  Namespace URI of element
83
     */
84 23
    protected function init(string $name, string $uri = null)
85
    {
86 23
        list($name, $prefix) = self::resolve($name);
87
88 23
        $this->_localName = $name;
89 23
        $this->_prefix    = $prefix;
90
91 23
        if ($uri !== null) {
92 11
            $this->namespace = $uri;
93
        }
94 23
    }
95
96
    /**
97
     * XmlElement constructor
98
     *
99
     * @param string $name    Element name, including prefix if needed
100
     * @param string $uri     Namespace URI of element
101
     * @param mixed  $content Content of element
102
     * @param array  $attributes
103
     */
104 19
    public function __construct(string $name, string $uri = null, $content = null, array $attributes = [])
105
    {
106 19
        $this->init($name, $uri);
107 19
        $this->append($content);
108 19
        foreach($attributes as $name => $value) {
109 1
            $this->setAttribute($name, $value);
110
        }
111 19
    }
112
113
    /**
114
     * Elements named constructor, same for every subclass.
115
     * It's used for factory creation.
116
     *
117
     * @param string $name Element name, including prefix if needed
118
     * @param string $uri  Namespace URI of element
119
     *
120
     * @return static
121
     */
122 4
    public static function plain(string $name, string $uri = null)
123
    {
124
        /** @var XmlElement $element */
125 4
        $element = (new \ReflectionClass(static::class))->newInstanceWithoutConstructor();
126 4
        $element->init($name, $uri);
127
128 4
        return $element;
129
    }
130
131
    /**
132
     * @see $innerXml
133
     * @return string
134
     */
135 3
    public function getInnerXml()
136
    {
137
        return implode('', array_map(function ($element) {
138 3
            if (is_string($element)) {
139 2
                return htmlspecialchars($element);
140
            } elseif ($element instanceof XmlElement) {
141 1
                return $element->xml(false);
142
            }
143
144
            return (string)$element;
145 3
        }, $this->_children));
146
    }
147
148
    /**
149
     * Returns XML representation of element
150
     *
151
     * @param bool $clean Result will be cleaned if set to true
152
     *
153
     * @return string
154
     */
155 3
    public function xml(bool $clean = true): string
156
    {
157 3
        if ($this->namespace && $this->_prefix === null) {
158 1
            $this->_prefix = $this->lookupPrefix($this->namespace);
159
        }
160
161 3
        $attributes = $this->attributes();
162
163 3
        $result = "<{$this->name}";
164
        $result .= ' ' . implode(' ', array_map(function ($key, $value) {
165 1
            return $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"';
166 3
        }, array_keys($attributes), array_values($attributes)));
167
168 3
        if (!empty($this->_children)) {
169 1
            $result .= ">{$this->innerXml}</{$this->name}>";
170
        } else {
171 3
            $result .= "/>";
172
        }
173
174 3
        return $clean && function_exists('tidy_repair_string') ? tidy_repair_string($result, self::$tidy) : $result;
175
    }
176
177
    /**
178
     * Looks up prefix associated with given URI
179
     *
180
     * @param string|null $uri
181
     * @return string|false
182
     */
183 12
    public function lookupPrefix(string $uri = null)
184
    {
185 12
        return $this->getNamespaces()[ $uri ] ?? false;
186
    }
187
188
    /**
189
     * Looks up URI associated with given prefix
190
     *
191
     * @param string|null $prefix
192
     * @return string|false
193
     */
194 18
    public function lookupUri(string $prefix = null)
195
    {
196 18
        return array_search($prefix, $this->getNamespaces()) ?: false;
197
    }
198
199
    /**
200
     * Returns element's namespaces
201
     *
202
     * @param bool $parent Include namespaces from parent?
203
     * @return array
204
     */
205 20
    public function getNamespaces($parent = true): array
206
    {
207 20
        if (!$this->_parent) {
208 20
            return $this->_namespaces;
209
        }
210
211 11
        if ($parent) {
212 11
            return array_merge($this->_namespaces, $this->_parent->getNamespaces());
213
        } else {
214 1
            return array_diff_assoc($this->_namespaces, $this->_parent->getNamespaces());
215
        }
216
    }
217
218
    /**
219
     * Sets XML attribute of element
220
     *
221
     * @param string      $attribute Attribute name, optionally with prefix
222
     * @param mixed       $value     Attribute value
223
     * @param string|null $uri       XML Namespace URI of attribute, prefix will be automatically looked up
224
     */
225 5
    public function setAttribute(string $attribute, $value, string $uri = null)
226
    {
227 5
        $attribute = $this->_prefix($attribute, $uri);
228 4
        if ($value === null) {
229 1
            unset($this->_attributes[ $attribute ]);
230
231 1
            return;
232
        }
233
234 4
        $this->_attributes[ $attribute ] = $value;
235 4
    }
236
237
    /**
238
     * Returns value of specified attribute.
239
     *
240
     * @param string      $attribute Attribute name, optionally with prefix
241
     * @param string|null $uri       XML Namespace URI of attribute, prefix will be automatically looked up
242
     * @return bool|mixed
243
     */
244 3
    public function getAttribute(string $attribute, string $uri = null)
245
    {
246 3
        return $this->_attributes[ $this->_prefix($attribute, $uri) ] ?? false;
247
    }
248
249
    /**
250
     * Checks if attribute exists
251
     *
252
     * @param string      $attribute Attribute name, optionally with prefix
253
     * @param string|null $uri       XML Namespace URI of attribute, prefix will be automatically looked up
254
     *
255
     * @return bool
256
     */
257 3
    public function hasAttribute(string $attribute, string $uri = null)
258
    {
259 3
        return isset($this->_attributes[ $this->_prefix($attribute, $uri) ]);
260
    }
261
262
    /**
263
     * Returns element's parent
264
     * @return XmlElement|null
265
     */
266 2
    public function getParent()
267
    {
268 2
        return $this->_parent;
269
    }
270
271
    /**
272
     * Sets element's parent
273
     * @param XmlElement $parent
274
     */
275 11
    protected function setParent(XmlElement $parent)
276
    {
277 11
        if (!$this->_prefix && ($prefix = $parent->lookupPrefix($this->namespace)) !== false) {
278 1
            $this->_namespaces[ $this->namespace ] = $prefix;
279 1
            $this->_prefix                         = $prefix;
280
        }
281
282 11
        $this->_parent = $parent;
283 11
        if ($this->namespace === false) {
284 6
            $this->namespace = $parent->namespace;
285
        }
286 11
    }
287
288
    /**
289
     * Appends child to element
290
     *
291
     * @param XmlElement|string $element
292
     *
293
     * @return XmlElement|string Same as $element
294
     */
295 19
    public function append($element)
296
    {
297 19
        if (empty($element)) {
298 18
            return false;
299
        }
300
301 14
        if(is_array($element)) {
302 1
            array_walk($element, [$this, 'append']);
303 1
            return $element;
304
        }
305
306 14
        if (!is_string($element) && !$element instanceof XmlElement) {
307 1
            throw new InvalidArgumentException(helper\format(
308 1
                '$element should be either string or object of {class} class. or array of given types, {type} given', [
309 1
                    'class' => XmlElement::class,
310 1
                    'type'  => helper\typeof($element)
311
                ]
312
            ));
313
        }
314
315 13
        if ($element instanceof XmlElement) {
316 11
            $element->parent = $this;
317
        }
318
319 13
        return $this->_children[] = $element;
320
    }
321
322
    /**
323
     * Returns namespace URI associated with element
324
     *
325
     * @return false|string
326
     */
327 18
    public function getNamespace()
328
    {
329 18
        return $this->lookupUri($this->prefix);
330
    }
331
332
    /**
333
     * Adds namespace to element, and associates it with prefix.
334
     *
335
     * @param string           $uri    Namespace URI
336
     * @param string|bool|null $prefix Prefix which will be used for namespace, false for using element's prefix
337
     *                                 and null for no prefix
338
     */
339 16
    public function setNamespace(string $uri, $prefix = false)
340
    {
341 16
        if ($prefix === false) {
342 14
            $prefix = $this->_prefix;
343
        }
344
345 16
        $this->_namespaces[ $uri ] = $prefix;
346 16
    }
347
348 7
    public function getName()
349
    {
350 7
        return ($this->_prefix ? $this->prefix . ':' : null) . $this->localName;
351
    }
352
353 7
    public function getChildren()
354
    {
355 7
        return $this->_children;
356
    }
357
358 18
    public function getPrefix()
359
    {
360 18
        return $this->_prefix;
361
    }
362
363 12
    public function getLocalName()
364
    {
365 12
        return $this->_localName;
366
    }
367
368 7
    public function getAttributes()
369
    {
370 7
        return $this->_attributes;
371
    }
372
373
    /**
374
     * Returns one element at specified index (for default the first one).
375
     *
376
     * @param string $name  Requested element tag name
377
     * @param string $uri   Requested element namespace
378
     * @param int    $index Index of element to retrieve
379
     *
380
     * @return XmlElement|false Retrieved element
381
     */
382 1
    public function element(string $name, string $uri = null, int $index = 0)
383
    {
384 1
        return array_values($this->elements($name, $uri))[ $index ] ?? false;
385
    }
386
387
    /**
388
     * Retrieves array of matching elements
389
     *
390
     * @param string      $name Requested element tag name
391
     * @param string|null $uri  Requested element namespace
392
     *
393
     * @return XmlElement[] Found Elements
394
     */
395 2
    public function elements($name, $uri = null) : array
396
    {
397 2
        $predicate = filter\tag($name);
398 2
        if ($uri !== null) {
399 1
            $predicate = filter\all($predicate, filter\xmlns($uri));
400
        }
401
402 2
        return $this->all($predicate);
403
    }
404
405
    /**
406
     * Filters element with given predicate
407
     *
408
     * @param callable|string $predicate Predicate or class name
409
     *
410
     * @return XmlElement[]
411
     */
412 2
    public function all($predicate)
413
    {
414 2
        return array_values(array_filter($this->_children, filter\predicate($predicate)));
415
    }
416
417
    /**
418
     * Iterates over matching elements
419
     *
420
     * @param callable|string $predicate Predicate or class name
421
     *
422
     * @return XmlElement|false
423
     */
424 2
    public function get($predicate)
425
    {
426 2
        $predicate = filter\predicate($predicate);
427 2
        foreach ($this->_children as $index => $child) {
428 2
            if ($predicate($child)) {
429 2
                return $child;
430
            }
431
        }
432
433 2
        return false;
434
    }
435
436 1
    public function has($predicate)
437
    {
438 1
        return $this->get($predicate) !== false;
439
    }
440
441
    /**
442
     * @param string|null $query
443
     * @return XPathQuery
444
     */
445 1
    public function query(string $query = null)
446
    {
447 1
        return new XPathQuery($query, $this);
448
    }
449
450
    /**
451
     * Helper for retrieving all arguments (including namespaces)
452
     *
453
     * @return array
454
     */
455 3
    private function attributes(): array
456
    {
457 3
        $namespaces = $this->getNamespaces(false);
458 3
        $namespaces = array_map(function ($prefix, $uri) {
459 1
            return [$prefix ? "xmlns:{$prefix}" : 'xmlns', $uri];
460 3
        }, array_values($namespaces), array_keys($namespaces));
461
462 3
        return array_merge(
463 3
            $this->_attributes,
464 3
            array_combine(array_column($namespaces, 0), array_column($namespaces, 1))
465
        );
466
    }
467
468
    /**
469
     * Prefixes $name with attribute associated with $uri
470
     *
471
     * @param string $name Name to prefix
472
     * @param string $uri  Namespace URI
473
     *
474
     * @return string
475
     */
476 5
    protected function _prefix(string $name, string $uri = null): string
477
    {
478 5
        if ($uri === null) {
479 4
            return $name;
480
        }
481
482 3
        if (($prefix = $this->lookupPrefix($uri)) === false) {
483 1
            throw new InvalidArgumentException(helper\format('URI "{uri}" is not a registered namespace', ['uri' => $uri]));
484
        }
485
486 2
        return "{$prefix}:{$name}";
487
    }
488
489 2
    public function __toString()
490
    {
491 2
        return trim($this->xml(true));
492
    }
493
494
    /**
495
     * Splits name into local-name and prefix
496
     *
497
     * @param $name
498
     * @return array [$name, $prefix]
499
     */
500 23
    public static function resolve($name)
501
    {
502 23
        $prefix = null;
503 23
        if (($pos = strpos($name, ':')) !== false) {
504 2
            $prefix = substr($name, 0, $pos);
505 2
            $name   = substr($name, $pos + 1);
506
        }
507
508 23
        return [$name, $prefix];
509
    }
510
}
511