Formatter::decodeValue()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 7
c 1
b 0
f 0
dl 0
loc 15
ccs 8
cts 8
cp 1
rs 10
cc 4
nc 4
nop 1
crap 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Inspirum\XML\Formatter;
6
7
use DOMNode;
8
use Stringable;
9
use function array_keys;
10
use function array_merge;
11
use function count;
12
use function in_array;
13
use function is_array;
14
use function is_bool;
15
use function is_numeric;
16
use function is_scalar;
17
use function ltrim;
18
use function rtrim;
19
use function sprintf;
20
use function str_starts_with;
21
use function trim;
22
use const XML_CDATA_SECTION_NODE;
23
use const XML_TEXT_NODE;
24
25
final class Formatter
26
{
27
    /**
28
     * Prefix namespaces with xmlns
29
     *
30
     * @param array<string,string> $namespaces
31
     *
32
     * @return array<string,string>
33
     */
34 8
    public static function namespacesToAttributes(array $namespaces): array
35
    {
36 8
        $attributes = [];
37
38 8
        foreach ($namespaces as $namespaceLocalName => $namespaceValue) {
39 8
            if (str_starts_with($namespaceLocalName, 'xmlns') === false) {
40 8
                $namespaceLocalName = rtrim(sprintf('xmlns:%s', $namespaceLocalName), ':');
41
            }
42
43 8
            $attributes[$namespaceLocalName] = $namespaceValue;
44
        }
45
46 8
        return $attributes;
47
    }
48
49
    /**
50
     * Normalize value.
51
     */
52 77
    public static function encodeValue(mixed $value): ?string
53
    {
54 77
        if ($value === null) {
55 77
            return null;
56
        }
57
58 73
        if (is_bool($value)) {
59 15
            $value = $value ? 'true' : 'false';
60
        }
61
62 73
        if (is_scalar($value) || $value instanceof Stringable) {
63 73
            return (string) $value;
64
        }
65
66 1
        return null;
67
    }
68
69
    /**
70
     * Normalize value.
71
     */
72 3
    public static function decodeValue(mixed $value): mixed
73
    {
74 3
        if (is_numeric($value)) {
75 3
            return $value + 0;
76
        }
77
78 3
        if (in_array($value, ['true', 'True'], true)) {
79 3
            return true;
80
        }
81
82 3
        if (in_array($value, ['false', 'False'], true)) {
83 2
            return false;
84
        }
85
86 3
        return $value;
87
    }
88
89
    /**
90
     * Convert DOM node to array
91
     */
92 20
    public static function nodeToArray(DOMNode $node, Config $config): mixed
93
    {
94 20
        return self::nodeToArrayRecursive($node, $config, 1);
95
    }
96
97 20
    private static function nodeToArrayRecursive(DOMNode $node, Config $config, int $depth): mixed
98
    {
99 20
        $value = null;
100
        /** @var array<string,mixed> $attributes */
101 20
        $attributes = [];
102
        /** @var array<string,array<mixed>> $nodes */
103 20
        $nodes = [];
104
105 20
        if ($node->hasAttributes()) {
106
            /** @var \DOMAttr $attribute */
107 13
            foreach ($node->attributes ?? [] as $attribute) {
108 13
                $attributes[$attribute->nodeName] = $config->isAutoCast()
109 2
                    ? self::decodeValue($attribute->nodeValue)
110 11
                    : $attribute->nodeValue;
111
            }
112
        }
113
114 20
        if ($node->hasChildNodes()) {
115
            /** @var \DOMNode $child */
116 18
            foreach ($node->childNodes ?? [] as $child) {
117 18
                if (in_array($child->nodeType, [XML_TEXT_NODE, XML_CDATA_SECTION_NODE])) {
118 18
                    if (trim((string) $child->nodeValue) !== '') {
119 18
                        $value = $config->isAutoCast()
120 2
                            ? self::decodeValue($child->nodeValue)
121 16
                            : $child->nodeValue;
122
                    }
123
124 18
                    continue;
125
                }
126
127 18
                $childNodes = self::nodeToArrayRecursive($child, $config, $depth + 1);
128 18
                if ($config->isFlatten()) {
129 5
                    self::flattenArray($nodes, $config->isWithoutRoot() && $depth === 1 ? '' : $child->nodeName, $childNodes, $config);
130
                } else {
131 13
                    $nodes[$child->nodeName][] = $childNodes;
132
                }
133
            }
134
        }
135
136 20
        if ($config->isFullResponse()) {
137 1
            return [
138 1
                $config->getAttributesName() => $attributes,
139 1
                $config->getValueName() => $value,
140 1
                $config->getNodesName() => $nodes,
141 1
            ];
142
        }
143
144 19
        if (count($nodes) === 0 && count($attributes) === 0) {
145 17
            return $value;
146
        }
147
148 18
        return self::simplifyArray($attributes, $value, $nodes, $config, $node);
149
    }
150
151
    /**
152
     * Flatten node to one-dimensional array
153
     *
154
     * @param array<string,array<mixed>> $nodes
155
     */
156 5
    private static function flattenArray(array &$nodes, string $nodeNames, mixed $childNodes, Config $config): void
157
    {
158 5
        if (!is_array($childNodes)) {
159 5
            $nodes[$nodeNames][] = $childNodes;
160
161 5
            return;
162
        }
163
164 5
        foreach ($childNodes as $childNodeName => $childNodeValues) {
165 5
            if ($childNodeName === $config->getAttributesName() && is_array($childNodeValues)) {
166 5
                foreach ($childNodeValues as $attributeName => $attributeValue) {
167 5
                    $nodeKey = sprintf('%s%s%s', $nodeNames, $config->getFlattenAttributes(), $attributeName);
168 5
                    $nodes[$nodeKey][] = $attributeValue;
169
                }
170 5
            } elseif ($childNodeName === $config->getValueName()) {
171 5
                $nodes[$nodeNames][] = $childNodeValues;
172
            } else {
173 5
                $nodeKey = ltrim(sprintf('%s%s%s', $nodeNames, $config->getFlattenNodes(), $childNodeName), $config->getFlattenNodes());
174 5
                if (is_array($childNodeValues)) {
175 5
                    $nodes[$nodeKey] = array_merge($nodes[$nodeKey] ?? [], $childNodeValues);
176
                } else {
177 4
                    $nodes[$nodeKey][] = $childNodeValues;
178
                }
179
            }
180
        }
181
    }
182
183
    /**
184
     * Remove unnecessary data
185
     *
186
     * @param array<string,mixed> $attributes
187
     * @param array<string,array<mixed>> $nodes
188
     *
189
     * @return array<int|string,mixed>
190
     */
191 18
    private static function simplifyArray(array $attributes, mixed $value, array $nodes, Config $config, DOMNode $node): array
192
    {
193 18
        $simpleResult = $nodes;
194 18
        foreach ($nodes as $nodeName => $values) {
195
            if (
196 17
                !$config->isAlwaysArray()
197 17
                && in_array($nodeName, $config->getAlwaysArrayNodeNames(), true) === false
198 17
                && in_array($node->nodeName . '.' . $nodeName, $config->getAlwaysArrayNodeNames(), true) === false
199 17
                && array_keys($values) === [0]
200
            ) {
201 14
                $simpleResult[$nodeName] = $values[0];
202
            }
203
        }
204
205 18
        if (count($attributes) > 0) {
206 12
            $simpleResult[$config->getAttributesName()] = $attributes;
207
        }
208
209 18
        if ($value !== null) {
210 9
            $simpleResult[$config->getValueName()] = $value;
211
        }
212
213 18
        return $simpleResult;
214
    }
215
}
216