Completed
Push — master ( 4aee11...dd7177 )
by Kacper
04:24
created

XmlElement::remove()   B

Complexity

Conditions 5
Paths 9

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 8
nc 9
nop 1
dl 0
loc 14
ccs 9
cts 9
cp 1
crap 5
rs 8.8571
c 0
b 0
f 0
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 function Kadet\Xmpp\Utils\filter\not;
23
use Kadet\Xmpp\Utils\helper;
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
47
    /**
48
     * Settings for tiding up XML output
49
     *
50
     * @var array
51
     */
52
    public static $tidy = [
53
        'indent'           => true,
54
        'input-xml'        => true,
55
        'output-xml'       => true,
56
        'drop-empty-paras' => false,
57
        'wrap'             => 0
58
    ];
59
60
    /** @var string */
61
    private $_localName;
62
    /** @var null|string|false */
63
    private $_prefix = null;
64
65
    /** @var array */
66
    private $_namespaces = [];
67
    /** @var array */
68
    private $_attributes = [];
69
70
    /**
71
     * @var XmlElement
72
     */
73
    private $_parent;
74
75
    /**
76
     * @var XmlElement[]
77
     */
78
    private $_children = [];
79
80
    /**
81
     * Initializes element with given name and URI
82
     *
83
     * @param string $name Element name, including prefix if needed
84
     * @param string $uri  Namespace URI of element
85
     */
86 25
    protected function init(string $name, string $uri = null)
87
    {
88 25
        list($name, $prefix) = self::resolve($name);
89
90 25
        $this->_localName = $name;
91 25
        $this->_prefix    = $prefix;
92
93 25
        if ($uri !== null) {
94 11
            $this->namespace = $uri;
95
        }
96 25
    }
97
98
    /**
99
     * XmlElement constructor
100
     *
101
     * @param string $name    Element name, including prefix if needed
102
     * @param string $uri     Namespace URI of element
103
     * @param array  $options {
104
     *     @var mixed  $content    Content of element
105
     *     @var array  $attributes Element attributes
106
     * }
107
     */
108 21
    public function __construct(string $name, string $uri = null, array $options = [])
109
    {
110 21
        $this->init($name, $uri);
111
112 21
        $this->applyOptions($options);
113 21
    }
114
115
    /**
116
     * Elements named constructor, same for every subclass.
117
     * It's used for factory creation.
118
     *
119
     * @param string $name Element name, including prefix if needed
120
     * @param string $uri  Namespace URI of element
121
     *
122
     * @return static
123
     */
124 4
    public static function plain(string $name, string $uri = null)
125
    {
126
        /** @var XmlElement $element */
127 4
        $element = (new \ReflectionClass(static::class))->newInstanceWithoutConstructor();
128 4
        $element->init($name, $uri);
129
130 4
        return $element;
131
    }
132
133
    /**
134
     * @see $innerXml
135
     * @return string
136
     */
137 3
    public function getInnerXml()
138
    {
139
        return implode('', array_map(function ($element) {
140 3
            if (is_string($element)) {
141 2
                return htmlspecialchars($element);
142
            } elseif ($element instanceof XmlElement) {
143 1
                return $element->xml(false);
144
            }
145
146
            return (string)$element;
147 3
        }, $this->_children));
148
    }
149
150
    public function setInnerXml($value)
151
    {
152
        $this->_children = [];
153
154
        $this->append($value);
155
    }
156
157
    public function getContent()
158
    {
159
        return $this->children;
160
    }
161
162 1
    public function setContent($value)
163
    {
164 1
        $this->_children = [];
165 1
        $this->append($value);
166 1
    }
167
168
    /**
169
     * Returns XML representation of element
170
     *
171
     * @param bool $clean Result will be cleaned if set to true
172
     *
173
     * @return string
174
     */
175 4
    public function xml(bool $clean = true): string
176
    {
177 4
        if ($this->namespace && $this->_prefix === null) {
178 1
            $this->_prefix = $this->lookupPrefix($this->namespace);
179
        }
180
181 4
        $attributes = $this->attributes();
182
183 4
        $result = "<{$this->fullName}";
184
        $result .= ' ' . implode(' ', array_map(function ($key, $value) {
185 2
            return $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"';
186 4
        }, array_keys($attributes), array_values($attributes)));
187
188 4
        if (!empty($this->_children)) {
189 1
            $result .= ">{$this->innerXml}</{$this->fullName}>";
190
        } else {
191 4
            $result .= "/>";
192
        }
193
194 4
        return $clean && function_exists('tidy_repair_string') ? tidy_repair_string($result, self::$tidy) : $result;
195
    }
196
197
    /**
198
     * Looks up prefix associated with given URI
199
     *
200
     * @param string|null $uri
201
     * @return string|false
202
     */
203 13
    public function lookupPrefix(string $uri = null)
204
    {
205 13
        return $this->getNamespaces()[ $uri ] ?? false;
206
    }
207
208
    /**
209
     * Looks up URI associated with given prefix
210
     *
211
     * @param string|null $prefix
212
     * @return string|false
213
     */
214 19
    public function lookupUri(string $prefix = null)
215
    {
216 19
        return array_search($prefix, $this->getNamespaces()) ?: false;
217
    }
218
219
    /**
220
     * Returns element's namespaces
221
     *
222
     * @param bool $parent Include namespaces from parent?
223
     * @return array
224
     */
225 21
    public function getNamespaces($parent = true): array
226
    {
227 21
        if (!$this->_parent) {
228 21
            return $this->_namespaces;
229
        }
230
231 12
        if ($parent) {
232 12
            return array_merge($this->_namespaces, $this->_parent->getNamespaces());
233
        } else {
234 2
            return array_diff_assoc($this->_namespaces, $this->_parent->getNamespaces());
235
        }
236
    }
237
238
    /**
239
     * Sets XML attribute of element
240
     *
241
     * @param string      $attribute Attribute name, optionally with prefix
242
     * @param mixed       $value     Attribute value
243
     * @param string|null $uri       XML Namespace URI of attribute, prefix will be automatically looked up
244
     */
245 6
    public function setAttribute(string $attribute, $value, string $uri = null)
246
    {
247 6
        $attribute = $this->_prefix($attribute, $uri);
248 5
        if ($value === null) {
249 1
            unset($this->_attributes[ $attribute ]);
250
251 1
            return;
252
        }
253
254 5
        $this->_attributes[ $attribute ] = $value;
255 5
    }
256
257
    /**
258
     * Returns value of specified attribute.
259
     *
260
     * @param string      $attribute Attribute name, optionally with prefix
261
     * @param string|null $uri       XML Namespace URI of attribute, prefix will be automatically looked up
262
     * @return bool|mixed
263
     */
264 4
    public function getAttribute(string $attribute, string $uri = null)
265
    {
266 4
        return $this->_attributes[ $this->_prefix($attribute, $uri) ] ?? false;
267
    }
268
269
    /**
270
     * Checks if attribute exists
271
     *
272
     * @param string      $attribute Attribute name, optionally with prefix
273
     * @param string|null $uri       XML Namespace URI of attribute, prefix will be automatically looked up
274
     *
275
     * @return bool
276
     */
277 3
    public function hasAttribute(string $attribute, string $uri = null)
278
    {
279 3
        return isset($this->_attributes[ $this->_prefix($attribute, $uri) ]);
280
    }
281
282
    /**
283
     * Returns all element's parents in order, oldest ancestor is the last element in returned array.
284
     * @return XmlElement[]
285
     */
286
    public function getParents()
287
    {
288
        return $this->_parent ? array_merge([ $this->_parent ], $this->_parent->getParents()) : [];
289
    }
290
291
    /**
292
     * Returns element's parent
293
     * @return XmlElement|null
294
     */
295 3
    public function getParent()
296
    {
297 3
        return $this->_parent;
298
    }
299
300
    /**
301
     * Sets element's parent
302
     * @param XmlElement $parent
303
     */
304 12
    protected function setParent(XmlElement $parent)
305
    {
306 12
        if (!$this->_prefix && ($prefix = $parent->lookupPrefix($this->namespace)) !== false) {
307 1
            $this->_namespaces[ $this->namespace ] = $prefix;
308 1
            $this->_prefix                         = $prefix;
309
        }
310
311 12
        $this->_parent = $parent;
312 12
        if ($this->namespace === false) {
313 7
            $this->namespace = $parent->namespace;
314
        }
315 12
    }
316
317
    /**
318
     * Appends child to element
319
     *
320
     * @param XmlElement|string $element
321
     *
322
     * @return XmlElement|string Same as $element
323
     */
324 16
    public function append($element)
325
    {
326 16
        if (empty($element)) {
327 1
            return false;
328
        }
329
330 15
        if(is_array($element)) {
331 2
            array_walk($element, [$this, 'appendChild']);
332 2
            return $element;
333
        }
334
335 13
        return $this->appendChild($element);
336
    }
337
338 1
    public function remove($element)
339
    {
340 1
        if(!$element instanceof \Closure) {
341 1
            $element = is_array($element) ? filter\in($element) : filter\same($element);
342
        }
343 1
        $old = $this->_children;
344 1
        $this->_children = array_filter($this->_children, not($element));
345
346 1
        foreach (array_diff($old, $this->_children) as $removed) {
347 1
            if($removed instanceof XmlElement) {
348 1
                $removed->_parent = null;
349
            }
350
        }
351 1
    }
352
353 15
    protected function appendChild($element) {
354 15
        if (!is_string($element) && !$element instanceof XmlElement) {
355 1
            throw new InvalidArgumentException(helper\format(
356 1
                '$element should be either string or object of {class} class. or array of given types, {type} given', [
357 1
                    'class' => XmlElement::class,
358 1
                    'type'  => helper\typeof($element)
359
                ]
360
            ));
361
        }
362
363 14
        if ($element instanceof XmlElement) {
364 12
            $element->parent = $this;
365
        }
366
367 14
        return $this->_children[] = $element;
368
    }
369
370
    /**
371
     * Returns namespace URI associated with element or specified prefix
372
     *
373
     * @param string|bool|null $prefix
374
     * @return false|string
375
     */
376 19
    public function getNamespace($prefix = false)
377
    {
378 19
        if ($prefix === false) {
379 19
            $prefix = $this->prefix;
380
        }
381
382 19
        return $this->lookupUri($prefix);
0 ignored issues
show
Bug introduced by
It seems like $prefix defined by parameter $prefix on line 376 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...
383
    }
384
385
    /**
386
     * Adds namespace to element, and associates it with prefix.
387
     *
388
     * @param string           $uri    Namespace URI
389
     * @param string|bool|null $prefix Prefix which will be used for namespace, false for using element's prefix
390
     *                                 and null for no prefix
391
     */
392 17
    public function setNamespace(string $uri, $prefix = false)
393
    {
394 17
        if ($prefix === false) {
395 15
            $prefix = $this->_prefix;
396
        }
397
398 17
        $this->_namespaces[ $uri ] = $prefix;
399 17
    }
400
401 8
    public function getFullName()
402
    {
403 8
        return ($this->_prefix ? $this->prefix . ':' : null) . $this->localName;
404
    }
405
406 8
    public function getChildren()
407
    {
408 8
        return $this->_children;
409
    }
410
411 19
    public function getPrefix()
412
    {
413 19
        return $this->_prefix;
414
    }
415
416 13
    public function getLocalName()
417
    {
418 13
        return $this->_localName;
419
    }
420
421 7
    public function getAttributes()
422
    {
423 7
        return $this->_attributes;
424
    }
425
426 2
    protected function setAttributes(array $attributes)
427
    {
428 2
        $this->_attributes = [];
429
430 2
        foreach ($attributes as $attribute => $value) {
431 2
            $this->setAttribute($attribute, $value);
432
        }
433 2
    }
434
435
    /**
436
     * Returns one element at specified index (for default the first one).
437
     *
438
     * @param string $name  Requested element tag name
439
     * @param string $uri   Requested element namespace
440
     * @param int    $index Index of element to retrieve
441
     *
442
     * @return XmlElement|false Retrieved element
443
     */
444 1
    public function element(string $name, string $uri = null, int $index = 0)
445
    {
446 1
        return array_values($this->elements($name, $uri))[ $index ] ?? false;
447
    }
448
449
    /**
450
     * Retrieves array of matching elements
451
     *
452
     * @param string      $name Requested element tag name
453
     * @param string|null $uri  Requested element namespace
454
     *
455
     * @return XmlElement[] Found Elements
456
     */
457 2
    public function elements($name, $uri = null) : array
458
    {
459 2
        $predicate = filter\element\name($name);
460 2
        if ($uri !== null) {
461 1
            $predicate = filter\all($predicate, filter\element\xmlns($uri));
462
        }
463
464 2
        return $this->all($predicate);
465
    }
466
467
    /**
468
     * Filters element with given predicate
469
     *
470
     * @param callable|string $predicate Predicate or class name
471
     *
472
     * @return XmlElement[]
473
     */
474 2
    public function all($predicate)
475
    {
476 2
        return array_values(array_filter($this->_children, filter\predicate($predicate)));
477
    }
478
479
    /**
480
     * Iterates over matching elements
481
     *
482
     * @param callable|string $predicate Predicate or class name
483
     *
484
     * @return XmlElement|false
485
     */
486 2
    public function get($predicate)
487
    {
488 2
        $predicate = filter\predicate($predicate);
489 2
        foreach ($this->_children as $index => $child) {
490 2
            if ($predicate($child)) {
491 2
                return $child;
492
            }
493
        }
494
495 2
        return false;
496
    }
497
498
    /**
499
     * Checks if any element matching predicate exists
500
     *
501
     * @param callable|string $predicate Predicate or class name
502
     *
503
     * @return bool
504
     */
505 1
    public function has($predicate)
506
    {
507 1
        return $this->get($predicate) !== false;
508
    }
509
510
    /**
511
     * @param string|null $query
512
     * @return XPathQuery
513
     */
514 1
    public function query(string $query = null)
515
    {
516 1
        return new XPathQuery($query, $this);
517
    }
518
519
    /**
520
     * Helper for retrieving all arguments (including namespaces)
521
     *
522
     * @return array
523
     */
524 4
    private function attributes(): array
525
    {
526 4
        $namespaces = $this->getNamespaces(false);
527 4
        $namespaces = array_map(function ($prefix, $uri) {
528 2
            return [$prefix ? "xmlns:{$prefix}" : 'xmlns', $uri];
529 4
        }, array_values($namespaces), array_keys($namespaces));
530
531 4
        return array_merge(
532 4
            $this->_attributes,
533 4
            array_combine(array_column($namespaces, 0), array_column($namespaces, 1))
534
        );
535
    }
536
537
    /**
538
     * Prefixes $name with attribute associated with $uri
539
     *
540
     * @param string $name Name to prefix
541
     * @param string $uri  Namespace URI
542
     *
543
     * @return string
544
     */
545 6
    protected function _prefix(string $name, string $uri = null): string
546
    {
547 6
        if ($uri === null) {
548 5
            return $name;
549
        }
550
551 3
        if (($prefix = $this->lookupPrefix($uri)) === false) {
552 1
            throw new InvalidArgumentException(helper\format('URI "{uri}" is not a registered namespace', ['uri' => $uri]));
553
        }
554
555 2
        return "{$prefix}:{$name}";
556
    }
557
558 3
    public function __toString()
559
    {
560 3
        return trim($this->xml(true));
561
    }
562
563
    /**
564
     * Splits name into local-name and prefix
565
     *
566
     * @param $name
567
     * @return array [$name, $prefix]
568
     */
569 25
    public static function resolve($name)
570
    {
571 25
        $prefix = null;
572 25
        if (($pos = strpos($name, ':')) !== false) {
573 2
            $prefix = substr($name, 0, $pos);
574 2
            $name   = substr($name, $pos + 1);
575
        }
576
577 25
        return [$name, $prefix];
578
    }
579
580
    /**
581
     * Casts XML Element object to another class, it's not recommended but should work, as child classes should
582
     * only decorate parent with additional getters and setters for accessing data.
583
     *
584
     * @param XmlElement $element
585
     * @return static
586
     */
587 1
    public static function cast(XmlElement $element)
588
    {
589
        /** @var static $return */
590 1
        $return = (new \ReflectionClass(static::class))->newInstanceWithoutConstructor();
591 1
        foreach (get_object_vars($element) as $property => $value) {
592 1
            $return->$property = $value;
593
        }
594
595 1
        return $return;
596
    }
597
598
    /**
599
     * When an object is cloned, PHP 5 will perform a shallow copy of all of the object's properties.
600
     * Any properties that are references to other variables, will remain references.
601
     * Once the cloning is complete, if a __clone() method is defined,
602
     * then the newly created object's __clone() method will be called, to allow any necessary properties that need to be changed.
603
     * NOT CALLABLE DIRECTLY.
604
     *
605
     * @link http://php.net/manual/en/language.oop5.cloning.php
606
     */
607
    public function __clone()
608
    {
609
        $children = $this->_children;
610
        $this->_children = [];
611
        $this->_parent = null;
612
613
        foreach ($children as $child) {
614
            $this->append(is_object($child) ? clone $child : $child);
615
        }
616
    }
617
}
618