diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..d4b9e70
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,11 @@
+; top-most EditorConfig file
+root = true
+
+; Unix-style newlines
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..09c43ea
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,10 @@
+src/Tests export-ignore
+.editorconfig export-ignore
+.gitattributes export-ignore
+.gitignore export-ignore
+.scrutinizer.yml export-ignore
+.travis.yml export-ignore
+CONTRIBUTING.md export-ignore
+CONDUCT.md export-ignore
+phpunit.xml.ci export-ignore
+phpunit.xml.dist export-ignore
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e45d856
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+build/
+vendor/
+composer.lock
+phpunit.xml
diff --git a/.scrutinizer.yml b/.scrutinizer.yml
new file mode 100644
index 0000000..6eeb828
--- /dev/null
+++ b/.scrutinizer.yml
@@ -0,0 +1,15 @@
+filter:
+ paths: [src/*]
+ excluded_paths: [src/Tests/*]
+
+checks:
+ php:
+ code_rating: true
+ duplication: true
+
+tools:
+ external_code_coverage:
+ timeout: 600
+ php_code_sniffer:
+ config:
+ standard: "PSR2"
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..5e6b3b0
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,21 @@
+language: php
+
+cache:
+ directories:
+ - $HOME/.composer/cache
+
+php:
+ - 7.0
+
+before_install:
+ - travis_retry composer self-update
+
+install:
+ - travis_retry composer update --no-interaction
+
+script:
+ - composer test-ci
+
+after_success:
+ - wget https://scrutinizer-ci.com/ocular.phar
+ - php ocular.phar code-coverage:upload --format=php-clover build/logs/clover.xml
diff --git a/CONDUCT.md b/CONDUCT.md
new file mode 100644
index 0000000..cf33a53
--- /dev/null
+++ b/CONDUCT.md
@@ -0,0 +1,51 @@
+# Contributor Code of Conduct
+
+As contributors and maintainers of this project, and in the interest of
+fostering an open and welcoming community, we pledge to respect all people who
+contribute through reporting issues, posting feature requests, updating
+documentation, submitting pull requests or patches, and other activities.
+
+We are committed to making participation in this project a harassment-free
+experience for everyone, regardless of level of experience, gender, gender
+identity and expression, sexual orientation, disability, personal appearance,
+body size, race, ethnicity, age, religion, or nationality.
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery
+* Personal attacks
+* Trolling or insulting/derogatory comments
+* Public or private harassment
+* Publishing other's private information, such as physical or electronic
+ addresses, without explicit permission
+* Other unethical or unprofessional conduct
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+By adopting this Code of Conduct, project maintainers commit themselves to
+fairly and consistently applying these principles to every aspect of managing
+this project. Project maintainers who do not follow or enforce the Code of
+Conduct may be permanently removed from the project team.
+
+This code of conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community.
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting a project maintainer at [Aurimas Niekis][email]. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. Maintainers are
+obligated to maintain confidentiality with regard to the reporter of an
+incident.
+
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 1.3.0, available at
+[http://contributor-covenant.org/version/1/3/0/][version]
+
+[homepage]: http://contributor-covenant.org
+[version]: http://contributor-covenant.org/version/1/3/0/
+[email]: mailto:aurimas@niekis.lt
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..3447245
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,93 @@
+# Contributing
+
+If you're here, you would like to contribute to this repository and you're really welcome!
+
+
+## Bug reports
+
+If you find a bug or a documentation issue, please report it or even better: fix it :). If you report it,
+please be as precise as possible. Here is a little list of required information:
+
+ - Precise description of the bug
+ - Details of your environment (for example: OS, PHP version, installed extensions)
+ - Backtrace which might help identifing the bug
+
+
+## Feature requests
+
+If you think a feature is missing, please report it or even better: implement it :). If you report it, describe the more
+precisely what you would like to see implemented and we will discuss what is the best approach for it. If you can do
+some research before submitting it and link the resources to your description, you're awesome! It will allow us to more
+easily understood/implement it.
+
+
+## Sending a Pull Request
+
+If you're here, you are going to fix a bug or implement a feature and you're the best!
+To do it, first fork the repository, clone it and create a new branch with the following commands:
+
+``` bash
+$ git clone git@github.com:your-name/xml-iterator.git
+$ git checkout -b feature-or-bug-fix-description
+```
+
+Then install the dependencies through [Composer](https://getcomposer.org/):
+
+``` bash
+$ composer install
+```
+
+Write code and tests. When you are ready, run the tests.
+(This is usually [PHPUnit](http://phpunit.de/))
+
+``` bash
+$ composer test
+```
+
+When you are ready with the code, tested it and documented it, you can commit and push it with the following commands:
+
+``` bash
+$ git commit -m "Feature or bug fix description"
+$ git push origin feature-or-bug-fix-description
+```
+
+**Note:** Please write your commit messages in the imperative and follow the
+[guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) for clear and concise messages.
+
+Then [create a pull request](https://help.github.com/articles/creating-a-pull-request/) on GitHub.
+
+Please make sure that each individual commit in your pull request is meaningful.
+If you had to make multiple intermediate commits while developing,
+please squash them before submitting with the following commands
+(here, we assume you would like to squash 3 commits in a single one):
+
+``` bash
+$ git rebase -i HEAD~3
+```
+
+If your branch conflicts with the master branch, you will need to rebase and repush it with the following commands:
+
+``` bash
+$ git remote add upstream git@github.com:ThrusterIO/xml-iterator.git
+$ git pull --rebase upstream master
+$ git push -f origin feature-or-bug-fix-description
+```
+
+
+## Coding standard
+
+This repository follows the [PSR-2 standard](http://www.php-fig.org/psr/psr-2/) and so, if you want to contribute,
+you must follow these rules.
+
+
+## Semver
+
+We are trying to follow [semver](http://semver.org/). When you are making BC breaking changes,
+please let us know why you think it is important.
+In this case, your patch can only be included in the next major version.
+
+
+## Code of Conduct
+
+This project is released with a [Contributor Code of Conduct](CONDUCT.md).
+By participating in this project you agree to abide by its terms.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a925760
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Aurimas Niekis
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..34e3698
--- /dev/null
+++ b/README.md
@@ -0,0 +1,44 @@
+# XMLIterator Component
+
+[![Latest Version](https://img.shields.io/github/release/ThrusterIO/xml-iterator.svg?style=flat-square)]
+(https://github.com/ThrusterIO/xml-iterator/releases)
+[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)]
+(LICENSE)
+[![Build Status](https://img.shields.io/travis/ThrusterIO/xml-iterator.svg?style=flat-square)]
+(https://travis-ci.org/ThrusterIO/xml-iterator)
+[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/ThrusterIO/xml-iterator.svg?style=flat-square)]
+(https://scrutinizer-ci.com/g/ThrusterIO/xml-iterator)
+[![Quality Score](https://img.shields.io/scrutinizer/g/ThrusterIO/xml-iterator.svg?style=flat-square)]
+(https://scrutinizer-ci.com/g/ThrusterIO/xml-iterator)
+[![Total Downloads](https://img.shields.io/packagist/dt/thruster/xml-iterator.svg?style=flat-square)]
+(https://packagist.org/packages/thruster/xml-iterator)
+
+[![Email](https://img.shields.io/badge/email-team@thruster.io-blue.svg?style=flat-square)]
+(mailto:team@thruster.io)
+
+The Thruster XMLIterator Component fork from original library [XMLReaderIterator](https://github.com/hakre/XMLReaderIterator)
+
+
+## Install
+
+Via Composer
+
+```bash
+$ composer require thruster/xml-iterator
+```
+
+## Testing
+
+```bash
+$ composer test
+```
+
+
+## Contributing
+
+Please see [CONTRIBUTING](CONTRIBUTING.md) and [CONDUCT](CONDUCT.md) for details.
+
+
+## License
+
+Please see [License File](LICENSE) for more information.
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..7643753
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,33 @@
+{
+ "name": "thruster/xml-iterator",
+ "type": "library",
+ "description": "Thruster XMLIterator Component",
+ "keywords": ["xml-iterator", "thruster", "xmlreader"],
+ "homepage": "https://thruster.io",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Aurimas Niekis",
+ "email": "aurimas@niekis.lt"
+ }
+ ],
+ "require": {
+ "php": ">=7.0",
+ "ext-xmlreader": "*"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~5.1"
+ },
+ "autoload": {
+ "psr-4": { "Thruster\\Component\\XMLIterator\\": "src" }
+ },
+ "scripts": {
+ "test": "vendor/bin/phpunit",
+ "test-ci": "vendor/bin/phpunit -c phpunit.xml.ci"
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ }
+}
diff --git a/phpunit.xml.ci b/phpunit.xml.ci
new file mode 100644
index 0000000..4c5d9c4
--- /dev/null
+++ b/phpunit.xml.ci
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+ ./src/Tests/
+
+
+
+
+
+ ./src
+
+ ./src/Tests
+
+
+
+
+
+
+
+
+
+
+
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..838a69a
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+ ./src/Tests/
+
+
+
+
+
+ ./src
+
+ ./src/Tests
+
+
+
+
diff --git a/src/AttributeFilter.php b/src/AttributeFilter.php
new file mode 100644
index 0000000..eb400c1
--- /dev/null
+++ b/src/AttributeFilter.php
@@ -0,0 +1,52 @@
+
+ */
+class AttributeFilter extends AttributeFilterBase
+{
+ private $compare;
+
+ /**
+ * @var bool
+ */
+ private $invert;
+
+ /**
+ * @param ElementIterator $elements
+ * @param string $attr name of the attribute, '*' for every attribute
+ * @param string|array $compare value(s) to compare against
+ * @param bool $invert
+ */
+ public function __construct(ElementIterator $elements, $attr, $compare, bool $invert = false)
+ {
+ parent::__construct($elements, $attr);
+
+ $this->compare = (array) $compare;
+ $this->invert = $invert;
+ }
+
+ public function accept()
+ {
+ $result = $this->search($this->getAttributeValues(), $this->compare);
+
+ return $this->invert ? !$result : $result;
+ }
+
+ private function search($values, $compares)
+ {
+ foreach ($compares as $compare) {
+ if (in_array($compare, $values)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
+
diff --git a/src/AttributeFilterBase.php b/src/AttributeFilterBase.php
new file mode 100644
index 0000000..e605cf9
--- /dev/null
+++ b/src/AttributeFilterBase.php
@@ -0,0 +1,38 @@
+
+ */
+abstract class AttributeFilterBase extends FilterBase
+{
+ private $attr;
+
+ /**
+ * @param ElementIterator $elements
+ * @param string $attr name of the attribute, '*' for every attribute
+ */
+ public function __construct(ElementIterator $elements, $attr)
+ {
+ parent::__construct($elements);
+
+ $this->attr = $attr;
+ }
+
+ protected function getAttributeValues()
+ {
+ $node = parent::current();
+
+ if ('*' === $this->attr) {
+ $attributes = $node->getAttributes()->getArrayCopy();
+ } else {
+ $attributes = (array) $node->getAttribute($this->attr);
+ }
+
+ return $attributes;
+ }
+}
diff --git a/src/AttributeIterator.php b/src/AttributeIterator.php
new file mode 100644
index 0000000..ba9b4c5
--- /dev/null
+++ b/src/AttributeIterator.php
@@ -0,0 +1,117 @@
+
+ */
+class AttributeIterator implements IteratorInterface, Countable, ArrayAccess
+{
+ /**
+ * @var XMLReader
+ */
+ private $reader;
+
+ /**
+ * @var bool
+ */
+ private $valid;
+
+ /**
+ * @var array
+ */
+ private $array;
+
+ public function __construct(XMLReader $reader)
+ {
+ $this->reader = $reader;
+ }
+
+ public function count()
+ {
+ return $this->reader->attributeCount;
+ }
+
+ public function current()
+ {
+ return $this->reader->value;
+ }
+
+ public function key()
+ {
+ return $this->reader->name;
+ }
+
+ public function next()
+ {
+ $this->valid = $this->reader->moveToNextAttribute();
+
+ if (!$this->valid) {
+ $this->reader->moveToElement();
+ }
+ }
+
+ public function rewind()
+ {
+ $this->valid = $this->reader->moveToFirstAttribute();
+ }
+
+ public function valid()
+ {
+ return $this->valid;
+ }
+
+ public function getArrayCopy()
+ {
+ if ($this->array === null) {
+ $this->array = iterator_to_array($this);
+ }
+
+ return $this->array;
+ }
+
+ public function getAttributeNames()
+ {
+ return array_keys($this->getArrayCopy());
+ }
+
+ public function offsetExists($offset)
+ {
+ $attributes = $this->getArrayCopy();
+
+ return isset($attributes[$offset]);
+ }
+
+ public function offsetGet($offset)
+ {
+ $attributes = $this->getArrayCopy();
+
+ return $attributes[$offset];
+ }
+
+ public function offsetSet($offset, $value)
+ {
+ throw new BadMethodCallException('XMLReader attributes are read-only');
+ }
+
+ public function offsetUnset($offset)
+ {
+ throw new BadMethodCallException('XMLReader attributes are read-only');
+ }
+
+ /**
+ * @return XMLReader
+ */
+ public function getReader()
+ {
+ return $this->getReader();
+ }
+}
diff --git a/src/AttributePreg.php b/src/AttributePreg.php
new file mode 100644
index 0000000..5ae1dd9
--- /dev/null
+++ b/src/AttributePreg.php
@@ -0,0 +1,49 @@
+
+ */
+class AttributePreg extends AttributeFilterBase
+{
+ /**
+ * @var string
+ */
+ private $pattern;
+
+ /**
+ * @var bool
+ */
+ private $invert;
+
+ /**
+ * @param ElementIterator $elements
+ * @param string $attr name of the attribute, '*' for every attribute
+ * @param string $pattern pcre based regex pattern for the attribute value
+ * @param bool $invert
+ *
+ * @throws InvalidArgumentException
+ */
+ public function __construct(ElementIterator $elements, string $attr, string $pattern, bool $invert = false)
+ {
+ parent::__construct($elements, $attr);
+
+ if (false === preg_match("$pattern", '')) {
+ throw new InvalidArgumentException("Invalid pcre pattern '$pattern'.");
+ }
+
+ $this->pattern = $pattern;
+ $this->invert = $invert;
+ }
+
+ public function accept()
+ {
+ return (bool) preg_grep($this->pattern, $this->getAttributeValues(), $this->invert ? PREG_GREP_INVERT : 0);
+ }
+}
diff --git a/src/ChildElementIterator.php b/src/ChildElementIterator.php
new file mode 100644
index 0000000..574a567
--- /dev/null
+++ b/src/ChildElementIterator.php
@@ -0,0 +1,109 @@
+
+ */
+class ChildElementIterator extends ElementIterator
+{
+ /**
+ * @var int
+ */
+ private $stopDepth;
+
+ /**
+ * @var bool
+ */
+ private $descendTree;
+
+ /**
+ * @var bool
+ */
+ private $didRewind;
+
+ /**
+ * @var int
+ */
+ private $index;
+
+ /**
+ * @inheritdoc
+ *
+ * @param bool $descendantAxis traverse children of children
+ */
+ public function __construct(XMLReader $reader, $name = null, bool $descendantAxis = false)
+ {
+ parent::__construct($reader, $name);
+
+ $this->descendTree = $descendantAxis;
+ }
+
+ /**
+ * @throws UnexpectedValueException
+ * @return void
+ */
+ public function rewind()
+ {
+ // this iterator can not really rewind. instead it places itself onto the
+ // first children.
+ if ($this->reader->nodeType === XMLReader::NONE) {
+ $this->moveToNextElement();
+ }
+
+ if ($this->stopDepth === null) {
+ $this->stopDepth = $this->reader->depth;
+ }
+
+ // move to first child - if any
+ parent::next();
+ parent::rewind();
+
+ $this->index = 0;
+ $this->didRewind = true;
+ }
+
+ public function next()
+ {
+ if ($this->valid()) {
+ $this->index++;
+ }
+
+ while ($this->valid()) {
+ parent::next();
+
+ if ($this->descendTree || $this->reader->depth === $this->stopDepth + 1) {
+ break;
+ }
+ };
+ }
+
+ public function valid()
+ {
+ if (!($valid = parent::valid())) {
+ return $valid;
+ }
+
+ return $this->reader->depth > $this->stopDepth;
+ }
+
+ /**
+ * @return Node
+ */
+ public function current()
+ {
+ $this->didRewind || self::rewind();
+
+ return parent::current();
+ }
+
+ public function key()
+ {
+ return $this->index;
+ }
+}
diff --git a/src/ChildIterator.php b/src/ChildIterator.php
new file mode 100644
index 0000000..d41d4cb
--- /dev/null
+++ b/src/ChildIterator.php
@@ -0,0 +1,37 @@
+
+ */
+class ChildIterator extends XMLIterator
+{
+ /**
+ * @var int
+ */
+ private $stopDepth;
+
+ public function __construct(XMLReader $reader)
+ {
+ parent::__construct($reader);
+
+ $this->stopDepth = $reader->depth;
+ }
+
+ public function rewind()
+ {
+ parent::next();
+ parent::rewind();
+ }
+
+ public function valid()
+ {
+ $parent = parent::valid();
+
+ return $parent && ($this->reader->depth > $this->stopDepth);
+ }
+}
diff --git a/src/Element.php b/src/Element.php
new file mode 100644
index 0000000..aa18081
--- /dev/null
+++ b/src/Element.php
@@ -0,0 +1,59 @@
+
+ */
+class Element extends Node
+{
+ private $name_;
+ private $attributes_;
+
+ public function __construct(XMLReader $reader)
+ {
+ parent::__construct($reader);
+
+ $this->initializeFrom($reader);
+ }
+
+ public function getXMLElementAround($innerXML = '')
+ {
+ return XMLBuild::wrapTag($this->name_, $this->attributes_, $innerXML);
+ }
+
+ public function getAttributes()
+ {
+ return $this->attributes_;
+ }
+
+ public function getAttribute(string $name, $default = null)
+ {
+ return $this->attributes_[$name] ?? $default;
+ }
+
+ public function __toString()
+ {
+ return $this->name_;
+ }
+
+ private function initializeFrom(XMLReader $reader)
+ {
+ if ($reader->nodeType !== XMLReader::ELEMENT) {
+ $node = new Node($reader);
+
+ throw new RuntimeException(sprintf(
+ 'Reader must be at an XMLReader::ELEMENT, is XMLReader::%s given.',
+ $node->getNodeTypeName()
+ ));
+ }
+
+ $this->name_ = $reader->name;
+ $this->attributes_ = parent::getAttributes()->getArrayCopy();
+ }
+}
diff --git a/src/ElementIterator.php b/src/ElementIterator.php
new file mode 100644
index 0000000..15a0891
--- /dev/null
+++ b/src/ElementIterator.php
@@ -0,0 +1,157 @@
+
+ */
+class ElementIterator extends XMLIterator
+{
+ /**
+ * @var int
+ */
+ private $index;
+
+ /**
+ * @var string
+ */
+ private $name;
+
+ /**
+ * @var bool
+ */
+ private $didRewind;
+
+ /**
+ * @param XMLReader $reader
+ * @param null|string $name element name, leave empty or use '*' for all elements
+ */
+ public function __construct(XMLReader $reader, $name = null)
+ {
+ parent::__construct($reader);
+
+ $this->setName($name);
+ }
+
+ /**
+ * @return void
+ */
+ public function rewind()
+ {
+ parent::rewind();
+
+ $this->ensureCurrentElementState();
+ $this->didRewind = true;
+ $this->index = 0;
+ }
+
+ /**
+ * @return Node|null
+ */
+ public function current()
+ {
+ $this->didRewind || self::rewind();
+ $this->ensureCurrentElementState();
+
+ return self::valid() ? new Node($this->reader) : null;
+ }
+
+ public function key()
+ {
+ return $this->index;
+ }
+
+ public function next()
+ {
+ if (parent::valid()) {
+ $this->index++;
+ }
+ parent::next();
+ $this->ensureCurrentElementState();
+ }
+
+ /**
+ * @return array
+ */
+ public function toArray()
+ {
+ $array = [];
+ $this->didRewind || $this->rewind();
+ if (!$this->valid()) {
+ return [];
+ }
+ $this->ensureCurrentElementState();
+ while ($this->valid()) {
+ $element = new Node($this->reader);
+ if ($this->reader->hasValue) {
+ $string = $this->reader->value;
+ } else {
+ $string = $element->readString();
+ }
+ if ($this->name) {
+ $array[] = $string;
+ } else {
+ $array[$element->name] = $string;
+ }
+ $this->moveToNextElementByName($this->name);
+ }
+
+ return $array;
+ }
+
+ /**
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->readString();
+ }
+
+ /**
+ * decorate method calls
+ *
+ * @param string $name
+ * @param array $args
+ *
+ * @return mixed
+ */
+ public function __call($name, $args)
+ {
+ return call_user_func_array([$this->current(), $name], $args);
+ }
+
+ /**
+ * decorate property get
+ *
+ * @param string $name
+ *
+ * @return string
+ */
+ public function __get($name)
+ {
+ return $this->current()->$name;
+ }
+
+ /**
+ * @param null|string $name
+ */
+ public function setName($name = null)
+ {
+ $this->name = '*' === $name ? null : $name;
+ }
+
+ /**
+ * take care the underlying XMLReader is at an element with a fitting name (if $this is looking for a name)
+ */
+ private function ensureCurrentElementState()
+ {
+ if ($this->reader->nodeType !== XMLReader::ELEMENT) {
+ $this->moveToNextElementByName($this->name);
+ } elseif ($this->name && $this->name !== $this->reader->name) {
+ $this->moveToNextElementByName($this->name);
+ }
+ }
+}
diff --git a/src/ElementXpathFilter.php b/src/ElementXpathFilter.php
new file mode 100644
index 0000000..ea0778e
--- /dev/null
+++ b/src/ElementXpathFilter.php
@@ -0,0 +1,40 @@
+
+ */
+class ElementXpathFilter extends FilterBase
+{
+ /**
+ * @var string
+ */
+ private $expression;
+
+ /**
+ * {@inheritDoc}
+ */
+ public function __construct(ElementIterator $iterator, string $expression)
+ {
+ parent::__construct($iterator);
+
+ $this->expression = $expression;
+ }
+
+ public function accept()
+ {
+ $buffer = $this->getInnerIterator()->getNodeTree();
+ $result = simplexml_load_string($buffer)->xpath($this->expression);
+ $count = count($result);
+
+ if ($count !== 1) {
+ return false;
+ }
+
+ return !($result[0]->children()->count());
+ }
+}
diff --git a/src/FilterBase.php b/src/FilterBase.php
new file mode 100644
index 0000000..90d5f46
--- /dev/null
+++ b/src/FilterBase.php
@@ -0,0 +1,27 @@
+
+ */
+abstract class FilterBase extends FilterIterator
+{
+ public function __construct(XMLIterator $elements)
+ {
+ parent::__construct($elements);
+ }
+
+ /**
+ * @return XMLReader
+ */
+ public function getReader()
+ {
+ return $this->getInnerIterator()->getReader();
+ }
+}
diff --git a/src/Iteration.php b/src/Iteration.php
new file mode 100644
index 0000000..7339b08
--- /dev/null
+++ b/src/Iteration.php
@@ -0,0 +1,93 @@
+
+ */
+class Iteration implements Iterator
+{
+ /**
+ * @var XMLReader
+ */
+ private $reader;
+
+ /**
+ * @var bool
+ */
+ private $valid;
+
+ /**
+ * @var int
+ */
+ private $index;
+
+ /**
+ * @var bool
+ */
+ private $skipNextRead;
+
+ public function __construct(XMLReader $reader)
+ {
+ $this->reader = $reader;
+ }
+
+ /**
+ * skip the next read on next next()
+ *
+ * this is useful of the reader has moved to the next node already inside a foreach iteration and the next
+ * next would move the reader one off.
+ *
+ * @see next
+ */
+ public function skipNextRead()
+ {
+ $this->skipNextRead = true;
+ }
+
+ /**
+ * @return XMLReader
+ */
+ public function current()
+ {
+ return $this->reader;
+ }
+
+ public function next()
+ {
+ $this->index++;
+
+ if ($this->skipNextRead) {
+ $this->skipNextRead = false;
+ $this->valid = $this->reader->nodeType;
+ } else {
+ $this->valid = $this->reader->read();
+ }
+ }
+
+ public function key()
+ {
+ return $this->index;
+ }
+
+ public function valid()
+ {
+ return $this->valid;
+ }
+
+ public function rewind()
+ {
+ if ($this->reader->nodeType !== XMLReader::NONE) {
+ throw new BadMethodCallException('Reader can not be rewound');
+ }
+
+ $this->index = 0;
+ $this->valid = $this->reader->read();
+ }
+}
diff --git a/src/NextIteration.php b/src/NextIteration.php
new file mode 100644
index 0000000..f1959f7
--- /dev/null
+++ b/src/NextIteration.php
@@ -0,0 +1,85 @@
+
+ */
+class NextIteration implements Iterator
+{
+ /**
+ * @var XMLReader
+ */
+ private $reader;
+
+ /**
+ * @var int
+ */
+ private $index;
+
+ /**
+ * @var bool
+ */
+ private $valid;
+
+ /**
+ * @var string
+ */
+ private $localName;
+
+ public function __construct(XMLReader $reader, $localName = null)
+ {
+ $this->reader = $reader;
+ $this->localName = $localName;
+ }
+
+ public function rewind()
+ {
+ $this->moveReaderToCurrent();
+ $this->index = 0;
+ }
+
+ public function valid()
+ {
+ return $this->valid;
+ }
+
+ public function current()
+ {
+ return $this->reader;
+ }
+
+ public function key()
+ {
+ return $this->index;
+ }
+
+ public function next()
+ {
+ $this->valid && $this->index++;
+
+ if ($this->localName) {
+ $this->valid = $this->reader->next($this->localName);
+ } else {
+ $this->valid = $this->reader->next();
+ }
+ }
+
+ /**
+ * move cursor to the next element but only if it's not yet there
+ */
+ private function moveReaderToCurrent()
+ {
+ if (($this->reader->nodeType === XMLReader::NONE) ||
+ ($this->reader->nodeType !== XMLReader::ELEMENT) ||
+ ($this->localName && $this->localName !== $this->reader->localName)
+ ) {
+ self::next();
+ }
+ }
+}
diff --git a/src/Node.php b/src/Node.php
new file mode 100644
index 0000000..117cdef
--- /dev/null
+++ b/src/Node.php
@@ -0,0 +1,324 @@
+
+ */
+class Node
+{
+ /**
+ * @var XMLReader
+ */
+ protected $reader;
+
+ /**
+ * @var int
+ */
+ private $nodeType;
+
+ /**
+ * @var string
+ */
+ private $name;
+
+ /**
+ * @var string
+ */
+ private $localName;
+
+ /**
+ * @var SimpleXMLElement
+ */
+ private $simpleXML;
+
+ /**
+ * @var AttributeIterator
+ */
+ private $attributes;
+
+ /**
+ * @var string
+ */
+ private $string;
+
+ public function __construct(XMLReader $reader)
+ {
+ $this->reader = $reader;
+ $this->nodeType = $reader->nodeType;
+ $this->name = $reader->name;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function __toString()
+ {
+ if (null === $this->string) {
+ $this->string = $this->readString();
+ }
+
+ return $this->string;
+ }
+
+ /**
+ * @return SimpleXMLElement
+ */
+ public function getSimpleXMLElement()
+ {
+ if (null === $this->simpleXML) {
+ if ($this->reader->nodeType !== XMLReader::ELEMENT) {
+ return null;
+ }
+
+ $node = $this->expand();
+ $this->simpleXML = simplexml_import_dom($node);
+ }
+
+ return $this->simpleXML;
+ }
+
+ /**
+ * @return AttributeIterator|array
+ */
+ public function getAttributes()
+ {
+ if (null === $this->attributes) {
+ $this->attributes = new AttributeIterator($this->reader);
+ }
+
+ return $this->attributes;
+ }
+
+ /**
+ * @param string $name
+ * @param null $default
+ *
+ * @return string
+ */
+ public function getAttribute(string $name, $default = null)
+ {
+ return $this->reader->getAttribute($name) ?? $default;
+ }
+
+ /**
+ * @param null $name
+ * @param bool $descendantAxis
+ *
+ * @return ChildElementIterator
+ */
+ public function getChildElements($name = null, bool $descendantAxis = false) : ChildElementIterator
+ {
+ return new ChildElementIterator($this->reader, $name, $descendantAxis);
+ }
+
+ /**
+ * @return ChildIterator|Node[]
+ */
+ public function getChildren() : ChildIterator
+ {
+ return new ChildIterator($this->reader);
+ }
+
+ /**
+ * @return string name
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * @return string local name
+ */
+ public function getLocalName()
+ {
+ return $this->localName;
+ }
+
+ public function getReader()
+ {
+ return $this->reader;
+ }
+
+ /**
+ * @return string
+ */
+ public function readOuterXml()
+ {
+ return $this->reader->readOuterXml();
+ }
+
+ /**
+ * XMLReader expand node and import it into a DOMNode with a DOMDocument
+ *
+ * This is for example useful for DOMDocument::saveXML() @see readOuterXml
+ * or getting a SimpleXMLElement out of it @see getSimpleXMLElement
+ *
+ * @throws BadMethodCallException
+ *
+ * @param DOMNode $baseNode
+ *
+ * @return DOMNode
+ */
+ public function expand(DOMNode $baseNode = null)
+ {
+ if (null === $baseNode) {
+ $baseNode = new DOMDocument();
+ }
+
+ if ($baseNode instanceof DOMDocument) {
+ $doc = $baseNode;
+ } else {
+ $doc = $baseNode->ownerDocument;
+ }
+
+ if (false === $node = $this->reader->expand($baseNode)) {
+ throw new BadMethodCallException('Unable to expand node.');
+ }
+
+ if ($node->ownerDocument !== $doc) {
+ $node = $doc->importNode($node, true);
+ }
+
+ return $node;
+ }
+
+ /**
+ * Decorated method
+ *
+ * @throws BadMethodCallException
+ * @return string
+ */
+ public function readString()
+ {
+ return $this->reader->readString();
+ }
+
+ /**
+ * Return node-type as human readable string (constant name)
+ *
+ * @param null $nodeType
+ *
+ * @return string
+ */
+ public function getNodeTypeName($nodeType = null)
+ {
+ $strings = [
+ XMLReader::NONE => 'NONE',
+ XMLReader::ELEMENT => 'ELEMENT',
+ XMLReader::ATTRIBUTE => 'ATTRIBUTE',
+ XMLREADER::TEXT => 'TEXT',
+ XMLREADER::CDATA => 'CDATA',
+ XMLReader::ENTITY_REF => 'ENTITY_REF',
+ XMLReader::ENTITY => 'ENTITY',
+ XMLReader::PI => 'PI',
+ XMLReader::COMMENT => 'COMMENT',
+ XMLReader::DOC => 'DOC',
+ XMLReader::DOC_TYPE => 'DOC_TYPE',
+ XMLReader::DOC_FRAGMENT => 'DOC_FRAGMENT',
+ XMLReader::NOTATION => 'NOTATION',
+ XMLReader::WHITESPACE => 'WHITESPACE',
+ XMLReader::SIGNIFICANT_WHITESPACE => 'SIGNIFICANT_WHITESPACE',
+ XMLReader::END_ELEMENT => 'END_ELEMENT',
+ XMLReader::END_ENTITY => 'END_ENTITY',
+ XMLReader::XML_DECLARATION => 'XML_DECLARATION',
+ ];
+
+ if (null === $nodeType) {
+ $nodeType = $this->nodeType;
+ }
+
+ return $strings[$nodeType];
+ }
+
+ /**
+ * decorate method calls
+ *
+ * @param string $name
+ * @param array $args
+ *
+ * @return mixed
+ */
+ public function __call($name, $args)
+ {
+ return call_user_func_array([$this->reader, $name], $args);
+ }
+
+ /**
+ * decorate property get
+ *
+ * @param string $name
+ *
+ * @return string
+ */
+ public function __get($name)
+ {
+ return $this->reader->$name;
+ }
+
+ /**
+ * debug utility method
+ *
+ * @param XMLReader $reader
+ * @param bool $return (optional) prints by default but can return string
+ *
+ * @return string|null
+ */
+ public static function dump(XMLReader $reader, bool $return = false)
+ {
+ $node = new self($reader);
+ $nodeType = $reader->nodeType;
+ $nodeName = $node->getNodeTypeName();
+ $extra = '';
+
+ if ($reader->nodeType === XMLReader::ELEMENT) {
+ $extra = '<' . $reader->name . '> ';
+ $extra .= sprintf("(isEmptyElement: %s) ", $reader->isEmptyElement ? 'Yes' : 'No');
+ }
+
+ if ($reader->nodeType === XMLReader::END_ELEMENT) {
+ $extra = '' . $reader->name . '> ';
+ }
+
+ if ($reader->nodeType === XMLReader::ATTRIBUTE) {
+ $str = $reader->value;
+ $len = strlen($str);
+ if ($len > 20) {
+ $str = substr($str, 0, 17) . '...';
+ }
+ $str = strtr($str, ["\n" => '\n']);
+ $extra = sprintf('%s = (%d) "%s" ', $reader->name, strlen($str), $str);
+ }
+
+ if ($reader->nodeType === XMLReader::TEXT || $reader->nodeType === XMLReader::WHITESPACE ||
+ $reader->nodeType === XMLReader::SIGNIFICANT_WHITESPACE
+ ) {
+ $str = $reader->readString();
+ $len = strlen($str);
+ if ($len > 20) {
+ $str = substr($str, 0, 17) . '...';
+ }
+ $str = strtr($str, ["\n" => '\n']);
+ $extra = sprintf('(%d) "%s" ', strlen($str), $str);
+ }
+
+ $label = sprintf("(#%d) %s %s", $nodeType, $nodeName, $extra);
+
+ if ($return) {
+ return $label;
+ }
+
+ printf("%s%s\n", str_repeat(' ', $reader->depth), $label);
+
+ return null;
+ }
+}
diff --git a/src/NodeTypeFilter.php b/src/NodeTypeFilter.php
new file mode 100644
index 0000000..1807dc7
--- /dev/null
+++ b/src/NodeTypeFilter.php
@@ -0,0 +1,53 @@
+
+ */
+class NodeTypeFilter extends FilterBase
+{
+ /**
+ * @var array
+ */
+ private $allowed;
+
+ /**
+ * @var XMLReader
+ */
+ private $reader;
+
+ /**
+ * @var bool
+ */
+ private $invert;
+
+ /**
+ * @param XMLIterator $iterator
+ * @param int|int[] $nodeType one or more type constants
+ * XMLReader::NONE XMLReader::ELEMENT XMLReader::ATTRIBUTE XMLReader::TEXT
+ * XMLReader::CDATA XMLReader::ENTITY_REF XMLReader::ENTITY XMLReader::PI
+ * XMLReader::COMMENT XMLReader::DOC XMLReader::DOC_TYPE XMLReader::DOC_FRAGMENT
+ * XMLReader::NOTATION XMLReader::WHITESPACE XMLReader::SIGNIFICANT_WHITESPACE
+ * XMLReader::END_ELEMENT XMLReader::END_ENTITY XMLReader::XML_DECLARATION
+ * @param bool $invert
+ */
+ public function __construct(XMLIterator $iterator, $nodeType, bool $invert = false)
+ {
+ parent::__construct($iterator);
+
+ $this->allowed = (array) $nodeType;
+ $this->reader = $iterator->getReader();
+ $this->invert = $invert;
+ }
+
+ public function accept()
+ {
+ $result = in_array($this->reader->nodeType, $this->allowed);
+
+ return $this->invert ? !$result : $result;
+ }
+}
diff --git a/src/Tests/ChildElementIteratorTest.php b/src/Tests/ChildElementIteratorTest.php
new file mode 100644
index 0000000..80651fa
--- /dev/null
+++ b/src/Tests/ChildElementIteratorTest.php
@@ -0,0 +1,83 @@
+
+ */
+class ChildElementIteratorTest extends \PHPUnit_Framework_TestCase
+{
+ public function testOteration()
+ {
+ $reader = new XMLReaderStub('');
+
+ $it = new ChildElementIterator($reader);
+
+ $this->assertEquals(false, $it->valid());
+ $this->assertSame(null, $it->valid());
+
+ $it->rewind();
+ $this->assertEquals(true, $it->valid());
+ $this->assertEquals('child', $it->current()->getName());
+
+ $it->next();
+ $this->assertEquals(false, $it->valid());
+
+ $reader = new XMLReaderStub('');
+ $base = new ElementIterator($reader);
+ $base->rewind();
+ $root = $base->current();
+ $this->assertEquals('root', $root->getName());
+ $children = $root->getChildElements();
+ $this->assertEquals('root', $reader->name);
+ $children->rewind();
+ $this->assertEquals('none', $reader->name);
+ $children->next();
+ $this->assertEquals('one', $reader->name);
+ $childChildren = new ChildElementIterator($reader);
+ $this->assertEquals('child', $childChildren->current()->getName());
+ $childChildren->next();
+ $this->assertEquals(false, $childChildren->valid());
+ $this->assertEquals('none', $reader->name);
+ $childChildren->next();
+ $this->assertEquals('none', $reader->name);
+
+ $this->assertEquals(true, $children->valid());
+ $children->next();
+ $this->assertEquals(false, $children->valid());
+
+
+ // children w/o descendants
+ $reader->rewind();
+ $expected = ['none', 'one', 'none'];
+ $root = $base->current();
+ $this->assertEquals('root', $root->getName());
+
+ $count = 0;
+ foreach ($root->getChildElements() as $index => $child) {
+ $this->assertSame($count++, $index);
+ $this->assertEquals($expected[$index], $reader->name);
+ }
+ $this->assertEquals(count($expected), $count);
+
+ // children w/ descendants
+ $reader->rewind();
+ $expected = ['none', 'one', 'child', 'none'];
+ $root = $base->current();
+ $this->assertEquals('root', $root->getName());
+
+ $count = 0;
+ foreach ($root->getChildElements(null, true) as $index => $child) {
+ $this->assertSame($count++, $index);
+ $this->assertEquals($expected[$index], $reader->name);
+ }
+ $this->assertEquals(count($expected), $count);
+
+ }
+}
diff --git a/src/Tests/ElementIteratorTest.php b/src/Tests/ElementIteratorTest.php
new file mode 100644
index 0000000..0713614
--- /dev/null
+++ b/src/Tests/ElementIteratorTest.php
@@ -0,0 +1,137 @@
+
+ */
+class ElementIteratorTest extends \PHPUnit_Framework_TestCase
+{
+ public function testCreationAndCurrent()
+ {
+ $reader = $this->createReader();
+
+ $it = new ElementIterator($reader);
+
+ $this->assertSame('xml', $it->current()->getName());
+ $it->next();
+ $this->assertSame('node1', $it->current()->getName());
+ $it->next();
+ $this->assertSame('info1', $it->current()->getName());
+ }
+
+ /** @test */
+ public function string()
+ {
+ $reader = new XMLReaderStub('has');
+
+ /** @var ElementIterator|Node[]|XMLReader $it */
+ $it = new ElementIterator($reader);
+
+ $it->rewind();
+ $this->assertEquals(true, $it->valid());
+ $this->assertEquals("has", (string) $it);
+ $this->assertEquals("has", $it->readString());
+ }
+
+ /** @test */
+ public function iteration()
+ {
+ $reader = new XMLReaderStub('has');
+
+ /** @var ElementIterator|Node[] $it */
+ $it = new ElementIterator($reader);
+
+ $this->assertEquals(false, $it->valid());
+ $this->assertSame(null, $it->valid());
+
+ $it->rewind();
+ $this->assertEquals(true, $it->valid());
+ $this->assertEquals('root', $it->current()->getName());
+ $this->assertEquals(0, $it->key());
+
+ $it->rewind();
+ $this->assertEquals(true, $it->valid());
+ $current = $it->current();
+ $this->assertEquals('root', $current->getName());
+ $this->assertEquals(0, $it->key());
+
+ $string = $current->readString();
+ $this->assertEquals('has', $string);
+
+ $it->next();
+ $this->assertEquals(true, $it->valid());
+ $current = $it->current();
+ $this->assertEquals('b', $current->getName());
+ $this->assertEquals(1, $it->key());
+
+ $it->next();
+ $this->assertEquals(false, $it->valid());
+ $current = $it->current();
+ $this->assertEquals(null, $current);
+
+ }
+
+ /** @test */
+ public function getChildren()
+ {
+ $reader = $this->createReader();
+
+ $it = new ElementIterator($reader);
+
+ $xml = $it->current();
+ $this->assertSame('xml', $xml->name); // ensure this is the root node
+ $it->next();
+
+ $array = $it->toArray();
+ $this->assertSame(7, count($array));
+ $this->assertSame("\n test\n ", $array['node4']);
+ }
+
+ /**
+ * @test
+ */
+ function iterateOverNamedElements()
+ {
+ $reader = new XMLReaderStub('12c3');
+ $it = new ElementIterator($reader, 'a');
+
+ $this->assertEquals(null, $it->valid());
+ $it->rewind();
+ $this->assertEquals(true, $it->valid());
+ $this->assertEquals('a', $it->current()->getName());
+ $it->next();
+ $this->assertEquals('a', $it->current()->getName());
+ $it->next();
+ $this->assertEquals('a', $it->current()->getName());
+ $this->assertEquals('3', $it);
+ $it->next();
+ $this->assertEquals(false, $it->valid());
+ }
+
+ private function createReader()
+ {
+ return new XMLReaderStub('
+
+
+
+
+
+
+
+
+
+
+
+ test
+
+ ');
+ }
+}
diff --git a/src/Tests/ElementTest.php b/src/Tests/ElementTest.php
new file mode 100644
index 0000000..ef7289a
--- /dev/null
+++ b/src/Tests/ElementTest.php
@@ -0,0 +1,59 @@
+
+ */
+class ElementTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var XMLReader
+ */
+ protected $reader;
+
+ protected function setUp()
+ {
+ $this->reader = new XMLReaderStub('node value');
+ }
+
+ public function testElementCreation()
+ {
+ $reader = $this->reader;
+ $reader->next();
+ $element = new Element($reader);
+ $this->assertSame($element->getNodeTypeName(), $element->getNodeTypeName(XMLReader::ELEMENT));
+ $this->assertSame($element->name, 'root');
+ }
+
+ public function testReaderAttributeHandling()
+ {
+ $reader = new XMLReaderStub("node value");
+ $reader->next();
+ $this->assertSame("first", $reader->getAttribute('pos'));
+ $this->assertSame("a\r\nb c \td", $reader->getAttribute('plue'), 'entity handling');
+ $element = new Element($reader);
+ $xml = $element->getXMLElementAround();
+ $this->assertSame("", $xml, 'XML generation');
+ }
+
+ public function testCheckNodeValue()
+ {
+ $reader = new XMLReaderStub('has');
+ /** @var ElementIterator|Node[] $it */
+ $it = new ElementIterator($reader);
+ $count = 0;
+ foreach ($it as $element) {
+ $this->assertEquals('has', $element->readString());
+ $count++;
+ }
+ $this->assertEquals(2, $count);
+ }
+}
diff --git a/src/Tests/Fixtures/data/dobs-items.xml b/src/Tests/Fixtures/data/dobs-items.xml
new file mode 100644
index 0000000..168b861
--- /dev/null
+++ b/src/Tests/Fixtures/data/dobs-items.xml
@@ -0,0 +1,11 @@
+
+
+ - item #1
+ - item #2
+ - item #3
+
+
+ - item #4
+ - item #5
+
+
diff --git a/src/Tests/Fixtures/data/features-basic.xml b/src/Tests/Fixtures/data/features-basic.xml
new file mode 100644
index 0000000..4fdb75b
--- /dev/null
+++ b/src/Tests/Fixtures/data/features-basic.xml
@@ -0,0 +1,10 @@
+
+
+
+
+ value
+ some text
+
+
+ &
+
diff --git a/src/Tests/Fixtures/data/features-xmlns.xml b/src/Tests/Fixtures/data/features-xmlns.xml
new file mode 100644
index 0000000..5c753bf
--- /dev/null
+++ b/src/Tests/Fixtures/data/features-xmlns.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+ &
+
diff --git a/src/Tests/Fixtures/data/movies.xml b/src/Tests/Fixtures/data/movies.xml
new file mode 100644
index 0000000..c8be2ca
--- /dev/null
+++ b/src/Tests/Fixtures/data/movies.xml
@@ -0,0 +1,32 @@
+
+
+
+
+ PHP: Behind the Parser
+
+
+ Ms. Coder
+ Onlivia Actora
+
+
+ Mr. Coder
+ El ActÓr
+
+
+
+ So, this language. It's like, a programming language. Or is it a
+ scripting language? All is revealed in this thrilling horror spoof
+ of a documentary.
+
+
+ PHP solves all my web problems
+
+ 7
+ 5
+
+
diff --git a/src/Tests/Fixtures/data/posts.xml b/src/Tests/Fixtures/data/posts.xml
new file mode 100644
index 0000000..8d0b397
--- /dev/null
+++ b/src/Tests/Fixtures/data/posts.xml
@@ -0,0 +1,119 @@
+
+
+
+
+ message 1
+ client
+
+
+
+
+
+ message 2
+ client
+
+
+
+
+
+ message 3
+ client
+
+
+
+
+
+ message 4
+ client
+
+
+
+
+
+ message 5
+ client
+
+
+
+
+
+ message 6
+ client
+
+
+
+
+
+ message 7
+ client
+
+
+
+
+
+ message 8.1
+ client
+
+
+
+ message 8.2
+ client
+
+
+
+ message 8.3
+ client
+
+
+
+
+
+ message 9
+ client
+
+
+
+ message 9 A
+ client
+
+
+
+
+
+
+
+ message 10
+ client
+
+
+
+
+
+ hello
+ client
+
+
+
+ hello client how can I help?
+ operator
+
+
+
+
+
+ good morning
+ client
+
+
+
+ good morning how can I help?
+ operator
+
+
+
+
diff --git a/src/Tests/Fixtures/data/rss-feed-media.xml b/src/Tests/Fixtures/data/rss-feed-media.xml
new file mode 100644
index 0000000..d8ab65e
--- /dev/null
+++ b/src/Tests/Fixtures/data/rss-feed-media.xml
@@ -0,0 +1,40 @@
+
+
+
+ flow-Media Catalog
+ http://catalog.flownetworks.com/catalogs/1/videos.mrss
+ Video Catalog
+
+ http://images.flow-media.com/flow_media_current.png
+ Get to know flow-Media
+ http://www.flow-media.com
+
+ flow-Media
+ -
+ ..
+ ..
+ ..
+ Wed, 01 May 2013 07:01:08 GMT
+ 9809880
+
+ 52985890
+
+
+
+ US
+
+
+
+
+
+
+
+
diff --git a/src/Tests/Fixtures/dav-significant-whitespace.xml b/src/Tests/Fixtures/dav-significant-whitespace.xml
new file mode 100644
index 0000000..98d898a
--- /dev/null
+++ b/src/Tests/Fixtures/dav-significant-whitespace.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Tests/IterationTest.php b/src/Tests/IterationTest.php
new file mode 100644
index 0000000..53b8587
--- /dev/null
+++ b/src/Tests/IterationTest.php
@@ -0,0 +1,81 @@
+
+ */
+class IterationTest extends \PHPUnit_Framework_TestCase
+{
+ public function testCreation()
+ {
+ $iterator = new Iteration(new XMLReaderStub(''));
+ $this->assertInstanceOf('\Thruster\Component\XMLIterator\Iteration', $iterator);
+ $this->assertInstanceOf('\Traversable', $iterator);
+ $this->assertInstanceOf('\Iterator', $iterator);
+ }
+
+ public function testIteration()
+ {
+ $reader = new XMLReaderStub('');
+ $iterator = new Iteration($reader);
+
+ $data = array(
+ array(XMLReader::ELEMENT, 0),
+ array(XMLReader::ELEMENT, 1),
+ array(XMLReader::END_ELEMENT, 1),
+ array(XMLReader::END_ELEMENT, 0),
+ );
+
+ $count = 0;
+
+ /* @var $reader XMLReader */
+ foreach ($iterator as $index => $reader) {
+ $this->assertSame($count, $index);
+ list($nodeType, $depth) = $data[$index];
+ $this->assertSame($nodeType, $reader->nodeType);
+ $this->assertSame($depth, $reader->depth);
+ $count++;
+ }
+
+ $this->assertSame(4, $count);
+ }
+
+ public function testSkipNextRead()
+ {
+ $reader = new XMLReaderStub('');
+ $iterator = new Iteration($reader);
+
+ $key = null;
+
+ foreach ($iterator as $key => $node) {
+ $this->assertEquals('r', $node->name);
+ if ($key >= 6) {
+ break;
+ }
+ $iterator->skipNextRead();
+ }
+
+ $this->assertEquals(6, $key);
+
+ $reader = new XMLReaderStub('');
+ $iterator = new Iteration($reader);
+
+ foreach ($iterator as $node) {
+ if ($node->name === 'r') {
+ continue;
+ }
+ $this->assertEquals(XMLReader::ELEMENT, $node->nodeType);
+ $this->assertEquals('a', $node->name);
+
+ $node->next();
+ $iterator->skipNextRead();
+ }
+ }
+}
diff --git a/src/Tests/NodeTest.php b/src/Tests/NodeTest.php
new file mode 100644
index 0000000..c39c44b
--- /dev/null
+++ b/src/Tests/NodeTest.php
@@ -0,0 +1,86 @@
+
+ */
+class NodeTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * some XMLReaderNode can not be turned into a SimpleXMLElement, this tests how robust XMLReaderNode
+ * is for the job.
+ */
+ public function testAsSimpleXMLforElementAndSignificantWhitespace()
+ {
+ $reader = new XMLReaderStub('
+
+ ');
+
+ $reader->read(); // (#1)
+
+ // test asSimpleXML() for XMLReader::ELEMENT
+ $this->assertSame(XMLReader::ELEMENT, $reader->nodeType);
+ $node = new Node($reader);
+ $sxml = $node->getSimpleXMLElement();
+ $this->assertInstanceOf('SimpleXMLElement', $sxml);
+
+ $reader->read(); // (#14) SIGNIFICANT_WHITESPACE
+
+ // test asSimpleXML() for XMLReader::SIGNIFICANT_WHITESPACE
+ $this->assertSame(XMLReader::SIGNIFICANT_WHITESPACE, $reader->nodeType);
+ $node = new Node($reader);
+ $sxml = $node->getSimpleXMLElement();
+ $this->assertNull($sxml);
+ }
+
+ public function testXxpand()
+ {
+ $reader = new XMLReaderStub('
+
+
+ Desktop 1 (d)
+ 499.99
+
+
+
+ Tablet 1 (t)
+ 1099.99
+
+ ');
+
+ $products = new ElementIterator($reader, 'product');
+ $doc = new \DOMDocument();
+ $xpath = new \DOMXPath($doc);
+ foreach ($products as $product) {
+ $node = $product->expand($doc);
+ $this->assertInstanceOf('DOMNode', $node);
+ $this->assertSame($node->ownerDocument, $doc);
+ $this->assertEquals('product', $xpath->evaluate('local-name(.)', $node));
+ $this->addToAssertionCount(1);
+ }
+ $this->assertGreaterThan(0, $previous = $this->getNumAssertions());
+
+ unset($doc);
+ $reader->rewind();
+ foreach ($products as $product) {
+ $node = $product->expand();
+ $this->assertInstanceOf('DOMNode', $node);
+ $this->assertInstanceOf('DOMDocument', $node->ownerDocument);
+ $doc = $node->ownerDocument;
+ $xpath = new \DOMXPath($doc);
+ $this->assertSame($node->ownerDocument, $doc);
+ $this->assertEquals('product', $xpath->evaluate('local-name(.)', $node));
+ $this->addToAssertionCount(1);
+ }
+
+ $this->assertGreaterThan($previous, $this->getNumAssertions());
+ }
+}
diff --git a/src/Tests/XMLIteratorTest.php b/src/Tests/XMLIteratorTest.php
new file mode 100644
index 0000000..ce7b1f0
--- /dev/null
+++ b/src/Tests/XMLIteratorTest.php
@@ -0,0 +1,49 @@
+
+ */
+class XMLIteratorTest extends \PHPUnit_Framework_TestCase
+{
+ public function testOteration()
+ {
+ $reader = new XMLReaderStub('12');
+
+ $it = new XMLIterator($reader);
+ $this->assertSame(null, $it->valid());
+
+ $it->rewind();
+ $this->assertSame(true, $it->valid());
+
+ $node = $it->current();
+ $this->assertEquals('r', $node->getName());
+ $this->assertEquals('12', (string) $node);
+
+ $it->moveToNextElementByName('a');
+ $node = $it->current();
+ $this->assertEquals('a', $node->getName());
+ $this->assertEquals('1', (string) $node);
+
+ $it->moveToNextElementByName('a');
+ $node = $it->current();
+ $this->assertEquals('a', $node->getName());
+ $this->assertEquals('2', (string) $node);
+
+ $it->next();
+ $it->next();
+ $this->assertEquals(XMLReader::END_ELEMENT, $reader->nodeType);
+ $this->assertEquals('a', $it->current()->getName());
+
+ $it->next();
+ $it->next();
+ $this->assertEquals(false, $it->valid());
+ }
+}
diff --git a/src/Tests/XMLReaderStub.php b/src/Tests/XMLReaderStub.php
new file mode 100644
index 0000000..89cb12c
--- /dev/null
+++ b/src/Tests/XMLReaderStub.php
@@ -0,0 +1,33 @@
+
+ */
+class XMLReaderStub extends XMLReader
+{
+ private $xml;
+
+ public function __construct($xml)
+ {
+ $this->xml = $xml;
+ $this->rewind();
+ }
+
+ public function rewind()
+ {
+ $xml = $this->xml;
+
+ if ($xml[0] === '<') {
+ $xml = 'data://text/xml;base64,' . base64_encode($this->xml);
+ }
+
+ $this->open($xml);
+ }
+}
diff --git a/src/Tests/XMLReaderTest.php b/src/Tests/XMLReaderTest.php
new file mode 100644
index 0000000..896a7ef
--- /dev/null
+++ b/src/Tests/XMLReaderTest.php
@@ -0,0 +1,81 @@
+
+ */
+class XMLReaderTest extends XMLReaderTestCase
+{
+ /**
+ * @dataProvider provideAllFiles
+ *
+ * @param string $xml
+ */
+ public function testReadBehavior($xml)
+ {
+ $reader = new XMLReaderStub($xml);
+
+ $it = new XMLIterator($reader);
+ $expected = array();
+ while ($reader->read()) {
+ $expected[] = Node::dump($reader, true);
+ }
+
+ $reader->rewind();
+ $index = 0;
+ foreach ($it as $index => $node) {
+ $this->assertEquals($expected[$index], Node::dump($reader, true));
+ }
+
+ $this->assertCount($index + 1, $expected);
+ }
+
+ /**
+ * @dataProvider provideAllFiles
+ *
+ * @param string $xml
+ */
+ public function testNextBehavior($xml)
+ {
+ $reader = new XMLReaderStub($xml);
+
+ $it = new NextIteration($reader);
+ $expected = array();
+ while ($reader->next()) {
+ $expected[] = Node::dump($reader, true);
+ }
+
+ $reader->rewind();
+ $index = 0;
+ foreach ($it as $index => $node) {
+ $this->assertEquals($expected[$index], Node::dump($reader, true));
+ }
+
+ $this->assertCount($index + 1, $expected);
+ }
+
+ /**
+ * @see readBahvior
+ * @see writeBehavior
+ */
+ public function provideAllFiles()
+ {
+ $result = array();
+
+ $path = __DIR__ . '/Fixtures';
+ $result = $this->addXmlFiles($result, $path);
+
+ $path = __DIR__ . '/Fixtures/data';
+ $result = $this->addXmlFiles($result, $path);
+
+ return $result;
+ }
+}
diff --git a/src/Tests/XMLReaderTestCase.php b/src/Tests/XMLReaderTestCase.php
new file mode 100644
index 0000000..fe86131
--- /dev/null
+++ b/src/Tests/XMLReaderTestCase.php
@@ -0,0 +1,63 @@
+
+ */
+class XMLReaderTestCase extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ // remove any xmlseq stream-wrapper as it might be a left-over from a previous test
+ if (in_array('xmlseq', stream_get_wrappers())) {
+ stream_wrapper_unregister('xmlseq');
+ }
+
+ parent::setUp();
+ }
+
+
+ /**
+ * helper method to create data-providers
+ *
+ * @param array $result
+ * @param $path
+ *
+ * @return array of arrays with one entry of each filename as string
+ */
+ protected function addXmlFiles(array $result, $path)
+ {
+ return $this->addFiles($result, $path, '~\.xml$~');
+ }
+
+ /**
+ * helper method to create data-providers
+ *
+ * @param array $result
+ * @param string $path
+ * @param string $pattern PCRE pattern matched against basename
+ *
+ * @return array of arrays with one entry of each filename as string
+ */
+ protected function addFiles(array $result, $path, $pattern)
+ {
+ /** @var \FilesystemIterator|\SplFileInfo[] $dir */
+ $dir = new \FilesystemIterator($path);
+ foreach ($dir as $file) {
+ if (!$file->isFile()) {
+ continue;
+ }
+ if (!preg_match($pattern, $file->getBasename())) {
+ continue;
+ }
+
+ $result[] = array((string) $file);
+ }
+
+ return $result;
+ }
+}
diff --git a/src/XMLBuild.php b/src/XMLBuild.php
new file mode 100644
index 0000000..e666e26
--- /dev/null
+++ b/src/XMLBuild.php
@@ -0,0 +1,147 @@
+
+ */
+abstract class XMLBuild
+{
+ /**
+ * indentLines()
+ *
+ * this will add a line-separator at the end of the last line because if it was
+ * empty it is not any longer and deserves one.
+ *
+ * @param string $lines
+ * @param string $indent (optional)
+ *
+ * @return string
+ */
+ public static function indentLines($lines, $indent = ' ')
+ {
+ $lineSeparator = "\n";
+ $buffer = '';
+ $line = strtok($lines, $lineSeparator);
+
+ while ($line) {
+ $buffer .= $indent . $line . $lineSeparator;
+ $line = strtok($lineSeparator);
+ }
+
+ strtok(null, null);
+
+ return $buffer;
+ }
+
+ /**
+ * @param string $name
+ * @param array|Traversable $attributes attributeName => attributeValue string pairs
+ * @param bool $emptyTag create an empty element tag (commonly known as short tags)
+ *
+ * @return string
+ */
+ public static function startTag($name, $attributes, $emptyTag = false)
+ {
+ $buffer = '<' . $name;
+ $buffer .= static::attributes($attributes);
+ $buffer .= $emptyTag ? '/>' : '>';
+
+ return $buffer;
+ }
+
+ /**
+ * @param array|Traversable $attributes attributeName => attributeValue string pairs
+ *
+ * @return string
+ */
+ public static function attributes($attributes)
+ {
+ $buffer = '';
+ foreach ($attributes as $name => $value) {
+ $buffer .= ' ' . $name . '="' . static::attributeValue($value) . '"';
+ }
+
+ return $buffer;
+ }
+
+ /**
+ * @param string $value
+ *
+ * @see XMLBuild::numericEntitiesSingleByte
+ *
+ * @return string
+ */
+ public static function attributeValue($value)
+ {
+ $buffer = $value;
+ // REC-xml/#AVNormalize - preserve
+ // REC-xml/#sec-line-ends - preserve
+ $buffer = preg_replace_callback('~\r\n|\r(?!\n)|\t~', ['self', 'numericEntitiesSingleByte'], $buffer);
+
+ return htmlspecialchars($buffer, ENT_QUOTES, 'UTF-8', false);
+ }
+
+ /**
+ * @param string $name
+ * @param array|Traversable $attributes attributeName => attributeValue string pairs
+ * @param string $innerXML
+ *
+ * @return string
+ */
+ public static function wrapTag($name, $attributes, $innerXML)
+ {
+ if (!strlen($innerXML)) {
+ return XMLBuild::startTag($name, $attributes, true);
+ }
+
+ return
+ XMLBuild::startTag($name, $attributes)
+ . "\n"
+ . XMLBuild::indentLines($innerXML)
+ . "$name>";
+ }
+
+ /**
+ * @param XMLReader $reader
+ *
+ * @return string
+ */
+ public static function readerNode(XMLReader $reader)
+ {
+ switch ($reader->nodeType) {
+ case XMLREADER::NONE:
+ return '%(0)%';
+ case XMLReader::ELEMENT:
+ return XMLBuild::startTag($reader->name, new AttributeIterator($reader));
+ default:
+ $node = new Node($reader);
+ $nodeTypeName = $node->getNodeTypeName();
+ $nodeType = $reader->nodeType;
+
+ return sprintf('%%%s (%d)%%', $nodeTypeName, $nodeType);
+ }
+ }
+
+ /**
+ * @param array $matches
+ *
+ * @return string
+ * @see attributeValue()
+ */
+ private static function numericEntitiesSingleByte($matches)
+ {
+ $buffer = str_split($matches[0]);
+
+ foreach ($buffer as &$char) {
+ $char = sprintf('%d;', ord($char));
+ }
+
+ return implode('', $buffer);
+ }
+}
diff --git a/src/XMLIterator.php b/src/XMLIterator.php
new file mode 100644
index 0000000..5f9e3e3
--- /dev/null
+++ b/src/XMLIterator.php
@@ -0,0 +1,174 @@
+
+ */
+class XMLIterator implements IteratorInterface
+{
+ /**
+ * @var XMLReader
+ */
+ protected $reader;
+
+ /**
+ * @var int
+ */
+ private $index;
+
+ /**
+ * @var bool
+ */
+ private $lastRead;
+
+ /**
+ * @var array
+ */
+ private $elementStack;
+
+ public function __construct(XMLReader $reader)
+ {
+ $this->reader = $reader;
+ }
+
+ /**
+ * @return XMLReader
+ */
+ public function getReader() : XMLReader
+ {
+ return $this->reader;
+ }
+
+ /**
+ * @param string $name
+ *
+ * @return bool|Node
+ */
+ public function moveToNextElementByName($name = null)
+ {
+ while (self::moveToNextElement()) {
+ if (!$name || $name === $this->reader->name) {
+ break;
+ }
+
+ self::next();
+ };
+
+ return self::valid() ? self::current() : false;
+ }
+
+ public function moveToNextElement()
+ {
+ return $this->moveToNextByNodeType(XMLReader::ELEMENT);
+ }
+
+ /**
+ * @param int $nodeType
+ *
+ * @return bool|Node
+ */
+ public function moveToNextByNodeType($nodeType)
+ {
+ if (null === self::valid()) {
+ self::rewind();
+ } elseif (self::valid()) {
+ self::next();
+ }
+
+ while (self::valid()) {
+ if ($this->reader->nodeType === $nodeType) {
+ break;
+ }
+
+ self::next();
+ }
+
+ return self::valid() ? self::current() : false;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function rewind()
+ {
+ // this iterator can not really rewind
+ if ($this->reader->nodeType === XMLREADER::NONE) {
+ self::next();
+ } elseif ($this->lastRead === null) {
+ $this->lastRead = true;
+ }
+
+ $this->index = 0;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function valid()
+ {
+ return $this->lastRead;
+ }
+
+ /**
+ * @return Node
+ */
+ public function current()
+ {
+ return new Node($this->reader);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function key()
+ {
+ return $this->index;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function next()
+ {
+ if ($this->lastRead = $this->reader->read() and $this->reader->nodeType === XMLReader::ELEMENT) {
+ $depth = $this->reader->depth;
+ $this->elementStack[$depth] = new Element($this->reader);
+
+ if (count($this->elementStack) !== $depth + 1) {
+ $this->elementStack = array_slice($this->elementStack, 0, $depth + 1);
+ }
+ }
+
+ $this->index++;
+ }
+
+ /**
+ * @return string
+ */
+ public function getNodePath() : string
+ {
+ return '/' . implode('/', $this->elementStack);
+ }
+
+ /**
+ * @return string
+ */
+ public function getNodeTree() : string
+ {
+ $stack = $this->elementStack;
+ $buffer = '';
+
+ /* @var $element Element */
+ while ($element = array_pop($stack)) {
+ $buffer = $element->getXMLElementAround($buffer);
+ }
+
+ return $buffer;
+ }
+}
diff --git a/src/XMLReader.php b/src/XMLReader.php
new file mode 100644
index 0000000..fdc96cf
--- /dev/null
+++ b/src/XMLReader.php
@@ -0,0 +1,16 @@
+
+ */
+class XMLReader extends BaseXMLReader
+{
+
+}