Skip to content

Commit 181a6b7

Browse files
committed
Update changelog
1 parent 78eb457 commit 181a6b7

File tree

6 files changed

+226
-33
lines changed

6 files changed

+226
-33
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

88

9-
## [Unreleased](https://github.com/inspirum/xml-php/compare/v2.2.0...master)
9+
## [Unreleased](https://github.com/inspirum/xml-php/compare/v2.3.0...master)
10+
11+
12+
## [v2.3.0 (2023-04-27)](https://github.com/inspirum/xml-php/compare/v2.2.0...v2.3.0)
13+
### Added
14+
- Added option for [**Reader**](./src/Reader/Reader.php) to get/iterate nodes by its xpath.
15+
1016

1117

1218
## [v2.2.0 (2023-04-24)](https://github.com/inspirum/xml-php/compare/v2.1.0...v2.2.0)

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ $items = function(): iterable {
163163
/** @var \Inspirum\XML\Reader\ReaderFactory $factory */
164164
$reader = $factory->create('/output/feeds/google.xml');
165165

166-
foreach ($reader->iterateNode('item', true) as $item) {
166+
foreach ($reader->iterateNode('/rss/channel/item', true) as $item) {
167167
yield $item->toString();
168168
}
169169
}

src/Reader/DefaultReader.php

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,18 @@
1515
use function array_keys;
1616
use function array_map;
1717
use function array_merge;
18+
use function count;
19+
use function explode;
1820
use function in_array;
1921
use function ksort;
22+
use function ltrim;
23+
use function str_starts_with;
2024
use const ARRAY_FILTER_USE_KEY;
2125

2226
final class DefaultReader implements Reader
2327
{
28+
private int $depth = 0;
29+
2430
public function __construct(
2531
private readonly XMLReader $reader,
2632
private readonly Document $document,
@@ -29,7 +35,7 @@ public function __construct(
2935

3036
public function __destruct()
3137
{
32-
$this->reader->close();
38+
$this->close();
3339
}
3440

3541
/**
@@ -43,9 +49,13 @@ public function iterateNode(string $nodeName, bool $withNamespaces = false): ite
4349
return yield from [];
4450
}
4551

46-
do {
47-
yield $this->readNode($withNamespaces ? $found->namespaces : null)->node;
48-
} while ($this->moveToNextNode($nodeName));
52+
$rootNamespaces = $withNamespaces ? $found->namespaces : null;
53+
54+
while ($found->found) {
55+
yield $this->readNode($rootNamespaces)->node;
56+
57+
$found = $this->moveToNode($nodeName);
58+
}
4959
}
5060

5161
public function nextNode(string $nodeName): ?Node
@@ -59,16 +69,44 @@ public function nextNode(string $nodeName): ?Node
5969
return $this->readNode()->node;
6070
}
6171

72+
public function close(): void
73+
{
74+
$this->reader->close();
75+
}
76+
6277
/**
6378
* @throws \Exception
6479
*/
6580
private function moveToNode(string $nodeName): MoveResult
6681
{
82+
$usePath = str_starts_with($nodeName, '/');
83+
$paths = explode('/', ltrim($nodeName, '/'));
84+
$maxDepth = count($paths) - 1;
85+
6786
$namespaces = [];
6887

6988
while ($this->read()) {
70-
if ($this->isNodeElementType() && $this->getNodeName() === $nodeName) {
71-
return MoveResult::found($namespaces);
89+
if ($this->isNodeElementType()) {
90+
if ($usePath && $this->getNodeName() !== $paths[$this->depth]) {
91+
$this->next();
92+
continue;
93+
}
94+
95+
if ($usePath && $this->depth === $maxDepth && $this->getNodeName() === $paths[$this->depth]) {
96+
return MoveResult::found($namespaces);
97+
}
98+
99+
if (!$usePath && $this->getNodeName() === $nodeName) {
100+
return MoveResult::found($namespaces);
101+
}
102+
103+
if (!$this->isNodeEmptyElementType()) {
104+
$this->depth++;
105+
}
106+
}
107+
108+
if ($this->isNodeElementEndType()) {
109+
$this->depth--;
72110
}
73111

74112
$namespaces = array_merge($namespaces, $this->getNodeNamespaces());
@@ -77,19 +115,6 @@ private function moveToNode(string $nodeName): MoveResult
77115
return MoveResult::notFound();
78116
}
79117

80-
private function moveToNextNode(string $nodeName): bool
81-
{
82-
$localName = Parser::getLocalName($nodeName);
83-
84-
while ($this->reader->next($localName)) {
85-
if ($this->getNodeName() === $nodeName) {
86-
return true;
87-
}
88-
}
89-
90-
return false;
91-
}
92-
93118
/**
94119
* @param array<string,string>|null $rootNamespaces
95120
*
@@ -155,7 +180,11 @@ private function createNode(string $name, mixed $text, array $attributes, array
155180

156181
if ($withNamespace) {
157182
$namespaceAttributes = $this->namespacesToAttributes($namespaces, $rootNamespaces);
158-
$namespaceAttributes = array_filter($namespaceAttributes, static fn($namespaceLocalName) => in_array(Parser::getLocalName($namespaceLocalName), $usedNamespaces), ARRAY_FILTER_USE_KEY);
183+
$namespaceAttributes = array_filter(
184+
$namespaceAttributes,
185+
static fn($namespaceLocalName) => in_array(Parser::getLocalName($namespaceLocalName), $usedNamespaces),
186+
ARRAY_FILTER_USE_KEY,
187+
);
159188

160189
$attributes = array_merge($namespaceAttributes, $attributes);
161190
}
@@ -205,6 +234,11 @@ private function read(): bool
205234
return Handler::withErrorHandlerForXMLReader(fn(): bool => $this->reader->read());
206235
}
207236

237+
private function next(?string $name = null): bool
238+
{
239+
return $this->reader->next($name);
240+
}
241+
208242
private function getNodeName(): string
209243
{
210244
return $this->reader->name;

src/Reader/Reader.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
interface Reader
1010
{
1111
/**
12-
* Parse file and yield next node
12+
* Parse file by node name or node xpath and yield next node
1313
*
1414
* @return iterable<\Inspirum\XML\Builder\Node>
1515
*
@@ -18,9 +18,14 @@ interface Reader
1818
public function iterateNode(string $nodeName, bool $withNamespaces = false): iterable;
1919

2020
/**
21-
* Get next node
21+
* Get next node by node name or node xpath
2222
*
2323
* @throws \Exception
2424
*/
2525
public function nextNode(string $nodeName): ?Node;
26+
27+
/**
28+
* Close the input
29+
*/
30+
public function close(): void;
2631
}

tests/Reader/DefaultReaderTest.php

Lines changed: 128 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ public function testNonExistingFilepath(): void
4242
$this->newReader('wrong.xml');
4343
}
4444

45+
public function testNextNode(): void
46+
{
47+
$reader = $this->newReader(self::getTestFilePath('sample_04.xml'));
48+
49+
$node = $reader->nextNode('updated');
50+
51+
self::assertSame('2020-08-25T13:53:38+00:00', $node?->getTextContent());
52+
}
53+
4554
public function testNextInvalidNode(): void
4655
{
4756
$reader = $this->newReader(self::getTestFilePath('sample_04.xml'));
@@ -51,13 +60,36 @@ public function testNextInvalidNode(): void
5160
self::assertNull($node);
5261
}
5362

54-
public function testNextNode(): void
63+
public function testNextNodeByPath(): void
5564
{
5665
$reader = $this->newReader(self::getTestFilePath('sample_04.xml'));
5766

58-
$node = $reader->nextNode('updated');
67+
$node = $reader->nextNode('/feed/errors2');
5968

60-
self::assertSame('2020-08-25T13:53:38+00:00', $node?->getTextContent());
69+
self::assertSame('<errors2 id="2"/>', $node?->toString());
70+
}
71+
72+
public function testNextInvalidNodeByPath(): void
73+
{
74+
$reader = $this->newReader(self::getTestFilePath('sample_04.xml'));
75+
76+
$node = $reader->nextNode('/feed/items/item1');
77+
78+
self::assertNull($node);
79+
}
80+
81+
public function testClose(): void
82+
{
83+
self::expectException(Throwable::class);
84+
self::expectExceptionMessage('Data must be loaded before reading');
85+
86+
$reader = $this->newReader(self::getTestFilePath('sample_04.xml'));
87+
88+
self::assertNotNull($reader->nextNode('item'));
89+
90+
$reader->close();
91+
92+
self::assertNotNull($reader->nextNode('item'));
6193
}
6294

6395
public function testPreviousNode(): void
@@ -126,6 +158,54 @@ public function testNextEmptyNode(): void
126158
self::assertSame('<errors2 id="2"/>', $node->toString());
127159
}
128160

161+
public function testNextNodes(): void
162+
{
163+
$reader = $this->newReader(self::getTestFilePath('sample_04.xml'));
164+
165+
$output = [
166+
$reader->nextNode('item')?->toString(),
167+
$reader->nextNode('item')?->toString(),
168+
$reader->nextNode('item')?->toString(),
169+
$reader->nextNode('item')?->toString(),
170+
];
171+
172+
self::assertSame(
173+
[
174+
'<item i="0"><id uuid="12345">1</id><name price="10.1">Test 1</name></item>',
175+
'<item i="1"><id uuid="61648">2</id><name price="5">Test 2</name></item>',
176+
'<item i="2"><id>3</id><name price="500">Test 3</name></item>',
177+
'<item i="3"><id uuid="894654">4</id><name>Test 4</name></item>',
178+
],
179+
$output,
180+
);
181+
}
182+
183+
public function testNextNodesByPath(): void
184+
{
185+
$reader = $this->newReader(self::getTestFilePath('sample_09.xml'));
186+
187+
$output = [
188+
$reader->nextNode('/g:root/data/a')?->toString(),
189+
$reader->nextNode('/g:root/data/a')?->toString(),
190+
$reader->nextNode('/g:root/data/c')?->toString(),
191+
$reader->nextNode('/g:root/a')?->toString(),
192+
$reader->nextNode('/g:root/h:data/a')?->toString(),
193+
$reader->nextNode('/g:root/a')?->toString(),
194+
];
195+
196+
self::assertSame(
197+
[
198+
'<a><id>2</id><prices><price>1</price><priceWithVat>1.21</priceWithVat></prices></a>',
199+
'<a>data1</a>',
200+
'<c>5</c>',
201+
'<a>6</a>',
202+
'<a>7</a>',
203+
null,
204+
],
205+
$output,
206+
);
207+
}
208+
129209
public function testIterateInvalidNodes(): void
130210
{
131211
$reader = $this->newReader(self::getTestFilePath('sample_04.xml'));
@@ -239,6 +319,45 @@ public function testIterateMultipleNamespaces(): void
239319
);
240320
}
241321

322+
public function testIteratePath(): void
323+
{
324+
$reader = $this->newReader(self::getTestFilePath('sample_09.xml'));
325+
326+
$output = [];
327+
foreach ($reader->iterateNode('/g:root/data/a') as $item) {
328+
$output[] = $item->toString();
329+
}
330+
331+
self::assertSame(
332+
[
333+
'<a><id>2</id><prices><price>1</price><priceWithVat>1.21</priceWithVat></prices></a>',
334+
'<a>data1</a>',
335+
'<a>4</a>',
336+
'<a>7</a>',
337+
],
338+
$output,
339+
);
340+
}
341+
342+
public function testIteratePathMultipleNamespaces(): void
343+
{
344+
$reader = $this->newReader(self::getTestFilePath('sample_08.xml'));
345+
346+
$output = [];
347+
foreach ($reader->iterateNode('/g:rss/channel/item', true) as $item) {
348+
$output[] = $item->toString();
349+
}
350+
351+
self::assertSame(
352+
[
353+
'<item attr="2"><id>1/L1</id><title>Title 1</title></item>',
354+
'<item xmlns:g="http://base.google.com/ns/1.0" xmlns:h="http://base.google.com/ns/2.0" attr="1"><data><g:id h:test="asd">1/L2</g:id><g:title test="bb">Title 2</g:title><link>https://www.example.com/v/1</link></data></item>',
355+
'<item xmlns:g="http://base.google.com/ns/1.0"><g:id>1/L3</g:id><title>Title 3</title></item>',
356+
],
357+
$output,
358+
);
359+
}
360+
242361
/**
243362
* @param array<array<string>|string> $expected
244363
*/
@@ -279,7 +398,7 @@ public function testIterateWihSimpleLoadString(string $file, bool $withNamespace
279398
public static function provideIterateWihSimpleLoadString(): iterable
280399
{
281400
yield [
282-
'file' => 'sample_04.xml',
401+
'file' => 'sample_04.xml',
283402
'withNamespaces' => false,
284403
'path' => '/item/id',
285404
'expected' => [
@@ -292,7 +411,7 @@ public static function provideIterateWihSimpleLoadString(): iterable
292411
];
293412

294413
yield [
295-
'file' => 'sample_08.xml',
414+
'file' => 'sample_08.xml',
296415
'withNamespaces' => false,
297416
'path' => '/item/id',
298417
'expected' => [
@@ -303,7 +422,7 @@ public static function provideIterateWihSimpleLoadString(): iterable
303422
];
304423

305424
yield [
306-
'file' => 'sample_08.xml',
425+
'file' => 'sample_08.xml',
307426
'withNamespaces' => false,
308427
'path' => '/item/g:id',
309428
'expected' => [
@@ -314,7 +433,7 @@ public static function provideIterateWihSimpleLoadString(): iterable
314433
];
315434

316435
yield [
317-
'file' => 'sample_08.xml',
436+
'file' => 'sample_08.xml',
318437
'withNamespaces' => true,
319438
'path' => '/item/id',
320439
'expected' => [
@@ -325,7 +444,7 @@ public static function provideIterateWihSimpleLoadString(): iterable
325444
];
326445

327446
yield [
328-
'file' => 'sample_08.xml',
447+
'file' => 'sample_08.xml',
329448
'withNamespaces' => true,
330449
'path' => '/item/g:id',
331450
'expected' => [
@@ -336,7 +455,7 @@ public static function provideIterateWihSimpleLoadString(): iterable
336455
];
337456

338457
yield [
339-
'file' => 'sample_08.xml',
458+
'file' => 'sample_08.xml',
340459
'withNamespaces' => true,
341460
'path' => '/item/data/g:title',
342461
'expected' => [

0 commit comments

Comments
 (0)