DefaultReader::isNodeElementType()   A
last analyzed

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 array_values;
19
use function count;
20
use function explode;
21
use function in_array;
22
use function ksort;
23
use function ltrim;
24
use function str_starts_with;
25
use const ARRAY_FILTER_USE_KEY;
26
27
final class DefaultReader implements Reader
28
{
29
    private int $depth = 0;
30
31
    private bool $next = false;
32
33 39
    public function __construct(
34
        private readonly XMLReader $reader,
35
        private readonly Document $document,
36
    ) {
37 39
    }
38
39 39
    public function __destruct()
40
    {
41 39
        $this->close();
42
    }
43
44
    /**
45
     * @inheritDoc
46
     */
47 26
    public function iterateNode(string $nodeName, bool $withNamespaces = false): iterable
48
    {
49 26
        $found = $this->moveToNode($nodeName);
50
51 25
        if ($found->found === false) {
52 1
            return yield from [];
53
        }
54
55 24
        $rootNamespaces = $withNamespaces ? $found->namespaces : null;
56
57 24
        while ($found->found) {
58 24
            yield $this->readNode($rootNamespaces)->node;
59
60 24
            $found = $this->moveToNode($nodeName);
61
        }
62
    }
63
64 14
    public function nextNode(string $nodeName): ?Node
65
    {
66 14
        $found = $this->moveToNode($nodeName);
67
68 13
        if ($found->found === false) {
69 4
            return null;
70
        }
71
72 11
        return $this->readNode()->node;
73
    }
74
75 39
    public function close(): void
76
    {
77 39
        $this->reader->close();
78
    }
79
80
    /**
81
     * @throws \Exception
82
     */
83 39
    private function moveToNode(string $nodeName): MoveResult
84
    {
85 39
        $usePath = str_starts_with($nodeName, '/');
86 39
        $paths = explode('/', ltrim($nodeName, '/'));
87 39
        $maxDepth = count($paths) - 1;
88
89 39
        $namespaces = [];
90
91 39
        while ($this->read()) {
92 37
            if ($this->isNodeElementType()) {
93 37
                if ($usePath && $this->getNodeName() !== ($paths[$this->depth] ?? null)) {
94 6
                    $this->next();
95 6
                    continue;
96
                }
97
98 37
                if ($usePath && $this->depth === $maxDepth && $this->getNodeName() === ($paths[$this->depth] ?? null)) {
99 5
                    return MoveResult::found($namespaces);
100
                }
101
102 37
                if (!$usePath && $this->getNodeName() === $nodeName) {
103 29
                    return MoveResult::found($namespaces);
104
                }
105
106 36
                if (!$this->isNodeEmptyElementType()) {
107 36
                    $this->depth++;
108
                }
109
            }
110
111 36
            if ($this->isNodeElementEndType()) {
112 33
                $this->depth--;
113
            }
114
115 36
            $namespaces = array_merge($namespaces, $this->getNodeNamespaces());
116
        }
117
118 29
        return MoveResult::notFound();
119
    }
120
121
    /**
122
     * @param array<string,string>|null $rootNamespaces
123
     *
124
     * @throws \Exception
125
     */
126 34
    private function readNode(?array $rootNamespaces = null): ReadResult
127
    {
128 34
        $name = $this->getNodeName();
129 34
        $attributes = $this->getNodeAttributes();
130 34
        $namespaces = Parser::parseNamespaces($attributes);
131
132 34
        if ($this->isNodeEmptyElementType()) {
133 3
            return $this->createEmptyNode($name, $attributes, $namespaces, $rootNamespaces);
134
        }
135
136
        /** @var list<\Inspirum\XML\Reader\ReadResult> $elements */
137 32
        $elements = [];
138 32
        $text = null;
139
140 32
        while ($this->read()) {
141 32
            if ($this->isNodeElementEndType() && $this->getNodeName() === $name) {
142 32
                return $this->createNode($name, $text, $attributes, $namespaces, $rootNamespaces, $elements);
0 ignored issues
show
Bug introduced by
$elements of type Inspirum\XML\Reader\list is incompatible with the type array expected by parameter $elements of Inspirum\XML\Reader\DefaultReader::createNode(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

142
                return $this->createNode($name, $text, $attributes, $namespaces, $rootNamespaces, /** @scrutinizer ignore-type */ $elements);
Loading history...
143
            }
144
145 31
            if ($this->isNodeTextType()) {
146 31
                $text = $this->getNodeValue();
147 29
            } elseif ($this->isNodeElementType()) {
148 29
                $elements[] = $this->readNode();
149
            }
150
        }
151
152
        // @codeCoverageIgnoreStart
153
        throw new Exception('\XMLReader::read() opening and ending tag mismatch');
154
        // @codeCoverageIgnoreEnd
155
    }
156
157
    /**
158
     * @param array<string,string> $attributes
159
     * @param array<string,string> $namespaces
160
     * @param array<string,string>|null $rootNamespaces
161
     */
162 3
    private function createEmptyNode(string $name, array $attributes, array $namespaces, ?array $rootNamespaces): ReadResult
163
    {
164 3
        return $this->createNode($name, null, $attributes, $namespaces, $rootNamespaces, []);
165
    }
166
167
    /**
168
     * @param array<string,string> $attributes
169
     * @param array<string,string> $namespaces
170
     * @param array<string,string>|null $rootNamespaces
171
     * @param list<\Inspirum\XML\Reader\ReadResult> $elements
0 ignored issues
show
Bug introduced by
The type Inspirum\XML\Reader\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
172
     *
173
     * @throws \DOMException
174
     */
175 34
    private function createNode(string $name, mixed $text, array $attributes, array $namespaces, ?array $rootNamespaces, array $elements): ReadResult
176
    {
177 34
        $usedNamespaces = $this->getUsedNamespaces($name, $attributes);
178
179 34
        $namespaces = array_merge($namespaces, ...array_map(static fn (ReadResult $element) => $element->namespaces, $elements));
180 34
        $usedNamespaces = array_merge($usedNamespaces, ...array_map(static fn (ReadResult $element) => $element->usedNamespaces, $elements));
181
182 34
        $withNamespace = $rootNamespaces !== null;
183
184 34
        if ($withNamespace) {
185 8
            $namespaceAttributes = $this->namespacesToAttributes($namespaces, $rootNamespaces);
186 8
            $namespaceAttributes = array_filter(
187 8
                $namespaceAttributes,
188 8
                static fn ($namespaceLocalName) => in_array(Parser::getLocalName($namespaceLocalName), $usedNamespaces),
189 8
                ARRAY_FILTER_USE_KEY,
190 8
            );
191
192 8
            $attributes = array_merge($namespaceAttributes, $attributes);
193
        }
194
195 34
        $node = $this->document->createTextElement($name, $text, $attributes, withNamespaces: $withNamespace);
196
197 34
        foreach ($elements as $element) {
198 29
            $node->append($element->node);
199
        }
200
201 34
        return ReadResult::create($node, $namespaces, $usedNamespaces);
202
    }
203
204
    /**
205
     * @param array<string,string> $attributes
206
     *
207
     * @return list<string>
208
     */
209 34
    private function getUsedNamespaces(string $name, array $attributes): array
210
    {
211 34
        return array_values(array_filter([
0 ignored issues
show
Bug Best Practice introduced by
The expression return array_values(arra...on(...) { /* ... */ })) returns the type array which is incompatible with the documented return type Inspirum\XML\Reader\list.
Loading history...
212 34
            Parser::getNamespacePrefix($name),
213 34
            ...array_map(static fn ($attributeName) => Parser::getNamespacePrefix($attributeName), array_keys($attributes)),
214 34
        ], static fn ($ns) => $ns !== null && $ns !== 'xmlns'));
215
    }
216
217
    /**
218
     * @param array<string,string> $namespaces
219
     * @param array<string,string> $rootNamespaces
220
     *
221
     * @return array<string,string>
222
     */
223 8
    private function namespacesToAttributes(array $namespaces, array $rootNamespaces): array
224
    {
225 8
        $mergedNamespaces = Formatter::namespacesToAttributes(array_merge($namespaces, $rootNamespaces));
226 8
        ksort($mergedNamespaces);
227
228 8
        return $mergedNamespaces;
229
    }
230
231
    /**
232
     * @throws \Exception
233
     */
234 39
    private function read(): bool
235
    {
236 39
        if ($this->next) {
237 6
            $this->next = false;
238
239 6
            return $this->reader->next();
240
        }
241
242 39
        return Handler::withErrorHandlerForXMLReader(fn (): bool => $this->reader->read());
243
    }
244
245 6
    private function next(): void
246
    {
247 6
        $this->next = true;
248
    }
249
250 37
    private function getNodeName(): string
251
    {
252 37
        return $this->reader->name;
253
    }
254
255 37
    private function getNodeType(): int
256
    {
257 37
        return $this->reader->nodeType;
258
    }
259
260 37
    private function getNodeValue(): string
261
    {
262 37
        return $this->reader->value;
263
    }
264
265 37
    private function isNodeElementType(): bool
266
    {
267 37
        return $this->isNodeType(XMLReader::ELEMENT);
268
    }
269
270 37
    private function isNodeEmptyElementType(): bool
271
    {
272 37
        return $this->reader->isEmptyElement;
273
    }
274
275 37
    private function isNodeElementEndType(): bool
276
    {
277 37
        return $this->isNodeType(XMLReader::END_ELEMENT);
278
    }
279
280 31
    private function isNodeTextType(): bool
281
    {
282 31
        return $this->isNodeType(XMLReader::TEXT) || $this->isNodeType(XMLReader::CDATA);
283
    }
284
285 37
    private function isNodeType(int $type): bool
286
    {
287 37
        return $this->getNodeType() === $type;
288
    }
289
290
    /**
291
     * @return array<string,string>
292
     */
293 37
    private function getNodeAttributes(): array
294
    {
295 37
        $attributes = [];
296
297 37
        if ($this->reader->hasAttributes) {
298 36
            while ($this->reader->moveToNextAttribute()) {
299 36
                $attributes[$this->getNodeName()] = $this->getNodeValue();
300
            }
301
302 36
            $this->reader->moveToElement();
303
        }
304
305 37
        return $attributes;
306
    }
307
308
    /**
309
     * @return array<string,string>
310
     */
311 36
    private function getNodeNamespaces(): array
312
    {
313 36
        return Parser::parseNamespaces($this->getNodeAttributes());
314
    }
315
}
316