Completed
Push — master ( 5f3a6c...fcd09b )
by Kacper
05:50
created

XmlElement::setInnerXml()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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