Completed
Push — master ( 6757f6...a9e5bc )
by Tomáš
35:28 queued 34:12
created

XMLReader::getNodeName()   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
        $localName = Formatter::getLocalName($nodeName);
139
140 5
        if ($localName === null) {
141
            return false;
142
        }
143
144 5
        while ($this->reader->next($localName)) {
145 5
            if ($this->getNodeName() === $nodeName) {
146 4
                return true;
147
            }
148
        }
149
150 5
        return false;
151
    }
152
153
    /**
154
     * Return associative array of element by name
155
     *
156
     * @return \Inspirum\XML\Services\XMLNode|null
157
     */
158 10
    private function readNode(): ?XMLNode
159
    {
160 10
        $nodeName   = $this->getNodeName();
161 10
        $attributes = $this->getNodeAttributes();
162
163 10
        if ($this->isNodeEmptyElementType()) {
164 1
            return $this->xml->createElement($nodeName, $attributes);
165
        }
166
167 9
        $node     = null;
168 9
        $text     = null;
169 9
        $elements = [];
170
171 9
        while ($this->read()) {
172 9
            if ($this->isNodeElementEndType() && $this->getNodeName() == $nodeName) {
173 9
                $node = $this->xml->createTextElement($nodeName, $text, $attributes);
174
175 9
                foreach ($elements as $element) {
176 6
                    $node->append($element);
177
                }
178
179 9
                break;
180 8
            } elseif ($this->isNodeTextType()) {
181 8
                $text = $this->getNodeValue();
182 6
            } elseif ($this->isNodeElementType()) {
183 6
                if ($this->isNodeEmptyElementType()) {
184 1
                    $elements[] = $this->xml->createElement($this->getNodeName());
185 1
                    continue;
186
                }
187
188 6
                $element = $this->readNode();
189
190 6
                if ($element instanceof XMLNode) {
191 6
                    $elements[] = $element;
192
                }
193
            }
194
        }
195
196 9
        return $node;
197
    }
198
199
    /**
200
     * Move to next node in document
201
     *
202
     * @return bool
203
     *
204
     * @throws \DOMException
205
     */
206 14
    private function read(): bool
207
    {
208 14
        return $this->withErrorHandler(function () {
209 14
            return $this->reader->read();
210 14
        });
211
    }
212
213
    /**
214
     * Get current node name
215
     *
216
     * @return string
217
     */
218 12
    private function getNodeName(): string
219
    {
220 12
        return $this->reader->name;
221
    }
222
223
    /**
224
     * Get current node type
225
     *
226
     * @return int
227
     */
228 12
    private function getNodeType(): int
229
    {
230 12
        return $this->reader->nodeType;
231
    }
232
233
    /**
234
     * Get current node value
235
     *
236
     * @return string
237
     */
238 10
    private function getNodeValue(): string
239
    {
240 10
        return $this->reader->value;
241
    }
242
243
    /**
244
     * If current node is element open tag
245
     *
246
     * @return bool
247
     */
248 12
    private function isNodeElementType(): bool
249
    {
250 12
        return $this->isNodeType(BaseXMLReader::ELEMENT);
251
    }
252
253
    /**
254
     * If current node is element open tag
255
     *
256
     * @return bool
257
     */
258 10
    private function isNodeEmptyElementType(): bool
259
    {
260 10
        return $this->reader->isEmptyElement;
261
    }
262
263
    /**
264
     * If current node is element close tag
265
     *
266
     * @return bool
267
     */
268 9
    private function isNodeElementEndType(): bool
269
    {
270 9
        return $this->isNodeType(BaseXMLReader::END_ELEMENT);
271
    }
272
273
    /**
274
     * If current node is text content
275
     *
276
     * @return bool
277
     */
278 8
    private function isNodeTextType(): bool
279
    {
280 8
        return $this->isNodeType(BaseXMLReader::TEXT) || $this->isNodeType(BaseXMLReader::CDATA);
281
    }
282
283
    /**
284
     * If current node is given node type
285
     *
286
     * @param int $type
287
     *
288
     * @return bool
289
     */
290 12
    private function isNodeType(int $type): bool
291
    {
292 12
        return $this->getNodeType() === $type;
293
    }
294
295
    /**
296
     * Get current node attributes
297
     *
298
     * @return array<string,string>
299
     */
300 10
    private function getNodeAttributes(): array
301
    {
302 10
        $attributes = [];
303
304 10
        if ($this->reader->hasAttributes) {
305 6
            while ($this->reader->moveToNextAttribute()) {
306 6
                $attributes[$this->getNodeName()] = $this->getNodeValue();
307
            }
308 6
            $this->reader->moveToElement();
309
        }
310
311 10
        return $attributes;
312
    }
313
314
    /**
315
     * Register custom error handler to throw Exception on warning message
316
     *
317
     * @param callable $callback
318
     *
319
     * @return mixed
320
     *
321
     * @throws \DOMException
322
     */
323 17
    protected function withErrorHandler(callable $callback)
324
    {
325 17
        set_error_handler(function (int $code, string $message, string $file, int $line, array $context): bool {
3 ignored issues
show
Unused Code introduced by
The parameter $file is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $line is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $context is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
326 4
            if (strpos($message, 'XMLReader::') !== false) {
327 4
                throw new Exception($message, $code);
328
            }
329
330
            return false;
331 17
        });
332
333 17
        $response = $callback();
334
335 15
        restore_error_handler();
336
337 15
        return $response;
338
    }
339
}
340