Passed
Push — master ( dcddfc...dfc3a0 )
by Tomáš
11:42
created

BaseNode::toString()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 6
c 1
b 0
f 0
dl 0
loc 11
ccs 7
cts 7
cp 1
rs 10
cc 2
nc 1
nop 1
crap 2
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 Inspirum\XML\Exception\Handler;
13
use Inspirum\XML\Formatter\Config;
14
use Inspirum\XML\Formatter\Formatter;
15
use Inspirum\XML\Parser\Parser;
16
use Throwable;
17
use function is_array;
18
use function is_string;
19
use function str_contains;
20
21
abstract class BaseNode implements Node
22
{
23 72
    protected function __construct(
24
        private readonly DOMDocument $document,
25
        private readonly ?DOMNode $node,
26
        private readonly NamespaceRegistry $namespaceRegistry,
27
    ) {
28 72
    }
29
30 48
    protected function createNode(DOMNode $element): Node
31
    {
32 48
        return new DefaultNode($this->document, $element, $this->namespaceRegistry);
33
    }
34
35 11
    public function getDocument(): DOMDocument
36
    {
37 11
        return $this->document;
38
    }
39
40 8
    public function getNode(): ?DOMNode
41
    {
42 8
        return $this->node;
43
    }
44
45
    /**
46
     * @inheritDoc
47
     */
48 38
    public function addElement(string $name, array $attributes = [], bool $withNamespaces = true): Node
49
    {
50 38
        return $this->addTextElement($name, null, $attributes, withNamespaces: $withNamespaces);
51
    }
52
53
    /**
54
     * @inheritDoc
55
     */
56 42
    public function addTextElement(string $name, mixed $value, array $attributes = [], bool $forcedEscape = false, bool $withNamespaces = true): Node
57
    {
58 42
        $element = $this->createFullDOMElement($name, $value, $attributes, $forcedEscape, $withNamespaces);
59
60 36
        $this->appendChild($element);
61
62 36
        return $this->createNode($element);
63
    }
64
65 8
    public function append(Node $element): void
66
    {
67 8
        if ($element->getNode() !== null) {
68 8
            $this->appendChild($element->getNode());
69
        }
70
    }
71
72
    /**
73
     * @inheritDoc
74
     */
75 1
    public function createElement(string $name, array $attributes = [], bool $withNamespaces = true): Node
76
    {
77 1
        return $this->createTextElement($name, null, $attributes, withNamespaces: $withNamespaces);
78
    }
79
80
    /**
81
     * @inheritDoc
82
     */
83 13
    public function createTextElement(string $name, mixed $value, array $attributes = [], bool $forcedEscape = false, bool $withNamespaces = true): Node
84
    {
85 13
        $element = $this->createFullDOMElement($name, $value, $attributes, $forcedEscape, $withNamespaces);
86
87 13
        return $this->createNode($element);
88
    }
89
90 4
    public function addXMLData(string $content): ?Node
91
    {
92 4
        if ($content === '') {
93 1
            return null;
94
        }
95
96 3
        $element = $this->createDOMFragment($content);
97
98 3
        $this->appendChild($element);
99
100 3
        return $this->createNode($element);
101
    }
102
103
    /**
104
     * Create new DOM element.
105
     *
106
     * @param array<string,mixed> $attributes
107
     *
108
     * @throws \DOMException
109
     */
110 54
    private function createFullDOMElement(string $name, mixed $value, array $attributes, bool $forcedEscape, bool $withNamespaces): DOMElement
111
    {
112 54
        $this->registerNamespaces($attributes);
113
114 51
        $element = $this->createDOMElementNS($name, null, $withNamespaces);
115
116 48
        $this->setDOMElementValue($element, $value, $forcedEscape);
117
118 48
        foreach ($attributes as $attributeName => $attributeValue) {
119 29
            $this->setDOMAttributeNS($element, $attributeName, $attributeValue, $withNamespaces);
120
        }
121
122 48
        return $element;
123
    }
124
125
    /**
126
     * Create new DOM fragment element
127
     */
128 3
    private function createDOMFragment(string $content): DOMDocumentFragment
129
    {
130 3
        $element = $this->document->createDocumentFragment();
131
132 3
        $element->appendXML($content);
133
134 3
        return $element;
135
    }
136
137
    /**
138
     * Create new DOM element with namespace if exists
139
     *
140
     * @throws \DOMException
141
     */
142 51
    private function createDOMElementNS(string $name, ?string $value, bool $withNamespaces): DOMElement
143
    {
144 51
        $prefix = Parser::getNamespacePrefix($name);
145 48
        $value  = Formatter::encodeValue($value);
146
147 48
        if ($withNamespaces && $prefix !== null && $this->namespaceRegistry->hasNamespace($prefix)) {
148 8
            return $this->document->createElementNS($this->namespaceRegistry->getNamespace($prefix), $name, (string) $value);
149
        }
150
151 44
        return $this->document->createElement($name, (string) $value);
152
    }
153
154
    /**
155
     * Set node value to element
156
     */
157 48
    private function setDOMElementValue(DOMElement $element, mixed $value, bool $forcedEscape): void
158
    {
159 48
        $value = Formatter::encodeValue($value);
160
161 48
        if ($value === '' || $value === null) {
162 42
            return;
163
        }
164
165
        try {
166 36
            if (str_contains($value, '&') || $forcedEscape) {
167 1
                throw new DOMException('DOMDocument::createElement(): unterminated entity reference');
168
            }
169
170 36
            $element->nodeValue = $value;
171 1
        } catch (Throwable) {
172 1
            $cdata = $this->document->createCDATASection($value);
173 1
            $element->appendChild($cdata);
174
        }
175
    }
176
177
    /**
178
     * Create new DOM attribute with namespace if exists
179
     *
180
     * @return void
181
     */
182 29
    private function setDOMAttributeNS(DOMElement $element, string $name, mixed $value, bool $withNamespaces): void
183
    {
184 29
        $prefix = Parser::getNamespacePrefix($name);
185 29
        $value  = Formatter::encodeValue($value);
186
187 29
        if ($withNamespaces && $prefix === 'xmlns') {
188 17
            $element->setAttributeNS('http://www.w3.org/2000/xmlns/', $name, (string) $value);
189 26
        } elseif ($withNamespaces && $prefix !== null && $this->namespaceRegistry->hasNamespace($prefix)) {
190 8
            $element->setAttributeNS($this->namespaceRegistry->getNamespace($prefix), $name, (string) $value);
191 20
        } elseif ($prefix !== 'xmlns') {
192 20
            $element->setAttribute($name, (string) $value);
193
        }
194
    }
195
196
    /**
197
     * Append child to parent node.
198
     */
199 44
    private function appendChild(DOMNode $element): void
200
    {
201 44
        $node = $this->node ?? $this->document;
202 44
        $node->appendChild($element);
203
    }
204
205
    /**
206
     * Register xmlns namespace URLs
207
     *
208
     * @param array<string,mixed> $attributes
209
     */
210 54
    private function registerNamespaces(array $attributes): void
211
    {
212 54
        foreach ($attributes as $attributeName => $attributeValue) {
213 32
            [$prefix, $namespaceLocalName] = Parser::parseQualifiedName($attributeName);
214
215 32
            if ($prefix === 'xmlns' && is_string($attributeValue)) {
216 17
                $this->namespaceRegistry->registerNamespace($namespaceLocalName, $attributeValue);
217
            }
218
        }
219
    }
220
221 8
    public function getTextContent(): ?string
222
    {
223 8
        $node = $this->node ?? $this->document;
224
225 8
        return $node->textContent;
226
    }
227
228
    /**
229
     * @inheritDoc
230
     */
231 1
    public function getAttributes(bool $autoCast = false): array
232
    {
233 1
        $node       = $this->node ?? $this->document;
234 1
        $attributes = [];
235
236 1
        if ($node->hasAttributes()) {
237
            /** @var \DOMAttr $attribute */
238 1
            foreach ($node->attributes ?? [] as $attribute) {
239 1
                $value                            = $attribute->nodeValue;
240 1
                $attributes[$attribute->nodeName] = $autoCast ? Formatter::decodeValue($value) : $value;
241
            }
242
        }
243
244 1
        return $attributes;
245
    }
246
247 30
    public function toString(bool $formatOutput = false): string
248
    {
249 30
        return Handler::withErrorHandlerForDOMDocument(function () use ($formatOutput): string {
250 30
            $this->document->formatOutput = $formatOutput;
251
252 30
            $xml = $this->document->saveXML($this->node);
253 30
            if ($xml === false) {
254 1
                throw new DOMException('\DOMDocument::saveXML() method failed');
255
            }
256
257 29
            return $xml;
258 30
        });
259
    }
260
261 1
    public function __toString(): string
262
    {
263 1
        return $this->toString();
264
    }
265
266
    /**
267
     * @inheritDoc
268
     */
269 13
    public function toArray(?Config $config = null): array
270
    {
271 13
        $result = Formatter::nodeToArray($this->node ?? $this->document, $config ?? new Config());
272
273 13
        if (is_array($result) === false) {
274 2
            $result = [$result];
275
        }
276
277 13
        return $result;
278
    }
279
280
    /**
281
     * @inheritDoc
282
     */
283 1
    public function __toArray(): array
284
    {
285 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...
286
    }
287
288
    /**
289
     * Convert to array
290
     *
291
     * @return array<int|string,mixed>
292
     */
293 1
    public function jsonSerialize(): array
294
    {
295 1
        return $this->toArray();
296
    }
297
}
298