Passed
Push — master ( 86abb8...57f290 )
by Tomáš
12:38
created

BaseNode   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 268
Duplicated Lines 0 %

Test Coverage

Coverage 97.85%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 42
eloc 70
c 1
b 0
f 0
dl 0
loc 268
ccs 91
cts 93
cp 0.9785
rs 9.0399

23 Methods

Rating   Name   Duplication   Size   Complexity  
A getDocument() 0 3 1
A createTextElement() 0 5 1
A addXMLData() 0 11 2
A createElement() 0 3 1
A getNode() 0 3 1
A __construct() 0 5 1
A addElement() 0 3 1
A addTextElement() 0 7 1
A createNode() 0 3 1
A append() 0 4 2
A toArray() 0 9 2
A __toArray() 0 3 1
A setDOMAttributeNS() 0 11 4
A getTextContent() 0 5 1
A jsonSerialize() 0 3 1
A registerNamespaces() 0 7 5
A __toString() 0 3 1
A createDOMFragment() 0 7 1
A setDOMElementValue() 0 17 6
A createFullDOMElement() 0 13 2
A appendChild() 0 4 1
A createDOMElementNS() 0 10 3
A toString() 0 11 2

How to fix   Complexity   

Complex Class

Complex classes like BaseNode often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BaseNode, and based on these observations, apply Extract Interface, too.

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