BaseNode::setDOMAttributeNS()   B
last analyzed

Complexity

Conditions 7
Paths 4

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 7

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 11
ccs 9
cts 9
cp 1
rs 8.8333
cc 7
nc 4
nop 4
crap 7
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Inspirum\XML\Builder;
6
7
use DOMDocument;
8
use DOMDocumentFragment;
9
use DOMElement;
10
use DOMException;
11
use DOMNode;
12
use DOMText;
13
use DOMXPath;
14
use Inspirum\XML\Exception\Handler;
15
use Inspirum\XML\Formatter\Config;
16
use Inspirum\XML\Formatter\DefaultConfig;
17
use Inspirum\XML\Formatter\Formatter;
18
use Inspirum\XML\Parser\Parser;
19
use Throwable;
20
use function is_array;
21
use function is_string;
22
use function str_contains;
23
24
abstract class BaseNode implements Node
25
{
26 102
    protected function __construct(
27
        private readonly DOMDocument $document,
28
        private readonly ?DOMNode $node,
29
        private readonly NamespaceRegistry $namespaceRegistry,
30
    ) {
31 102
    }
32
33 77
    protected function createNode(DOMNode $element): Node
34
    {
35 77
        return new DefaultNode($this->document, $element, $this->namespaceRegistry);
36
    }
37
38 11
    public function getDocument(): DOMDocument
39
    {
40 11
        return $this->document;
41
    }
42
43 29
    public function getNode(): ?DOMNode
44
    {
45 29
        return $this->node;
46
    }
47
48
    /**
49
     * @inheritDoc
50
     */
51 43
    public function addElement(string $name, array $attributes = [], bool $withNamespaces = true): Node
52
    {
53 43
        return $this->addTextElement($name, null, $attributes, withNamespaces: $withNamespaces);
54
    }
55
56
    /**
57
     * @inheritDoc
58
     */
59 49
    public function addTextElement(string $name, mixed $value, array $attributes = [], bool $forcedEscape = false, bool $withNamespaces = true): Node
60
    {
61 49
        $element = $this->createFullDOMElement($name, $value, $attributes, $forcedEscape, $withNamespaces);
62
63 43
        $this->appendChild($element);
64
65 43
        return $this->createNode($element);
66
    }
67
68
    public function addElementFromNode(DOMNode $node, bool $forcedEscape = false, bool $withNamespaces = true): Node
69
    {
70
        $element = $this->createFullDOMElementFromNode($node, $forcedEscape, $withNamespaces);
71
72
        $this->appendChild($element);
73
74
        return $this->createNode($element);
75
    }
76
77 29
    public function append(Node $element): void
78
    {
79 29
        if ($element->getNode() !== null) {
80 29
            $this->appendChild($element->getNode());
81
        }
82
    }
83
84
    /**
85
     * @inheritDoc
86
     */
87 1
    public function createElement(string $name, array $attributes = [], bool $withNamespaces = true): Node
88
    {
89 1
        return $this->createTextElement($name, null, $attributes, withNamespaces: $withNamespaces);
90
    }
91
92
    /**
93
     * @inheritDoc
94
     */
95 35
    public function createTextElement(string $name, mixed $value, array $attributes = [], bool $forcedEscape = false, bool $withNamespaces = true): Node
96
    {
97 35
        $element = $this->createFullDOMElement($name, $value, $attributes, $forcedEscape, $withNamespaces);
98
99 35
        return $this->createNode($element);
100
    }
101
102 7
    public function createElementFromNode(DOMNode $node, bool $forcedEscape = false, bool $withNamespaces = true): Node
103
    {
104 7
        $element = $this->createFullDOMElementFromNode($node, $forcedEscape, $withNamespaces);
105
106 7
        return $this->createNode($element);
107
    }
108
109 4
    public function addXMLData(string $content): ?Node
110
    {
111 4
        if ($content === '') {
112 1
            return null;
113
        }
114
115 3
        $element = $this->createDOMFragment($content);
116
117 3
        $this->appendChild($element);
118
119 3
        return $this->createNode($element);
120
    }
121
122
    /**
123
     * Create new DOM element.
124
     *
125
     * @param array<string,mixed> $attributes
126
     *
127
     * @throws \DOMException
128
     */
129 83
    private function createFullDOMElement(string $name, mixed $value, array $attributes, bool $forcedEscape, bool $withNamespaces): DOMElement
130
    {
131 83
        $this->registerNamespaces($attributes);
132
133 80
        $element = $this->createDOMElementNS($name, null, $withNamespaces);
134
135 77
        $this->setDOMElementValue($element, $value, $forcedEscape);
136
137 77
        foreach ($attributes as $attributeName => $attributeValue) {
138 53
            $this->setDOMAttributeNS($element, $attributeName, $attributeValue, $withNamespaces);
139
        }
140
141 77
        return $element;
142
    }
143
144 7
    private function createFullDOMElementFromNode(DOMNode $node, bool $forcedEscape = false, bool $withNamespaces = true): DOMElement
145
    {
146 7
        $value = null;
147 7
        $childElements = [];
148
149
        /** @var \DOMNode $child */
150 7
        foreach ($node->childNodes as $child) {
151 7
            if ($child instanceof DOMText) {
152 7
                $value = $child->textContent;
153 7
                continue;
154
            }
155
156 1
            $childElements[] = $this->createFullDOMElementFromNode($child, $forcedEscape, $withNamespaces);
157
        }
158
159 7
        $element = $this->createFullDOMElement($node->nodeName, $value, $this->getAttributesFromNode($node), $forcedEscape, $withNamespaces);
160
161 7
        foreach ($childElements as $childElement) {
162 1
            $element->appendChild($childElement);
163
        }
164
165 7
        return $element;
166
    }
167
168
    /**
169
     * Create new DOM fragment element
170
     */
171 3
    private function createDOMFragment(string $content): DOMDocumentFragment
172
    {
173 3
        $element = $this->document->createDocumentFragment();
174
175 3
        $element->appendXML($content);
176
177 3
        return $element;
178
    }
179
180
    /**
181
     * Create new DOM element with namespace if exists
182
     *
183
     * @throws \DOMException
184
     */
185 80
    private function createDOMElementNS(string $name, ?string $value, bool $withNamespaces): DOMElement
186
    {
187 80
        $prefix = Parser::getNamespacePrefix($name);
188 77
        $value = Formatter::encodeValue($value);
189
190 77
        if ($withNamespaces && $prefix !== null && $this->namespaceRegistry->hasNamespace($prefix)) {
191 10
            return $this->document->createElementNS($this->namespaceRegistry->getNamespace($prefix), $name, (string) $value);
192
        }
193
194 73
        return $this->document->createElement($name, (string) $value);
195
    }
196
197
    /**
198
     * Set node value to element
199
     */
200 77
    private function setDOMElementValue(DOMElement $element, mixed $value, bool $forcedEscape): void
201
    {
202 77
        $value = Formatter::encodeValue($value);
203
204 77
        if ($value === '' || $value === null) {
205 69
            return;
206
        }
207
208
        try {
209 64
            if (str_contains($value, '&') || $forcedEscape) {
210 11
                throw new DOMException('DOMDocument::createElement(): unterminated entity reference');
211
            }
212
213 64
            $element->nodeValue = $value;
214 11
        } catch (Throwable) {
215 11
            $cdata = $this->document->createCDATASection($value);
216 11
            $element->appendChild($cdata);
217
        }
218
    }
219
220
    /**
221
     * Create new DOM attribute with namespace if exists
222
     */
223 53
    private function setDOMAttributeNS(DOMElement $element, string $name, mixed $value, bool $withNamespaces): void
224
    {
225 53
        $prefix = Parser::getNamespacePrefix($name);
226 53
        $value = Formatter::encodeValue($value);
227
228 53
        if ($withNamespaces && $prefix === 'xmlns') {
229 23
            $element->setAttributeNS('http://www.w3.org/2000/xmlns/', $name, (string) $value);
230 50
        } elseif ($withNamespaces && $prefix !== null && $this->namespaceRegistry->hasNamespace($prefix)) {
231 8
            $element->setAttributeNS($this->namespaceRegistry->getNamespace($prefix), $name, (string) $value);
232 44
        } elseif ($prefix !== 'xmlns') {
233 44
            $element->setAttribute($name, (string) $value);
234
        }
235
    }
236
237
    /**
238
     * Append child to parent node.
239
     */
240 72
    private function appendChild(DOMNode $element): void
241
    {
242 72
        $node = $this->resolveNode();
243 72
        $node->appendChild($element);
244
    }
245
246
    /**
247
     * Register xmlns namespace URLs
248
     *
249
     * @param array<string,mixed> $attributes
250
     */
251 83
    private function registerNamespaces(array $attributes): void
252
    {
253 83
        foreach ($attributes as $attributeName => $attributeValue) {
254 56
            [$prefix, $namespaceLocalName] = Parser::parseQualifiedName($attributeName);
255
256 56
            if ($prefix === 'xmlns' && is_string($attributeValue)) {
257 27
                $this->namespaceRegistry->registerNamespace($namespaceLocalName, $attributeValue);
258
            }
259
        }
260
    }
261
262 9
    public function getTextContent(): ?string
263
    {
264 9
        $node = $this->resolveNode();
265
266 9
        return $node->textContent;
267
    }
268
269
    /**
270
     * @inheritDoc
271
     */
272 1
    public function getAttributes(bool $autoCast = false): array
273
    {
274 1
        $node = $this->resolveNode();
275
276 1
        return $this->getAttributesFromNode($node, $autoCast);
277
    }
278
279
    /**
280
     * Get attributes from \DOMNode
281
     *
282
     * @return ($autoCast is true ? array<string,mixed> : array<string,string>)
0 ignored issues
show
Documentation Bug introduced by
The doc comment ($autoCast at position 1 could not be parsed: Unknown type name '$autoCast' at position 1 in ($autoCast.
Loading history...
283
     */
284 8
    private function getAttributesFromNode(DOMNode $node, bool $autoCast = false): array
285
    {
286 8
        $attributes = [];
287
288 8
        if ($node->hasAttributes()) {
289
            /** @var \DOMAttr $attribute */
290 5
            foreach ($node->attributes ?? [] as $attribute) {
291 5
                $value = $attribute->nodeValue;
292 5
                $attributes[$attribute->nodeName] = $autoCast ? Formatter::decodeValue($value) : $value;
293
            }
294
        }
295
296 8
        return $attributes;
297
    }
298
299
    /**
300
     * @inheritDoc
301
     */
302 8
    public function xpath(string $expression): ?array
303
    {
304 8
        $xpath = new DOMXPath($this->toDOMDocument());
305
306 8
        $nodes = $xpath->query($expression);
307 7
        if ($nodes === false) {
308
            return null;
309
        }
310
311 7
        $results = [];
312 7
        foreach ($nodes as $node) {
313 7
            $results[] = $this->createElementFromNode($node);
314
        }
315
316 7
        return $results;
317
    }
318
319
    /**
320
     * Copy current node to new \DOMDocument
321
     */
322 8
    private function toDOMDocument(): DOMDocument
323
    {
324 8
        $doc = new DOMDocument($this->document->xmlVersion ?? '1.0', $this->document->encoding ?? 'UTF-8');
325 8
        $doc->loadXML($this->toString());
326
327 8
        return $doc;
328
    }
329
330
    /**
331
     * Resolve current node
332
     */
333 76
    private function resolveNode(): DOMNode
334
    {
335 76
        return $this->node ?? $this->document;
336
    }
337
338 50
    public function toString(bool $formatOutput = false): string
339
    {
340 50
        return Handler::withErrorHandlerForDOMDocument(function () use ($formatOutput): string {
341 50
            $this->document->formatOutput = $formatOutput;
342
343 50
            $xml = $this->document->saveXML($this->node);
344 50
            if ($xml === false) {
345 1
                throw new DOMException('\DOMDocument::saveXML() method failed');
346
            }
347
348 49
            return $xml;
349 50
        });
350
    }
351
352 1
    public function __toString(): string
353
    {
354 1
        return $this->toString();
355
    }
356
357
    /**
358
     * @inheritDoc
359
     */
360 20
    public function toArray(?Config $config = null): array
361
    {
362 20
        $result = Formatter::nodeToArray($this->resolveNode(), $config ?? new DefaultConfig());
363
364 20
        if (is_array($result) === false) {
365 2
            $result = [$result];
366
        }
367
368 20
        return $result;
369
    }
370
371
    /**
372
     * @inheritDoc
373
     */
374 1
    public function __toArray(): array
375
    {
376 1
        return $this->toArray();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->toArray() returns the type array<integer,array> which is incompatible with the return type mandated by Inspirum\Arrayable\Arrayable::__toArray() of Inspirum\Arrayable\TValue[].

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
377
    }
378
379
    /**
380
     * Convert to array
381
     *
382
     * @return array<int|string,mixed>
383
     */
384 1
    public function jsonSerialize(): array
385
    {
386 1
        return $this->toArray();
387
    }
388
}
389