Passed
Push — master ( dfc3a0...187a17 )
by Tomáš
01:45
created

DefaultReader::getNodeValue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Inspirum\XML\Reader;
6
7
use Exception;
8
use Inspirum\XML\Builder\Document;
9
use Inspirum\XML\Builder\Node;
10
use Inspirum\XML\Exception\Handler;
11
use Inspirum\XML\Formatter\Formatter;
12
use Inspirum\XML\Parser\Parser;
13
use XMLReader;
14
use function array_filter;
15
use function array_keys;
16
use function array_map;
17
use function array_merge;
18
use function in_array;
19
use function ksort;
20
use const ARRAY_FILTER_USE_KEY;
21
22
final class DefaultReader implements Reader
23
{
24 21
    public function __construct(
25
        private readonly XMLReader $reader,
26
        private readonly Document $document,
27
    ) {
28 21
    }
29
30 21
    public function __destruct()
31
    {
32 21
        $this->reader->close();
33
    }
34
35
    /**
36
     * @inheritDoc
37
     */
38 14
    public function iterateNode(string $nodeName, bool $withNamespaces = false): iterable
39
    {
40 14
        $found = $this->moveToNode($nodeName);
41
42 13
        if ($found->found === false) {
43 1
            return yield from [];
44
        }
45
46
        do {
47 12
            yield $this->readNode($withNamespaces ? $found->namespaces : null)->node;
48 12
        } while ($this->moveToNextNode($nodeName));
49
    }
50
51 8
    public function nextNode(string $nodeName): ?Node
52
    {
53 8
        $found = $this->moveToNode($nodeName);
54
55 7
        if ($found->found === false) {
56 2
            return null;
57
        }
58
59 6
        return $this->readNode()->node;
60
    }
61
62
    /**
63
     * @throws \Exception
64
     */
65 21
    private function moveToNode(string $nodeName): MoveResult
66
    {
67 21
        $namespaces = [];
68
69 21
        while ($this->read()) {
70 19
            if ($this->isNodeElementType() && $this->getNodeName() === $nodeName) {
71 17
                return MoveResult::found($namespaces);
72
            }
73
74 18
            $namespaces = array_merge($namespaces, $this->getNodeNamespaces());
75
        }
76
77 3
        return MoveResult::notFound();
78
    }
79
80 12
    private function moveToNextNode(string $nodeName): bool
81
    {
82 12
        $localName = Parser::getLocalName($nodeName);
83
84 12
        while ($this->reader->next($localName)) {
85 12
            if ($this->getNodeName() === $nodeName) {
86 11
                return true;
87
            }
88
        }
89
90 12
        return false;
91
    }
92
93
    /**
94
     * @param array<string,string>|null $rootNamespaces
95
     *
96
     * @throws \Exception
97
     */
98 17
    private function readNode(?array $rootNamespaces = null): ReadResult
99
    {
100 17
        $name       = $this->getNodeName();
101 17
        $attributes = $this->getNodeAttributes();
102 17
        $namespaces = Parser::parseNamespaces($attributes);
103
104 17
        if ($this->isNodeEmptyElementType()) {
105 2
            return $this->createEmptyNode($name, $attributes, $namespaces, $rootNamespaces);
106
        }
107
108
        /** @var array<\Inspirum\XML\Reader\ReadResult> $elements */
109 16
        $elements = [];
110 16
        $text     = null;
111
112 16
        while ($this->read()) {
113 16
            if ($this->isNodeElementEndType() && $this->getNodeName() === $name) {
114 16
                return $this->createNode($name, $text, $attributes, $namespaces, $rootNamespaces, $elements);
115
            }
116
117 15
            if ($this->isNodeTextType()) {
118 15
                $text = $this->getNodeValue();
119 13
            } elseif ($this->isNodeElementType()) {
120 13
                $elements[] = $this->readNode();
121
            }
122
        }
123
124
        // @codeCoverageIgnoreStart
125
        throw new Exception('\XMLReader::read() opening and ending tag mismatch');
126
        // @codeCoverageIgnoreEnd
127
    }
128
129
    /**
130
     * @param array<string,string>      $attributes
131
     * @param array<string,string>      $namespaces
132
     * @param array<string,string>|null $rootNamespaces
133
     */
134 2
    private function createEmptyNode(string $name, array $attributes, array $namespaces, ?array $rootNamespaces): ReadResult
135
    {
136 2
        return $this->createNode($name, null, $attributes, $namespaces, $rootNamespaces, []);
137
    }
138
139
    /**
140
     * @param array<string,string>                   $attributes
141
     * @param array<string,string>                   $namespaces
142
     * @param array<string,string>|null              $rootNamespaces
143
     * @param array<\Inspirum\XML\Reader\ReadResult> $elements
144
     *
145
     * @throws \DOMException
146
     */
147 17
    private function createNode(string $name, mixed $text, array $attributes, array $namespaces, ?array $rootNamespaces, array $elements): ReadResult
148
    {
149 17
        $usedNamespaces = $this->getUsedNamespaces($name, $attributes);
150
151 17
        $namespaces     = array_merge($namespaces, ...array_map(static fn(ReadResult $element) => $element->namespaces, $elements));
152 17
        $usedNamespaces = array_merge($usedNamespaces, ...array_map(static fn(ReadResult $element) => $element->usedNamespaces, $elements));
153
154 17
        $withNamespace = $rootNamespaces !== null;
155
156 17
        if ($withNamespace) {
157 4
            $namespaceAttributes = $this->namespacesToAttributes($namespaces, $rootNamespaces);
158 4
            $namespaceAttributes = array_filter($namespaceAttributes, static fn($namespaceLocalName) => in_array(Parser::getLocalName($namespaceLocalName), $usedNamespaces), ARRAY_FILTER_USE_KEY);
159
160 4
            $attributes = array_merge($namespaceAttributes, $attributes);
161
        }
162
163 17
        $node = $this->document->createTextElement($name, $text, $attributes, withNamespaces: $withNamespace);
164
165 17
        foreach ($elements as $element) {
166 13
            $node->append($element->node);
167
        }
168
169 17
        return ReadResult::create($node, $namespaces, $usedNamespaces);
170
    }
171
172
    /**
173
     * @param string               $name
174
     * @param array<string,string> $attributes
175
     *
176
     * @return array<string>
177
     */
178 17
    private function getUsedNamespaces(string $name, array $attributes): array
179
    {
180 17
        return array_filter([
181 17
            Parser::getNamespacePrefix($name),
182 17
            ...array_map(static fn($attributeName) => Parser::getNamespacePrefix($attributeName), array_keys($attributes)),
183 17
        ], static fn($ns) => $ns !== null && $ns !== 'xmlns');
184
    }
185
186
    /**
187
     * @param array<string,string> $namespaces
188
     * @param array<string,string> $rootNamespaces
189
     *
190
     * @return array<string,string>
191
     */
192 4
    private function namespacesToAttributes(array $namespaces, array $rootNamespaces): array
193
    {
194 4
        $mergedNamespaces = Formatter::namespacesToAttributes(array_merge($namespaces, $rootNamespaces));
195 4
        ksort($mergedNamespaces);
196
197 4
        return $mergedNamespaces;
198
    }
199
200
    /**
201
     * @throws \Exception
202
     */
203 21
    private function read(): bool
204
    {
205 21
        return Handler::withErrorHandlerForXMLReader(fn(): bool => $this->reader->read());
206
    }
207
208 19
    private function getNodeName(): string
209
    {
210 19
        return $this->reader->name;
211
    }
212
213 19
    private function getNodeType(): int
214
    {
215 19
        return $this->reader->nodeType;
216
    }
217
218 19
    private function getNodeValue(): string
219
    {
220 19
        return $this->reader->value;
221
    }
222
223 19
    private function isNodeElementType(): bool
224
    {
225 19
        return $this->isNodeType(XMLReader::ELEMENT);
226
    }
227
228 17
    private function isNodeEmptyElementType(): bool
229
    {
230 17
        return $this->reader->isEmptyElement;
231
    }
232
233 16
    private function isNodeElementEndType(): bool
234
    {
235 16
        return $this->isNodeType(XMLReader::END_ELEMENT);
236
    }
237
238 15
    private function isNodeTextType(): bool
239
    {
240 15
        return $this->isNodeType(XMLReader::TEXT) || $this->isNodeType(XMLReader::CDATA);
241
    }
242
243 19
    private function isNodeType(int $type): bool
244
    {
245 19
        return $this->getNodeType() === $type;
246
    }
247
248
    /**
249
     * @return array<string,string>
250
     */
251 19
    private function getNodeAttributes(): array
252
    {
253 19
        $attributes = [];
254
255 19
        if ($this->reader->hasAttributes) {
256 19
            while ($this->reader->moveToNextAttribute()) {
257 19
                $attributes[$this->getNodeName()] = $this->getNodeValue();
258
            }
259
260 19
            $this->reader->moveToElement();
261
        }
262
263 19
        return $attributes;
264
    }
265
266
    /**
267
     * @return array<string,string>
268
     */
269 18
    private function getNodeNamespaces(): array
270
    {
271 18
        return Parser::parseNamespaces($this->getNodeAttributes());
272
    }
273
}
274