Completed
Push — master ( 787a24...f25cb1 )
by Tomáš
42:21
created

XMLReader::iterateNode()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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