Passed
Push — master ( 9afec5...cf9ded )
by Daniel
02:28
created

XmlDocument::withoutNamespaces()   B

Complexity

Conditions 8
Paths 4

Size

Total Lines 31
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 8.013

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 16
c 1
b 0
f 0
nc 4
nop 0
dl 0
loc 31
ccs 16
cts 17
cp 0.9412
crap 8.013
rs 8.4444
1
<?php
2
3
namespace OrcaServices\NovaApi\Xml;
4
5
use Cake\Chronos\Chronos;
6
use DOMAttr;
7
use DOMDocument;
8
use DOMElement;
9
use DOMException;
10
use DOMNameSpaceNode;
11
use DOMNode;
12
use DOMNodeList;
13
use DOMXPath;
14
use InvalidArgumentException;
15
use OrcaServices\NovaApi\Exception\InvalidXmlException;
16
17
/**
18
 * XML DOM reader.
19
 */
20
final class XmlDocument
21
{
22
    /**
23
     * @var DOMXPath The xpath
24
     */
25
    private $xpath;
26
27
    /**
28
     * The constructor.
29
     *
30
     * @param DOMXPath $xpath The DOM xpath
31
     */
32 11
    public function __construct(DOMXPath $xpath)
33
    {
34 11
        $this->xpath = $xpath;
35 11
    }
36
37
    /**
38
     * Create instance.
39
     *
40
     * @param string $xmlContent The xml content
41
     *
42
     * @throws DOMException
43
     *
44
     * @return self The document
45
     */
46 11
    public static function createFromXmlString(string $xmlContent): self
47
    {
48 11
        $dom = new DOMDocument();
49 11
        $dom->preserveWhiteSpace = false;
50 11
        $dom->formatOutput = true;
51 11
        $success = $dom->loadXML($xmlContent);
52
53 11
        if ($success === false) {
54
            throw new DOMException('The XML content is not well-formed');
55
        }
56
57 11
        return new self(new DOMXPath($dom));
58
    }
59
60
    /**
61
     * Create instance.
62
     *
63
     * @param DOMDocument $dom The dom document
64
     *
65
     * @return self The document
66
     */
67
    public static function createFromDom(DOMDocument $dom): self
68
    {
69
        $dom->preserveWhiteSpace = false;
70
        $dom->formatOutput = true;
71
72
        return new self(new DOMXPath($dom));
73
    }
74
75
    /**
76
     * Add all namespaces automatically.
77
     *
78
     * @return void
79
     */
80
    public function registerAllNamespaces()
81
    {
82
        foreach ($this->xpath->query('//namespace::*') ?: [] as $namespaceNode) {
83
            $prefix = str_replace('xmlns:', '', $namespaceNode->nodeName);
84
            $namespaceUri = $namespaceNode->nodeValue;
85
            $this->xpath->registerNamespace($prefix, $namespaceUri);
86
        }
87
    }
88
89
    /**
90
     * Check if namespace exists.
91
     *
92
     * @param string $namespace The namespace name
93
     *
94
     * @return bool Status
95
     */
96 11
    public function existsNamespace(string $namespace): bool
97
    {
98 11
        $nodes = $this->xpath->query('//namespace::' . $namespace);
99
100 11
        return !empty($nodes) && $nodes->length !== 0;
101
    }
102
103
    /**
104
     * Get value of the first node.
105
     *
106
     * @param string $expression The xpath expression
107
     * @param DOMElement|DOMNode|null $contextNode The optional context node
108
     *
109
     * @throws InvalidXmlException
110
     *
111
     * @return string The node value
112
     */
113 5
    public function getNodeValue(string $expression, $contextNode = null): string
114
    {
115 5
        $nodes = $this->queryNodes($expression, $contextNode);
116
117 5
        if (empty($nodes)
118 5
            || $nodes->length === 0
119 5
            || !($nodes->item(0) instanceof DOMElement)
120 5
            || !($nodes->item(0) instanceof DOMNode)
121
        ) {
122
            throw new InvalidXmlException(sprintf('XML DOM node [%s] not found.', $expression));
123
        }
124
125 5
        return $nodes->item(0)->nodeValue;
126
    }
127
128
    /**
129
     * Get value of the first node.
130
     *
131
     * @param string $expression The xpath expression
132
     * @param DOMXPath $xpath The xpath object
133
     * @param DOMElement|DOMNode|null $contextNode The optional context node
134
     *
135
     * @return string|null The node value
136
     */
137
    private function findSingleNodeValue(string $expression, DOMXPath $xpath, $contextNode = null)
138
    {
139
        if ($contextNode === null) {
140
            $node = $xpath->query($expression);
141
        } else {
142
            $node = $xpath->query($expression, $contextNode);
143
        }
144
145
        if (empty($node)
146
            || $node->length === 0
147
            || !($node->item(0) instanceof DOMElement)
148
            || !($node->item(0) instanceof DOMNode)
149
        ) {
150
            return null;
151
        }
152
153
        return $node->item(0)->nodeValue;
154
    }
155
156
    /**
157
     * Get value of the first node.
158
     *
159
     * @param string $expression The xpath expression
160
     * @param DOMElement|DOMNode|null $contextNode The optional context node
161
     *
162
     * @return string|null The node value
163
     */
164
    public function findNodeValue(string $expression, $contextNode = null)
165
    {
166
        $nodes = $this->queryNodes($expression, $contextNode);
167
168
        if (empty($nodes)
169
            || $nodes->length === 0
170
            || !($nodes->item(0) instanceof DOMElement)
171
            || !($nodes->item(0) instanceof DOMNode)
172
        ) {
173
            return null;
174
        }
175
176
        return $nodes->item(0)->nodeValue;
177
    }
178
179
    /**
180
     * Get value of the first node.
181
     *
182
     * @param string $expression The xpath expression
183
     * @param DOMElement|DOMNode|null $contextNode The optional context node
184
     *
185
     * @throws InvalidXmlException
186
     *
187
     * @return string The node value
188
     */
189 9
    public function getAttributeValue(string $expression, $contextNode = null): string
190
    {
191 9
        $nodes = $this->queryNodes($expression, $contextNode);
192
193 9
        if (empty($nodes) || $nodes->length === 0 || !($nodes instanceof DOMNodeList)) {
194
            throw new InvalidXmlException(sprintf('XML DOM attribute [%s] not found.', $expression));
195
        }
196
197 9
        $attribute = $nodes->item(0);
198 9
        if (!($attribute instanceof DOMAttr)) {
199
            throw new InvalidXmlException(sprintf('XML DOM attribute [%s] not found.', $expression));
200
        }
201
202 9
        return $attribute->nodeValue;
203
    }
204
205
    /**
206
     * Query nodes.
207
     *
208
     * @param string $expression The xpath expression
209
     * @param DOMElement|DOMNode|null $contextNode The optional context node
210
     *
211
     * @throws InvalidXmlException
212
     *
213
     * @return DOMNodeList The node list
214
     */
215 11
    public function queryNodes(string $expression, $contextNode = null): DOMNodeList
216
    {
217 11
        if ($contextNode === null) {
218 11
            $nodes = $this->xpath->query($expression);
219
        } else {
220 11
            $nodes = $this->xpath->query($expression, $contextNode);
221
        }
222
223
        // The expression is malformed or the context node is invalid
224 11
        if ($nodes === false) {
225
            throw new InvalidXmlException(sprintf('Invalid Xpath expression: %s', $expression));
226
        }
227
228 11
        return $nodes;
229
    }
230
231
    /**
232
     * Query nodes.
233
     *
234
     * @param string $expression The xpath expression
235
     * @param DOMElement|DOMNode|null $contextNode The optional context node
236
     *
237
     * @throws InvalidXmlException
238
     *
239
     * @return DOMNode The node
240
     */
241 11
    public function queryFirstNode(string $expression, $contextNode = null): DOMNode
242
    {
243 11
        $nodes = $this->queryNodes($expression, $contextNode);
244
245 11
        if ($nodes->length === 0) {
246
            throw new InvalidXmlException(sprintf('Node not found by expression: %s', $expression));
247
        }
248
249 11
        return $this->getFirstNode($nodes);
250
    }
251
252
    /**
253
     * Get value of the first node.
254
     *
255
     * @param string $expression The xpath expression
256
     * @param DOMElement|DOMNode|null $contextNode The optional context node
257
     *
258
     * @return string|null The node value
259
     */
260 6
    public function findAttributeValue(string $expression, $contextNode = null)
261
    {
262 6
        $nodes = $this->queryNodes($expression, $contextNode);
263
264 6
        if ($nodes->length === 0 || !($nodes instanceof DOMNodeList)) {
265 4
            return null;
266
        }
267
268 6
        $attribute = $nodes->item(0);
269 6
        if (!($attribute instanceof DOMAttr)) {
270
            return null;
271
        }
272
273 6
        return $attribute->nodeValue;
274
    }
275
276
    /**
277
     * Get node value as Chronos object.
278
     *
279
     * @param string $expression The xpath expression
280
     * @param DOMElement|DOMNode|null $contextNode The optional context node
281
     *
282
     * @throws InvalidXmlException
283
     *
284
     * @return Chronos The chronos instance
285
     */
286
    public function getNodeValueAsChronos(string $expression, $contextNode = null): Chronos
287
    {
288
        $value = $this->findSingleNodeValue($expression, $this->xpath, $contextNode);
289
290
        if ($value === null) {
291
            throw new InvalidXmlException(sprintf('DOM node not found: %s', $expression));
292
        }
293
294
        return $this->createChronosFromXsDateTime($value);
295
    }
296
297
    /**
298
     * Create Chronos from XML date time string with timezone offset.
299
     *
300
     * @param string $dateTime Date Time string
301
     *
302
     * @throws InvalidArgumentException
303
     *
304
     * @return Chronos The Chronos object
305
     */
306 4
    public function createChronosFromXsDateTime(string $dateTime): Chronos
307
    {
308 4
        $timestamp = strtotime($dateTime);
309
310 4
        if ($timestamp === false) {
311
            throw new InvalidArgumentException(sprintf('Invalid date: %s', $dateTime));
312
        }
313
314 4
        return Chronos::createFromTimestamp($timestamp);
315
    }
316
317
    /**
318
     * Get first node from DOMNodeList.
319
     *
320
     * @param DOMNodeList $nodes The DOMNodeList
321
     *
322
     * @throws InvalidXmlException
323
     *
324
     * @return DOMNode The first node
325
     */
326 11
    public function getFirstNode(DOMNodeList $nodes): DOMNode
327
    {
328 11
        if (empty($nodes->length)) {
329
            throw new InvalidXmlException('No DOM nodes found');
330
        }
331 11
        $node = $nodes->item(0);
332
333 11
        if ($node === null) {
334
            throw new InvalidXmlException('First DOM node not found');
335
        }
336
337 11
        return $node;
338
    }
339
340
    /**
341
     * Get DOMDocument.
342
     *
343
     * @return DOMDocument The DOMDocument
344
     */
345
    public function getDom(): DOMDocument
346
    {
347
        return $this->xpath->document;
348
    }
349
350
    /**
351
     * Get xpath.
352
     *
353
     * @return DOMXPath The xpath
354
     */
355
    public function getXpath(): DOMXPath
356
    {
357
        return $this->xpath;
358
    }
359
360
    /**
361
     * Get XML content.
362
     *
363
     * @return string The xml content
364
     */
365
    public function getXml(): string
366
    {
367
        return (string)$this->xpath->document->saveXML();
368
    }
369
370
    /**
371
     * Remove all namespaces from DOM.
372
     *
373
     * @return self The new instance
374
     */
375 11
    public function withoutNamespaces(): self
376
    {
377 11
        $dom = new DOMDocument();
378 11
        $dom->formatOutput = true;
379
380 11
        $domSource = clone $this->xpath->document;
381 11
        $domSource->formatOutput = true;
382
383 11
        $dom->loadXML(preg_replace('/\sxmlns="(.*?)"/', '', $domSource->saveXML() ?: '') ?: '');
384 11
        $xpath = new DOMXPath($dom);
385
386
        /** @var DOMNameSpaceNode|DOMAttr $namespaceNode */
387 11
        foreach ($xpath->query('//namespace::*') ?: [] as $namespaceNode) {
388 11
            if (!isset($namespaceNode->nodeName)) {
389
                continue;
390
            }
391
392 11
            $prefix = str_replace('xmlns:', '', $namespaceNode->nodeName);
393 11
            $nodes = $xpath->query("//*[namespace::{$prefix}]") ?: [];
394
395
            /** @var DOMElement $node */
396 11
            foreach ($nodes as $node) {
397 11
                $namespaceUri = $node->lookupNamespaceUri($prefix);
398 11
                $node->removeAttributeNS($namespaceUri, $prefix);
399
            }
400
        }
401
402
        // Important: Reload document to remove invalid xpath references from old dom
403 11
        $dom->loadXML((string)$dom->saveXML());
404
405 11
        return new self(new DOMXPath($dom));
406
    }
407
}
408