diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d887bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +composer.lock +vendor +docs/build diff --git a/README.md b/README.md new file mode 100644 index 0000000..62bcfb0 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +DOM +=== + +This library provides a wrapper for the PHP DOM library which makes your life +easier. + +It wraps the `\DOMDocument`, `\DOMElement` and `\DOMXpath` classes.` + +Example: + +```php +$dom = new Document(); +$element = $dom->createRoot('example'); +$element->appendChild('boo', 'hello'); +$element->appendChild('baz', 'world'); + +echo $dom->saveXml(); +// +// +// hello +// world +// + +$element->appendChild('number', 5); +$element->appendChild('number', 10); + +echo $element->evaluate('sum(./number)'); // 15 + +$nodeList = $element->query('./number'); + +echo $nodeList->length; // 2 +``` + +Document +-------- + +The `PhpBench\Dom\Document` class wraps the `\DOMDocument` class and replaces the +`\DOMElement` class with the `PhpBench\Dom\Element` class. + +It implements the `XPathAware` interface. + +- `createRoot($name, $value = null)`: Create and return a new root node with `$name` and optional + `$value`. +- `query($query, $context = null)`: Execute a given XPath query on the + document. +- `queryOne($query, $context = null)`: Execute a given XPath query on the + document and return the first element or `NULL`. +- `evaluate($query, $context = null)`: Evaluate the given XPath expression. + +Element +------- + +- `appendElement($name $value)`: Create and return an element with name + `$name` and value `$value`. +- `query`, `queryOne` and `evalauate`: As with Document but will use the context of this element by + default. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..77ccadc --- /dev/null +++ b/composer.json @@ -0,0 +1,33 @@ +{ + "name": "phpbench/dom", + "description": "DOM wrapper to simplify working with the PHP DOM implementation", + "license": "MIT", + "authors": [ + { + "name": "Daniel Leech", + "email": "daniel@dantleech.com" + } + ], + "require": { + "php": "^5.0", + "ext-dom": "*" + }, + "require-dev": { + "phpunit/phpunit": "^4.6" + }, + "autoload": { + "psr-4": { + "PhpBench\\Dom\\": "lib/" + } + }, + "autoload-dev": { + "psr-4": { + "PhpBench\\Dom\\Tests\\": "tests/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + } +} diff --git a/lib/Document.php b/lib/Document.php new file mode 100644 index 0000000..de3fc58 --- /dev/null +++ b/lib/Document.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpBench\Dom; + +use PhpBench\Dom\Element; +use PhpBench\Dom\XPath; +use PhpBench\Dom\XPathAware; + +/** + * Wrapper for the \DOMDocument class. + */ +class Document extends \DOMDocument implements XPathAware +{ + /** + * @var XPath + */ + private $xpath; + + /** + * @param string $version + * @param mixed $encoding + */ + public function __construct($version = '1.0', $encoding = null) + { + parent::__construct($version, $encoding); + $this->registerNodeClass('DOMElement', 'PhpBench\Dom\Element'); + } + + /** + * Create and return a root DOM element + * + * @param string $name + * @return Element + */ + public function createRoot($name) + { + return $this->appendChild(new Element($name)); + } + + /** + * Return the XPath object bound to this document. + * + * @return XPath + */ + public function xpath() + { + if ($this->xpath) { + return $this->xpath; + } + + $this->xpath = new XPath($this); + + return $this->xpath; + } + + /** + * {@inheritdoc} + */ + public function query($query, \DOMNode $context = null) + { + return $this->xpath()->query($query, $context); + } + + /** + * {@inheritdoc} + */ + public function queryOne($query, \DOMNode $context = null) + { + return $this->xpath()->queryOne($query, $context); + } + + /** + * {@inheritdoc} + */ + public function evaluate($expression, \DOMNode $context = null) + { + return $this->xpath()->evaluate($expression, $context); + } +} diff --git a/lib/Element.php b/lib/Element.php new file mode 100644 index 0000000..1d76e57 --- /dev/null +++ b/lib/Element.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpBench\Dom; + +use PhpBench\Dom\XPathAware; + +/** + * Wrapper for the \DOMElement class. + */ +class Element extends \DOMElement implements XPathAware +{ + /** + * Create and append an element with the given name and optionally given value. + * + * @param string $name + * @param mixed $value + * @return Element + */ + public function appendElement($name, $value = null) + { + return $this->appendChild(new self($name, $value)); + } + + /** + * {@inheritdoc} + */ + public function query($xpath, \DOMNode $context = null) + { + return $this->ownerDocument->xpath()->query($xpath, $context ?: $this); + } + + public function queryOne($xpath, \DOMNode $context = null) + { + return $this->ownerDocument->xpath()->queryOne($xpath, $context ?: $this); + } + + /** + * {@inheritdoc} + */ + public function evaluate($expression, \DOMNode $context = null) + { + return $this->ownerDocument->xpath()->evaluate($expression, $context ?: $this); + } +} diff --git a/lib/Exception/InvalidQueryException.php b/lib/Exception/InvalidQueryException.php new file mode 100644 index 0000000..0d3bae8 --- /dev/null +++ b/lib/Exception/InvalidQueryException.php @@ -0,0 +1,7 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpBench\Dom; + +/** + * Wrapper for the \DOMXPath class. + */ +class XPath extends \DOMXPath +{ + /** + * {@inheritdoc} + */ + public function evaluate($expr, \DOMNode $contextEl = null, $registerNodeNs = null) + { + $result = $this->execute('evaluate', 'expression', $expr, $contextEl, $registerNodeNs); + return $result; + } + + /** + * {@inheritdoc} + */ + public function query($expr, \DOMNode $contextEl = null, $registerNodeNs = null) + { + return $this->execute('query', 'query', $expr, $contextEl, $registerNodeNs); + } + + /** + * Query for one node + */ + public function queryOne($expr, \DOMNode $contextEl = null, $registerNodeNs = null) + { + $nodeList = $this->query($expr, $contextEl, $registerNodeNs); + + if (0 === $nodeList->length) { + return null; + } + + return $nodeList->item(0); + } + + /** + * Execute the given xpath method and cactch any errors. + */ + private function execute($method, $context, $query, \DOMNode $contextEl = null, $registerNodeNs) + { + libxml_use_internal_errors(true); + + $value = @parent::$method($query, $contextEl, $registerNodeNs); + + if (false === $value) { + $xmlErrors = libxml_get_errors(); + $errors = array(); + foreach ($xmlErrors as $xmlError) { + $errors[] = sprintf('[%s] %s', $xmlError->code, $xmlError->message); + } + + throw new Exception\InvalidQueryException(sprintf( + 'Errors encountered when evaluating XPath %s "%s": %s%s', + $context, $query, PHP_EOL, implode(PHP_EOL, $errors) + )); + } + + libxml_use_internal_errors(false); + + return $value; + } +} diff --git a/lib/XPathAware.php b/lib/XPathAware.php new file mode 100644 index 0000000..46bfded --- /dev/null +++ b/lib/XPathAware.php @@ -0,0 +1,41 @@ + + + + + + + + ./tests + + + + + + . + + vendor/ + + + + + diff --git a/tests/Unit/DocumentTest.php b/tests/Unit/DocumentTest.php new file mode 100644 index 0000000..494ead2 --- /dev/null +++ b/tests/Unit/DocumentTest.php @@ -0,0 +1,66 @@ +document = new Document(1.0); + } + + /** + * It should perform an XPath query + */ + public function testQuery() + { + $this->document->loadXml($this->getXml()); + $nodeList = $this->document->query('//record'); + $this->assertInstanceOf('DOMNodeList', $nodeList); + $this->assertEquals(2, $nodeList->length); + } + + /** + * It should evaluate an XPath expression + */ + public function testEvaluate() + { + $this->document->loadXml($this->getXml()); + $result = $this->document->evaluate('count(//record)'); + $this->assertEquals(2, $result); + } + + /** + * It should create a root element + */ + public function testCreateRoot() + { + $this->document->createRoot('hello'); + $this->assertContains('', $this->document->saveXml()); + } + + private function getXml() + { + $xml = << + + + Hello + + + World + + +EOT + ; + + return $xml; + } +} diff --git a/tests/Unit/ElementTest.php b/tests/Unit/ElementTest.php new file mode 100644 index 0000000..772b35e --- /dev/null +++ b/tests/Unit/ElementTest.php @@ -0,0 +1,90 @@ +document = new Document(); + $this->element = $this->document->createRoot('test'); + } + + /** + * It should create and append a child element + */ + public function testAppendElement() + { + $element = $this->element->appendElement('hello'); + $result = $this->document->evaluate('count(//hello)'); + $this->assertInstanceOf('PhpBench\Dom\Element', $element); + $this->assertEquals(1, $result); + } + + /** + * It should exeucte an XPath query + */ + public function testQuery() + { + $boo = $this->element->appendElement('boo'); + $nodeList = $this->element->query('.//*'); + $this->assertInstanceOf('DOMNodeList', $nodeList); + $this->assertEquals(1, $nodeList->length); + $nodeList = $boo->query('.//*'); + $this->assertEquals(0, $nodeList->length); + } + + /** + * It should evaluate an XPath expression + */ + public function testEvaluate() + { + $boo = $this->element->appendElement('boo'); + $count = $this->element->evaluate('count(.//*)'); + $this->assertEquals(1, $count); + $count = $boo->evaluate('count(.//*)'); + $this->assertEquals(0, $count); + } + + /** + * It should query for one element + */ + public function testQueryOne() + { + $boo = $this->element->appendElement('boo'); + $node = $this->element->queryOne('./boo'); + $this->assertSame($boo, $node); + } + + /** + * It should return null if one element is queried for an it none exist. + */ + public function testQueryOneNone() + { + $node = $this->element->queryOne('./boo'); + $this->assertNull($node); + } + + private function getXml() + { + $xml = << + + + Hello + + + World + + +EOT + ; + + return $xml; + } +} diff --git a/tests/Unit/XPathTest.php b/tests/Unit/XPathTest.php new file mode 100644 index 0000000..0e1e542 --- /dev/null +++ b/tests/Unit/XPathTest.php @@ -0,0 +1,51 @@ +getDocument()->query('//article[noexistfunc() = "as"]'); + } + + /** + * It should throw an exception if the xpath expression is invalid + * + * @expectedException PhpBench\Dom\Exception\InvalidQueryException + * @expectedExceptionMessage function noexistfunc not found + */ + public function testEvaluateException() + { + $this->getDocument()->evaluate('//article[noexistfunc() = "as"]'); + } + + private function getDocument() + { + $xml = << + +
+ Morning +
+
+ Afternoon +
+
+EOT + ; + + $document = new Document(); + $document->loadXml($xml); + + return $document; + } +}