Completed
Push — master ( 596cf5...e454e2 )
by Kacper
04:16
created

XmlElement::setParent()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 7
nc 4
nop 1
dl 0
loc 12
ccs 8
cts 8
cp 1
crap 4
rs 9.2
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 */
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);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->lookupPrefix($this->namespace) can also be of type false. However, the property $_prefix is declared as type null|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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 View Code Duplication
    public function setAttribute(string $attribute, $value, string $uri = null)
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
221
    {
222 5
        if ($uri === 'http://www.w3.org/2000/xmlns/') {
223 1
            $this->setNamespace($value, $attribute);
224 1
            return;
225
        }
226
227 4
        if ($uri !== null) {
228 3
            $attribute = $this->_prefix($attribute, $uri);
229
        }
230
231 3
        $this->_attributes[$attribute] = $value;
232 3
    }
233
234
    /**
235
     * Returns value of specified attribute.
236
     *
237
     * For `http://www.w3.org/2000/xmlns/` URI it acts like `lookupUri($attribute)`
238
     *
239
     * @param string      $attribute
240
     * @param string|null $uri
241
     * @return bool|mixed
242
     */
243 2 View Code Duplication
    public function getAttribute(string $attribute, string $uri = null)
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
244
    {
245 2
        if ($uri === 'http://www.w3.org/2000/xmlns/') {
246
            return $this->lookupUri($attribute);
247
        }
248
249 2
        if ($uri !== null) {
250 1
            $attribute = $this->_prefix($attribute, $uri);
251
        }
252
253 2
        return $this->_attributes[$attribute] ?? false;
254
    }
255
256
    /**
257
     * Returns element's parent
258
     * @return XmlElement|null
259
     */
260 1
    public function getParent()
261
    {
262 1
        return $this->_parent;
263
    }
264
265
    /**
266
     * Sets element's parent
267
     * @param XmlElement $parent
268
     */
269 8
    protected function setParent(XmlElement $parent)
270
    {
271 8
        if (!$this->_prefix && ($prefix = $parent->lookupPrefix($this->namespace)) !== false) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->_prefix of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
272 1
            $this->_namespaces[$this->namespace] = $prefix;
273 1
            $this->_prefix = $prefix;
274
        }
275
276 8
        $this->_parent = $parent;
277 8
        if ($this->namespace === false) {
278 3
            $this->namespace = $parent->namespace;
279
        }
280 8
    }
281
282
    /**
283
     * Appends child to element
284
     *
285
     * @param XmlElement|string $element
286
     *
287
     * @return XmlElement|string Same as $element
288
     */
289 10
    public function append($element)
290
    {
291 10
        if (!is_string($element) && !$element instanceof XmlElement) {
292 1
            throw new InvalidArgumentException(helper\format('$element should be either string or object of {class} class, {type} given', [
293 1
                'class' => XmlElement::class,
294 1
                'type' => helper\typeof($element)
295
            ]));
296
        }
297
298 9
        if ($element instanceof XmlElement) {
299 8
            $element->parent = $this;
300
        }
301
302 9
        return $this->_children[] = $element;
303
    }
304
305
    /**
306
     * Returns namespace URI associated with element
307
     *
308
     * @return false|string
309
     */
310 13
    public function getNamespace()
311
    {
312 13
        return $this->lookupUri($this->prefix);
313
    }
314
315
    /**
316
     * Adds namespace to element, and associates it with prefix.
317
     *
318
     * @param string           $uri    Namespace URI
319
     * @param string|bool|null $prefix Prefix which will be used for namespace, false for using element's prefix
320
     *                                 and null for no prefix
321
     */
322 13
    public function setNamespace(string $uri, $prefix = false)
323
    {
324 13
        if ($prefix === false) {
325 10
            $prefix = $this->_prefix;
326
        }
327
328 13
        $this->_namespaces[$uri] = $prefix;
329 13
    }
330
331 6
    public function getName()
332
    {
333 6
        return ($this->_prefix ? $this->prefix . ':' : null) . $this->localName;
334
    }
335
336 5
    public function getChildren()
337
    {
338 5
        return $this->_children;
339
    }
340
341 13
    public function getPrefix()
342
    {
343 13
        return $this->_prefix;
344
    }
345
346 8
    public function getLocalName()
347
    {
348 8
        return $this->_localName;
349
    }
350
351 6
    public function getAttributes()
352
    {
353 6
        return $this->_attributes;
354
    }
355
356
    /**
357
     * Returns one element at specified index (for default the first one).
358
     *
359
     * @param string $name  Requested element tag name
360
     * @param string $uri   Requested element namespace
361
     * @param int    $index Index of element to retrieve
362
     *
363
     * @return XmlElement|false Retrieved element
364
     */
365 1
    public function element(string $name, string $uri = null, int $index = 0)
366
    {
367 1
        return array_values($this->elements($name, $uri))[$index] ?? false;
0 ignored issues
show
Bug introduced by
It seems like $uri defined by parameter $uri on line 365 can also be of type string; however, Kadet\Xmpp\Xml\XmlElement::elements() does only seem to accept null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
368
    }
369
370
    /**
371
     * Retrieves array of matching elements
372
     *
373
     * @param string $name Requested element tag name
374
     * @param null   $uri  Requested element namespace
375
     *
376
     * @return XmlElement[] Found Elements
377
     */
378 2
    public function elements($name, $uri = null) : array
379
    {
380 2
        $predicate = filter\tag($name);
381 2
        if ($uri !== null) {
382 1
            $predicate = filter\all($predicate, filter\xmlns($uri));
383
        }
384
385 2
        return $this->all($predicate);
386
    }
387
388
    /**
389
     * Filters element with given predicate
390
     *
391
     * @param callable|string $predicate Predicate or class name
392
     *
393
     * @return XmlElement[]
394
     */
395 2
    public function all($predicate)
396
    {
397 2
        return array_filter($this->_children, filter\predicate($predicate));
398
    }
399
400
    /**
401
     * @param string|null $query
402
     * @return XPathQuery
403
     */
404 1
    public function query(string $query = null)
405
    {
406 1
        return new XPathQuery($query, $this);
407
    }
408
409
    /**
410
     * Helper for retrieving all arguments (including namespaces)
411
     *
412
     * @return array
413
     */
414 2
    private function attributes(): array
415
    {
416 2
        $namespaces = $this->getNamespaces(false);
417 2
        $namespaces = array_map(function ($prefix, $uri) {
418 1
            return [$prefix ? "xmlns:{$prefix}" : 'xmlns', $uri];
419 2
        }, array_values($namespaces), array_keys($namespaces));
420
421 2
        return array_merge(
422 2
            $this->_attributes,
423 2
            array_combine(array_column($namespaces, 0), array_column($namespaces, 1))
424
        );
425
    }
426
427
    /**
428
     * Prefixes $name with attribute associated with $uri
429
     *
430
     * @param string $name Name to prefix
431
     * @param string $uri  Namespace URI
432
     *
433
     * @return string
434
     */
435 3
    protected function _prefix(string $name, string $uri): string
436
    {
437 3
        if (($prefix = $this->lookupPrefix($uri)) === false) {
438 1
            throw new InvalidArgumentException(helper\format('URI "{uri}" is not a registered namespace', ['uri' => $uri]));
439
        }
440
441 2
        return "{$prefix}:{$name}";
442
    }
443
444 1
    public function __toString()
445
    {
446 1
        return trim($this->xml(true));
447
    }
448
449
    /**
450
     * Splits name into local-name and prefix
451
     *
452
     * @param $name
453
     * @return array [$name, $prefix]
454
     */
455 19
    public static function resolve($name)
456
    {
457 19
        $prefix = null;
458 19
        if (($pos = strpos($name, ':')) !== false) {
459 2
            $prefix = substr($name, 0, $pos);
460 2
            $name = substr($name, $pos + 1);
461
        }
462
463 19
        return [$name, $prefix];
464
    }
465
}
466