Passed
Branch master (66e368)
by Tomáš
12:23
created

Formatter::flattenArray()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 22
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 8

Importance

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