<?php

// Nested.php

include 'Nested/Exception.php';
include 'Nested/Child.php';
include 'Nested/Children.php';
include 'Nested/Child/Properties.php';


class Nested {

	/**
	 *
	 * @var DOMDocument
	 */
	protected $doc;

	/**
	 * Properties stack
	 * Each instance of Nested has its own array in this collection. The key of that array is the
	 * instance id
	 * @var array
	 */
	static public $propertiesCollection = array();

	/**
	 * The number of instances of Nested
	 * @var int
	 */
	static protected $instance_count = 0;

	/**
	 * The instance number. this is the key for the properties array of self::$propertiesCollection
	 * @var int
	 */
	protected $instance_id;

	/**
	 * If an element is added, when the parent element isn't already added, the element
	 * will stored (temporary) in this array
	 *
	 * @var array
	 */
	protected $noParentCollection = array();

	/**
	 * Create the DOMDocument object
	 */
	public function __construct(){
		$this->instance_id = ++self::$instance_count;
		self::$propertiesCollection[$this->instance_id] = array();
		$this->doc = new DOMDocument();
	}

	/**
	 *
	 * @param $id
	 * @param $parent_id
	 * @param $properties
	 * @return unknown_type
	 */
	public function addChild($id,$parent_id=0,$properties=array()){
		if(!(ctype_digit($id) || is_int($id)) || $id <= 0 || isset(self::$propertiesCollection[$this->instance_id][$id])){
			throw new Nested_Exception('The ID must be a unique positive integer');
		}

		// Properties
		self::$propertiesCollection[$this->instance_id][$id] = new Nested_Child_Properties($properties);

		// Create the element
		$newElmt = $this->doc->createElement('child');

		// Add ID attribute
		$idAttr = $this->doc->createAttribute('id');
		$idAttr->appendChild($this->doc->createTextNode('id'.$id));
		$newElmt->appendChild($idAttr);
		$newElmt->setIdAttribute('id',true);

		// Add the other attributes. These attributes can you use to create xPath queries
		foreach($properties as $key => $property){
			if($key != 'id'){
				$attr = $this->doc->createAttribute((string)$key);
				$attr->appendChild($this->doc->createTextNode((string)$property));
				$newElmt->appendChild($attr);
			}
		}

		// If there's a parent element found, append this new element to the parent element
		if($parent_id > 0){
			$parentElmt = (int)$parent_id > 0 ? $this->doc->getElementById('id'.$parent_id) : null;
			if($parentElmt instanceof DOMElement){
				$parentElmt->appendChild($newElmt);
			}else{
				// The parent is not found, save the element and wait till the parent element
				// gets added
				$this->noParentCollection[$parent_id] = $newElmt;
			}
		}else{
			$this->doc->appendChild($newElmt);
		}

		// There is a child element of this element who is already added
		if(isset($this->noParentCollection[$id])){
			$newElmt->appendChild($this->noParentCollection[$id]);
			unset($this->noParentCollection[$id]);
		}
	}

	/**
	 *
	 * @param int $id
	 * @return Nested_Child
	 */
	public function getElement($id){
		$elmt = $this->doc->getElementById('id'.$id);
		if($elmt instanceof DOMElement){
			return new Nested_Child($elmt,$this->instance_id);
		}
		throw new Nested_Exception('This element does not exists');
	}

	/**
	 * Get the elements at the 'root' level
	 * @return Nested_Children
	 */
	public function getElements(){
		$childElmts = $this->doc->childNodes;
		$children = new Nested_Children();
		foreach($childElmts as $elmt){
			$children->addChild(new Nested_Child($elmt,$this->instance_id));
		}
		return $children;
	}

	/**
	 * You can run xPath queries too to select child elements
	 * @param string $query
	 * @return unknown_type
	 */
	public function xPath($query){
		$xpath = new DOMXPath($this->doc);
		$result = $xpath->evaluate($query);
		if($result instanceof DOMNodeList){
			$children = new Nested_Children();
			foreach($result as $elmt){
				if($elmt instanceof DOMElement){
					$children->addChild(new Nested_Child($elmt,$this->instance_id));
				}
			}
			return $children;
		}
		return $result;
	}

	/**
	 * Shows the hierarchy in XML
	 * @param int $id If you want to view the hierarchy of an element
	 * @return void
	 */
	public function debug($id=null){
		$this->doc->formatOutput = true;

		$node = null;
		if(!empty($id)){
			$node = $this->getElement($id)->getDOMElement();
		}
		echo '<pre>'.htmlentities($this->doc->saveXML($node),true).'</pre>';
	}

	/**
	 * Clear the memory
	 * @return void
	 */
	public function __destruct(){
		unset($this->doc);
	}

}



// Nested/Child.php

class Awf_Nested_Child {

	/**
	 *
	 * @var DOMElement
	 */
	protected $elmt;

	/**
	 *
	 * @var int
	 */
	protected $id;

	protected $nested_instance_id;

	/**
	 *
	 * @param DOMElement $elmt
	 */
	public function __construct(DOMElement $elmt,$nested_instance_id){
		$this->elmt = $elmt;
		$this->nested_instance_id = $nested_instance_id;
	}

	/**
	 * Get the element ID
	 * @return int
	 */
	public function getId(){
		if(empty($this->id)){
			$this->id = (int) substr($this->elmt->getAttribute('id'),2);
		}
		return $this->id;
	}

	/**
	 * Get the parent element
	 * @return Awf_Nested_Child
	 */
	public function getParent(){
		$parent = $this->elmt->parentNode;
		if($parent instanceof DOMElement){
			return new Awf_Nested_Child($parent,$this->nested_instance_id);
		}
		throw new Awf_Nested_Exception('This parent element does not exists');
	}

	/**
	 * Get the element properties
	 * @return Awf_Nested_Child_Properties
	 */
	public function getProperties(){
		$properties = isset(Awf_Nested::$propertiesCollection[$this->nested_instance_id][$this->getId()]) ?
			Awf_Nested::$propertiesCollection[$this->nested_instance_id][$this->getId()] :
			new Awf_Nested_Child_Properties(array());
		return $properties;
	}

	/**
	 * Check if the element has child nodes
	 * @return bool
	 */
	public function hasChildren(){
		return $this->elmt->hasChildNodes();
	}

	/**
	 * Get the element children
	 * @return Awf_Nested_Children
	 */
	public function getChildren(){
		$childElmts = $this->elmt->childNodes;
		$children = new Awf_Nested_Children();
		foreach($childElmts as $elmt){
			$children->addChild(new Awf_Nested_Child($elmt,$this->nested_instance_id));
		}
		return $children;
	}

	/**
	 *
	 * @return DOMElement
	 */
	public function getDOMElement(){
		return $this->elmt;
	}
}


// Nested/Children.php


class Nested_Children implements Iterator, Countable {

	/**
	 * The children
	 *
	 * @var array
	 */
	protected $children = array();

	/**
	 * Add a child
	 *
	 * @param Nested_Child $child
	 */
	public function addChild(Nested_Child $child){
		$this->children[$child->getId()] = $child;
	}

	/**
	 * Get a child
	 *
	 * @param string $key The ID of the child
	 * @return Nested_Child
	 */
	public function __get($key){
		if(isset($this->children[$key])){
			return $this->children[$key];
		}
		return false;
	}

	/**
	 * If the Child is already added to the children
	 *
	 * @param string $key The ID of the child
	 * @return bool true if the child isset, otherwise false
	 */
	public function __isset($key){
		return isset($this->children[$key]);
	}

	/**
	 * Count the children
	 *
	 * @return int
	 */
	public function count(){
		return count($this->children);
	}


	/**
	 *
	 * @return bool
	 */
	public function rewind() {
		reset($this->children);
	}

	/**
	 *
	 * @return Nested_Child
	 */
	public function current() {
		return current($this->children);
	}

	/**
	* @var string|int
	 */
	public function key() {
		return key($this->children);
    }

	/**
	 *
	 * @return Nested_Child|false
	 */
    public function next() {
		return next($this->children);
	}

	/**
	 *
	 * @return bool
	 */
	public function valid() {
		return $this->current() !== false;
	}
}


// Nested/Child/Properties.php


class Nested_Child_Properties implements Iterator, Countable {

	/**
	 * The properties of a element
	 *
	 * @var array
	 */
	protected $properties = array();

	/**
	 * Create a properties instance
	 *
	 * @param array $properties
	 */
	public function __construct(array $properties){
		$this->properties = $properties;
	}

	/**
	 * Return a property
	 *
	 * @param string $key Property Key
	 * @return mixed The property value, if the property does not exists, it returns false
	 */
	public function __get($key){
		if(isset($this->properties[$key])){
			return $this->properties[$key];
		}
		return false;
	}

	/**
	 * Set a property
	 *
	 * @param string $key
	 * @param mixed $value
	 */
	public function __set($key,$value){
		$this->properties[$key] = $value;
	}

	/**
	 * Checks if the property isset
	 *
	 * @param string $key
	 * @return bool True if the property isset, otherwise false
	 */
	public function __isset($key){
		return isset($this->properties[(string)$key]);
	}

	public function count(){
		return count($this->properties);
	}

	/**
	 * Make the properties iteratable
	 */

	public function rewind() {
		reset($this->properties);
	}

	public function current() {
		return current($this->properties);
	}

	public function key() {
		return key($this->properties);
	}

	public function next() {
		return next($this->properties);
	}

	public function valid() {
		return $this->properties() !== false;
	}

}

// Nested/Exception.php

class Nested_Exception extends Exception {
		
}


// Voorbeeld

$i = 1;

$nested = new Nested();

// Elementen toevoegen
$nested->addChild($i,0,array('id'=>$i++,'naam'=>'bar','tekst'=>'foo'));
$nested->addChild($i,0,array('id'=>$i++,'naam'=>'bar','tekst'=>'foo'));

// Zoals je ziet, heeft dit element de parent met id=5. Deze is helaas nog 
//niet toegevoegd. Daarom zal dit element even tijdelijk worden opgeslagen 
//en wordt alsnog toegevoegd als child van id=5 als het element met id=5 
//toegevoegd wordt.
$nested->addChild($i,5,array('id'=>$i++,'naam'=>'bar','tekst'=>'foo'));

$nested->addChild($i,1,array('id'=>$i++,'naam'=>'bar','tekst'=>'foo'));

// Nu wordt dus het element met ID=3 toegevoegd aan dit element
$nested->addChild($i,2,array('id'=>$i++,'naam'=>'bar','tekst'=>'foo'));
$nested->addChild($i,2,array('id'=>$i++,'naam'=>'bar','tekst'=>'foo'));

$children = $nested->getElement(1)->getChildren();
foreach($children as $child){
	echo $child->getProperties()->tekst.'<br />';
}

echo $nested->getElement(4)->getParent()->getProperties()->naam;

$nested->debug();

?>

