Completed
Push — master ( 939cef...4aee11 )
by Kacper
03:59
created

XmlElement::cast()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 10
cc 2
eloc 5
nc 2
nop 1
ccs 5
cts 5
cp 1
crap 2
rs 9.4285
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          $name       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 3
    public function xml(bool $clean = true): string
176
    {
177 3
        if ($this->namespace && $this->_prefix === null) {
178 1
            $this->_prefix = $this->lookupPrefix($this->namespace);
179
        }
180
181 3
        $attributes = $this->attributes();
182
183 3
        $result = "<{$this->name}";
184
        $result .= ' ' . implode(' ', array_map(function ($key, $value) {
185 1
            return $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"';
186 3
        }, array_keys($attributes), array_values($attributes)));
187
188 3
        if (!empty($this->_children)) {
189 1
            $result .= ">{$this->innerXml}</{$this->name}>";
190
        } else {
191 3
            $result .= "/>";
192
        }
193
194 3
        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 1
            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
        $this->_children = array_filter($this->_children, not(filter\same($element)));
341 1
        if($element instanceof XmlElement) {
342 1
            $element->_parent = null;
343
        }
344 1
    }
345
346 15
    protected function appendChild($element) {
347 15
        if (!is_string($element) && !$element instanceof XmlElement) {
348 1
            throw new InvalidArgumentException(helper\format(
349 1
                '$element should be either string or object of {class} class. or array of given types, {type} given', [
350 1
                    'class' => XmlElement::class,
351 1
                    'type'  => helper\typeof($element)
352
                ]
353
            ));
354
        }
355
356 14
        if ($element instanceof XmlElement) {
357 12
            $element->parent = $this;
358
        }
359
360 14
        return $this->_children[] = $element;
361
    }
362
363
    /**
364
     * Returns namespace URI associated with element or specified prefix
365
     *
366
     * @param string|bool|null $prefix
367
     * @return false|string
368
     */
369 19
    public function getNamespace($prefix = false)
370
    {
371 19
        if ($prefix === false) {
372 19
            $prefix = $this->prefix;
373
        }
374
375 19
        return $this->lookupUri($prefix);
0 ignored issues
show
Bug introduced by
It seems like $prefix defined by parameter $prefix on line 369 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...
376
    }
377
378
    /**
379
     * Adds namespace to element, and associates it with prefix.
380
     *
381
     * @param string           $uri    Namespace URI
382
     * @param string|bool|null $prefix Prefix which will be used for namespace, false for using element's prefix
383
     *                                 and null for no prefix
384
     */
385 17
    public function setNamespace(string $uri, $prefix = false)
386
    {
387 17
        if ($prefix === false) {
388 15
            $prefix = $this->_prefix;
389
        }
390
391 17
        $this->_namespaces[ $uri ] = $prefix;
392 17
    }
393
394 7
    public function getName()
395
    {
396 7
        return ($this->_prefix ? $this->prefix . ':' : null) . $this->localName;
397
    }
398
399 8
    public function getChildren()
400
    {
401 8
        return $this->_children;
402
    }
403
404 19
    public function getPrefix()
405
    {
406 19
        return $this->_prefix;
407
    }
408
409 12
    public function getLocalName()
410
    {
411 12
        return $this->_localName;
412
    }
413
414 7
    public function getAttributes()
415
    {
416 7
        return $this->_attributes;
417
    }
418
419 2
    protected function setAttributes(array $attributes)
420
    {
421 2
        $this->_attributes = [];
422
423 2
        foreach ($attributes as $attribute => $value) {
424 2
            $this->setAttribute($attribute, $value);
425
        }
426 2
    }
427
428
    /**
429
     * Returns one element at specified index (for default the first one).
430
     *
431
     * @param string $name  Requested element tag name
432
     * @param string $uri   Requested element namespace
433
     * @param int    $index Index of element to retrieve
434
     *
435
     * @return XmlElement|false Retrieved element
436
     */
437 1
    public function element(string $name, string $uri = null, int $index = 0)
438
    {
439 1
        return array_values($this->elements($name, $uri))[ $index ] ?? false;
440
    }
441
442
    /**
443
     * Retrieves array of matching elements
444
     *
445
     * @param string      $name Requested element tag name
446
     * @param string|null $uri  Requested element namespace
447
     *
448
     * @return XmlElement[] Found Elements
449
     */
450 2
    public function elements($name, $uri = null) : array
451
    {
452 2
        $predicate = filter\element\name($name);
453 2
        if ($uri !== null) {
454 1
            $predicate = filter\all($predicate, filter\element\xmlns($uri));
455
        }
456
457 2
        return $this->all($predicate);
458
    }
459
460
    /**
461
     * Filters element with given predicate
462
     *
463
     * @param callable|string $predicate Predicate or class name
464
     *
465
     * @return XmlElement[]
466
     */
467 2
    public function all($predicate)
468
    {
469 2
        return array_values(array_filter($this->_children, filter\predicate($predicate)));
470
    }
471
472
    /**
473
     * Iterates over matching elements
474
     *
475
     * @param callable|string $predicate Predicate or class name
476
     *
477
     * @return XmlElement|false
478
     */
479 2
    public function get($predicate)
480
    {
481 2
        $predicate = filter\predicate($predicate);
482 2
        foreach ($this->_children as $index => $child) {
483 2
            if ($predicate($child)) {
484 2
                return $child;
485
            }
486
        }
487
488 2
        return false;
489
    }
490
491
    /**
492
     * Checks if any element matching predicate exists
493
     *
494
     * @param callable|string $predicate Predicate or class name
495
     *
496
     * @return bool
497
     */
498 1
    public function has($predicate)
499
    {
500 1
        return $this->get($predicate) !== false;
501
    }
502
503
    /**
504
     * @param string|null $query
505
     * @return XPathQuery
506
     */
507 1
    public function query(string $query = null)
508
    {
509 1
        return new XPathQuery($query, $this);
510
    }
511
512
    /**
513
     * Helper for retrieving all arguments (including namespaces)
514
     *
515
     * @return array
516
     */
517 3
    private function attributes(): array
518
    {
519 3
        $namespaces = $this->getNamespaces(false);
520 3
        $namespaces = array_map(function ($prefix, $uri) {
521 1
            return [$prefix ? "xmlns:{$prefix}" : 'xmlns', $uri];
522 3
        }, array_values($namespaces), array_keys($namespaces));
523
524 3
        return array_merge(
525 3
            $this->_attributes,
526 3
            array_combine(array_column($namespaces, 0), array_column($namespaces, 1))
527
        );
528
    }
529
530
    /**
531
     * Prefixes $name with attribute associated with $uri
532
     *
533
     * @param string $name Name to prefix
534
     * @param string $uri  Namespace URI
535
     *
536
     * @return string
537
     */
538 6
    protected function _prefix(string $name, string $uri = null): string
539
    {
540 6
        if ($uri === null) {
541 5
            return $name;
542
        }
543
544 3
        if (($prefix = $this->lookupPrefix($uri)) === false) {
545 1
            throw new InvalidArgumentException(helper\format('URI "{uri}" is not a registered namespace', ['uri' => $uri]));
546
        }
547
548 2
        return "{$prefix}:{$name}";
549
    }
550
551 2
    public function __toString()
552
    {
553 2
        return trim($this->xml(true));
554
    }
555
556
    /**
557
     * Splits name into local-name and prefix
558
     *
559
     * @param $name
560
     * @return array [$name, $prefix]
561
     */
562 25
    public static function resolve($name)
563
    {
564 25
        $prefix = null;
565 25
        if (($pos = strpos($name, ':')) !== false) {
566 2
            $prefix = substr($name, 0, $pos);
567 2
            $name   = substr($name, $pos + 1);
568
        }
569
570 25
        return [$name, $prefix];
571
    }
572
573
    /**
574
     * Casts XML Element object to another class, it's not recommended but should work, as child classes should
575
     * only decorate parent with additional getters and setters for accessing data.
576
     *
577
     * @param XmlElement $element
578
     * @return static
579
     */
580 1
    public static function cast(XmlElement $element)
581
    {
582
        /** @var static $return */
583 1
        $return = (new \ReflectionClass(static::class))->newInstanceWithoutConstructor();
584 1
        foreach (get_object_vars($element) as $property => $value) {
585 1
            $return->$property = $value;
586
        }
587
588 1
        return $return;
589
    }
590
}
591