Completed
Push — master ( 4799e1...634839 )
by Kacper
02:53
created

XmlElement::append()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 6.105

Importance

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