Browse Source

Merge pull request #5857 from cakephp/3.0-tree-level

3.0 - Allow setting level (depth) of tree nodes on save.
Mark Story 11 years ago
parent
commit
b39cd52367

+ 80 - 2
src/ORM/Behavior/TreeBehavior.php

@@ -68,7 +68,8 @@ class TreeBehavior extends Behavior
         'parent' => 'parent_id',
         'left' => 'lft',
         'right' => 'rght',
-        'scope' => null
+        'scope' => null,
+        'level' => null
     ];
 
     /**
@@ -88,6 +89,7 @@ class TreeBehavior extends Behavior
         $parent = $entity->get($config['parent']);
         $primaryKey = $this->_getPrimaryKey();
         $dirty = $entity->dirty($config['parent']);
+        $level = $config['level'];
 
         if ($isNew && $parent) {
             if ($entity->get($primaryKey[0]) == $parent) {
@@ -99,20 +101,92 @@ class TreeBehavior extends Behavior
             $entity->set($config['left'], $edge);
             $entity->set($config['right'], $edge + 1);
             $this->_sync(2, '+', ">= {$edge}");
+
+            if ($level) {
+                $entity->set($config[$level], $parentNode[$level] + 1);
+            }
+            return;
         }
 
         if ($isNew && !$parent) {
             $edge = $this->_getMax();
             $entity->set($config['left'], $edge + 1);
             $entity->set($config['right'], $edge + 2);
+
+            if ($level) {
+                $entity->set($config[$level], 0);
+            }
+            return;
         }
 
         if (!$isNew && $dirty && $parent) {
             $this->_setParent($entity, $parent);
+
+            if ($level) {
+                $parentNode = $this->_getNode($parent);
+                $entity->set($config[$level], $parentNode[$level] + 1);
+            }
+            return;
         }
 
         if (!$isNew && $dirty && !$parent) {
             $this->_setAsRoot($entity);
+
+            if ($level) {
+                $entity->set($config[$level], 0);
+            }
+        }
+    }
+
+    /**
+     * After save listener.
+     *
+     * Manages updating level of descendents of currently saved entity.
+     *
+     * @param \Cake\Event\Event $event The beforeSave event that was fired
+     * @param \Cake\ORM\Entity $entity the entity that is going to be saved
+     * @return void
+     */
+    public function afterSave(Event $event, Entity $entity)
+    {
+        if (!$this->_config['level'] || $entity->isNew()) {
+            return;
+        }
+
+        $this->_setChildrenLevel($entity);
+    }
+
+    /**
+     * Set level for descendents.
+     *
+     * @param \Cake\ORM\Entity $entity The entity whose descendents need to be updated.
+     * @return void
+     */
+    protected function _setChildrenLevel(Entity $entity)
+    {
+        $config = $this->config();
+
+        if ($entity->get($config['left']) + 1 === $entity->get($config['right'])) {
+            return;
+        }
+
+        $primaryKey = $this->_getPrimaryKey();
+        $primaryKeyValue = $entity->get($primaryKey);
+        $depths = [$primaryKeyValue => $entity->get($config['level'])];
+
+        $children = $this->_table->find('children', [
+            'for' => $primaryKeyValue,
+            'fields' => [$this->_getPrimaryKey(), $config['parent'], $config['level']],
+            'order' => $config['left']
+        ]);
+
+        foreach ($children as $node) {
+            $parentIdValue = $node->get($config['parent']);
+            $depth = $depths[$parentIdValue] + 1;
+            $depths[$node->get($primaryKey)] = $depth;
+
+            $node->set($config['level'], $depth);
+            $this->_table->save($node, ['checkRules' => false, 'atomic' => false]);
         }
     }
 
@@ -624,9 +698,13 @@ class TreeBehavior extends Behavior
         $config = $this->config();
         list($parent, $left, $right) = [$config['parent'], $config['left'], $config['right']];
         $primaryKey = $this->_getPrimaryKey();
+        $fields = [$parent, $left, $right];
+        if ($config['level']) {
+            $fields[] = $config['level'];
+        }
 
         $node = $this->_scope($this->_table->find())
-            ->select([$parent, $left, $right])
+            ->select($fields)
             ->where([$this->_table->alias() . '.' . $primaryKey => $id])
             ->first();
 

+ 23 - 11
tests/Fixture/NumberTreesFixture.php

@@ -36,6 +36,7 @@ class NumberTreesFixture extends TestFixture
         'parent_id' => 'integer',
         'lft' => ['type' => 'integer'],
         'rght' => ['type' => 'integer'],
+        'level' => ['type' => 'integer'],
         '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]]
     ];
 
@@ -61,67 +62,78 @@ class NumberTreesFixture extends TestFixture
             'name' => 'electronics',
             'parent_id' => null,
             'lft' => '1',
-            'rght' => '20'
+            'rght' => '20',
+            'level' => 0
         ],
         [
             'name' => 'televisions',
             'parent_id' => '1',
             'lft' => '2',
-            'rght' => '9'
+            'rght' => '9',
+            'level' => 1
         ],
         [
             'name' => 'tube',
             'parent_id' => '2',
             'lft' => '3',
-            'rght' => '4'
+            'rght' => '4',
+            'level' => 2
         ],
         [
             'name' => 'lcd',
             'parent_id' => '2',
             'lft' => '5',
-            'rght' => '6'
+            'rght' => '6',
+            'level' => 2
         ],
         [
             'name' => 'plasma',
             'parent_id' => '2',
             'lft' => '7',
-            'rght' => '8'
+            'rght' => '8',
+            'level' => 2
         ],
         [
             'name' => 'portable',
             'parent_id' => '1',
             'lft' => '10',
-            'rght' => '19'
+            'rght' => '19',
+            'level' => 1
         ],
         [
             'name' => 'mp3',
             'parent_id' => '6',
             'lft' => '11',
-            'rght' => '14'
+            'rght' => '14',
+            'level' => 2
         ],
         [
             'name' => 'flash',
             'parent_id' => '7',
             'lft' => '12',
-            'rght' => '13'
+            'rght' => '13',
+            'level' => 3
         ],
         [
             'name' => 'cd',
             'parent_id' => '6',
             'lft' => '15',
-            'rght' => '16'
+            'rght' => '16',
+            'level' => 2
         ],
         [
             'name' => 'radios',
             'parent_id' => '6',
             'lft' => '17',
-            'rght' => '18'
+            'rght' => '18',
+            'level' => 2
         ],
         [
             'name' => 'alien hardware',
             'parent_id' => null,
             'lft' => '21',
-            'rght' => '22'
+            'rght' => '22',
+            'level' => 0
         ]
     ];
 }

+ 58 - 1
tests/TestCase/ORM/Behavior/TreeBehaviorTest.php

@@ -502,7 +502,7 @@ class TreeBehaviorTest extends TestCase
     {
         $table = $this->table;
         $entity = new Entity(
-            ['name' => 'New Orphan', 'parent_id' => null],
+            ['name' => 'New Orphan', 'parent_id' => null, 'level' => null],
             ['markNew' => true]
         );
         $expected = $table->find()->order('lft')->hydrate(false)->toArray();
@@ -879,6 +879,63 @@ class TreeBehaviorTest extends TestCase
     }
 
     /**
+     * Test setting level for new nodes
+     *
+     * @return void
+     */
+    public function testSetLevelNewNode()
+    {
+        $this->table->behaviors()->Tree->config('level', 'level');
+
+        $entity = new Entity(['parent_id' => null, 'name' => 'Depth 0']);
+        $this->table->save($entity);
+        $entity = $this->table->get(12);
+        $this->assertEquals(0, $entity->level);
+
+        $entity = new Entity(['parent_id' => 1, 'name' => 'Depth 1']);
+        $this->table->save($entity);
+        $entity = $this->table->get(13);
+        $this->assertEquals(1, $entity->level);
+
+        $entity = new Entity(['parent_id' => 8, 'name' => 'Depth 4']);
+        $this->table->save($entity);
+        $entity = $this->table->get(14);
+        $this->assertEquals(4, $entity->level);
+    }
+
+    /**
+     * Test setting level for existing nodes
+     *
+     * @return void
+     */
+    public function testSetLevelExistingNode()
+    {
+        $this->table->behaviors()->Tree->config('level', 'level');
+
+        // Leaf node
+        $entity = $this->table->get(4);
+        $this->assertEquals(2, $entity->level);
+        $this->table->save($entity);
+        $entity = $this->table->get(4);
+        $this->assertEquals(2, $entity->level);
+
+        // Non leaf node so depth of descendents will also change
+        $entity = $this->table->get(6);
+        $this->assertEquals(1, $entity->level);
+
+        $entity->parent_id = null;
+        $this->table->save($entity);
+        $entity = $this->table->get(6);
+        $this->assertEquals(0, $entity->level);
+
+        $entity = $this->table->get(7);
+        $this->assertEquals(1, $entity->level);
+
+        $entity = $this->table->get(8);
+        $this->assertEquals(2, $entity->level);
+    }
+
+    /**
      * Custom assertion use to verify tha a tree is returned in the expected order
      * and that it is still valid
      *