Przeglądaj źródła

[Tree] Added buildTree method to Closure strategy. General refactor

comfortablynumb 13 lat temu
rodzic
commit
f5a0a2b64e

+ 20 - 0
lib/Gedmo/Exception/FeatureNotImplementedException.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace Gedmo\Exception;
+
+use Gedmo\Exception;
+
+/**
+ * FeatureNotImplementedException
+ *
+ * @author Gustavo Falco <comfortablynumb84@gmail.com>
+ * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
+ * @package Gedmo.Exception
+ * @subpackage FeatureNotImplementedException
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+class FeatureNotImplementedException
+    extends \RuntimeException
+    implements Exception
+{}

+ 162 - 1
lib/Gedmo/Tree/Entity/Repository/AbstractTreeRepository.php

@@ -4,7 +4,8 @@ namespace Gedmo\Tree\Entity\Repository;
 
 use Doctrine\ORM\EntityRepository,
     Doctrine\ORM\EntityManager,
-    Doctrine\ORM\Mapping\ClassMetadata;
+    Doctrine\ORM\Mapping\ClassMetadata,
+    Gedmo\Tool\Wrapper\EntityWrapper;
 
 abstract class AbstractTreeRepository extends EntityRepository
 {
@@ -44,6 +45,154 @@ abstract class AbstractTreeRepository extends EntityRepository
         }
     }
 
+    /**
+     * Retrieves the nested array or the decorated output.
+     * Uses @options to handle decorations
+     *
+     * @throws \Gedmo\Exception\InvalidArgumentException
+     * @param object $node - from which node to start reordering the tree
+     * @param boolean $direct - true to take only direct children
+     * @param array $options :
+     *     decorate: boolean (false) - retrieves tree as UL->LI tree
+     *     nodeDecorator: Closure (null) - uses $node as argument and returns decorated item as string
+     *     rootOpen: string || Closure ('<ul>') - branch start, closure will be given $children as a parameter
+     *     rootClose: string ('</ul>') - branch close
+     *     childStart: string || Closure ('<li>') - start of node, closure will be given $node as a parameter
+     *     childClose: string ('</li>') - close of node
+     *     childSort: array || keys allowed: field: field to sort on, dir: direction. 'asc' or 'desc'
+     *
+     * @return array|string
+     */
+    public function childrenHierarchy($node = null, $direct = false, array $options = array())
+    {
+        $meta = $this->getClassMetadata();
+        $config = $this->listener->getConfiguration($this->_em, $meta->name);
+
+        if ($node !== null) {
+            if ($node instanceof $meta->name) {
+                $wrapped = new EntityWrapper($node, $this->_em);
+                if (!$wrapped->hasValidIdentifier()) {
+                    throw new InvalidArgumentException("Node is not managed by UnitOfWork");
+                }
+            }
+        }
+
+        // Gets the array of $node results. It must be ordered by depth
+        $nodes = $this->getNodesHierarchy($node, $direct, $config, $options);
+
+        return $this->buildTree($nodes, $options);
+    }
+
+    /**
+     * Retrieves the nested array or the decorated output.
+     * Uses @options to handle decorations
+     * NOTE: @nodes should be fetched and hydrated as array
+     *
+     * @throws \Gedmo\Exception\InvalidArgumentException
+     * @param array $nodes - list o nodes to build tree
+     * @param array $options :
+     *     decorate: boolean (false) - retrieves tree as UL->LI tree
+     *     nodeDecorator: Closure (null) - uses $node as argument and returns decorated item as string
+     *     rootOpen: string || Closure ('<ul>') - branch start, closure will be given $children as a parameter
+     *     rootClose: string ('</ul>') - branch close
+     *     childStart: string || Closure ('<li>') - start of node, closure will be given $node as a parameter
+     *     childClose: string ('</li>') - close of node
+     *
+     * @return array|string
+     */
+    public function buildTree(array $nodes, array $options = array())
+    {
+        $meta = $this->getClassMetadata();
+        $nestedTree = $this->buildTreeArray($nodes);
+
+        $default = array(
+            'decorate' => false,
+            'rootOpen' => '<ul>',
+            'rootClose' => '</ul>',
+            'childOpen' => '<li>',
+            'childClose' => '</li>',
+            'nodeDecorator' => function ($node) use ($meta) {
+                // override and change it, guessing which field to use
+                if ($meta->hasField('title')) {
+                    $field = 'title';
+                } else if ($meta->hasField('name')) {
+                    $field = 'name';
+                } else {
+                    throw new InvalidArgumentException("Cannot find any representation field");
+                }
+                return $node[$field];
+            }
+        );
+        $options = array_merge($default, $options);
+        // If you don't want any html output it will return the nested array
+        if (!$options['decorate']) {
+            return $nestedTree;
+        } elseif (!count($nestedTree)) {
+            return '';
+        }
+
+        $build = function($tree) use (&$build, &$options) {
+            $output = is_string($options['rootOpen']) ? $options['rootOpen'] : $options['rootOpen']($tree);
+            foreach ($tree as $node) {
+                $output .= is_string($options['childOpen']) ? $options['childOpen'] : $options['childOpen']($node);
+                $output .= $options['nodeDecorator']($node);
+                if (count($node['__children']) > 0) {
+                    $output .= $build($node['__children']);
+                }
+                $output .= is_string($options['childClose']) ? $options['childClose'] : $options['childClose']($node);
+            }
+            return $output . (is_string($options['rootClose']) ? $options['rootClose'] : $options['rootClose']($tree));
+        };
+
+        return $build($nestedTree);
+    }
+
+    /**
+     * Process nodes and produce an array with the
+     * structure of the tree
+     *
+     * @param array - Array of nodes
+     *
+     * @return array - Array with tree structure
+     */
+    public function buildTreeArray(array $nodes)
+    {
+        $meta = $this->getClassMetadata();
+        $config = $this->listener->getConfiguration($this->_em, $meta->name);
+        $nestedTree = array();
+        $l = 0;
+
+        if (count($nodes) > 0) {
+            // Node Stack. Used to help building the hierarchy
+            $stack = array();
+            foreach ($nodes as $child) {
+                $item = $child;
+                $item['__children'] = array();
+                // Number of stack items
+                $l = count($stack);
+                // Check if we're dealing with different levels
+                while($l > 0 && $stack[$l - 1][$config['level']] >= $item[$config['level']]) {
+                    array_pop($stack);
+                    $l--;
+                }
+                // Stack is empty (we are inspecting the root)
+                if ($l == 0) {
+                    // Assigning the root child
+                    $i = count($nestedTree);
+                    $nestedTree[$i] = $item;
+                    $stack[] = &$nestedTree[$i];
+                } else {
+                    // Add child to parent
+                    $i = count($stack[$l - 1]['__children']);
+                    $stack[$l - 1]['__children'][$i] = $item;
+                    $stack[] = &$stack[$l - 1]['__children'][$i];
+                }
+            }
+        }
+
+        return $nestedTree;
+    }
+
     /**
      * Checks if current repository is right
      * for currently used tree strategy
@@ -51,4 +200,16 @@ abstract class AbstractTreeRepository extends EntityRepository
      * @return bool
      */
     abstract protected function validate();
+
+    /**
+     * Returns an array of nodes suitable for method buildTree
+     *
+     * @param object - Root node
+     * @param bool - Obtain direct children?
+     * @param array - Metadata configuration
+     * @param array - Options
+     *
+     * @return array - Array of nodes
+     */
+    abstract public function getNodesHierarchy($node, $direct, array $config, array $options = array());
 }

+ 79 - 1
lib/Gedmo/Tree/Entity/Repository/ClosureTreeRepository.php

@@ -273,13 +273,91 @@ class ClosureTreeRepository extends AbstractTreeRepository
         } catch (\Exception $e) {
             $this->_em->close();
             $this->_em->getConnection()->rollback();
-            throw new \Gedmo\Exception\RuntimeException('Transaction failed', null, $e);
+            throw new \Gedmo\Exception\RuntimeException('Transaction failed: '.$e->getMessage(), null, $e);
         }
         // remove from identity map
         $this->_em->getUnitOfWork()->removeFromIdentityMap($node);
         $node = null;
     }
 
+    /**
+     * Process nodes and produce an array with the
+     * structure of the tree
+     *
+     * @param array - Array of nodes
+     *
+     * @return array - Array with tree structure
+     */
+    public function buildTreeArray(array $nodes)
+    {
+        $meta = $this->getClassMetadata();
+        $config = $this->listener->getConfiguration($this->_em, $meta->name);
+        $nestedTree = array();
+        $levelField = $config['level'];
+        $idField = $meta->getSingleIdentifierFieldName();
+
+        if (count($nodes) > 0) {
+            $l = 1;
+            $refs = array();
+
+            foreach ($nodes as $n) {
+                $node = $n[0]['descendant'];
+                $node['__children'] = array();
+
+                if ($l < $node[$levelField]) {
+                    $l = $node[$levelField];
+                }
+
+                if ($l == 1) {
+                    $tmp = &$nestedTree;
+                } else {
+                    $tmp = &$refs[$n['parent_id']]['__children'];
+                }
+
+                $key = count($tmp);
+                $tmp[$key] = $node;
+                $refs[$node[$idField]] = &$tmp[$key];
+            }
+
+            unset($refs);
+        }
+
+        return $nestedTree;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getNodesHierarchy($node, $direct, array $config, array $options = array())
+    {
+        $meta = $this->getClassMetadata();
+        $idField = $meta->getSingleIdentifierFieldName();
+
+        $q = $this->_em->createQueryBuilder()
+            ->select('c, node, p.'.$idField.' AS parent_id')
+            ->from($config['closure'], 'c')
+            ->innerJoin('c.descendant', 'node')
+            ->leftJoin('node.parent', 'p')
+            ->where('c.ancestor = :node')
+            ->addOrderBy('node.'.$config['level'], 'asc');
+
+        $defaultOptions = array();
+        $options = array_merge($defaultOptions, $options);
+
+        if (isset($options['childSort']) && is_array($options['childSort']) &&
+            isset($options['childSort']['field']) && isset($options['childSort']['dir'])) {
+            $q->addOrderBy(
+                'node.'.$options['childSort']['field'],
+                strtolower($options['childSort']['dir']) == 'asc' ? 'asc' : 'desc'
+            );
+        }
+
+        $q = $q->getQuery();
+        $q->setParameters(compact('node'));
+
+        return $q->getArrayResult();
+    }
+
     /**
      * {@inheritdoc}
      */

+ 26 - 1
lib/Gedmo/Tree/Entity/Repository/MaterializedPathRepository.php

@@ -5,7 +5,8 @@ namespace Gedmo\Tree\Entity\Repository;
 use Gedmo\Exception\InvalidArgumentException,
     Gedmo\Tree\Strategy,
     Gedmo\Tree\Strategy\ORM\MaterializedPath,
-    Gedmo\Tool\Wrapper\EntityWrapper;
+    Gedmo\Tool\Wrapper\EntityWrapper,
+    Gedmo\Exception\FeatureNotImplementedException;
 
 /**
  * The MaterializedPathRepository has some useful functions
@@ -147,6 +148,30 @@ class MaterializedPathRepository extends AbstractTreeRepository
         return $this->getChildrenQuery($node, $direct, $sortByField, $direction)->execute();
     }
 
+    /**
+     * {@inheritdoc}
+     */
+    public function getNodesHierarchy($node, $direct, array $config, array $options = array())
+    {
+        throw new FeatureNotImplementedException('You can\'t build an array with the tree structure for this strategy yet.');
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function buildTree(array $nodes, array $options = array())
+    {
+        throw new FeatureNotImplementedException('You can\'t build an array with the tree structure for this strategy yet.');
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function childrenHierarchy($node = null, $direct = false, array $options = array())
+    {
+        throw new FeatureNotImplementedException('You can\'t build an array with the tree structure for this strategy yet.');
+    }
+
     /**
      * {@inheritdoc}
      */

+ 3 - 128
lib/Gedmo/Tree/Entity/Repository/NestedTreeRepository.php

@@ -836,46 +836,16 @@ class NestedTreeRepository extends AbstractTreeRepository
     }
 
     /**
-     * Retrieves the nested array or the decorated output.
-     * Uses @options to handle decorations
-     *
-     * @throws \Gedmo\Exception\InvalidArgumentException
-     * @param object $node - from which node to start reordering the tree
-     * @param boolean $direct - true to take only direct children
-     * @param array $options :
-     *     decorate: boolean (false) - retrieves tree as UL->LI tree
-     *     nodeDecorator: Closure (null) - uses $node as argument and returns decorated item as string
-     *     rootOpen: string || Closure ('<ul>') - branch start, closure will be given $children as a parameter
-     *     rootClose: string ('</ul>') - branch close
-     *     childStart: string || Closure ('<li>') - start of node, closure will be given $node as a parameter
-     *     childClose: string ('</li>') - close of node
-     *
-     * @return array|string
+     * {@inheritdoc}
      */
-    public function childrenHierarchy($node = null, $direct = false, array $options = array())
+    public function getNodesHierarchy($node, $direct, array $config, array $options = array())
     {
-        $meta = $this->getClassMetadata();
-        $config = $this->listener->getConfiguration($this->_em, $meta->name);
-
-        if ($node !== null) {
-            if ($node instanceof $meta->name) {
-                $wrapped = new EntityWrapper($node, $this->_em);
-                if (!$wrapped->hasValidIdentifier()) {
-                    throw new InvalidArgumentException("Node is not managed by UnitOfWork");
-                }
-            }
-        }
-
-        // Gets the array of $node results.
-        // It must be order by 'root' and 'left' field
-        $nodes = $this->childrenQuery(
+        return $this->childrenQuery(
             $node,
             $direct,
             isset($config['root']) ? array($config['root'], $config['left']) : $config['left'],
             'ASC'
         )->getArrayResult();
-
-        return $this->buildTree($nodes, $options);
     }
 
     /**
@@ -1073,99 +1043,4 @@ class NestedTreeRepository extends AbstractTreeRepository
         // remove from identity map
         $this->_em->getUnitOfWork()->removeFromIdentityMap($wrapped->getObject());
     }
-
-    /**
-     * Retrieves the nested array or the decorated output.
-     * Uses @options to handle decorations
-     * NOTE: @nodes should be fetched and hydrated as array
-     *
-     * @throws \Gedmo\Exception\InvalidArgumentException
-     * @param array $nodes - list o nodes to build tree
-     * @param array $options :
-     *     decorate: boolean (false) - retrieves tree as UL->LI tree
-     *     nodeDecorator: Closure (null) - uses $node as argument and returns decorated item as string
-     *     rootOpen: string || Closure ('<ul>') - branch start, closure will be given $children as a parameter
-     *     rootClose: string ('</ul>') - branch close
-     *     childStart: string || Closure ('<li>') - start of node, closure will be given $node as a parameter
-     *     childClose: string ('</li>') - close of node
-     *
-     * @return array|string
-     */
-    public function buildTree(array $nodes, array $options = array())
-    {
-        //process the nested tree into a nested array
-        $meta = $this->getClassMetadata();
-        $config = $this->listener->getConfiguration($this->_em, $meta->name);
-        $nestedTree = array();
-        $l = 0;
-
-        if (count($nodes) > 0) {
-            // Node Stack. Used to help building the hierarchy
-            $stack = array();
-            foreach ($nodes as $child) {
-                $item = $child;
-                $item['__children'] = array();
-                // Number of stack items
-                $l = count($stack);
-                // Check if we're dealing with different levels
-                while($l > 0 && $stack[$l - 1][$config['level']] >= $item[$config['level']]) {
-                    array_pop($stack);
-                    $l--;
-                }
-                // Stack is empty (we are inspecting the root)
-                if ($l == 0) {
-                    // Assigning the root child
-                    $i = count($nestedTree);
-                    $nestedTree[$i] = $item;
-                    $stack[] = &$nestedTree[$i];
-                } else {
-                    // Add child to parent
-                    $i = count($stack[$l - 1]['__children']);
-                    $stack[$l - 1]['__children'][$i] = $item;
-                    $stack[] = &$stack[$l - 1]['__children'][$i];
-                }
-            }
-        }
-
-        $default = array(
-            'decorate' => false,
-            'rootOpen' => '<ul>',
-            'rootClose' => '</ul>',
-            'childOpen' => '<li>',
-            'childClose' => '</li>',
-            'nodeDecorator' => function ($node) use ($meta) {
-                // override and change it, guessing which field to use
-                if ($meta->hasField('title')) {
-                    $field = 'title';
-                } else if ($meta->hasField('name')) {
-                    $field = 'name';
-                } else {
-                    throw new InvalidArgumentException("Cannot find any representation field");
-                }
-                return $node[$field];
-            }
-        );
-        $options = array_merge($default, $options);
-        // If you don't want any html output it will return the nested array
-        if (!$options['decorate']) {
-            return $nestedTree;
-        } elseif (!count($nestedTree)) {
-            return '';
-        }
-
-        $build = function($tree) use (&$build, &$options) {
-            $output = is_string($options['rootOpen']) ? $options['rootOpen'] : $options['rootOpen']($tree);
-            foreach ($tree as $node) {
-                $output .= is_string($options['childOpen']) ? $options['childOpen'] : $options['childOpen']($node);
-                $output .= $options['nodeDecorator']($node);
-                if (count($node['__children']) > 0) {
-                    $output .= $build($node['__children']);
-                }
-                $output .= is_string($options['childClose']) ? $options['childClose'] : $options['childClose']($node);
-            }
-            return $output . (is_string($options['rootClose']) ? $options['rootClose'] : $options['rootClose']($tree));
-        };
-
-        return $build($nestedTree);
-    }
 }

+ 3 - 0
lib/Gedmo/Tree/Mapping/Validator.php

@@ -177,6 +177,9 @@ class Validator
         if (!isset($config['closure'])) {
             $missingFields[] = 'closure class';
         }
+        if (!isset($config['level'])) {
+            $missingFields[] = 'level';
+        }
         if ($missingFields) {
             throw new InvalidMappingException("Missing properties: " . implode(', ', $missingFields) . " in class - {$meta->name}");
         }

+ 114 - 2
lib/Gedmo/Tree/Strategy/ORM/Closure.php

@@ -11,6 +11,8 @@ use Gedmo\Tree\TreeListener;
 use Doctrine\ORM\Version;
 use Gedmo\Tool\Wrapper\AbstractWrapper;
 use Gedmo\Mapping\Event\AdapterInterface;
+use Doctrine\Common\Persistence\ObjectManager;
+use Doctrine\ORM\Mapping\ClassMetadata;
 
 /**
  * This strategy makes tree act like
@@ -41,6 +43,23 @@ class Closure implements Strategy
      */
     private $pendingChildNodeInserts = array();
 
+    /**
+     * List of nodes which has their parents updated, but using
+     * new nodes. They have to wait until their parents are inserted
+     * on DB to make the update
+     *
+     * @var array
+     */
+    private $pendingNodeUpdates = array();
+
+    /**
+     * List of pending Nodes, which needs their "level"
+     * field value set
+     *
+     * @var array
+     */
+    private $pendingNodesLevelProcess = array();
+
     /**
      * {@inheritdoc}
      */
@@ -194,7 +213,15 @@ class Closure implements Strategy
      * {@inheritdoc}
      */
     public function processPostUpdate($em, $entity, AdapterInterface $ea)
-    {}
+    {
+        $meta = $em->getClassMetadata(get_class($entity));
+        $config = $this->listener->getConfiguration($em, $meta->name);
+
+        // Process TreeLevel field value
+        if (!empty($config)) {
+            $this->setLevelFieldOnPendingNodes($em);
+        }
+    }
 
     /**
      * {@inheritdoc}
@@ -248,13 +275,82 @@ class Closure implements Strategy
                         $depthColumnName => $ancestor['depth'] + 1
                     );
                 }
+
+                $this->pendingNodesLevelProcess[$nodeId] = $node;
+            } else {
+                $uow->scheduleExtraUpdate($node, array($config['level'] => array(null, 1)));
+                $ea->setOriginalObjectProperty($uow, spl_object_hash($node), $config['level'], 1);
             }
+
             foreach ($entries as $closure) {
                 if (!$em->getConnection()->insert($closureTable, $closure)) {
                     throw new RuntimeException('Failed to insert new Closure record');
                 }
             }
         }
+
+        // Process pending node updates
+        if (!empty($this->pendingNodeUpdates)) {
+            foreach ($this->pendingNodeUpdates as $info) {
+                $this->updateNode($em, $info['node'], $info['oldParent']);
+            }
+
+            $this->pendingNodeUpdates = array();
+        }
+
+        // Process TreeLevel field value
+        $this->setLevelFieldOnPendingNodes($em);
+    }
+
+    /**
+     * Process pending entities to set their "level" value
+     *
+     * @param \Doctrine\Common\Persistence\ObjectManager $em
+     * @param \Doctrine\ORM\Mapping\ClassMetadata $meta
+     * @param array $config
+     */
+    protected function setLevelFieldOnPendingNodes(ObjectManager $em)
+    {
+        if (!empty($this->pendingNodesLevelProcess)) {
+            $first = array_slice($this->pendingNodesLevelProcess, 0, 1);
+            $meta = $em->getClassMetadata(get_class($first[0]));
+            unset($first);
+            $config = $this->listener->getConfiguration($em, $meta->name);
+            $closureClass = $config['closure'];
+            $closureMeta = $em->getClassMetadata($closureClass);
+            $uow = $em->getUnitOfWork();
+
+            foreach ($this->pendingNodesLevelProcess as $node) {
+                $children = $em->getRepository($meta->name)->children($node);
+
+                foreach ($children as $child) {
+                    $this->pendingNodesLevelProcess[AbstractWrapper::wrap($child, $em)->getIdentifier()] = $child;
+                }
+            }
+
+            // We calculate levels for all nodes
+            $sql = 'SELECT c.descendant, MAX(c.depth) + 1 AS level ';
+            $sql .= 'FROM '.$closureMeta->getTableName().' c ';
+            $sql .= 'WHERE c.descendant IN ('.implode(', ', array_keys($this->pendingNodesLevelProcess)).') ';
+            $sql .= 'GROUP BY c.descendant';
+
+            $levels = $em->getConnection()->executeQuery($sql)->fetchAll(\PDO::FETCH_KEY_PAIR);
+
+            // Now we update levels
+            foreach ($this->pendingNodesLevelProcess as $nodeId => $node) {
+                // Update new level
+                $level = $levels[$nodeId];
+                $uow->scheduleExtraUpdate(
+                    $node,
+                    array($config['level'] => array(
+                        $meta->getReflectionProperty($config['level'])->getValue($node), $level
+                    ))
+                );
+                $uow->setOriginalEntityProperty(spl_object_hash($node), $config['level'], $level);
+            }
+
+            $this->pendingNodesLevelProcess = array();
+        }
     }
 
     /**
@@ -266,8 +362,20 @@ class Closure implements Strategy
         $config = $this->listener->getConfiguration($em, $meta->name);
         $uow = $em->getUnitOfWork();
         $changeSet = $uow->getEntityChangeSet($node);
+
         if (array_key_exists($config['parent'], $changeSet)) {
-            $this->updateNode($em, $node, $changeSet[$config['parent']][0]);
+            // If new parent is new, we need to delay the update of the node
+            // until it is inserted on DB
+            $parent = $changeSet[$config['parent']][1] ? AbstractWrapper::wrap($changeSet[$config['parent']][1], $em) : null;
+
+            if ($parent && !$parent->getIdentifier()) {
+                $this->pendingNodeUpdates[spl_object_hash($node)] = array(
+                    'node'      => $node,
+                    'oldParent' => $changeSet[$config['parent']][0]
+                );
+            } else {
+                $this->updateNode($em, $node, $changeSet[$config['parent']][0]);
+            }
         }
     }
 
@@ -318,6 +426,7 @@ class Closure implements Strategy
                 throw new RuntimeException('Failed to remove old closures');
             }
         }
+
         if ($parent) {
             $wrappedParent = AbstractWrapper::wrap($parent, $em);
             $parentId = $wrappedParent->getIdentifier();
@@ -327,11 +436,14 @@ class Closure implements Strategy
             $query .= " AND c2.ancestor = :nodeId";
 
             $closures = $conn->fetchAll($query, compact('nodeId', 'parentId'));
+
             foreach ($closures as $closure) {
                 if (!$conn->insert($table, $closure)) {
                     throw new RuntimeException('Failed to insert new Closure record');
                 }
             }
         }
+
+        $this->pendingNodesLevelProcess[$nodeId] = $node;
     }
 }

+ 4 - 0
tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.ClosureTree.dcm.xml

@@ -11,6 +11,10 @@
 
         <field name="name" type="string" length="128"/>
 
+        <field name="level" type="integer">
+            <gedmo:tree-level />
+        </field>
+
         <many-to-one field="parent" target-entity="ClosureTree">
             <join-column name="parent_id" referenced-column-name="id" on-delete="CASCADE"/>
             <gedmo:tree-parent/>

+ 4 - 0
tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.ClosureCategory.dcm.yml

@@ -15,6 +15,10 @@ Mapping\Fixture\Yaml\ClosureCategory:
     title:
       type: string
       length: 64
+    level:
+      type: integer
+      gedmo:
+        - treeLevel
   manyToOne:
     parent:
       targetEntity: Mapping\Fixture\Yaml\ClosureCategory

+ 2 - 0
tests/Gedmo/Mapping/Fixture/Xml/ClosureTree.php

@@ -9,4 +9,6 @@ class ClosureTree
     private $name;
 
     private $parent;
+
+    private $level;
 }

+ 12 - 0
tests/Gedmo/Mapping/Fixture/Yaml/ClosureCategory.php

@@ -12,6 +12,8 @@ class ClosureCategory
 
     private $parent;
 
+    private $level;
+
     /**
      * Get id
      *
@@ -81,4 +83,14 @@ class ClosureCategory
     {
         return $this->parent;
     }
+
+    public function setLevel($level)
+    {
+        $this->level = $level;
+    }
+
+    public function getLevel()
+    {
+        return $this->level;
+    }
 }

+ 67 - 5
tests/Gedmo/Tree/ClosureTreeRepositoryTest.php

@@ -125,6 +125,67 @@ class ClosureTreeRepositoryTest extends BaseTestCaseORM
         $this->assertCount(4, $repo->children(null, true));
     }
 
+    public function testBuildTree()
+    {
+        $repo = $this->em->getRepository(self::CATEGORY);
+        $roots = $repo->getRootNodes();
+        $tree = $repo->childrenHierarchy(
+            $roots[0],
+            false,
+            array('childSort' => array('field' => 'title', 'dir' => 'asc'))
+        );
+
+        $fruits = $tree[0]['__children'][0];
+        $milk = $tree[0]['__children'][1];
+        $vegitables = $tree[0]['__children'][2];
+
+        $this->assertEquals('Food', $tree[0]['title']);
+        $this->assertEquals('Fruits', $fruits['title']);
+        $this->assertEquals('Berries', $fruits['__children'][0]['title']);
+        $this->assertEquals('Strawberries', $fruits['__children'][0]['__children'][0]['title']);
+        $this->assertEquals('Milk', $milk['title']);
+        $this->assertEquals('Cheese', $milk['__children'][0]['title']);
+        $this->assertEquals('Mould cheese', $milk['__children'][0]['__children'][0]['title']);
+        $this->assertEquals('Vegitables', $vegitables['title']);
+        $this->assertEquals('Cabbages', $vegitables['__children'][0]['title']);
+        $this->assertEquals('Carrots', $vegitables['__children'][1]['title']);
+
+        $food = $repo->findOneByTitle('Food');
+        $vegitables = $repo->findOneByTitle('Vegitables');
+
+        $boringFood = new Category();
+        $boringFood->setTitle('Boring Food');
+        $boringFood->setParent($food);
+        $vegitables->setParent($boringFood);
+
+        $this->em->persist($boringFood);
+
+        $this->em->flush();
+
+        $tree = $repo->childrenHierarchy(
+            $roots[0],
+            false,
+            array('childSort' => array('field' => 'title', 'dir' => 'asc'))
+        );
+
+        $boringFood = $tree[0]['__children'][0];
+        $fruits = $tree[0]['__children'][1];
+        $milk = $tree[0]['__children'][2];
+        $vegitables = $boringFood['__children'][0];
+
+        $this->assertEquals('Food', $tree[0]['title']);
+        $this->assertEquals('Fruits', $fruits['title']);
+        $this->assertEquals('Berries', $fruits['__children'][0]['title']);
+        $this->assertEquals('Strawberries', $fruits['__children'][0]['__children'][0]['title']);
+        $this->assertEquals('Milk', $milk['title']);
+        $this->assertEquals('Cheese', $milk['__children'][0]['title']);
+        $this->assertEquals('Mould cheese', $milk['__children'][0]['__children'][0]['title']);
+        $this->assertEquals('Boring Food', $boringFood['title']);
+        $this->assertEquals('Vegitables', $boringFood['__children'][0]['title']);
+        $this->assertEquals('Cabbages', $vegitables['__children'][0]['title']);
+        $this->assertEquals('Carrots', $vegitables['__children'][1]['title']);
+    }
+
     protected function getUsedEntityFixtures()
     {
         return array(
@@ -139,6 +200,11 @@ class ClosureTreeRepositoryTest extends BaseTestCaseORM
         $food->setTitle("Food");
         $this->em->persist($food);
 
+        $vegitables = new Category;
+        $vegitables->setTitle('Vegitables');
+        $vegitables->setParent($food);
+        $this->em->persist($vegitables);
+
         $fruits = new Category;
         $fruits->setTitle('Fruits');
         $fruits->setParent($food);
@@ -164,11 +230,6 @@ class ClosureTreeRepositoryTest extends BaseTestCaseORM
         $strawberries->setParent($berries);
         $this->em->persist($strawberries);
 
-        $vegitables = new Category;
-        $vegitables->setTitle('Vegitables');
-        $vegitables->setParent($food);
-        $this->em->persist($vegitables);
-
         $cabbages = new Category;
         $cabbages->setTitle('Cabbages');
         $cabbages->setParent($vegitables);
@@ -195,5 +256,6 @@ class ClosureTreeRepositoryTest extends BaseTestCaseORM
         $this->em->persist($mouldCheese);
 
         $this->em->flush();
+        $this->em->clear();
     }
 }

+ 16 - 0
tests/Gedmo/Tree/Fixture/Closure/Category.php

@@ -24,6 +24,12 @@ class Category
      */
     private $title;
 
+    /**
+     * @ORM\Column(name="level", type="integer", nullable=true)
+     * @Gedmo\TreeLevel
+     */
+    private $level;
+
     /**
      * @Gedmo\TreeParent
      * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE")
@@ -60,4 +66,14 @@ class Category
     {
         $this->closures[] = $closure;
     }
+
+    public function setLevel($level)
+    {
+        $this->level = $level;
+    }
+
+    public function getLevel()
+    {
+        return $this->level;
+    }
 }

+ 26 - 0
tests/Gedmo/Tree/Fixture/Closure/Person.php

@@ -36,6 +36,12 @@ class Person
      */
     private $parent;
 
+    /**
+     * @ORM\Column(name="level", type="integer")
+     * @Gedmo\TreeLevel
+     */
+    private $level;
+
     public function getId()
     {
         return $this->id;
@@ -65,4 +71,24 @@ class Person
     {
         $this->closures[] = $closure;
     }
+
+    public function setLevel($level)
+    {
+        $this->level = $level;
+    }
+
+    public function getLevel()
+    {
+        return $this->level;
+    }
+
+    public function setFullName($fullName)
+    {
+        $this->fullName = $fullName;
+    }
+
+    public function getFullName()
+    {
+        return $this->fullName;
+    }
 }