Completed
Push — master ( ed15b8...6757f6 )
by Tomáš
41:49
created

XMLReader::getNodeName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
1
<?php
2
3
namespace Inspirum\XML\Services;
4
5
use Exception;
6
use XMLReader as BaseXMLReader;
7
8
class XMLReader
9
{
10
    /**
11
     * XML Reader
12
     *
13
     * @var \XMLReader
14
     */
15
    private $reader;
16
17
    /**
18
     * XML document
19
     *
20
     * @var \Inspirum\XML\Services\XML
21
     */
22
    private $xml;
23
24
    /**
25
     * XMLReader constructor
26
     *
27
     * @param string $filepath
28
     */
29 17
    public function __construct(string $filepath)
30
    {
31 17
        $this->reader = $this->open($filepath);
32 15
        $this->xml    = new XML();
33 15
    }
34
35
    /**
36
     * XMLReader destructor
37
     */
38 13
    public function __destruct()
39
    {
40 13
        $this->reader->close();
41 13
    }
42
43
    /**
44
     * Open file
45
     *
46
     * @param string $filepath
47
     *
48
     * @return \XMLReader
49
     *
50
     * @throws \Exception
51
     */
52 17
    private function open(string $filepath): BaseXMLReader
53
    {
54 17
        $xmlReader = new BaseXMLReader();
55
56 17
        $opened = $this->withErrorHandler(function () use ($xmlReader, $filepath) {
57 17
            return $xmlReader->open($filepath);
58 17
        });
59
60 15
        if ($opened == false) {
61
            // @codeCoverageIgnoreStart
62
            throw new Exception('\XMLReader::open() method failed');
63
            // @codeCoverageIgnoreEnd
64
        }
65
66 15
        return $xmlReader;
67
    }
68
69
    /**
70
     * Parse file and yield next node
71
     *
72
     * @param string $nodeName
73
     *
74
     * @return \Generator|\Inspirum\XML\Services\XMLNode[]
75
     */
76 7
    public function iterateNode(string $nodeName): iterable
77
    {
78 7
        $found = $this->moveToNode($nodeName);
79
80 6
        if ($found === false) {
81 1
            return yield from [];
82
        }
83
84
        do {
85 5
            $item = $this->readNode();
86
87 5
            if ($item !== null) {
88 5
                yield $item;
89
            }
90 5
        } while ($this->moveToNextNode($nodeName));
91 5
    }
92
93
    /**
94
     * Get next node
95
     *
96
     * @param string $nodeName
97
     *
98
     * @return \Inspirum\XML\Services\XMLNode|null
99
     */
100 8
    public function nextNode(string $nodeName): ?XMLNode
101
    {
102 8
        $found = $this->moveToNode($nodeName);
103
104 7
        if ($found === false) {
105 2
            return null;
106
        }
107
108 6
        return $this->readNode();
109
    }
110
111
    /**
112
     * Move to first element by tag name
113
     *
114
     * @param string $nodeName
115
     *
116
     * @return bool
117
     */
118 14
    private function moveToNode(string $nodeName): bool
119
    {
120 14
        while ($this->read()) {
121 12
            if ($this->isNodeElementType() && $this->getNodeName() === $nodeName) {
122 10
                return true;
123
            }
124
        }
125
126 3
        return false;
127
    }
128
129
    /**
130
     * Move to next sibling element by tag name
131
     *
132
     * @param string $nodeName
133
     *
134
     * @return bool
135
     */
136 5
    private function moveToNextNode(string $nodeName): bool
137
    {
138 5
        $localName = Formatter::getLocalName($nodeName);
139 5
140 4
        if ($localName === null) {
141
            return false;
142
        }
143
144 5
        while ($this->reader->next($localName)) {
145
            if ($this->getNodeName() === $nodeName) {
146
                return true;
147
            }
148
        }
149
150
        return false;
151
    }
152 10
153
    /**
154 10
     * Return associative array of element by name
155 10
     *
156
     * @return \Inspirum\XML\Services\XMLNode|null
157 10
     */
158 1
    private function readNode(): ?XMLNode
159
    {
160
        $nodeName   = $this->getNodeName();
161 9
        $attributes = $this->getNodeAttributes();
162 9
163 9
        if ($this->isNodeEmptyElementType()) {
164
            return $this->xml->createElement($nodeName, $attributes);
165 9
        }
166 9
167 9
        $node     = null;
168
        $text     = null;
169 9
        $elements = [];
170 6
171
        while ($this->read()) {
172
            if ($this->isNodeElementEndType() && $this->getNodeName() == $nodeName) {
173 9
                $node = $this->xml->createTextElement($nodeName, $text, $attributes);
174 8
175 8
                foreach ($elements as $element) {
176 6
                    $node->append($element);
177 6
                }
178 1
179 1
                break;
180
            } elseif ($this->isNodeTextType()) {
181
                $text = $this->getNodeValue();
182 6
            } elseif ($this->isNodeElementType()) {
183
                if ($this->isNodeEmptyElementType()) {
184 6
                    $elements[] = $this->xml->createElement($this->getNodeName());
185 6
                    continue;
186
                }
187
188
                $element = $this->readNode();
189
190 9
                if ($element instanceof XMLNode) {
191
                    $elements[] = $element;
192
                }
193
            }
194
        }
195
196
        return $node;
197
    }
198
199
    /**
200 14
     * Move to next node in document
201
     *
202 14
     * @return bool
203 14
     *
204 14
     * @throws \DOMException
205
     */
206
    private function read(): bool
207
    {
208
        return $this->withErrorHandler(function () {
209
            return $this->reader->read();
210
        });
211
    }
212 12
213
    /**
214 12
     * Get current node name
215
     *
216
     * @return string
217
     */
218
    private function getNodeName(): string
219
    {
220
        return $this->reader->name;
221
    }
222 12
223
    /**
224 12
     * Get current node type
225
     *
226
     * @return int
227
     */
228
    private function getNodeType(): int
229
    {
230
        return $this->reader->nodeType;
231
    }
232 10
233
    /**
234 10
     * Get current node value
235
     *
236
     * @return string
237
     */
238
    private function getNodeValue(): string
239
    {
240
        return $this->reader->value;
241
    }
242 12
243
    /**
244 12
     * If current node is element open tag
245
     *
246
     * @return bool
247
     */
248
    private function isNodeElementType(): bool
249
    {
250
        return $this->isNodeType(BaseXMLReader::ELEMENT);
251
    }
252 10
253
    /**
254 10
     * If current node is element open tag
255
     *
256
     * @return bool
257
     */
258
    private function isNodeEmptyElementType(): bool
259
    {
260
        return $this->reader->isEmptyElement;
261
    }
262 9
263
    /**
264 9
     * If current node is element close tag
265
     *
266
     * @return bool
267
     */
268
    private function isNodeElementEndType(): bool
269
    {
270
        return $this->isNodeType(BaseXMLReader::END_ELEMENT);
271
    }
272 8
273
    /**
274 8
     * If current node is text content
275
     *
276
     * @return bool
277
     */
278
    private function isNodeTextType(): bool
279
    {
280
        return $this->isNodeType(BaseXMLReader::TEXT) || $this->isNodeType(BaseXMLReader::CDATA);
281
    }
282
283
    /**
284 12
     * If current node is given node type
285
     *
286 12
     * @param int $type
287
     *
288
     * @return bool
289
     */
290
    private function isNodeType(int $type): bool
291
    {
292
        return $this->getNodeType() === $type;
293
    }
294 10
295
    /**
296 10
     * Get current node attributes
297
     *
298 10
     * @return array<string,string>
299 6
     */
300 6
    private function getNodeAttributes(): array
301
    {
302 6
        $attributes = [];
303
304
        if ($this->reader->hasAttributes) {
305 10
            while ($this->reader->moveToNextAttribute()) {
306
                $attributes[$this->getNodeName()] = $this->getNodeValue();
307
            }
308
            $this->reader->moveToElement();
309
        }
310
311
        return $attributes;
312
    }
313
314
    /**
315
     * Register custom error handler to throw Exception on warning message
316
     *
317 17
     * @param callable $callback
318
     *
319 17
     * @return mixed
320 4
     *
321 4
     * @throws \DOMException
322
     */
323 17
    protected function withErrorHandler(callable $callback)
324
    {
325 17
        set_error_handler(function (int $code, string $message) {
326
            if (strpos($message, 'XMLReader::') !== false) {
327 15
                throw new Exception($message, $code);
328
            }
329 15
        });
330
331
        $response = $callback();
332
333
        restore_error_handler();
334
335
        return $response;
336
    }
337
}
338