Passed
Push — master ( dcddfc...dfc3a0 )
by Tomáš
11:42
created

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