From 17264790d762e89189819ebc3cbdea5ef05ec22a Mon Sep 17 00:00:00 2001 From: ADmad Date: Sat, 7 Feb 2015 19:43:31 +0530 Subject: [PATCH] Allow setting level (depth) of tree nodes on save. --- src/ORM/Behavior/TreeBehavior.php | 82 ++++++++++++++++++- tests/Fixture/NumberTreesFixture.php | 34 +++++--- .../ORM/Behavior/TreeBehaviorTest.php | 59 ++++++++++++- 3 files changed, 161 insertions(+), 14 deletions(-) diff --git a/src/ORM/Behavior/TreeBehavior.php b/src/ORM/Behavior/TreeBehavior.php index 1de5a06e305..c764a7300ab 100644 --- a/src/ORM/Behavior/TreeBehavior.php +++ b/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 @@ public function beforeSave(Event $event, Entity $entity) $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 @@ public function beforeSave(Event $event, Entity $entity) $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 + * @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 @@ protected function _getNode($id) $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(); diff --git a/tests/Fixture/NumberTreesFixture.php b/tests/Fixture/NumberTreesFixture.php index 07e2ffda996..712e94c11df 100644 --- a/tests/Fixture/NumberTreesFixture.php +++ b/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 ] ]; } diff --git a/tests/TestCase/ORM/Behavior/TreeBehaviorTest.php b/tests/TestCase/ORM/Behavior/TreeBehaviorTest.php index 74defa82f28..ba41d918079 100644 --- a/tests/TestCase/ORM/Behavior/TreeBehaviorTest.php +++ b/tests/TestCase/ORM/Behavior/TreeBehaviorTest.php @@ -502,7 +502,7 @@ public function testAddOrphan() { $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(); @@ -878,6 +878,63 @@ public function testGetLevel() $this->assertFalse($result); } + /** + * 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