Skip to content

Instantly share code, notes, and snippets.

@dtalley
Created December 21, 2011 01:39
Show Gist options
  • Select an option

  • Save dtalley/1504149 to your computer and use it in GitHub Desktop.

Select an option

Save dtalley/1504149 to your computer and use it in GitHub Desktop.

Revisions

  1. Orionark revised this gist Dec 21, 2011. 1 changed file with 21 additions and 2 deletions.
    23 changes: 21 additions & 2 deletions datatree.php
    Original file line number Diff line number Diff line change
    @@ -181,12 +181,31 @@ public function delete( $node ) {
    }
    if( strlen( $path ) == 0 ) {
    if( isset( $this->_elements[$name] ) ) {
    unset( $this->_elements[$name] );
    $elements = $this->elements( $name, $filters );
    foreach( $elements as $element ) {
    array_splice(
    $this->_elements[$name],
    array_search(
    $element, $this->_elements[$name]
    ),
    1
    );
    }
    if( count( $this->_elements[$name] ) == 0 ) {
    unset( $this->_elements[$name] );
    }
    }
    if( isset( $this->_values[$name] ) ) {
    unset( $this->_values[$name] );
    }
    return;
    return $this;
    } else {
    $elements = $this->elements( $name, $filters );
    $total = count( $elements );
    if( $total > 0 ) {
    return $elements[$total-1]->delete( $path );
    }
    return $this;
    }
    }
    }
  2. Orionark revised this gist Dec 21, 2011. 1 changed file with 118 additions and 24 deletions.
    142 changes: 118 additions & 24 deletions datatree.php
    Original file line number Diff line number Diff line change
    @@ -22,6 +22,9 @@ function __construct( $parent = NULL, $level = 0 ) {
    function __destruct() {}

    protected function setParent( $parent ) {
    if( $this->_parent !== NULL ) {
    $this->_parent->delete( $this );
    }
    $this->_parent = $parent;
    }

    @@ -44,10 +47,13 @@ public function start( $path ) {
    return NULL;
    }
    //If $path is empty, we've arrived at our destination
    if( strlen( $path ) === 0 ) {
    if( strlen( $path ) == 0 ) {
    return $this;
    }
    $this->parse( $path, $name, $filters );
    if( $name == ".." ) {
    return $this->_parent->start( $path );
    }
    //If the requested element doesn't exist, create it
    if( !isset( $this->_elements[$name] ) ) {
    $this->_elements[$name] = array();
    @@ -103,7 +109,7 @@ public function store( $name, $value ) {
    */
    if(
    is_object( $value ) &&
    get_class( $value ) === get_class( $this )
    get_class( $value ) == get_class( $this )
    ) {
    if( !isset( $this->_elements[$name] ) ) {
    $this->_elements[$name] = array();
    @@ -125,6 +131,66 @@ public function store( $name, $value ) {
    return $this;
    }

    /**
    * Delete a valid path or direct descendant
    * from this tree.
    */
    public function delete( $node ) {
    /**
    * If the provided argument is a
    * tree object, we know it's a direct
    * descendant of this tree, so
    * delete it from the elements array.
    */
    if(
    is_object( $node ) &&
    get_class( $node ) == get_class( $this )
    ) {
    foreach(
    $this->_elements as $name => $elements
    ) {
    $total = count( $this->_elements[$name] );
    for( $i = 0; $i < $total; $i++ ) {
    $element = $this->_elements[$name][$i];
    if( $element == $node ) {
    array_splice(
    $this->_elements[$name],
    $i,
    1
    );
    /**
    * If the elements array that the
    * provided object belongs to is now
    * empty, delete it entirely.
    */
    if( $total == 1 ) {
    unset( $this->_elements[$name] );
    }
    return $this;
    }
    }
    }
    } else {
    $path =& $node;
    $this->parse( $path, $name, $filters );
    if( $name == ".." ) {
    if( $this->_parent === NULL ) {
    return NULL;
    }
    return $this->_parent->delete( $path );
    }
    if( strlen( $path ) == 0 ) {
    if( isset( $this->_elements[$name] ) ) {
    unset( $this->_elements[$name] );
    }
    if( isset( $this->_values[$name] ) ) {
    unset( $this->_values[$name] );
    }
    return;
    }
    }
    }

    /**
    * Get a value or tree object from within
    * this tree, as specified by a path.
    @@ -136,6 +202,12 @@ public function get( $path ) {
    return $this;
    }
    $this->parse( $path, $name, $filters );
    if( $name == ".." ) {
    if( $this->_parent === NULL ) {
    return NULL;
    }
    return $this->_parent->get( $path );
    }
    /**
    * First try to follow the requested path
    * via an element.
    @@ -165,14 +237,22 @@ public function count( $path ) {
    return 0;
    }
    $this->parse( $path, $name, $filters );
    if( $name == ".." ) {
    if( $this->_parent === NULL ) {
    return 0;
    }
    return $this->_parent->count( $path );
    }
    if( isset( $this->_elements[$name] ) ) {
    $elements = $this->elements( $name, $filters );
    $total = count( $elements );
    //If we're at our destination node, return the count
    if( strlen( $path ) == 0 ) {
    return $total;
    }
    return $elements[$total-1]->count( $path );
    if( $total > 0 ) {
    return $elements[$total-1]->count( $path );
    }
    } else if( isset( $this->_values[$name] ) ) {
    //If no element exists, but a value exists
    return 1;
    @@ -284,8 +364,8 @@ protected function match( array $filters ) {
    if( is_array( $filter ) ) {
    $test[] = $this->match( $filter );
    } else if(
    $filter === "and" ||
    $filter === "or"
    $filter == "and" ||
    $filter == "or"
    ) {
    $operators[] = $filter;
    } else {
    @@ -295,9 +375,9 @@ protected function match( array $filters ) {
    $total = count( $test );
    $result = $test[0];
    for( $i = 1; $i < $total; $i++ ) {
    if( $operators[$i-1] === "and" ) {
    if( $operators[$i-1] == "and" ) {
    $result = $result && $test[$i];
    } else if( $operators[$i-1] === "or" ) {
    } else if( $operators[$i-1] == "or" ) {
    $result = $result || $test[$i];
    }
    }
    @@ -350,7 +430,11 @@ protected function test( $condition ) {
    "[!!]",
    $condition
    );
    $string = str_replace( $random, "'", $string );
    $string = str_replace(
    $random,
    "'",
    $string
    );
    }
    //Check for a valid operator
    preg_match(
    @@ -401,43 +485,43 @@ protected function test( $condition ) {
    }
    }
    if(
    $operator === "=" &&
    $operator == "=" &&
    $test == $value
    ) {
    return true;
    } else if(
    $operator === ">" &&
    $operator == ">" &&
    $test > $value
    ) {
    return true;
    } else if(
    $operator === "<" &&
    $operator == "<" &&
    $test < $value
    ) {
    return true;
    } else if(
    $operator === ">=" &&
    $operator == ">=" &&
    $test >= $value
    ) {
    return true;
    } else if(
    $operator === "<=" &&
    $operator == "<=" &&
    $test <= $value
    ) {
    return true;
    }
    return false;
    } else {
    if( isset( $this->_elements[$name] ) ) {
    $elements = $this->elements( $name, $filters );
    $total = count( $elements );
    $elements = $this->elements( $name, $filters );
    $total = count( $elements );
    if( $total > 0 ) {
    if( $string ) {
    $value = "'" . $value . "'";
    }
    return $elements[$total-1]->test(
    $path . " " . $operator . " " . $value
    );
    }
    }
    }
    return false;
    }
    @@ -448,6 +532,12 @@ protected function test( $condition ) {
    * provided set of filters
    */
    private function elements( $name, $filters ) {
    if( $name == ".." ) {
    if( $this->_parent === NULL ) {
    return array();
    }
    return array($this->_parent);
    }
    if( !isset( $this->_elements[$name] ) ) {
    return array();
    }
    @@ -506,7 +596,7 @@ private function parse(
    * beginning of the path string. They are not
    * needed.
    */
    while( substr( $path, 0, 1 ) === "/" ) {
    while( substr( $path, 0, 1 ) == "/" ) {
    $path = substr( $path, 1 );
    }
    /**
    @@ -549,7 +639,7 @@ private function parse(
    * If the remaining path begins with a [ character,
    * then we have a filter to deal with.
    */
    if( substr( $path, 0, 1 ) === "[" ) {
    if( substr( $path, 0, 1 ) == "[" ) {
    /**
    * We match the content at the beginning of the
    * remaining path that's between a [ and a ],
    @@ -560,21 +650,25 @@ private function parse(
    $path,
    $matches
    );
    $data = isset( $matches[1] ) ? $matches[1] : false;
    $data = (
    isset( $matches[1] ) ?
    $matches[1] :
    false
    );
    /**
    * If the filter data isn't found, something is
    * wrong with the path string.
    */
    if( $data === false ) {
    throw new Exception( "Malformed filter string." );
    throw new Exception(
    "Malformed filter string."
    );
    }
    }
    $filters = array();
    if( strlen( $data ) > 0 ) {
    //Escape the filter data so it'll play nice with regexp
    $replace = str_replace( "(", "\(", $data );
    $replace = str_replace( ")", "\)", $replace );
    $replace = str_replace( "/", "\/", $replace );
    $replace = preg_quote( $data );
    $path = preg_replace(
    "/\[" . $replace . "\]/",
    "",
  3. Orionark revised this gist Dec 21, 2011. 1 changed file with 172 additions and 46 deletions.
    218 changes: 172 additions & 46 deletions datatree.php
    Original file line number Diff line number Diff line change
    @@ -12,16 +12,26 @@ class DataTree {
    private $_elements = array();
    private $_values = array();

    private $_parent = NULL;
    private $_level = 0;

    function __construct( $level = 0 ) {
    $_level = $level;
    function __construct( $parent = NULL, $level = 0 ) {
    $this->_parent = $parent;
    $this->_level = $level;
    }
    function __destruct() {}

    protected function setParent( $parent ) {
    $this->_parent = $parent;
    }

    protected function setLevel( $level ) {
    $this->_level = $level;
    }

    /**
    * Create a valid path through this
    * DataTree object. If the path doesn't
    * object. If the path doesn't
    * already exist, create it. If the
    * destination node already exists, fork
    * the tree at that point and create a new
    @@ -62,10 +72,16 @@ public function start( $path ) {
    * to create it.
    */
    if(
    ( strlen( $path ) == 0 && $total_filters == 0 ) ||
    (
    strlen( $path ) == 0 &&
    $total_filters == 0
    ) ||
    count( $this->_elements[$name] ) == 0
    ) {
    $this->_elements[$name][] = new DataTree( $this->_level+1 );
    $this->_elements[$name][] = new DataTree(
    $this,
    $this->_level+1
    );
    }
    $elements = $this->elements( $name, $filters );
    $total = count( $elements );
    @@ -78,7 +94,7 @@ public function start( $path ) {
    public function store( $name, $value ) {
    //We can't store a value into nothingness
    if( !is_string( $name ) || !$name ) {
    return false;
    return $this;
    }
    /**
    * If the provided value is an instance of this
    @@ -93,18 +109,20 @@ public function store( $name, $value ) {
    $this->_elements[$name] = array();
    }
    $this->_elements[$name][] = $value;
    $value->setParent( $this );
    $value->setLevel( $this->_level+1 );
    } else {
    /**
    * If an element exists with this name,
    * this value would be inaccessible anyway,
    * so don't perform the store.
    */
    if( isset( $this->_elements[$name] ) ) {
    return false;
    return $this;
    }
    $this->_values[$name] = $value;
    }
    return true;
    return $this;
    }

    /**
    @@ -194,11 +212,16 @@ protected function save_json( $as_string = true ) {
    * the current name, don't return it
    * as an array.
    */
    if( $total == 1 ) {
    $return[$name] = $this->_elements[$name][0]->save_json( false );
    } else {
    for( $i = 0; $i < $total; $i++ ) {
    $return[$name][] = $this->_elements[$name][$i]->save_json( false );
    for( $i = 0; $i < $total; $i++ ) {
    $element = $this->_elements[$name][$i];
    if( $total == 1 ) {
    $return[$name] = (
    $element->save_json( false )
    );
    } else {
    $return[$name][] = (
    $element->save_json( false )
    );
    }
    }
    }
    @@ -229,8 +252,9 @@ protected function save_xml() {
    foreach( $this->_elements as $name => $tree ) {
    $total = count( $this->_elements[$name] );
    for( $i = 0; $i < $total; $i++ ) {
    $element = $this->_elements[$name][$i];
    $return .= "<" . $name . ">";
    $return .= $this->_elements[$name][$i]->save_xml();
    $return .= $element->save_xml();
    $return .= "</" . $name . ">";
    }
    }
    @@ -296,10 +320,22 @@ protected function test( $condition ) {
    $random .= rand(0,9) . "";
    }
    //This is to protect against escaped single quotes
    $condition = str_replace( "\\'", $random, $condition );
    $condition = str_replace(
    "\\'",
    $random,
    $condition
    );
    //Check if the value is enclosed by single quotes
    preg_match( "/'([^']*?)'/", $condition, $strings );
    $string = isset( $strings[1] ) ? $strings[1] : false;
    preg_match(
    "/'([^']*?)'/",
    $condition,
    $strings
    );
    $string = (
    isset( $strings[1] ) ?
    $strings[1] :
    false
    );
    /**
    * If it is, store the value inside the
    * quotes and replace it with a token
    @@ -309,19 +345,39 @@ protected function test( $condition ) {
    * in a bit.
    */
    if( $string !== false ) {
    $condition = str_replace( "'" . $string . "'", "[!!]", $condition );
    $condition = str_replace(
    "'" . $string . "'",
    "[!!]",
    $condition
    );
    $string = str_replace( $random, "'", $string );
    }
    //Check for a valid operator
    preg_match( "/\s*(=|>|<|<=|>=)\s*/", $condition, $operators );
    $operator = isset( $operators[1] ) ? trim( $operators[1] ) : false;
    //If a filter doesn't have an operator, what good is it?
    preg_match(
    "/\s*(=|>|<|<=|>=)\s*/",
    $condition,
    $operators
    );
    $operator = (
    isset( $operators[1] ) ?
    trim( $operators[1] ) :
    false
    );
    /**
    * If a filter doesn't have an operator in it,
    * it's not a properly formatted filter.
    */
    if( $operator === false ) {
    throw new Exception(
    "No operator found in filter '" . $condition . "'."
    "No operator found in filter '" .
    $condition .
    "'."
    );
    }
    $split = preg_split( "/\s*(=|>|<|<=|>=)\s*/", $condition );
    $split = preg_split(
    "/\s*(=|>|<|<=|>=)\s*/",
    $condition
    );
    $path = $split[0];
    $this->parse( $path, $name, $filters );
    $value = $split[1];
    @@ -344,15 +400,30 @@ protected function test( $condition ) {
    $value = (int)$value;
    }
    }
    if( $operator === "=" && $test == $value ) {
    if(
    $operator === "=" &&
    $test == $value
    ) {
    return true;
    } else if( $operator === ">" && $test > $value ) {
    } else if(
    $operator === ">" &&
    $test > $value
    ) {
    return true;
    } else if( $operator === "<" && $test < $value ) {
    } else if(
    $operator === "<" &&
    $test < $value
    ) {
    return true;
    } else if( $operator === ">=" && $test >= $value ) {
    } else if(
    $operator === ">=" &&
    $test >= $value
    ) {
    return true;
    } else if( $operator === "<=" && $test <= $value ) {
    } else if(
    $operator === "<=" &&
    $test <= $value
    ) {
    return true;
    }
    return false;
    @@ -371,6 +442,11 @@ protected function test( $condition ) {
    return false;
    }

    /**
    * Return an array of elements
    * that match the given name and
    * provided set of filters
    */
    private function elements( $name, $filters ) {
    if( !isset( $this->_elements[$name] ) ) {
    return array();
    @@ -379,18 +455,29 @@ private function elements( $name, $filters ) {
    if( count( $filters ) > 0 ) {
    if( count( $filters ) == 1 ) {
    //If the filter is just a number, it's an index
    if( !preg_match( "/[^0-9]/", $filters[0] ) ) {
    if(
    !preg_match(
    "/[^0-9]/",
    $filters[0]
    )
    ) {
    $index = (int)$filters[0];
    if( isset( $this->_elements[$name][$index] ) ) {
    return array( $this->_elements[$name][$index] );
    if(
    isset( $this->_elements[$name][$index] )
    ) {
    return array(
    $this->_elements[$name][$index]
    );
    }
    return NULL;
    }
    }
    $return = array();
    //Loop through and check all the filters
    for( $i = 0; $i < $total; $i++ ) {
    if( $this->_elements[$name][$i]->match( $filters ) ) {
    $return[] = $this->_elements[$name][$i];
    $element = $this->_elements[$name][$i];
    if( $element->match( $filters ) ) {
    $return[] = $element;
    }
    }
    return $return;
    @@ -409,7 +496,11 @@ private function elements( $name, $filters ) {
    * @param $filters An array of filters that's
    * pulled from the end of the immediate name
    */
    private function parse( &$path, &$name, &$filters ) {
    private function parse(
    &$path,
    &$name,
    &$filters
    ) {
    /**
    * Trim any excess forward slashes from the
    * beginning of the path string. They are not
    @@ -419,22 +510,40 @@ private function parse( &$path, &$name, &$filters ) {
    $path = substr( $path, 1 );
    }
    /**
    * Match any string with letters, numbers, *, _ and
    * - at the beginning of the path string. This
    * is the immediate name.
    * Match any string with letters,
    * numbers, *, _ and - at the beginning
    * of the path string. This is the
    * immediate name.
    */
    preg_match(
    "/^([a-zA-Z0-9*_\-]+)/",
    $path,
    $matches
    );
    $name = isset( $matches[1] ) ? $matches[1] : false;
    //If the name wasn't found, something is wrong with the path
    $name = (
    isset( $matches[1] ) ?
    $matches[1] :
    false
    );
    /**
    * If the name wasn't found, something
    * is wrong with the path.
    */
    if( $name === false ) {
    throw new Exception( "Malformed path." );
    throw new Exception(
    "Malformed path."
    );
    }
    //Remove the extracted immediate name from the path string
    $path = preg_replace( "/" . $name . "/", "", $path, 1 );
    /**
    * Remove the extracted immediate name
    * from the path string.
    */
    $path = preg_replace(
    "/" . $name . "/",
    "",
    $path,
    1
    );
    $data = "";
    /**
    * If the remaining path begins with a [ character,
    @@ -466,9 +575,18 @@ private function parse( &$path, &$name, &$filters ) {
    $replace = str_replace( "(", "\(", $data );
    $replace = str_replace( ")", "\)", $replace );
    $replace = str_replace( "/", "\/", $replace );
    $path = preg_replace( "/\[" . $replace . "\]/", "", $path, 1 );
    $path = preg_replace(
    "/\[" . $replace . "\]/",
    "",
    $path,
    1
    );

    preg_match_all( "/(\s+and|or\s+)/i", $data, $matches );
    preg_match_all(
    "/(\s+and|or\s+)/i",
    $data,
    $matches
    );
    $split = preg_split( "/\s+and|or\s+/i", $data );
    $total = count( $split );
    $stack = array();
    @@ -485,7 +603,11 @@ private function parse( &$path, &$name, &$filters ) {
    }
    $use = $condition;
    if( $close_count > $open_count ) {
    for( $i = 0; $i < $close_count - $open_count; $i++ ) {
    for(
    $i = 0;
    $i < $close_count - $open_count;
    $i++
    ) {
    $use = substr( $use, 0, strlen( $use ) - 1 );
    }
    }
    @@ -497,7 +619,11 @@ private function parse( &$path, &$name, &$filters ) {
    $new = array_pop( $stack );
    $new[] = $filters;
    $filters = $new;
    $condition = substr( $condition, 0, strlen( $condition ) - 1 );
    $condition = substr(
    $condition,
    0,
    strlen( $condition ) - 1
    );
    $level--;
    }
    if( $i < $total - 1 ) {
  4. Orionark revised this gist Dec 21, 2011. 1 changed file with 87 additions and 0 deletions.
    87 changes: 87 additions & 0 deletions datatree.php
    Original file line number Diff line number Diff line change
    @@ -162,6 +162,93 @@ public function count( $path ) {
    return 0;
    }

    /**
    * Save out this tree's contents in one
    * of the several supported formats.
    */
    public function save( $format ) {
    //JavaScript Object Notation
    if( $format == "json" ) {
    return $this->save_json();
    //eXtensible Markup Language
    } else if( $format == "xml" ) {
    return $this->save_xml();
    }
    return "";
    }

    /**
    * Save this tree's contents in JSON
    * (JavaScript Object Notation) format.
    */
    protected function save_json( $as_string = true ) {
    $return = array();
    //Loop through and insert each element
    foreach( $this->_elements as $name => $tree ) {
    if( !isset( $return[$name] ) ) {
    $return[$name] = array();
    }
    $total = count( $this->_elements[$name] );
    /**
    * If there's only one element for
    * the current name, don't return it
    * as an array.
    */
    if( $total == 1 ) {
    $return[$name] = $this->_elements[$name][0]->save_json( false );
    } else {
    for( $i = 0; $i < $total; $i++ ) {
    $return[$name][] = $this->_elements[$name][$i]->save_json( false );
    }
    }
    }
    //Loop through and insert each value
    foreach( $this->_values as $name => $value ) {
    /**
    * Don't return this value if an
    * element with the same name exists.
    */
    if( !isset( $return[$name] ) ) {
    $return[$name] = $value;
    }
    }

    if( !$as_string ) {
    return $return;
    }
    return json_encode( $return );
    }

    /**
    * Save this tree's contents in XML
    * eXtensible Markup Language format.
    */
    protected function save_xml() {
    $return = "";
    //Loop through and insert each element
    foreach( $this->_elements as $name => $tree ) {
    $total = count( $this->_elements[$name] );
    for( $i = 0; $i < $total; $i++ ) {
    $return .= "<" . $name . ">";
    $return .= $this->_elements[$name][$i]->save_xml();
    $return .= "</" . $name . ">";
    }
    }
    //Loop through and insert each value
    foreach( $this->_values as $name => $value ) {
    /**
    * Don't return this value if an
    * element with the same name exists.
    */
    if( !isset( $this->_elements[$name] ) ) {
    $return .= "<" . $name . ">";
    $return .= $value;
    $return .= "</" . $name . ">";
    }
    }
    return $return;
    }

    /**
    * Test this tree against the provided set of
    * filters.
  5. Orionark revised this gist Dec 21, 2011. 1 changed file with 5 additions and 1 deletion.
    6 changes: 5 additions & 1 deletion datatree.php
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,5 @@
    <?php

    /**
    * Useful for keeping track of a tree-like
    * collection of data, for templates, API
    @@ -418,4 +420,6 @@ private function parse( &$path, &$name, &$filters ) {
    }
    } //end parse

    }
    }

    ?>
  6. Orionark created this gist Dec 21, 2011.
    421 changes: 421 additions & 0 deletions datatree.php
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,421 @@
    /**
    * Useful for keeping track of a tree-like
    * collection of data, for templates, API
    * returns, and accessing it with simple,
    * powerful paths, much like a file structure
    * or XPath.
    */
    class DataTree {

    private $_elements = array();
    private $_values = array();

    private $_level = 0;

    function __construct( $level = 0 ) {
    $_level = $level;
    }
    function __destruct() {}

    /**
    * Create a valid path through this
    * DataTree object. If the path doesn't
    * already exist, create it. If the
    * destination node already exists, fork
    * the tree at that point and create a new
    * path.
    * @param $path The path string to create
    */
    public function start( $path ) {
    //If $path is not a string, somebody screwed up
    if( !is_string( $path ) ) {
    return NULL;
    }
    //If $path is empty, we've arrived at our destination
    if( strlen( $path ) === 0 ) {
    return $this;
    }
    $this->parse( $path, $name, $filters );
    //If the requested element doesn't exist, create it
    if( !isset( $this->_elements[$name] ) ) {
    $this->_elements[$name] = array();
    }
    /**
    * If there's already a value with the specified
    * name, delete it, because it will be
    * inaccessible anyway.
    */
    if( isset( $this->_values[$name] ) ) {
    trigger_error(
    "Overwrote data value with data element.",
    E_USER_WARNING
    );
    unset( $this->_values[$name] );
    }
    /**
    * If we're at our destination, we have to fork
    * the tree and create a new path. Otherwise,
    * if we're not at our destination but the
    * requested path doesn't exist, fork the tree
    * to create it.
    */
    if(
    ( strlen( $path ) == 0 && $total_filters == 0 ) ||
    count( $this->_elements[$name] ) == 0
    ) {
    $this->_elements[$name][] = new DataTree( $this->_level+1 );
    }
    $elements = $this->elements( $name, $filters );
    $total = count( $elements );
    return $elements[$total-1]->start( $path );
    }

    /**
    * Store a value by name within this tree.
    */
    public function store( $name, $value ) {
    //We can't store a value into nothingness
    if( !is_string( $name ) || !$name ) {
    return false;
    }
    /**
    * If the provided value is an instance of this
    * class, we should store it as an element
    * rather than as a value.
    */
    if(
    is_object( $value ) &&
    get_class( $value ) === get_class( $this )
    ) {
    if( !isset( $this->_elements[$name] ) ) {
    $this->_elements[$name] = array();
    }
    $this->_elements[$name][] = $value;
    } else {
    /**
    * If an element exists with this name,
    * this value would be inaccessible anyway,
    * so don't perform the store.
    */
    if( isset( $this->_elements[$name] ) ) {
    return false;
    }
    $this->_values[$name] = $value;
    }
    return true;
    }

    /**
    * Get a value or tree object from within
    * this tree, as specified by a path.
    * @param $path The path to search for
    */
    public function get( $path ) {
    //If $path is empty, we're at our destination
    if( strlen( $path ) == 0 ) {
    return $this;
    }
    $this->parse( $path, $name, $filters );
    /**
    * First try to follow the requested path
    * via an element.
    */
    if( isset( $this->_elements[$name] ) ) {
    $elements = $this->elements( $name, $filters );
    $total = count( $elements );
    if( $total > 0 ) {
    return $elements[$total-1]->get( $path );
    }
    } else if( isset( $this->_values[$name] ) ) {
    return $this->_values[$name];
    }
    return NULL;
    }

    /**
    * Count the number of elements that are
    * found at a specific path and return
    * that number.
    * @param $path The path to search for
    * @return int The number of elements
    * at the requested path destination
    */
    public function count( $path ) {
    if( strlen( $path ) == 0 ) {
    return 0;
    }
    $this->parse( $path, $name, $filters );
    if( isset( $this->_elements[$name] ) ) {
    $elements = $this->elements( $name, $filters );
    $total = count( $elements );
    //If we're at our destination node, return the count
    if( strlen( $path ) == 0 ) {
    return $total;
    }
    return $elements[$total-1]->count( $path );
    } else if( isset( $this->_values[$name] ) ) {
    //If no element exists, but a value exists
    return 1;
    }
    return 0;
    }

    /**
    * Test this tree against the provided set of
    * filters.
    */
    protected function match( array $filters ) {
    $test = array();
    $operators = array();
    foreach( $filters as $filter ) {
    if( is_array( $filter ) ) {
    $test[] = $this->match( $filter );
    } else if(
    $filter === "and" ||
    $filter === "or"
    ) {
    $operators[] = $filter;
    } else {
    $test[] = $this->test( $filter );
    }
    }
    $total = count( $test );
    $result = $test[0];
    for( $i = 1; $i < $total; $i++ ) {
    if( $operators[$i-1] === "and" ) {
    $result = $result && $test[$i];
    } else if( $operators[$i-1] === "or" ) {
    $result = $result || $test[$i];
    }
    }
    return $result;
    }

    /**
    * Test a condition against this tree.
    * A condition is anything in the form:
    * path|operator|value, without the pipes.
    * For example: name=Fred. The value can also
    * be enclosed in single quotes, like:
    * name='Fred'. The operator can be any
    * equality or inequality operator. The
    * value can only be a string or a number.
    */
    protected function test( $condition ) {
    $random = "";
    while( strlen( $random ) < 7 ) {
    $random .= rand(0,9) . "";
    }
    //This is to protect against escaped single quotes
    $condition = str_replace( "\\'", $random, $condition );
    //Check if the value is enclosed by single quotes
    preg_match( "/'([^']*?)'/", $condition, $strings );
    $string = isset( $strings[1] ) ? $strings[1] : false;
    /**
    * If it is, store the value inside the
    * quotes and replace it with a token
    * that we can replace again later. This
    * makes sure nothing inside the single
    * quotes messes with our string splitting
    * in a bit.
    */
    if( $string !== false ) {
    $condition = str_replace( "'" . $string . "'", "[!!]", $condition );
    $string = str_replace( $random, "'", $string );
    }
    //Check for a valid operator
    preg_match( "/\s*(=|>|<|<=|>=)\s*/", $condition, $operators );
    $operator = isset( $operators[1] ) ? trim( $operators[1] ) : false;
    //If a filter doesn't have an operator, what good is it?
    if( $operator === false ) {
    throw new Exception(
    "No operator found in filter '" . $condition . "'."
    );
    }
    $split = preg_split( "/\s*(=|>|<|<=|>=)\s*/", $condition );
    $path = $split[0];
    $this->parse( $path, $name, $filters );
    $value = $split[1];
    //If we pulled out a string, inject it now
    if( $string ) {
    $value = str_replace( "[!!]", $string, $value );
    }
    //If $path is null, we've reached the destination
    if( strlen( $path ) == 0 ) {
    //If the name can't be found, this can't be true
    if( !isset( $this->_values[$name] ) ) {
    return false;
    }
    $test = $this->_values[$name];
    //If the value is a number, cast it for comparison
    if( !preg_match( "/[^0-9.]/", $value ) ) {
    if( strpos( ".", $value ) >= 0 ) {
    $value = (float)$value;
    } else {
    $value = (int)$value;
    }
    }
    if( $operator === "=" && $test == $value ) {
    return true;
    } else if( $operator === ">" && $test > $value ) {
    return true;
    } else if( $operator === "<" && $test < $value ) {
    return true;
    } else if( $operator === ">=" && $test >= $value ) {
    return true;
    } else if( $operator === "<=" && $test <= $value ) {
    return true;
    }
    return false;
    } else {
    if( isset( $this->_elements[$name] ) ) {
    $elements = $this->elements( $name, $filters );
    $total = count( $elements );
    if( $string ) {
    $value = "'" . $value . "'";
    }
    return $elements[$total-1]->test(
    $path . " " . $operator . " " . $value
    );
    }
    }
    return false;
    }

    private function elements( $name, $filters ) {
    if( !isset( $this->_elements[$name] ) ) {
    return array();
    }
    $total = count( $this->_elements[$name] );
    if( count( $filters ) > 0 ) {
    if( count( $filters ) == 1 ) {
    //If the filter is just a number, it's an index
    if( !preg_match( "/[^0-9]/", $filters[0] ) ) {
    $index = (int)$filters[0];
    if( isset( $this->_elements[$name][$index] ) ) {
    return array( $this->_elements[$name][$index] );
    }
    return NULL;
    }
    }
    $return = array();
    for( $i = 0; $i < $total; $i++ ) {
    if( $this->_elements[$name][$i]->match( $filters ) ) {
    $return[] = $this->_elements[$name][$i];
    }
    }
    return $return;
    }
    return $this->_elements[$name];
    }

    /**
    * Parse through a path string and find the
    * immediate name, any filters that need to
    * be applied to the immediate name, and the
    * excess path once those elements are removed.
    * @param $path The path to parse
    * @param $name The immediate name that's pulled
    * from the path
    * @param $filters An array of filters that's
    * pulled from the end of the immediate name
    */
    private function parse( &$path, &$name, &$filters ) {
    /**
    * Trim any excess forward slashes from the
    * beginning of the path string. They are not
    * needed.
    */
    while( substr( $path, 0, 1 ) === "/" ) {
    $path = substr( $path, 1 );
    }
    /**
    * Match any string with letters, numbers, *, _ and
    * - at the beginning of the path string. This
    * is the immediate name.
    */
    preg_match(
    "/^([a-zA-Z0-9*_\-]+)/",
    $path,
    $matches
    );
    $name = isset( $matches[1] ) ? $matches[1] : false;
    //If the name wasn't found, something is wrong with the path
    if( $name === false ) {
    throw new Exception( "Malformed path." );
    }
    //Remove the extracted immediate name from the path string
    $path = preg_replace( "/" . $name . "/", "", $path, 1 );
    $data = "";
    /**
    * If the remaining path begins with a [ character,
    * then we have a filter to deal with.
    */
    if( substr( $path, 0, 1 ) === "[" ) {
    /**
    * We match the content at the beginning of the
    * remaining path that's between a [ and a ],
    * proceeded by a / or the end of the path string
    */
    preg_match(
    "/^\[(.+)\](\/|\$)/s",
    $path,
    $matches
    );
    $data = isset( $matches[1] ) ? $matches[1] : false;
    /**
    * If the filter data isn't found, something is
    * wrong with the path string.
    */
    if( $data === false ) {
    throw new Exception( "Malformed filter string." );
    }
    }
    $filters = array();
    if( strlen( $data ) > 0 ) {
    //Escape the filter data so it'll play nice with regexp
    $replace = str_replace( "(", "\(", $data );
    $replace = str_replace( ")", "\)", $replace );
    $replace = str_replace( "/", "\/", $replace );
    $path = preg_replace( "/\[" . $replace . "\]/", "", $path, 1 );

    preg_match_all( "/(\s+and|or\s+)/i", $data, $matches );
    $split = preg_split( "/\s+and|or\s+/i", $data );
    $total = count( $split );
    $stack = array();
    $level = 0;
    for( $i = 0; $i < $total; $i++ ) {
    $condition = trim( $split[$i] );
    $open_count = substr_count( $condition, "(" );
    $close_count = substr_count( $condition, ")" );
    while( substr( $condition, 0, 1 ) == "(" ) {
    $stack[] = $filters;
    $filters = array();
    $condition = substr( $condition, 1 );
    $level++;
    }
    $use = $condition;
    if( $close_count > $open_count ) {
    for( $i = 0; $i < $close_count - $open_count; $i++ ) {
    $use = substr( $use, 0, strlen( $use ) - 1 );
    }
    }
    $filters[] = $use;
    while(
    count( $stack ) > 0 &&
    substr( $condition, -1, 1 ) == ")"
    ) {
    $new = array_pop( $stack );
    $new[] = $filters;
    $filters = $new;
    $condition = substr( $condition, 0, strlen( $condition ) - 1 );
    $level--;
    }
    if( $i < $total - 1 ) {
    $filters[] = trim( $matches[1][$i] );
    }
    }
    }
    } //end parse

    }