From b01f9925e8d2c49cbdf2007c6affb139fa538f03 Mon Sep 17 00:00:00 2001 From: Julian Wundrak Date: Tue, 16 Aug 2022 08:33:36 +0200 Subject: [PATCH 1/2] Support read children without using "skipNextRead" --- src/XMLChildElementIterator.php | 39 +++++++++++++++++++++++++++++++-- src/XMLReaderIterator.php | 8 ++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/XMLChildElementIterator.php b/src/XMLChildElementIterator.php index c192063..2974090 100644 --- a/src/XMLChildElementIterator.php +++ b/src/XMLChildElementIterator.php @@ -53,6 +53,11 @@ class XMLChildElementIterator extends XMLElementIterator */ private $name; + /** + * @var int + */ + private $innerDepth; + /** * @inheritdoc * @@ -81,6 +86,11 @@ public function rewind() !$this->moveToNextByNodeType(XMLReader::ELEMENT); } + // handles e.g. -> no children available + if ($this->reader->isEmptyElement) { + return; + } + if ($this->stopDepth === null) { $this->stopDepth = $this->reader->depth; } @@ -89,6 +99,7 @@ public function rewind() $result = $this->nextChildElementByName($this->name); $this->index = $result ? 0 : null; + $this->innerDepth = 1; $this->didRewind = true; } @@ -160,7 +171,7 @@ private function nextChildElementByName($name = null) } } - return (bool)$next; + return $next; } /** @@ -168,7 +179,7 @@ private function nextChildElementByName($name = null) */ private function nextElement() { - while ($this->reader->read()) { + while ($this->readNext()) { if (XMLReader::ELEMENT !== $this->reader->nodeType) { continue; } @@ -177,4 +188,28 @@ private function nextElement() } return false; } + + /** + * Wrap reading to track opening and closing tags to prevent reading not-children nodes + * + * @return bool + */ + protected function readNext() + { + // update inner depth + if ($this->reader->nodeType === XMLReader::ELEMENT && !$this->reader->isEmptyElement) { + $this->innerDepth++; + } elseif ($this->reader->nodeType === XMLReader::END_ELEMENT) { + $this->innerDepth--; + + // all children read? Abort to prevent reading next node + if ($this->innerDepth === 0) { + // set pointer behind closing-tag + parent::readNext(); + return false; + } + } + + return parent::readNext(); + } } diff --git a/src/XMLReaderIterator.php b/src/XMLReaderIterator.php index 4d17045..dbcc019 100644 --- a/src/XMLReaderIterator.php +++ b/src/XMLReaderIterator.php @@ -167,11 +167,17 @@ public function next() if ($this->skipNextRead) { $this->skipNextRead = false; $this->lastRead = $this->reader->nodeType !== XMLReader::NONE; - } elseif ($this->lastRead = $this->reader->read() and $this->reader->nodeType === XMLReader::ELEMENT) { + } elseif ($this->lastRead = $this->readNext() and $this->reader->nodeType === XMLReader::ELEMENT) { $this->touchElementStack(); } } + #[\ReturnTypeWillChange] + protected function readNext() + { + return $this->reader->read(); + } + /** * @return string * @since 0.0.19 From d4138bea1c57f81e9a5257dd9cf29cece11d7857 Mon Sep 17 00:00:00 2001 From: Julian Wundrak Date: Tue, 16 Aug 2022 08:34:00 +0200 Subject: [PATCH 2/2] Provide example and tests --- examples/child-nested-read.php | 19 +++++++ examples/data/products.xml | 52 +++++++++++++++++++ .../Expectations/child-nested-read_php.out | 21 ++++++++ tests/unit/XMLChildElementIteratorTest.php | 23 +++++++- 4 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 examples/child-nested-read.php create mode 100644 examples/data/products.xml create mode 100644 tests/functional/examples/Expectations/child-nested-read_php.out diff --git a/examples/child-nested-read.php b/examples/child-nested-read.php new file mode 100644 index 0000000..608da19 --- /dev/null +++ b/examples/child-nested-read.php @@ -0,0 +1,19 @@ + $list */ +$iterator = new XMLElementIterator(XMLReader::open($xmlFile)); +$list = new XMLElementXpathFilter($iterator, '//product'); + +foreach ($list as $item) { + printf('Found product "%s"' . \PHP_EOL, $item->getAttribute('sku')); + + foreach ($item->getChildElements('attributes') as $attributeList) { + foreach ($attributeList->getChildElements() as $attribute) { + printf(' - %s: %s' . \PHP_EOL, $attribute->name, (string)$attribute); + } + } +} \ No newline at end of file diff --git a/examples/data/products.xml b/examples/data/products.xml new file mode 100644 index 0000000..d274797 --- /dev/null +++ b/examples/data/products.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + Test product + + + + + Test product in blue + 15.00 + + + + + Test product in red + 12.00 + + + + + + + Simple test product + 10.99 + + + + + Another product + 99.99 + + + + + L + + + + + M + + + + + diff --git a/tests/functional/examples/Expectations/child-nested-read_php.out b/tests/functional/examples/Expectations/child-nested-read_php.out new file mode 100644 index 0000000..e4c07e4 --- /dev/null +++ b/tests/functional/examples/Expectations/child-nested-read_php.out @@ -0,0 +1,21 @@ +Found product "empty_product" +Found product "empty_attributes" +Found product "no_attributes" +Found product "foo" + - name: Test product +Found product "foo_blue" + - name: Test product in blue + - price: 15.00 +Found product "foo_red" + - name: Test product in red + - price: 12.00 +Found product "bar" + - name: Simple test product + - price: 10.99 +Found product "foobar" + - name: Another product + - price: 99.99 +Found product "foobar_l" + - size: L +Found product "foobar_m" + - size: M diff --git a/tests/unit/XMLChildElementIteratorTest.php b/tests/unit/XMLChildElementIteratorTest.php index 26957ed..e4d229a 100644 --- a/tests/unit/XMLChildElementIteratorTest.php +++ b/tests/unit/XMLChildElementIteratorTest.php @@ -91,7 +91,6 @@ public function testIteration() $this->assertEquals($expected[$index], $reader->name); } $this->assertEquals(count($expected), $count); - } /** @@ -142,4 +141,26 @@ public function testDescendantChildren() $array = iterator_to_array($children, false); $this->assertSame(array(), $array, 'all children have been consumed by foreach'); } + + /** + * Test child-iterator without calling 'skipNextRead' method + */ + public function testIterationOnChildrenStopBeforeReadingNextElement() + { + $reader = XMLReader::open(__DIR__ . '/../../examples/data/sample-rss-091.xml'); + $this->assertTrue(!!$reader, 'fixture document can be opened successfully'); + $items = new XMLElementIterator($reader, 'item'); + foreach ($items as $idx => $item) { + $children = $items->getChildElements(); + $children->rewind(); + foreach (array('title', 'link', 'description') as $tagName) { + $this->assertTrue($children->valid()); + $this->assertSame($tagName, $children->name); + $children->next(); + } + $this->assertFalse($children->valid()); + } + $this->assertSame(6, $idx, 'sample-rss-091.xml element has 7 item elements'); + $this->assertEmpty(\iterator_to_array($items), 'all children have been consumed by foreach'); + } }