Completed
Push — master ( e454e2...30b9c0 )
by Kacper
02:38
created

XmlElement::hasAttribute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 4
ccs 0
cts 0
cp 0
crap 2
rs 10
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 8
            $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 9
    public function lookupPrefix(string $uri = null)
177
    {
178 9
        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 13
    public function lookupUri(string $prefix = null)
188
    {
189 13
        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 8
        if ($parent) {
205 8
            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
     * For `http://www.w3.org/2000/xmlns/` URI it acts like `setNamespace($value, $attribute)`
215
     *
216
     * @param string      $attribute Attribute name, optionally with prefix
217
     * @param mixed       $value     Attribute value
218
     * @param string|null $uri       XML Namespace URI of attribute, prefix will be automatically looked up
219
     */
220 5
    public function setAttribute(string $attribute, $value, string $uri = null)
221
    {
222 5
        if ($uri === 'http://www.w3.org/2000/xmlns/') {
223 1
            $this->setNamespace($value, $attribute);
224 1
            return;
225
        }
226
227 4
        $this->_attributes[$this->_prefix($attribute, $uri)] = $value;
228 3
    }
229
230
    /**
231 3
     * Returns value of specified attribute.
232 3
     *
233
     * For `http://www.w3.org/2000/xmlns/` URI it acts like `lookupUri($attribute)`
234
     *
235
     * @param string      $attribute Attribute name, optionally with prefix
236
     * @param string|null $uri       XML Namespace URI of attribute, prefix will be automatically looked up
237
     * @return bool|mixed
238
     */
239
    public function getAttribute(string $attribute, string $uri = null)
240
    {
241
        if ($uri === 'http://www.w3.org/2000/xmlns/') {
242
            return $this->lookupUri($attribute);
243 2
        }
244
245 2
        return $this->_attributes[$this->_prefix($attribute, $uri)] ?? false;
246
    }
247
248
    /**
249 2
     * Checks if attribute exists
250 1
     *
251
     * @param string      $attribute Attribute name, optionally with prefix
252
     * @param string|null $uri       XML Namespace URI of attribute, prefix will be automatically looked up
253 2
     *
254
     * @return bool
255
     */
256
    public function hasAttribute(string $attribute, string $uri = null)
257
    {
258
        return isset($this->_attributes[$this->_prefix($attribute, $uri)]);
259
    }
260 1
261
    /**
262 1
     * Returns element's parent
263
     * @return XmlElement|null
264
     */
265
    public function getParent()
266
    {
267
        return $this->_parent;
268
    }
269 8
270
    /**
271 8
     * Sets element's parent
272 1
     * @param XmlElement $parent
273 1
     */
274
    protected function setParent(XmlElement $parent)
275
    {
276 8
        if (!$this->_prefix && ($prefix = $parent->lookupPrefix($this->namespace)) !== false) {
277 8
            $this->_namespaces[$this->namespace] = $prefix;
278 3
            $this->_prefix = $prefix;
279
        }
280 8
281
        $this->_parent = $parent;
282
        if ($this->namespace === false) {
283
            $this->namespace = $parent->namespace;
284
        }
285
    }
286
287
    /**
288
     * Appends child to element
289 10
     *
290
     * @param XmlElement|string $element
291 10
     *
292 1
     * @return XmlElement|string Same as $element
293 1
     */
294 1
    public function append($element)
295
    {
296
        if (!is_string($element) && !$element instanceof XmlElement) {
297
            throw new InvalidArgumentException(helper\format('$element should be either string or object of {class} class, {type} given', [
298 9
                'class' => XmlElement::class,
299 8
                'type' => helper\typeof($element)
300
            ]));
301
        }
302 9
303
        if ($element instanceof XmlElement) {
304
            $element->parent = $this;
305
        }
306
307
        return $this->_children[] = $element;
308
    }
309
310 13
    /**
311
     * Returns namespace URI associated with element
312 13
     *
313
     * @return false|string
314
     */
315
    public function getNamespace()
316
    {
317
        return $this->lookupUri($this->prefix);
318
    }
319
320
    /**
321
     * Adds namespace to element, and associates it with prefix.
322 13
     *
323
     * @param string           $uri    Namespace URI
324 13
     * @param string|bool|null $prefix Prefix which will be used for namespace, false for using element's prefix
325 10
     *                                 and null for no prefix
326
     */
327
    public function setNamespace(string $uri, $prefix = false)
328 13
    {
329 13
        if ($prefix === false) {
330
            $prefix = $this->_prefix;
331 6
        }
332
333 6
        $this->_namespaces[$uri] = $prefix;
334
    }
335
336 5
    public function getName()
337
    {
338 5
        return ($this->_prefix ? $this->prefix . ':' : null) . $this->localName;
339
    }
340
341 13
    public function getChildren()
342
    {
343 13
        return $this->_children;
344
    }
345
346 8
    public function getPrefix()
347
    {
348 8
        return $this->_prefix;
349
    }
350
351 6
    public function getLocalName()
352
    {
353 6
        return $this->_localName;
354
    }
355
356
    public function getAttributes()
357
    {
358
        return $this->_attributes;
359
    }
360
361
    /**
362
     * Returns one element at specified index (for default the first one).
363
     *
364
     * @param string $name  Requested element tag name
365 1
     * @param string $uri   Requested element namespace
366
     * @param int    $index Index of element to retrieve
367 1
     *
368
     * @return XmlElement|false Retrieved element
369
     */
370
    public function element(string $name, string $uri = null, int $index = 0)
371
    {
372
        return array_values($this->elements($name, $uri))[$index] ?? false;
373
    }
374
375
    /**
376
     * Retrieves array of matching elements
377
     *
378 2
     * @param string      $name Requested element tag name
379
     * @param string|null $uri  Requested element namespace
380 2
     *
381 2
     * @return XmlElement[] Found Elements
382 1
     */
383
    public function elements($name, $uri = null) : array
384
    {
385 2
        $predicate = filter\tag($name);
386
        if ($uri !== null) {
387
            $predicate = filter\all($predicate, filter\xmlns($uri));
388
        }
389
390
        return $this->all($predicate);
391
    }
392
393
    /**
394
     * Filters element with given predicate
395 2
     *
396
     * @param callable|string $predicate Predicate or class name
397 2
     *
398
     * @return XmlElement[]
399
     */
400
    public function all($predicate)
401
    {
402
        return array_filter($this->_children, filter\predicate($predicate));
403
    }
404 1
405
    /**
406 1
     * @param string|null $query
407
     * @return XPathQuery
408
     */
409
    public function query(string $query = null)
410
    {
411
        return new XPathQuery($query, $this);
412
    }
413
414 2
    /**
415
     * Helper for retrieving all arguments (including namespaces)
416 2
     *
417 2
     * @return array
418 1
     */
419 2
    private function attributes(): array
420
    {
421 2
        $namespaces = $this->getNamespaces(false);
422 2
        $namespaces = array_map(function ($prefix, $uri) {
423 2
            return [$prefix ? "xmlns:{$prefix}" : 'xmlns', $uri];
424
        }, array_values($namespaces), array_keys($namespaces));
425
426
        return array_merge(
427
            $this->_attributes,
428
            array_combine(array_column($namespaces, 0), array_column($namespaces, 1))
429
        );
430
    }
431
432
    /**
433
     * Prefixes $name with attribute associated with $uri
434
     *
435 3
     * @param string $name Name to prefix
436
     * @param string $uri  Namespace URI
437 3
     *
438 1
     * @return string
439
     */
440
    protected function _prefix(string $name, string $uri = null): string
441 2
    {
442
        if($uri === null) {
443
            return $name;
444 1
        }
445
446 1
        if (($prefix = $this->lookupPrefix($uri)) === false) {
447
            throw new InvalidArgumentException(helper\format('URI "{uri}" is not a registered namespace', ['uri' => $uri]));
448
        }
449
450
        return "{$prefix}:{$name}";
451
    }
452
453
    public function __toString()
454
    {
455 19
        return trim($this->xml(true));
456
    }
457 19
458 19
    /**
459 2
     * Splits name into local-name and prefix
460 2
     *
461
     * @param $name
462
     * @return array [$name, $prefix]
463 19
     */
464
    public static function resolve($name)
465
    {
466
        $prefix = null;
467
        if (($pos = strpos($name, ':')) !== false) {
468
            $prefix = substr($name, 0, $pos);
469
            $name = substr($name, $pos + 1);
470
        }
471
472
        return [$name, $prefix];
473
    }
474
}
475