Passed
Push — master ( 78eb45...181a6b )
by Tomáš
01:35
created

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