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/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
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');
+ }
}