Completed
Push — master ( 1d0faf...7a5aed )
by Kacper
05:34
created

XmlElement::__toString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
ccs 2
cts 2
cp 1
crap 1
rs 10
c 1
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 20
    protected function init(string $name, string $uri = null)
84
    {
85 20
        list($name, $prefix) = self::resolve($name);
86
87 20
        $this->_localName = $name;
88 20
        $this->_prefix    = $prefix;
89
90 20
        if ($uri !== null) {
91 9
            $this->namespace = $uri;
92
        }
93 20
    }
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 16
    public function __construct(string $name, string $uri = null)
102
    {
103 16
        $this->init($name, $uri);
104 16
    }
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 3
    public function xml(bool $clean = true): string
149
    {
150 3
        if ($this->namespace && $this->_prefix === null) {
151 1
            $this->_prefix = $this->lookupPrefix($this->namespace);
152
        }
153
154 3
        $attributes = $this->attributes();
155
156 3
        $result = "<{$this->name}";
157
        $result .= ' ' . implode(' ', array_map(function ($key, $value) {
158 1
                return $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"';
159 3
            }, array_keys($attributes), array_values($attributes)));
160
161 3
        if (!empty($this->_children)) {
162 1
            $result .= ">{$this->innerXml}</{$this->name}>";
163
        } else {
164 3
            $result .= "/>";
165
        }
166
167 3
        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 15
    public function lookupUri(string $prefix = null)
188
    {
189 15
        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 17
    public function getNamespaces($parent = true): array
199
    {
200 17
        if (!$this->_parent) {
201 17
            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 12
    public function append($element)
282
    {
283 12
        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 11
        if (empty($element)) {
291 1
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by Kadet\Xmpp\Xml\XmlElement::append of type Kadet\Xmpp\Xml\XmlElement|string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
292
        }
293
294 10
        if ($element instanceof XmlElement) {
295 9
            $element->parent = $this;
296
        }
297
298 10
        return $this->_children[] = $element;
299
    }
300
301
    /**
302
     * Returns namespace URI associated with element
303
     *
304
     * @return false|string
305
     */
306 15
    public function getNamespace()
307
    {
308 15
        return $this->lookupUri($this->prefix);
309
    }
310
311
    /**
312
     * Adds namespace to element, and associates it with prefix.
313
     *
314
     * @param string           $uri    Namespace URI
315
     * @param string|bool|null $prefix Prefix which will be used for namespace, false for using element's prefix
316
     *                                 and null for no prefix
317
     */
318 13
    public function setNamespace(string $uri, $prefix = false)
319
    {
320 13
        if ($prefix === false) {
321 11
            $prefix = $this->_prefix;
322
        }
323
324 13
        $this->_namespaces[$uri] = $prefix;
325 13
    }
326
327 7
    public function getName()
328
    {
329 7
        return ($this->_prefix ? $this->prefix . ':' : null) . $this->localName;
330
    }
331
332 5
    public function getChildren()
333
    {
334 5
        return $this->_children;
335
    }
336
337 15
    public function getPrefix()
338
    {
339 15
        return $this->_prefix;
340
    }
341
342 10
    public function getLocalName()
343
    {
344 10
        return $this->_localName;
345
    }
346
347 6
    public function getAttributes()
348
    {
349 6
        return $this->_attributes;
350
    }
351
352
    /**
353
     * Returns one element at specified index (for default the first one).
354
     *
355
     * @param string $name  Requested element tag name
356
     * @param string $uri   Requested element namespace
357
     * @param int    $index Index of element to retrieve
358
     *
359
     * @return XmlElement|false Retrieved element
360
     */
361 1
    public function element(string $name, string $uri = null, int $index = 0)
362
    {
363 1
        return array_values($this->elements($name, $uri))[$index] ?? false;
364
    }
365
366
    /**
367
     * Retrieves array of matching elements
368
     *
369
     * @param string      $name Requested element tag name
370
     * @param string|null $uri  Requested element namespace
371
     *
372
     * @return XmlElement[] Found Elements
373
     */
374 2
    public function elements($name, $uri = null) : array
375
    {
376 2
        $predicate = filter\tag($name);
377 2
        if ($uri !== null) {
378 1
            $predicate = filter\all($predicate, filter\xmlns($uri));
379
        }
380
381 2
        return $this->all($predicate);
382
    }
383
384
    /**
385
     * Filters element with given predicate
386
     *
387
     * @param callable|string $predicate Predicate or class name
388
     *
389
     * @return XmlElement[]
390
     */
391 2
    public function all($predicate)
392
    {
393 2
        return array_values(array_filter($this->_children, filter\predicate($predicate)));
394
    }
395
396
    /**
397
     * Iterates over matching elements
398
     *
399
     * @param callable|string $predicate Predicate or class name
400
     *
401
     * @return XmlElement|false
402
     */
403 1
    public function get($predicate)
404
    {
405 1
        $predicate = filter\predicate($predicate);
406 1
        foreach ($this->_children as $index => $child) {
407 1
            if ($predicate($child)) {
408 1
                return $child;
409
            }
410
        }
411
412 1
        return false;
413
    }
414
415
    /**
416
     * @param string|null $query
417
     * @return XPathQuery
418
     */
419 1
    public function query(string $query = null)
420
    {
421 1
        return new XPathQuery($query, $this);
422
    }
423
424
    /**
425
     * Helper for retrieving all arguments (including namespaces)
426
     *
427
     * @return array
428
     */
429 3
    private function attributes(): array
430
    {
431 3
        $namespaces = $this->getNamespaces(false);
432 3
        $namespaces = array_map(function ($prefix, $uri) {
433 1
            return [$prefix ? "xmlns:{$prefix}" : 'xmlns', $uri];
434 3
        }, array_values($namespaces), array_keys($namespaces));
435
436 3
        return array_merge(
437 3
            $this->_attributes,
438 3
            array_combine(array_column($namespaces, 0), array_column($namespaces, 1))
439
        );
440
    }
441
442
    /**
443
     * Prefixes $name with attribute associated with $uri
444
     *
445
     * @param string $name Name to prefix
446
     * @param string $uri  Namespace URI
447
     *
448
     * @return string
449
     */
450 4
    protected function _prefix(string $name, string $uri = null): string
451
    {
452 4
        if ($uri === null) {
453 3
            return $name;
454
        }
455
456 3
        if (($prefix = $this->lookupPrefix($uri)) === false) {
457 1
            throw new InvalidArgumentException(helper\format('URI "{uri}" is not a registered namespace', ['uri' => $uri]));
458
        }
459
460 2
        return "{$prefix}:{$name}";
461
    }
462
463 2
    public function __toString()
464
    {
465 2
        return trim($this->xml(true));
466
    }
467
468
    /**
469
     * Splits name into local-name and prefix
470
     *
471
     * @param $name
472
     * @return array [$name, $prefix]
473
     */
474 20
    public static function resolve($name)
475
    {
476 20
        $prefix = null;
477 20
        if (($pos = strpos($name, ':')) !== false) {
478 2
            $prefix = substr($name, 0, $pos);
479 2
            $name   = substr($name, $pos + 1);
480
        }
481
482 20
        return [$name, $prefix];
483
    }
484
}
485