Completed
Push — master ( b75b1a...3a3aab )
by Dan Michael O.
01:14
created

QuiteSimpleXMLElement::getSimpleXMLElement()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 15
rs 8.8571
cc 5
eloc 9
nc 5
nop 1
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 QuiteSimpleXMLElement
50
{
51
    /**
52
     * @var array
53
     */
54
    public $namespaces;
55
56
    /**
57
     * @var SimpleXMLElement
58
     */
59
    public $el;
60
61
    /**
62
     * QuiteSimpleXMLElement constructor.
63
     *
64
     * @param string|SimpleXMLElement|QuiteSimpleXMLElement    $elem
65
     * @param QuiteSimpleXMLElement                            $inherit_from
66
     * @throws InvalidXMLException
67
     * @throws InvalidArgumentException
68
     */
69
    public function __construct($elem, QuiteSimpleXMLElement $inherit_from = null)
70
    {
71
        $this->namespaces = [];
72
73
        $this->el = $this->getSimpleXMLElement($elem);
74
        if (is_null($this->el)) {
75
            throw new InvalidArgumentException('QuiteSimpleXMLElement expects a string or a QuiteSimpleXMLElement/SimpleXMLElement object.');
76
        }
77
78
        if (is_null($inherit_from)) {
79
            $this->namespaces = $this->el->getNamespaces(true);
80
        } else {
81
            foreach ($inherit_from->namespaces as $prefix => $uri) {
82
                $this->registerXPathNamespace($prefix, $uri);
83
            }
84
        }
85
    }
86
87
    /**
88
     * Internal helper method to get a SimpleXMLElement from either a string
89
     * or a SimpleXMLElement/QuiteSimpleXMLElement object.
90
     *
91
     * @param string|SimpleXMLElement|QuiteSimpleXMLElement $elem
92
     * @return SimpleXMLElement
93
     * @throws InvalidXMLException
94
     * @throws InvalidArgumentException
95
     */
96
    private function getSimpleXMLElement($elem)
97
    {
98
        if (gettype($elem) == 'string') {
99
            return $this->initFromString($elem);
100
        }
101
102
        if (gettype($elem) == 'object') {
103
            switch (get_class($elem)) {
104
                case 'Danmichaelo\QuiteSimpleXMLElement\QuiteSimpleXMLElement':
105
                    return $elem->el();
106
                case 'SimpleXMLElement':
107
                    return $elem;
108
            }
109
        }
110
    }
111
112
    /**
113
     * Internal helper method to parse content from string.
114
     *
115
     * @param string $content
116
     * @return SimpleXMLElement
117
     */
118
    private function initFromString($content)
119
    {
120
        try {
121
            return new SimpleXMLElement($content);
122
        } catch (Exception $e) {
123
            throw new InvalidXMLException('Invalid XML encountered: ' . $content);
124
        }
125
    }
126
127
    /**
128
     * Register a new xpath namespace.
129
     *
130
     * @param string $prefix
131
     * @param string $uri
132
     */
133
    public function registerXPathNamespace($prefix, $uri)
134
    {
135
        $this->el->registerXPathNamespace($prefix, $uri);
136
        $this->namespaces[$prefix] = $uri;
137
    }
138
139
    /**
140
     * Register an array of new xpath namespaces.
141
     *
142
     * @param array $namespaces
143
     */
144
    public function registerXPathNamespaces($namespaces)
145
    {
146
        // Convenience method to add multiple namespaces at once
147
        foreach ($namespaces as $prefix => $uri) {
148
            $this->registerXPathNamespace($prefix, $uri);
149
        }
150
    }
151
152
    /**
153
     * Get the text of the first node matching an XPath query. By default,
154
     * the text will be trimmed, but if you want the untrimmed text, set
155
     * the second paramter to False.
156
     *
157
     * @param string $path
158
     * @param bool   $trim
159
     * @return string
160
     */
161
    public function text($path='.', $trim=true)
162
    {
163
        $text = strval($this->first($path));
164
165
        return $trim ? trim($text) : $text;
166
    }
167
168
    /**
169
     * Get a node attribute value.
170
     *
171
     * @param string $attribute
172
     * @return string
173
     */
174
    public function attr($attribute)
175
    {
176
        return trim((string) $this->el->attributes()->{$attribute});
177
    }
178
179
    /**
180
     * Get the first node matching an XPath query, or null if no match.
181
     *
182
     * @param string $path
183
     * @return QuiteSimpleXMLElement
184
     */
185
    public function first($path)
186
    {
187
        // Convenience method
188
        $x = $this->xpath($path);
189
190
        return count($x) ? $x[0] : null;
191
    }
192
193
    /**
194
     * Check if the document has at least one node matching an XPath query.
195
     *
196
     * @param string $path
197
     * @return bool
198
     */
199
    public function has($path)
200
    {
201
        $x = $this->xpath($path);
202
203
        return count($x) ? true : false;
204
    }
205
206
    /**
207
     * Get the wrapped SimpleXMLElement object.
208
     *
209
     * @return SimpleXMLElement
210
     */
211
    public function el()
212
    {
213
        return $this->el;
214
    }
215
216
    /**
217
     * Get all nodes matching an XPath query.
218
     *
219
     * @param string $path
220
     * @return QuiteSimpleXMLElement[]
221
     */
222
    public function xpath($path)
223
    {
224
        return array_map(function ($el) {
225
            return new QuiteSimpleXMLElement($el, $this);
226
        }, $this->el->xpath($path));
227
    }
228
229
    /**
230
     * Alias for `xpath()`.
231
     *
232
     * @param $path
233
     * @return QuiteSimpleXMLElement[]
234
     */
235
    public function all($path)
236
    {
237
        return $this->xpath($path);
238
    }
239
240
    /**
241
     * Returns the *untrimmed* text content of the node.
242
     * @return string
243
     */
244
    public function __toString()
245
    {
246
        return (string) $this->el;
247
    }
248
249
    /**
250
     * Returns the child elements.
251
     *
252
     * Note: By default, only children without namespace will be returned. You can
253
     * specify a namespace prefix to get children with that namespace prefix.
254
     *
255
     * Tip: You could also use `xpath('child::node()')`.
256
     *
257
     * @param null $ns
258
     * @return QuiteSimpleXMLElement[]
259
     */
260
    public function children($ns=null)
261
    {
262
        $ch = is_null($ns)
263
            ? $this->el->children()
264
            : $this->el->children($this->namespaces[$ns]);
265
266
        $o = [];
267
        foreach ($ch as $c) {
268
            $o[] = new self($c, $this);
269
        }
270
271
        return $o;
272
    }
273
274
    /**
275
     * Returns the number of child elements.
276
     *
277
     * Note: By default, only children without namespace will be counted. You can
278
     * specify a namespace prefix to count children with that namespace prefix.
279
     *
280
     * @param null $ns
281
     * @return int
282
     */
283
    public function count($ns = null)
284
    {
285
        return count($this->children($ns));
286
    }
287
288
    /**
289
     * Returns and element's attributes.
290
     *
291
     * @return SimpleXMLElement a `SimpleXMLElement` object that can be
292
     * iterated over to loop through the attributes on the tag.
293
     */
294
    public function attributes()
295
    {
296
        return $this->el->attributes();
297
    }
298
299
    /**
300
     * Returns the XML as text.
301
     *
302
     * @return string
303
     */
304
    public function asXML()
305
    {
306
        return $this->el->asXML();
307
    }
308
309
    /**
310
     * Get the element name.
311
     *
312
     * @return string
313
     */
314
    public function getName()
315
    {
316
        return $this->el->getName();
317
    }
318
319
    /**
320
     * Return a namespace array.
321
     *
322
     * @return array
323
     */
324
    public function getNamespaces()
325
    {
326
        return $this->namespaces;
327
    }
328
329
    /**
330
     * Set the node value.
331
     *
332
     * @param string $value
333
     */
334
    public function setValue($value)
335
    {
336
        $this->el[0] = $value;
337
    }
338
339
    /**
340
     * Return the current object as DOMElement.
341
     *
342
     * @return \DOMElement
343
     */
344
    public function asDOMElement()
345
    {
346
        return dom_import_simplexml($this->el);
347
    }
348
349
    /**
350
     * Replaces the current node. Thanks to @hakre
351
     * <http://stackoverflow.com/questions/17661167/how-to-replace-xml-node-with-simplexmlelement-php>.
352
     *
353
     * @param QuiteSimpleXMLElement $element
354
     */
355
    public function replace(QuiteSimpleXMLElement $element)
356
    {
357
        $oldNode = $this->asDOMElement();
358
        $newNode = $oldNode->ownerDocument->importNode(
359
            $element->asDOMElement(),
360
            true
361
        );
362
        $oldNode->parentNode->replaceChild($newNode, $oldNode);
363
    }
364
365
    /**
366
     * Static helper method to make initialization easier.
367
     *
368
     * @param       $input
369
     * @param array $ns
370
     * @return QuiteSimpleXMLElement
371
     */
372
    public static function make($input, $ns = [])
373
    {
374
        $elem = new self($input);
375
        $elem->registerXPathNamespaces($ns);
376
377
        return $elem;
378
    }
379
}
380