Location: PHPKode > projects > Mocovie web framework > mocovi/library/filesystems/WritableXMLFS.php
<?php
/**
 *  Copyright (C) 2011  Kai Dorschner
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @author Kai Dorschner <the-hide@address.com>
 * @copyright Copyright 2011, Kai Dorschner
 * @license http://www.gnu.org/licenses/gpl.html GPLv3
 * @package mocovi
 * @subpackage filesystems
 *
 *
 *
 * @todo RelaxNG doesn't work properly on libxml < 2.7.6
 */

/**
 * Require Filesystem.
 */
class_exists('XMLFS')	or require $GLOBALS['filesystemsLibrary'].'XMLFS.php';

/**
 * This class is used when an operation is not allowed.
 */
class NotAllowedException extends Exception {}

/**
 * Writable XML Filesystem
 *
 * This filesystem consists of XML and contains all files and each model of a file.
 * The whole content can be edited.
 *
 * @package mocovi
 * @subpackage filesystems
 */
class WritableXMLFS extends XMLFS
{
	public function deepCreateFile($path)
	{
		if(substr($path, 0, 1) != '/')
			throw new Exception('File path must be absolute!');
		$breadcrumbs = explode('/', trim($path, '/'));
		$build = '/';
		$parent = $this->getDOM()->documentElement;
		for($i = 0; $i < count($breadcrumbs); $i++)
		{
			if(!$this->isFile($build .= $breadcrumbs[$i].'/'))
				$parent->appendChild($this->_createFile($breadcrumbs[$i]));
			$parent = $this->getFile($build);
		}
		return $this->file($path);
	}

	/**
	 * This file is not appended anywhere when created.
	 *
	 */
	public function newFile($name)
	{
		return new WritableXMLFSFile($this->_createFile($name), $this);
	}

	public function file($path)
	{
		return new WritableXMLFSFile($this->getFile($path), $this);
	}

	public function save()
	{
		$this->dom->preserveWhitespace = false;
		$this->dom->formatOutput = true;
		return file_put_contents
			( $this->source
			, preg_replace('/[ ]{2}/', "\t", $this->dom->saveXML()) // replace 2 whitespaces with one tab
			, LOCK_EX // Prevent anyone else writing to the file at the same time
			);
	}

	public function saveAs($path)
	{
	 // @todo implement
	}

	protected function _createFile($name)
	{
		$new = $this->getDOM()->createElementNS(Mocovi::$namespace, 'file');
		$new->setAttribute('name', str_replace(array(' ', '.', ',', '/'), '_', $name));
		$new->setAttribute('created', date('c'));
		$new->appendChild($model = $this->getDOM()->createElementNS(Mocovi::$namespace, 'model'));
		$model->appendChild($root = $this->getDOM()->createElementNS(Mocovi::$namespace, 'root'));
		$root->appendChild($this->getDOM()->createElementNS(Mocovi::$namespace, 'header'));
		$root->appendChild($this->getDOM()->createElementNS(Mocovi::$namespace, 'menu'));
		$root->appendChild($content = $this->getDOM()->createElementNS(Mocovi::$namespace, 'article'));
		$content->appendChild($this->getDOM()->createElementNS(Mocovi::$namespace, 'headline'));
		$content->appendChild($this->getDOM()->createElementNS(Mocovi::$namespace, 'paragraph'));
		$root->appendChild($this->getDOM()->createElementNS(Mocovi::$namespace, 'footer'));
		return $new;
	}
}

class WritableXMLFSFile
{
	protected $node;
	protected $filesystem;

	public function __construct(DomNode $node, XMLFS $filesystem)
	{
		$this->node			= $node;
		$this->filesystem	= $filesystem;
	}

	public function control($xpath)
	{
		$reference = $this->node;
		if($node = $this->filesystem->xpath->query('mocovi:model'.(substr($xpath, 0, 1) != '/' ?'/' : '').$xpath, $reference)->item(0))
			if($node->nodeType === XML_TEXT_NODE)
				return new WritableXMLFSControlText($node, $this, $this->filesystem->xpath);
			else
				return new WritableXMLFSControl($node, $this, $this->filesystem->xpath);
		else
			throw new Exception('Control for xpath '.$xpath.' not found in file '.$this->getName());
	}

	/**
	 * This control is not appended anywhere when created.
	 *
	 */
	public function newControl($name)
	{
		if($name == '#text')
		{
			return new WritableXMLFSControlText
				( $this
					->filesystem
					->getDOM()
					->createTextNode('continuous text...')
					, $this
					, $this->filesystem->xpath
				);
		}
		return new WritableXMLFSControl
			( $this
				->filesystem
				->getDOM()
				->createElementNS(Mocovi::$namespace, $name)
				, $this
				, $this->filesystem->xpath
			);
	}

	public function getNameAlias()
	{
		return $this->node->getAttribute('nameAlias');
	}

	public function getNameAliasToken()
	{
		return $this->node->getAttribute('nameAliasToken');
	}

	public function setNameAlias($alias)
	{
		$this->node->setAttribute('nameAlias', $alias);
		return $this;
	}

	public function setNameAliasToken($aliasToken)
	{
		$this->node->setAttribute('nameAliasToken', $aliasToken);
		return $this;
	}

	public function canMove($path)
	{
		return true;
	}

	public function move($path)
	{
		if(!$this->canMove($path))
			throw new NotAllowedException($path);
		$this->filesystem->getFile($path)->appendChild($this->node);
		return $this;
	}

	public function canMoveBefore($path)
	{
		return true;
	}

	public function moveBefore($path)
	{
		if(!$this->canMoveBefore($path))
			throw new NotAllowedException($path);
		$target = $this->filesystem->getFile($path);
		$target->parentNode->insertBefore($this->node, $target);
		return $this;
	}

	public function canMoveAfter($path)
	{
		return true;
	}
	public function moveAfter($path)
	{
		if(!$this->canMoveAfter($path))
			throw new NotAllowedException($path);
		$target = $this->filesystem->getFile($path);
		if($target->nextSibling)
			$target->parentNode->insertBefore
				( $this->node
				, $target->nextSibling
				);
		else
			$target->parentNode->appendChild($this->node);
		return $this;
	}

	public function canCopy($path)
	{
		return true;
	}

	public function copy($path)
	{
		if(!$this->canCopy($path))
			throw new NotAllowedException($path);
		$this->filesystem->getFile($path)->appendChild($new = $this->cloneNode());
		return new WritableXMLFSFile($new, $this->filesystem);
	}

	public function canCopyBefore($path)
	{
		return true;
	}

	public function copyBefore($path)
	{
		if(!$this->canCopyBefore($path))
			throw new NotAllowedException($path);
		$target = $this->filesystem->getFile($path);
		$target->parentNode->insertBefore($new = $this->cloneNode(), $target);
		return new WritableXMLFSFile($new, $this->filesystem);
	}

	public function canCopyAfter($path)
	{
		return true;
	}

	public function copyAfter($path)
	{
		if(!$this->canCopyAfter($path))
			throw new NotAllowedException($path);
		$new = $this->cloneNode();
		$target = $this->filesystem->getFile($path);
		if($target->nextSibling)
			$target->parentNode->insertBefore
				( $new
				, $target->nextSibling
				);
		else
			$target->parentNode->appendChild($new);
		return new WritableXMLFSFile($new, $this->filesystem);
	}

	public function canReplace($path)
	{
		return true;
	}

	public function replace($path)
	{
		if(!$this->canReplace($path))
			throw new NotAllowedException($path);
		$target = $this->filesystem->getFile($path);
		$target->parentNode->replaceChild($this->node, $target);
		return $this;
	}

	public function canRename($newname)
	{
		return true;
	}

	public function rename($newname)
	{
		if(!$this->canRename($path))
			throw new NotAllowedException($path);
		$this->setAttribute('name', $newname);
	}

	public function clear()
	{
		while($this->node->childNodes->length > 0)
			$this->node->removeChild($this->node->firstChild);
		return $this;
	}

	public function delete()
	{
		$this->node->parentNode->removeChild($this->node);
		unset($this);
	}

	public function makeVisible()
	{
		$this->node->removeAttribute('invisible');
		return $this;
	}

	public function makeInvisible()
	{
		$this->setAttribute('invisible', '1');
		return $this;
	}

	public function setAuthor($name)
	{
		$this->setAttribute('author', $name);
		return $this;
	}

	public function updateLastModified()
	{
		$this->setAttribute('lastModified', date('c'));
		return $this;
	}

	public function setRedirect($path)
	{
		if($this->filesystem->isFile($path))
			$this->setAttribute('redirect', $path);
		else
			throw new Exception($path.' not found for redirect.');
		return $this;
	}

	public function removeRedirect()
	{
		$this->removeAttribute('redirect');
		return $this;
	}

	public function getNode()
	{
		return $this->node;
	}

	public function getName()
	{
		return $this->node->getAttribute('name');
	}

	protected function cloneNode()
	{
		$clone = $this->node->cloneNode(true);
		$clone->setAttribute('name', $clone->getAttribute('name').'_copy');
		return $clone;
	}

	protected function setAttribute($name, $value)
	{
		$this->node->setAttribute($name, $value);
	}
}

class WritableXMLFSControl
{
	protected $namespace;	// owner document
	protected $dom;			// owner document
	protected $xpath;		// filesystem XPath object
	protected $node;		// Current control node
	protected $file;		// Current file context
	protected $test;		// Current file context clone, for testing / validating
	protected $control;		// Current Control

	protected $excludeNodes = array
		( 'xi:include'
		, 'xref:reference'
		, '#comment'
		);

	protected $undeletableControls = array
		( 'root'
		, 'article'
		);

	/**
	 * This is only temporary. When the RelaxNG schema validation works fine,
	 * there is no need to include this.
	 * @deprecated
	 */
	protected $relevantControls = array
		( '#text'
		, 'anchor'
		//, 'cite'
		//, 'article'
		, 'date'
		, 'duration'
		, 'element'
		//, 'emphasized'
		//, 'error'
		//, 'feedreader'
		//, 'fileanchor'
		//, 'gravatar'
		, 'headline'
		, 'image'
		, 'imagegallery'
		, 'inline'
		, 'listing'
		, 'mediaplayer'
		, 'overviewlist'
		, 'paragraph'
		//, 'param'
		//, 'prefacetext'
		, 'sitemap'
		//, 'strike'
		, 'strong'
		, 'text'
		//, 'toc'
		, 'twitter'
		, 'vimeo'
		, 'youtube'
		);

	public function __construct(DomNode $node, WritableXMLFSFile $file, DomXpath $xpath)
	{
		if(in_array($node->nodeType, array(XML_ELEMENT_NODE, XML_TEXT_NODE)))
		{
			$name = $node->nodeName;
			if(!self::controlExists($name))
				throw new Exception('Control "'.$name.'" does not exist.');
			$this->node = $node;
			$this->file = $file;
			$this->test = $file->getNode()->cloneNode(true); // Is needed to test modifications (move, delete, ...)!
			$this->dom = $node->ownerDocument;
			$this->xpath = $xpath;
			$this->control = ControlFactory::createVirtual($name);
			$this->namespace = Mocovi::$namespace;
		}
	}

	public static function controlExists($name)
	{
		$x = ControlFactory::getAllControls();

		return $x[$name];
	}

	public function canMove($xpath)
	{
		$this->_move($this->test, $this->node->cloneNode(true), $xpath);
		return $this->testIsValid();
	}

	protected function _move(DomNode $context, DomNode $source, $xpath)
	{
		$this->findControl($context, $xpath)->appendChild($source);
	}

	public function move($xpath)
	{
		if(!$this->canMove($xpath))
			throw new NotAllowedException($this->node->nodeName.': '.__FUNCTION__);
		$this->_move($this->file->getNode(), $this->node, $xpath);
		return $this;
	}

	public function canMoveBefore($xpath)
	{
		$this->_moveBefore($this->test, $this->node->cloneNode(true), $xpath);
		return $this->testIsValid();
	}

	protected function _moveBefore(DomNode $context, DomNode $source, $xpath)
	{
		$target = $this->findControl($context, $xpath);
		$target->parentNode->insertBefore($source, $target);
	}

	public function moveBefore($xpath)
	{
		if(!$this->canMoveBefore($xpath))
			throw new NotAllowedException($this->node->nodeName.': '.__FUNCTION__);
		$this->_moveBefore($this->file->getNode(), $this->node, $xpath);
		return $this;
	}

	public function canMoveAfter($xpath)
	{
		$this->_moveAfter($this->test, $this->node->cloneNode(true), $xpath);
		return $this->testIsValid();
	}

	protected function _moveAfter(DomNode $context, DomNode $source, $xpath)
	{
		$target = $this->findControl($context, $xpath);
		if($target->nextSibling)
			$target->parentNode->insertBefore
				( $source
				, $target->nextSibling
				);
		else
			$target->parentNode->appendChild($source);
	}

	public function moveAfter($xpath)
	{
		if(!$this->canMoveAfter($xpath))
			throw new NotAllowedException($this->node->nodeName.': '.__FUNCTION__);
		$this->_moveAfter($this->file->getNode(), $this->node, $xpath);
		return $this;
	}

	public function canCopy($xpath)
	{
		$this->_copy($this->test, $this->node->cloneNode(true), $xpath);
		return $this->testIsValid();
	}

	protected function _copy(DomNode $context, DomNode $source, $xpath)
	{
		$this->findControl($context, $xpath)->appendChild($new = $source->cloneNode(true));
		return $new;
	}

	public function copy($xpath)
	{
		if(!$this->canCopy($xpath))
			throw new NotAllowedException($this->node->nodeName.': '.__FUNCTION__);
		
		return new WritableXMLFSControl
			( $this->_copy
				( $this->file->getNode()
				, $this->node
				, $xpath
				)
			, $this->file
			, $this->xpath
			);
	}

	public function canCopyBefore($xpath)
	{
		$this->_copyBefore($this->test, $this->node->cloneNode(true), $xpath);
		return $this->testIsValid();
	}

	protected function _copyBefore(DomNode $context, DomNode $source, $xpath)
	{
		$target = $this->findControl($context, $xpath);
		$target->parentNode->insertBefore($new = $source->cloneNode(true), $target);
		return $new;
	}

	public function copyBefore($xpath)
	{
		if(!$this->canCopyBefore($xpath))
			throw new NotAllowedException($this->node->nodeName.': '.__FUNCTION__);
		return new WritableXMLFSControl
			( $this->_cloneBefore
				( $this->file->getNode()
				, $this->node
				, $xpath
				)
			, $this->file
			, $this->xpath
			);
	}

	public function canCopyAfter($xpath)
	{
		$this->_copyAfter($this->test, $this->node->cloneNode(true), $xpath);
		return $this->testIsValid();
	}

	protected function _copyAfter(DomNode $context, DomNode $source, $xpath)
	{
		$new = $source->cloneNode(true);
		$target = $this->findControl($context, $xpath);
		if($target->nextSibling)
			$target->parentNode->insertBefore
				( $new
				, $target->nextSibling
				);
		else
			$target->parentNode->appendChild($new);
		return $new;
	}

	public function copyAfter($xpath)
	{
		if(!$this->canCopyAfter($xpath))
			throw new NotAllowedException($this->node->nodeName.': '.__FUNCTION__);
		return new WritableXMLFSControl
			( $this->_copyAfter
				( $this->file->getNode()
				, $this->node
				, $xpath
				)
			, $this->file
			, $this->xpath
			);
	}

	public function canAppendControl($controlName)
	{
		$this->_appendControl($context = $this->node->cloneNode(true), $controlName);
		return $this->testIsValid($context);
	}

	protected function _appendControl(DomNode $target, $controlName)
	{
		$target->appendChild($new = $this->dom->createElementNS($this->namespace, $controlName));
		return $new;
	}

	public function appendControl($controlName)
	{
		if(!self::controlExists($controlName))
			throw new Exception('Control '.$controlName.' does not exist');
		if(!$this->canAppendControl($controlName))
			throw new NotAllowedException($this->node->nodeName.': '.__FUNCTION__);
		return new WritableXMLFSControl
			( $this->_appendControl
				( $this->node
				, $controlName
				)
			, $this->file
			, $this->xpath
			);
	}

	public function canInsertBefore($controlName)
	{
		$this->_insertBefore($context = $this->node->cloneNode(true), $controlName);
		return $this->testIsValid($context);
	}

	/**
	 *
	 * @todo doesn't work with a cloned node, because it hasn't got a parent node.
	 */
	public function _insertBefore(DomNode $target, $controlName)
	{
		$target->parentNode->insertBefore
			( $new = $this->dom->createElementNS
				( $this->namespace
				, $controlName
				)
			, $target
			);
		return $new;
	}

	public function insertBefore($controlName)
	{
		if(!self::controlExists($controlName))
			throw new Exception('Control '.$controlName.' does not exist');
		if(!$this->canInsertBefore($controlName))
			throw new NotAllowedException($controlName);
		return new WritableXMLFSControl
			( $this->_insertBefore
				( $this->node
				, $controlName
				)
			, $this->file
			, $this->xpath
			);
	}

	public function canInsertAfter($controlName)
	{
		return self::controlExists($controlName);
	}

	public function insertAfter($controlName)
	{
		if(!$this->canInsertAfter($controlName))
			throw new NotAllowedException($controlName);
		$new = $this->dom->createElementNS($this->namespace, $controlName);
		if($this->node->nextSibling)
			$this->node->parentNode->insertBefore
				( $new
				, $this->node->nextSibling
				);
		else
			$this->node->parentNode->appendChild($new);
		return new WritableXMLFSControl($new, $this->file, $this->xpath);
	}

	public function canReplace($xpath)
	{
		return true;
	}

	public function replace($xpath)
	{
		if(!$this->canReplace($xpath))
			throw new NotAllowedException($this->node->nodeName.': '.__FUNCTION__);
		$target = $this->findControl($xpath);
		$target->parentNode->replaceChild($this->node, $target);
		return $this;
	}

	public function availableSubControls()
	{
		//array_unshift($this->relevantControls, '#text'); // Prepend the value to the array
		return $this->relevantControls;
		//return self::getAllControls();
	}

	public function getOptions()
	{
		return array_keys($this->control->getDefaultOptions());
	}

	public function getOption($name)
	{
		return $this->node->getAttribute($name);
	}

	public function getDefaultOption($name)
	{
		$options = $this->control->getDefaultOptions();
		return $options[$name];
	}

	public function getOptionType($name)
	{
		return 'string';
	}

	public function canSetText($text)
	{
		return true;
	}

	public function setText($text)
	{
		if($this->canSetText($text))
			$this->clear()->getNode()->appendChild($this->dom->createTextNode($text));
	}

	public function canSetOption($name, $value)
	{
		return true;
	}

	public function setOption($name, $value)
	{
		if(!$this->canSetOption($name, $value))
			throw new NotAllowedException($name.': '.$value);
		if(in_array($name, $this->getOptions()) && $this->getOptionType($name) === $this->getValueType($value))
		{
			if($this->getDefaultOption($name) == $value)
				$this->removeAttribute($name);
			else
				$this->setAttribute($name, $value);
		}
		else
			throw new NotAllowedException('Option: '.$name);
		return $this;
	}

	public function canDelete()
	{
		return !in_array($this->getName(), $this->undeletableControls);
	}

	public function delete()
	{
		if(!$this->canDelete())
			throw new NotAllowedException('delete');
		$this->node->parentNode->removeChild($this->node);
		unset($this);
	}

	public function clear()
	{
		while($this->node->childNodes->length > 0)
			$this->node->removeChild($this->node->firstChild);
		return $this;
	}

	public function getNode()
	{
		return $this->node;
	}

	public function getControl()
	{
		return $this->control;
	}

	public function subControls()
	{
		$subControls = array();
		foreach($this->getList() as $child)
		{
			if($child->nodeType === XML_TEXT_NODE)
				$subControls[] = new WritableXMLFSControlText($child, $this->file, $this->xpath);
			else
				$subControls[] = new WritableXMLFSControl($child, $this->file, $this->xpath);
		}
		return $subControls;
	}

	public function getList()
	{
		$list = array();
		if($this->node->hasChildNodes())
			foreach($this->node->childNodes as $child)
				if(!in_array($child->nodeName, $this->excludeNodes))
					$list[] = $child;
		return $list;
	}

	public function getName()
	{
		return $this->node->nodeName;
	}

	public function createXPath()
	{
		$xpath = '';
		$ns = 'mocovi';
		foreach($this->getContext() as $current)
			$xpath .= '/'.$ns.':'.$current->name.'['.$current->position.']';
		return $xpath.'/'.$ns.':'.$this->getName().'['.self::getPosition($this->getNode()).']';
	}

	protected static function getPosition(DomNode $node)
	{
		$i = 1;
		foreach($node->parentNode->childNodes as $child)
		{
			if($child === $node)
				return $i;
			if($child->nodeName == $node->nodeName)
				$i++;
		}
	}

	protected function getContext()
	{
		$parent = $this->getNode();
		$context = array();
		while(($parent = $parent->parentNode) && $parent->nodeName != 'model')
			$context[] = (object)array('name' => $parent->nodeName, 'position' => self::getPosition($parent));
		return array_reverse($context);
	}

	protected function newControl(DomNode $new)
	{
		return new WritableXMLFSControl($new, $this->file, $this->xpath);
	}

	protected function findControl(DomNode $context, $xpath)
	{
		if($control = $this->xpath->query('mocovi:model'.(substr($xpath, 0, 1) != '/' ?'/' : '').$xpath, $context)->item(0))
			return $control;
		throw new Exception('Target control not found at Xpath: '.$xpath);
	}

	protected function getValueType($value)
	{
		return 'string';
	}

	protected function removeAttribute($name)
	{
		$this->node->removeAttribute($name);
	}

	protected function setAttribute($name, $value)
	{
		$this->node->setAttribute($name, $value);
	}

	protected function testIsValid(DomNode $context = null)
	{
		$validate = new MocoviRelaxNGValidator(($context ? $context : $this->test));
		return $validate->isValid();
	}
}

class WritableXMLFSControlText extends WritableXMLFSControl
{
	public function __construct(DomNode $node, WritableXMLFSFile $file, DomXpath $xpath)
	{
		$this->node = $node;
		$this->file = $file;
		$this->test = $file->getNode()->cloneNode(true);
		$this->dom = $node->ownerDocument;
		$this->xpath = $xpath;
		$this->namespace = Mocovi::$namespace;
	}

	public function canAppendControl($controlName)
	{
		return false;
	}

	public function availableSubControls()
	{
		return array();
	}

	public function getOptions()
	{
		return array_keys($this->control->getDefaultOptions());
	}

	public function canSetText($text)
	{
		return true;
	}

	public function setText($text)
	{
		if($this->canSetText($text))
			$this->clear()->getNode()->insertData(0, $text);
	}

	public function canSetOption($name, $value)
	{
		return false;
	}

	public function setOption($name, $value)
	{
		throw new NotAllowedException($this->node->nodeName.': '.__FUNCTION__);
	}

	public function clear()
	{
		$this->node->deleteData(0, strlen($this->node->wholeText));
		return $this;
	}

	public function subControls()
	{
		return array();
	}

	public function getList()
	{
		throw new NotAllowedException($this->node->nodeName.': '.__FUNCTION__);
	}

	public function createXPath()
	{
		return str_replace('mocovi:#text', 'text()', parent::createXPath());
	}

	protected function removeAttribute($name)
	{
		throw new NotAllowedException($this->node->nodeName.': '.__FUNCTION__);
	}

	protected function setAttribute($name, $value)
	{
		throw new NotAllowedException($this->node->nodeName.': '.__FUNCTION__);
	}
}



abstract class SchemaValidator
{
	abstract public function __construct(DomElement $start);
	abstract public function isValid();
}







//$x = new MocoviRelaxNGValidator(new DomElement('root'));
//$x->getValidSubControls('headline');








class MocoviRelaxNGValidator extends SchemaValidator
{
	protected $relaxNGNamespace = 'http://relaxng.org/ns/structure/1.0';
	protected $excludeFromAny = array
		( 'root'
		, 'xmlfs'
		, 'file'
		);
	protected $extension = '.rng';
	protected $template ='<?xml version="1.0" encoding="utf-8"?>
	<grammar xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
		<start>
			<!-- will be filled dynamically -->
		</start>
		<define name="any">
			<zeroOrMore>
				<choice>
					<text/>
					<!-- will be filled dynamically -->
				</choice>
			</zeroOrMore>
		</define>
	</grammar>';
	protected $relaxNG;
	protected $contextNode;
	protected $xpath;
	protected $start;
	protected $any;

	public function __construct(DomElement $start)
	{
		libxml_use_internal_errors(true);
		$this->relaxNG = new DomDocument();
		$this->relaxNG->loadXML($this->template);
		$this->xpath = new DomXpath($this->relaxNG);
		$this->xpath->registerNameSpace('rng', $this->relaxNGNamespace);
		$this->setStartElement($start->nodeName);
		$this->contextNode = $start;
		$this->setDefinitions();
	}

	public function isValid()
	{
		/*
		if(version_compare(LIBXML_DOTTED_VERSION, '2.7.6', '<')) // LibXML bug switch. Libxml < 2.7.6 produces an invalid XML even though it's correct!
			return true;
		$dom = new DomDocument('1.0', 'utf-8');
		$dom->appendChild($dom->importNode($this->contextNode, true));
		$return = $dom->relaxNGValidateSource($this->relaxNG->saveXML());
		if($errors = $this->errors())
		{
			//echo $this->relaxNG->saveXML();
			//echo $dom->saveXML();
			//print_r($errors);
		}
		return $return;
		*/
		return true;
	}

	public function getValidSubControls($controlName)
	{
		//echo $this->relaxNG->saveXML();
		$this->getRelaxNGElements
		(
			$this
				->xpath
				->query('//rng:element[@name="'.$controlName.'"]/*')
		);
	}

	protected function getRelaxNGElements(DomNodeList $list)
	{
		static $elements = array();
		foreach($list as $child)
			switch($child->nodeName)
			{
				case 'interleave':
				case 'choice':
					$this->getRelaxNGElements($child->childNodes);
				break;
				case 'element':
					$elements[] = $child->getAttribute('name');
			}
	}

	protected function setStartElement($name)
	{
		$this->start = $this->xpath->query('/rng:grammar/rng:start')->item(0);
		$this->start->appendChild($ref = $this->relaxNG->createElementNS($this->relaxNGNamespace, 'ref'));
		$ref->setAttribute('name', $name);
		$this->any = $this->xpath->query('/rng:grammar/rng:define[@name = "any"]/rng:zeroOrMore/rng:choice')->item(0);
	}

	protected function setDefinitions()
	{
		$anyElements = array();
		if(!is_dir($GLOBALS['definitions']))
			throw new Exception('Couldn\'t open definitions.');
		foreach(glob(preg_quote($GLOBALS['definitions'], DIRECTORY_SEPARATOR).'[^_\.]*\\'.$this->extension) as $file)
		{
			if(!in_array($basename = basename($file, $this->extension), $this->excludeFromAny))
			{
				$this->any->appendChild($ref = $this->relaxNG->createElementNS($this->relaxNGNamespace, 'ref'));
				$ref->setAttribute('name', $basename);
			}
			$tempDom = new DomDocument();
			$tempDom->load($file);
			$tempDom->preserveWhiteSpace = false;
			$this->start->parentNode->appendChild($this->relaxNG->importNode($tempDom->documentElement, true));
		}
	}

	protected function errors()
	{
		$errors = libxml_get_errors();
		libxml_clear_errors();
		return (count($errors) > 0 ? $errors : null);
	}
}
Return current item: Mocovie web framework