XmlDocument::findSingleNodeValue()   A
last analyzed

Complexity

Conditions 6
Paths 4

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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