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;
+ }
+}