Completed
Push — master ( 30b9c0...3cecaa )
by Kacper
02:48
created

XmlElement::find()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

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