Completed
Branch master (f25cb1)
by Tomáš
01:19
created

XMLReader::isNodeEmptyElementType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 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 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
        while ($this->reader->next(Formatter::getLocalName($nodeName))) {
139 5
            if ($this->getNodeName() === $nodeName) {
140 4
                return true;
141
            }
142
        }
143
144 5
        return false;
145
    }
146
147
    /**
148
     * Return associative array of element by name
149
     *
150
     * @return \Inspirum\XML\Services\XMLNode|null
151
     */
152 10
    private function readNode(): ?XMLNode
153
    {
154 10
        $nodeName   = $this->getNodeName();
155 10
        $attributes = $this->getNodeAttributes();
156
157 10
        if ($this->isNodeEmptyElementType()) {
158 1
            return $this->xml->createElement($nodeName, $attributes);
159
        }
160
161 9
        $node     = null;
162 9
        $text     = null;
163 9
        $elements = [];
164
165 9
        while ($this->read()) {
166 9
            if ($this->isNodeElementEndType() && $this->getNodeName() == $nodeName) {
167 9
                $node = $this->xml->createTextElement($nodeName, $text, $attributes);
168
169 9
                foreach ($elements as $element) {
170 6
                    $node->append($element);
171
                }
172
173 9
                break;
174 8
            } elseif ($this->isNodeTextType()) {
175 8
                $text = $this->getNodeValue();
176 6
            } elseif ($this->isNodeElementType()) {
177 6
                if ($this->isNodeEmptyElementType()) {
178 1
                    $elements[] = $this->xml->createElement($this->getNodeName());
179 1
                    continue;
180
                }
181
182 6
                $element = $this->readNode();
183
184 6
                if ($element instanceof XMLNode) {
185 6
                    $elements[] = $element;
186
                }
187
            }
188
        }
189
190 9
        return $node;
191
    }
192
193
    /**
194
     * Move to next node in document
195
     *
196
     * @return bool
197
     *
198
     * @throws \DOMException
199
     */
200 14
    private function read(): bool
201
    {
202 14
        return $this->withErrorHandler(function () {
203 14
            return $this->reader->read();
204 14
        });
205
    }
206
207
    /**
208
     * Get current node name
209
     *
210
     * @return string
211
     */
212 12
    private function getNodeName(): string
213
    {
214 12
        return $this->reader->name;
215
    }
216
217
    /**
218
     * Get current node type
219
     *
220
     * @return int
221
     */
222 12
    private function getNodeType(): int
223
    {
224 12
        return $this->reader->nodeType;
225
    }
226
227
    /**
228
     * Get current node value
229
     *
230
     * @return string
231
     */
232 10
    private function getNodeValue(): string
233
    {
234 10
        return $this->reader->value;
235
    }
236
237
    /**
238
     * If current node is element open tag
239
     *
240
     * @return bool
241
     */
242 12
    private function isNodeElementType(): bool
243
    {
244 12
        return $this->isNodeType(BaseXMLReader::ELEMENT);
245
    }
246
247
    /**
248
     * If current node is element open tag
249
     *
250
     * @return bool
251
     */
252 10
    private function isNodeEmptyElementType(): bool
253
    {
254 10
        return $this->reader->isEmptyElement;
255
    }
256
257
    /**
258
     * If current node is element close tag
259
     *
260
     * @return bool
261
     */
262 9
    private function isNodeElementEndType(): bool
263
    {
264 9
        return $this->isNodeType(BaseXMLReader::END_ELEMENT);
265
    }
266
267
    /**
268
     * If current node is text content
269
     *
270
     * @return bool
271
     */
272 8
    private function isNodeTextType(): bool
273
    {
274 8
        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 12
    private function isNodeType(int $type): bool
285
    {
286 12
        return $this->getNodeType() === $type;
287
    }
288
289
    /**
290
     * Get current node attributes
291
     *
292
     * @return array<string,string>
293
     */
294 10
    private function getNodeAttributes(): array
295
    {
296 10
        $attributes = [];
297
298 10
        if ($this->reader->hasAttributes) {
299 6
            while ($this->reader->moveToNextAttribute()) {
300 6
                $attributes[$this->getNodeName()] = $this->getNodeValue();
301
            }
302 6
            $this->reader->moveToElement();
303
        }
304
305 10
        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 26
    protected function withErrorHandler(callable $callback)
318
    {
319 17
        set_error_handler(function (int $code, string $message) {
320 13
            if (strpos($message, 'XMLReader::') !== false) {
321 4
                throw new Exception($message, $code);
322
            }
323 26
        });
324
325 17
        $response = $callback();
326
327 15
        restore_error_handler();
328
329 15
        return $response;
330
    }
331
}
332