Completed
Push — master ( a265cd...516478 )
by Dan Michael O.
01:04
created

QuiteSimpleXMLElement::__construct()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 3
nop 2
1
<?php
2
/*
3
 * (c) Dan Michael O. Heggø (2013)
4
 *
5
 * QuiteSimpleXMLElement is a wrapper around SimpleXMLElement to add a quite simple
6
 * feature not present in SimpleXMLElement: inheritance of namespaces.
7
 *
8
 * My first attempt was to extend the original SimpleXMLElement class, but
9
 * unfortunately the constructor is static and cannot be overriden!
10
 *
11
 * It's easier to understand with a simple example:
12
 *
13
 *     $xml = '<root xmlns:dc="http://purl.org/dc/elements/1.1/">
14
 *         <dc:a>
15
 *           <dc:b >
16
 *             1.9
17
 *           </dc:a>
18
 *         </dc:b>
19
 *       </root>';
20
 *
21
 *     $root = new SimpleXMLElement($xml);
22
 *     $root->registerXPathNamespace('d', 'http://purl.org/dc/elements/1.1/');
23
 *     $a = $root->xpath('d:a');
24
 *     $a[0]->registerXPathNamespace('d', 'http://purl.org/dc/elements/1.1/');
25
 *     $b = $a[0]->xpath('d:b');
26
 *     echo trim((string)$b[0]);
27
 *
28
 * Since namespaces are not inherited, we have to register them over and over again.
29
 * Using QuiteSimpleXMLElement instead;
30
 *
31
 *     $root = new QuiteSimpleXMLElement($xml);
32
 *     $root->registerXPathNamespace('d', 'http://purl.org/dc/elements/1.1/');
33
 *     $a = $root->xpath('d:a');
34
 *     $b = $a->xpath('d:b');
35
 *     echo trim((string)$b[0]);
36
 *
37
 * And while we're at it, we can add a few convenience methods.
38
 *
39
 * SimpleXmlElement reference:
40
 * https://github.com/draffter/FollowFunctionPHP/blob/master/_php/SimpleXML.php
41
 */
42
43
namespace Danmichaelo\QuiteSimpleXMLElement;
44
45
use Exception;
46
use InvalidArgumentException;
47
use SimpleXMLElement;
48
49
class InvalidXMLException extends Exception
50
{
51
    public function __construct($message = null, $code = 0, Exception $previous = null)
52
    {
53
        parent::__construct($message, $code, $previous);
54
    }
55
}
56
57
class QuiteSimpleXMLElement
58
{
59
    public $namespaces;
60
    public $el;
61
62
    /**
63
     * QuiteSimpleXMLElement constructor.
64
     *
65
     * @param string|SimpleXMLElement|QuiteSimpleXMLElement    $elem
66
     * @param QuiteSimpleXMLElement                            $inherit_from
67
     * @throws InvalidXMLException
68
     * @throws InvalidArgumentException
69
     */
70
    public function __construct($elem, QuiteSimpleXMLElement $inherit_from = null)
71
    {
72
        $this->namespaces = [];
73
74
        $this->el = $this->getElement($elem);
75
76
        if (is_null($inherit_from)) {
77
            $this->namespaces = $this->el->getNamespaces(true);
78
        } else {
79
            foreach ($inherit_from->namespaces as $prefix => $uri) {
80
                $this->registerXPathNamespace($prefix, $uri);
81
            }
82
        }
83
    }
84
85
    /**
86
     * Internal helper method to get a SimpleXMLElement from either a string
87
     * or a SimpleXMLElement/QuiteSimpleXMLElement object.
88
     *
89
     * @param string|SimpleXMLElement|QuiteSimpleXMLElement $elem
90
     * @return SimpleXMLElement
91
     * @throws InvalidXMLException
92
     * @throws InvalidArgumentException
93
     */
94
    protected function getElement($elem)
95
    {
96
        if (gettype($elem) == 'string') {
97
            try {
98
                return new SimpleXMLElement($elem);
99
            } catch (Exception $e) {
100
                throw new InvalidXMLException('Invalid XML encountered: ' . $elem);
101
            }
102
        }
103
104
        if (gettype($elem) == 'object') {
105
            if (in_array(get_class($elem),
106
                ['Danmichaelo\QuiteSimpleXMLElement\QuiteSimpleXMLElement', 'SimpleXMLElement'])) {
107
                return $elem; // assume it's a SimpleXMLElement
0 ignored issues
show
Bug Compatibility introduced by
The expression return $elem; of type SimpleXMLElement|Danmich...t\QuiteSimpleXMLElement is incompatible with the return type documented by Danmichaelo\QuiteSimpleX...eXMLElement::getElement of type SimpleXMLElement as it can also be of type Danmichaelo\QuiteSimpleX...t\QuiteSimpleXMLElement which is not included in this return type.
Loading history...
108
            } else {
109
                throw new InvalidArgumentException('Unknown object given to QuiteSimpleXMLElement. Expected SimpleXMLElement or QuiteSimpleXMLElement.');
110
            }
111
        }
112
113
        throw new InvalidArgumentException('QuiteSimpleXMLElement expects a string or a QuiteSimpleXMLElement/SimpleXMLElement object.');
114
    }
115
116
    /**
117
     * Register a new xpath namespace.
118
     *
119
     * @param string $prefix
120
     * @param string $uri
121
     */
122
    public function registerXPathNamespace($prefix, $uri)
123
    {
124
        $this->el->registerXPathNamespace($prefix, $uri);
125
        $this->namespaces[$prefix] = $uri;
126
    }
127
128
    /**
129
     * Register an array of new xpath namespaces.
130
     *
131
     * @param array $namespaces
132
     */
133
    public function registerXPathNamespaces($namespaces)
134
    {
135
        // Convenience method to add multiple namespaces at once
136
        foreach ($namespaces as $prefix => $uri) {
137
            $this->registerXPathNamespace($prefix, $uri);
138
        }
139
    }
140
141
    /**
142
     * Get the text of the first node matching an XPath query. By default,
143
     * the text will be trimmed, but if you want the untrimmed text, set
144
     * the second paramter to False.
145
     *
146
     * @param string $path
147
     * @param bool   $trim
148
     * @return string
149
     */
150
    public function text($path='.', $trim=true)
151
    {
152
        $text = strval($this->first($path));
153
154
        return $trim ? trim($text) : $text;
155
    }
156
157
    /**
158
     * Get a node attribute value.
159
     *
160
     * @param string $attribute
161
     * @return string
162
     */
163
    public function attr($attribute)
164
    {
165
        return trim((string) $this->el->attributes()->{$attribute});
166
    }
167
168
    /**
169
     * Get the first node matching an XPath query, or null if no match.
170
     *
171
     * @param string $path
172
     * @return QuiteSimpleXMLElement
173
     */
174
    public function first($path)
175
    {
176
        // Convenience method
177
        $x = $this->xpath($path);
178
179
        return count($x) ? $x[0] : null;
180
    }
181
182
    /**
183
     * Check if the document has at least one node matching an XPath query.
184
     *
185
     * @param string $path
186
     * @return bool
187
     */
188
    public function has($path)
189
    {
190
        $x = $this->xpath($path);
191
192
        return count($x) ? true : false;
193
    }
194
195
    /**
196
     * Get the wrapped SimpleXMLElement object.
197
     *
198
     * @return SimpleXMLElement
199
     */
200
    public function el()
201
    {
202
        return $this->el;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->el; of type string[]|SimpleXMLElement adds the type string[] to the return on line 202 which is incompatible with the return type documented by Danmichaelo\QuiteSimpleX...iteSimpleXMLElement::el of type SimpleXMLElement.
Loading history...
203
    }
204
205
    /**
206
     * Get all nodes matching an XPath query.
207
     *
208
     * @param string $path
209
     * @return QuiteSimpleXMLElement[]
210
     */
211
    public function xpath($path)
212
    {
213
        return array_map(function ($el) {
214
            return new QuiteSimpleXMLElement($el, $this);
215
        }, $this->el->xpath($path));
216
    }
217
218
    /**
219
     * Alias for `xpath()`.
220
     *
221
     * @param $path
222
     * @return QuiteSimpleXMLElement[]
223
     */
224
    public function all($path)
225
    {
226
        return $this->xpath($path);
227
    }
228
229
    /**
230
     * Returns the *untrimmed* text content of the node.
231
     * @return string
232
     */
233
    public function __toString()
234
    {
235
        return (string) $this->el;
236
    }
237
238
    /**
239
     * Returns the child elements.
240
     *
241
     * Note: By default, only children without namespace will be returned. You can
242
     * specify a namespace prefix to get children with that namespace prefix.
243
     *
244
     * Tip: You could also use `xpath('child::node()')`.
245
     *
246
     * @param null $ns
247
     * @return QuiteSimpleXMLElement[]
248
     */
249
    public function children($ns=null)
250
    {
251
        $ch = is_null($ns)
252
            ? $this->el->children()
253
            : $this->el->children($this->namespaces[$ns]);
254
255
        $o = [];
256
        foreach ($ch as $c) {
257
            $o[] = new self($c, $this);
258
        }
259
260
        return $o;
261
    }
262
263
    /**
264
     * Returns the number of child elements.
265
     *
266
     * Note: By default, only children without namespace will be counted. You can
267
     * specify a namespace prefix to count children with that namespace prefix.
268
     *
269
     * @param null $ns
270
     * @return int
271
     */
272
    public function count($ns = null)
273
    {
274
        return count($this->children($ns));
275
    }
276
277
    /**
278
     * Returns and element's attributes.
279
     *
280
     * @return SimpleXMLElement a `SimpleXMLElement` object that can be
281
     * iterated over to loop through the attributes on the tag.
282
     */
283
    public function attributes()
284
    {
285
        return $this->el->attributes();
286
    }
287
288
    /**
289
     * Returns the XML as text.
290
     *
291
     * @return string
292
     */
293
    public function asXML()
294
    {
295
        return $this->el->asXML();
296
    }
297
298
    /**
299
     * Get the element name.
300
     *
301
     * @return string
302
     */
303
    public function getName()
304
    {
305
        return $this->el->getName();
306
    }
307
308
    /**
309
     * Return a namespace array.
310
     *
311
     * @return array
312
     */
313
    public function getNamespaces()
314
    {
315
        return $this->namespaces;
316
    }
317
318
    /**
319
     * Set the node value.
320
     *
321
     * @param string $value
322
     */
323
    public function setValue($value)
324
    {
325
        $this->el[0] = $value;
326
    }
327
328
    /**
329
     * Return the current object as DOMElement.
330
     *
331
     * @return \DOMElement
332
     */
333
    public function asDOMElement()
334
    {
335
        return dom_import_simplexml($this->el);
336
    }
337
338
    /**
339
     * Replaces the current node. Thanks to @hakre
340
     * <http://stackoverflow.com/questions/17661167/how-to-replace-xml-node-with-simplexmlelement-php>.
341
     *
342
     * @param QuiteSimpleXMLElement $element
343
     */
344
    public function replace(QuiteSimpleXMLElement $element)
345
    {
346
        $oldNode = $this->asDOMElement();
347
        $newNode = $oldNode->ownerDocument->importNode(
348
            $element->asDOMElement(),
349
            true
350
        );
351
        $oldNode->parentNode->replaceChild($newNode, $oldNode);
352
    }
353
354
    /**
355
     * Static helper method to make initialization easier.
356
     *
357
     * @param       $input
358
     * @param array $ns
359
     * @return QuiteSimpleXMLElement
360
     */
361
    public static function make($input, $ns = [])
362
    {
363
        $elem = new self($input);
364
        $elem->registerXPathNamespaces($ns);
365
366
        return $elem;
367
    }
368
}
369