Browse Source

Starting to implement Tree saving operations

Jose Lorenzo Rodriguez 12 years ago
parent
commit
c70718b99b

+ 92 - 0
src/Model/Behavior/TreeBehavior.php

@@ -61,6 +61,98 @@ class TreeBehavior extends Behavior {
 		$this->_table = $table;
 	}
 
+/**
+ * Before save listener.
+ * Transparently managse setting the lft and rght fields if the parent field is
+ * included in the parameters to be saved.
+ *
+ * @param \Cake\Event\Event the beforeSave event that was fired
+ * @param \Cake\ORM\Entity the entity that is going to be saved
+ * @return void
+ */
+	public function beforeSave(Event $event, Entity $entity) {
+		$isNew = $entity->isNew();
+		$config = $this->config('parent');
+		$parent = $entity->get($config['parent']);
+		$primaryKey = (array)$this->_table->primaryKey();
+		$dirty = $entity->dirty($config['parent']);
+
+		if ($isNew && $parent) {
+			if ($entity->get($primaryKey[0]) == $parent) {
+				throw new \RuntimeException("Cannot set a node's parent as itself");
+			}
+
+			$parentNode = $this->_getParent($parent);
+			$edge = $parentNode->get($config['right']);
+			$entity->set($config['left'], $edge);
+			$entity->set($config['right'], $edge + 1);
+			$this->_sync(2, '+', ">= {$config['right']}");
+		}
+
+		if ($isNew && !$parent) {
+			$edge = $this->_getMax();
+			$entity->set($config['left'],$edge + 1);
+			$entity->set($config['right'], $edge + 2);
+		}
+
+		if (!$isNew && $dirty && $parent) {
+			$this->_setParent($entity, $parent);
+		}
+	}
+
+	protected function _getParent($id) {
+		$config = $this->config();
+		$parentNode = $this->_scope($this->_table->find())
+			->select([$config['lft'], $config['rght']])
+			->where([$primaryKey[0] => $parent]);
+
+		if (!$parentNode) {
+			throw new \Cake\ORM\Error\RecordNotFoundException(
+				"Parent node \"{$parent}\ was not found in the tree."
+			);
+		}
+
+		return $parentNode;
+	}
+
+	protected function _setParent($entity, $parent) {
+		$config = $this->config();
+		$parentNode = $this->_getParent($parent);
+		$parentNodeLeft = $parentNode->get($config['left']);
+		$parentNoderight = $parentNode->get($config['right']);
+
+		$right = $entity->get($config['right']);
+		$left = $entity->get($config['left']);
+
+		$diff = $right - $left + 1;
+		$targetLeft = $parentRight - $diff;
+		$targetRight = $parentRight - 1;
+
+		// Values for moving to the left
+		$min = $parentRight;
+		$max = $left - 1;
+
+		if ($left < $targetLeft) {
+			//Moving to the right
+			$min = $right + 1;
+			$max = $parentRight - 1;
+			$diff *= -1;
+		}
+
+		$this->_sync($diff, '+', "BETWEEN {$min} AND {$max}");
+
+		if ($right - $left > 1) {
+			//Correcting internal subtree
+			$internalLeft = $left + 1;
+			$internalRight = $right - 1;
+			$this->_sync($targetLeft - $left, '+', "BETWEEN {$internalLeft} AND {$internalRight}");
+		}
+
+		//Allocating new position
+		$entity->set($config['left'], $targetLeft);
+		$entity->set($config['right'], $targetRight);
+	}
+
 	public function findPath($query, $options) {
 		if (empty($options['for'])) {
 			throw new \InvalidArgumentException("The 'for' key is required for find('path')");

+ 0 - 11
tests/Fixture/NumberTreeFixture.php

@@ -61,77 +61,66 @@ class NumberTreeFixture extends TestFixture {
  */
 	public $records = array(
 		array(
-			'id' => '1',
 			'name' => 'electronics',
 			'parent_id' => null,
 			'lft' => '1',
 			'rght' => '20'
 		),
 		array(
-			'id' => '2',
 			'name' => 'televisions',
 			'parent_id' => '1',
 			'lft' => '2',
 			'rght' => '9'
 		),
 		array(
-			'id' => '3',
 			'name' => 'tube',
 			'parent_id' => '2',
 			'lft' => '3',
 			'rght' => '4'
 		),
 		array(
-			'id' => '4',
 			'name' => 'lcd',
 			'parent_id' => '2',
 			'lft' => '5',
 			'rght' => '6'
 		),
 		array(
-			'id' => '5',
 			'name' => 'plasma',
 			'parent_id' => '2',
 			'lft' => '7',
 			'rght' => '8'
 		),
 		array(
-			'id' => '6',
 			'name' => 'portable',
 			'parent_id' => '1',
 			'lft' => '10',
 			'rght' => '19'
 		),
 		array(
-			'id' => '7',
 			'name' => 'mp3',
 			'parent_id' => '6',
 			'lft' => '11',
 			'rght' => '14'
 		),
 		array(
-			'id' => '8',
 			'name' => 'flash',
 			'parent_id' => '7',
 			'lft' => '12',
 			'rght' => '13'
 		),
 		array(
-			'id' => '9',
 			'name' => 'cd',
 			'parent_id' => '6',
 			'lft' => '15',
 			'rght' => '16'
 		),
 		array(
-			'id' => '10',
 			'name' => 'radios',
 			'parent_id' => '6',
 			'lft' => '17',
 			'rght' => '18'
 		),
 		array(
-			'id' => '11',
 			'name' => 'alien hardware',
 			'parent_id' => null,
 			'lft' => '21',

+ 22 - 0
tests/TestCase/Model/Behavior/TreeBehaviorTest.php

@@ -302,4 +302,26 @@ class TreeBehaviorTest extends TestCase {
 		$this->assertEquals($expected2, $result2);
 	}
 
+/**
+ * Tests adding a new orphan node
+ *
+ * @return void
+ */
+	public function testAddOrphan() {
+		$table = TableRegistry::get('NumberTrees');
+		$table->addBehavior('Tree');
+		$entity = new Entity(
+			['name' => 'New Orphan', 'parent_id' => null],
+			['markNew' => true]
+		);
+		$expected = $table->find()->order('lft')->hydrate(false)->toArray();
+		$this->assertSame($entity, $table->save($entity));
+		$this->assertEquals(23, $entity->lft);
+		$this->assertEquals(24, $entity->rght);
+
+		$expected[] = $entity->toArray();
+		$results = $table->find()->order('lft')->hydrate(false)->toArray();
+		$this->assertEquals($expected, $results);
+	}
+
 }