Skip to content

Commit 19fe168

Browse files
committed
Adds move tree to position
1 parent 38d630c commit 19fe168

File tree

3 files changed

+164
-24
lines changed

3 files changed

+164
-24
lines changed

src/NestedSetInterface.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,24 @@ public function findAncestors(Leaf $leaf);
7474
public function getLeaf($id, $revision_id);
7575

7676
/**
77-
* Moves a Leaf and its sub-tree under a new parent leaf.
77+
* Moves a Leaf and its sub-tree into a new position.
7878
*
79-
* @param \PNX\Tree\Leaf $parent
80-
* The parent leaf to move under.
79+
* @param int $newLeftPosition
80+
* The new left position for the leaf.
8181
* @param \PNX\Tree\Leaf $leaf
8282
* The leaf to move.
8383
*/
84-
public function moveSubTree(Leaf $parent, Leaf $leaf);
84+
public function moveSubTree($newLeftPosition, Leaf $leaf);
85+
86+
/**
87+
* Gets a leaf at a specified left position.
88+
*
89+
* @param int $left
90+
* The left position.
91+
*
92+
* @return Leaf
93+
* The leaf.
94+
*/
95+
public function getLeafAtPosition($left);
8596

8697
}

src/Storage/DbalNestedSet.php

Lines changed: 103 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,7 @@ public function addLeaf(Leaf $parent, Leaf $child) {
3737
try {
3838
$this->connection->beginTransaction();
3939

40-
if ($parent->getRight() - $parent->getLeft() === 1) {
41-
// We are on a leaf node.
42-
$right = $parent->getLeft();
43-
$depth = $parent->getDepth() + 1;
44-
}
45-
else {
46-
// Find right most child.
47-
/** @var Leaf $rightChild */
48-
$rightChild = $this->findRightMostChild($parent);
49-
$right = $rightChild->getRight();
50-
$depth = $rightChild->getDepth();
51-
}
40+
list($right, $depth) = $this->getInsertionPosition($parent);
5241

5342
// Move everything across two places.
5443
$this->connection->executeUpdate('UPDATE tree SET nested_right = nested_right + 2 WHERE nested_right > ?',
@@ -183,7 +172,7 @@ public function deleteLeaf(Leaf $leaf) {
183172
$this->connection->beginTransaction();
184173

185174
// Delete the leaf.
186-
$this->connection->executeUpdate('DELETE from tree WHERE nested_left = ?',
175+
$this->connection->executeUpdate('DELETE FROM tree WHERE nested_left = ?',
187176
[$left]
188177
);
189178

@@ -225,7 +214,7 @@ public function deleteSubTree(Leaf $leaf) {
225214
$this->connection->beginTransaction();
226215

227216
// Delete the leaf.
228-
$this->connection->executeUpdate('DELETE from tree WHERE nested_left BETWEEN ? AND ?',
217+
$this->connection->executeUpdate('DELETE FROM tree WHERE nested_left BETWEEN ? AND ?',
229218
[$left, $right]
230219
);
231220

@@ -249,8 +238,106 @@ public function deleteSubTree(Leaf $leaf) {
249238
/**
250239
* {@inheritdoc}
251240
*/
252-
public function moveSubTree(Leaf $parent, Leaf $leaf) {
253-
// TODO: Implement moveSubTree() method.
241+
public function moveSubTree($newLeftPostion, Leaf $leaf) {
242+
243+
try {
244+
// Calculate position adjustment variables.
245+
$width = $leaf->getRight() - $leaf->getLeft() + 1;
246+
$distance = $newLeftPostion - $leaf->getLeft();
247+
$tempPos = $leaf->getLeft();
248+
249+
$this->connection->beginTransaction();
250+
251+
// Calculate depth difference.
252+
$newLeaf = $this->getLeafAtPosition($newLeftPostion);
253+
$depthDiff = $newLeaf->getDepth() - $leaf->getDepth();
254+
255+
// Backwards movement must account for new space.
256+
if ($distance < 0) {
257+
$distance -= $width;
258+
$tempPos += $width;
259+
}
260+
261+
// Create new space for subtree.
262+
$this->connection->executeUpdate('UPDATE tree SET nested_left = nested_left + ? WHERE nested_left >= ?',
263+
[$width, $newLeftPostion]
264+
);
265+
266+
$this->connection->executeUpdate('UPDATE tree SET nested_right = nested_right + ? WHERE nested_right >= ?',
267+
[$width, $newLeftPostion]
268+
);
269+
270+
// Move subtree into new space.
271+
$this->connection->executeUpdate('UPDATE tree SET nested_left = nested_left + ?, nested_right = nested_right + ?, depth = depth + ? WHERE nested_left >= ? AND nested_right < ?',
272+
[$distance, $distance, $depthDiff, $tempPos, $tempPos + $width]
273+
);
274+
275+
// Remove old space vacated by subtree.
276+
$this->connection->executeUpdate('UPDATE tree SET nested_left = nested_left - ? WHERE nested_left > ?',
277+
[$width, $leaf->getRight()]
278+
);
279+
280+
$this->connection->executeUpdate('UPDATE tree SET nested_right = nested_right - ? WHERE nested_right > ?',
281+
[$width, $leaf->getRight()]
282+
);
283+
}
284+
catch (Exception $e) {
285+
$this->connection->rollBack();
286+
throw $e;
287+
}
288+
289+
}
290+
291+
/**
292+
* Determines if this leaf is a 'leaf', i.e. has no children.
293+
*
294+
* @param \PNX\Tree\Leaf $leaf
295+
* The leaf to check.
296+
*
297+
* @return bool
298+
* TRUE if there are no children. FALSE otherwise.
299+
*/
300+
protected function isLeaf(Leaf $leaf) {
301+
return $leaf->getRight() - $leaf->getLeft() === 1;
302+
}
303+
304+
/**
305+
* {@inheritdoc}
306+
*/
307+
public function getLeafAtPosition($left) {
308+
$result = $this->connection->fetchAssoc("SELECT id, revision_id, nested_left, nested_right, depth FROM tree WHERE nested_left = ?",
309+
[$left]
310+
);
311+
if ($result) {
312+
return new Leaf($result['id'], $result['revision_id'], $result['nested_left'], $result['nested_right'], $result['depth']);
313+
}
314+
}
315+
316+
/**
317+
* Gets the insertion position under the given parent.
318+
*
319+
* Takes into account if the parent has no children.
320+
*
321+
* @param \PNX\Tree\Leaf $parent
322+
* The parent leaf.
323+
*
324+
* @return int[]
325+
* The right and depth postiions.
326+
*/
327+
protected function getInsertionPosition(Leaf $parent) {
328+
if ($this->isLeaf($parent)) {
329+
// We are on a leaf node.
330+
$right = $parent->getLeft();
331+
$depth = $parent->getDepth() + 1;
332+
}
333+
else {
334+
// Find right most child.
335+
/** @var Leaf $rightChild */
336+
$rightChild = $this->findRightMostChild($parent);
337+
$right = $rightChild->getRight();
338+
$depth = $rightChild->getDepth();
339+
}
340+
return [$right, $depth];
254341
}
255342

256343
}

tests/Functional/DbalNestedSetTest.php

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,9 @@ public function testDeleteLeaf() {
214214
}
215215

216216
/**
217-
* Tests deleting a leaf and its descendants.
217+
* Tests deleting a leaf and its sub-tree.
218218
*/
219-
public function testDeleteLeafAndDescendants() {
219+
public function testDeleteSubTree() {
220220

221221
$leaf = $this->nestedSet->getLeaf(4, 1);
222222

@@ -234,6 +234,48 @@ public function testDeleteLeafAndDescendants() {
234234
$this->assertNull($leaf);
235235
}
236236

237+
/**
238+
* Tests moving a sub-tree.
239+
*/
240+
public function testMoveSubTree() {
241+
print "BEFORE:" . PHP_EOL;
242+
$tree = $this->nestedSet->getTree();
243+
$this->printTree($tree);
244+
245+
$leaf = $this->nestedSet->getLeaf(7, 1);
246+
247+
$newPosition = 2;
248+
$this->nestedSet->moveSubTree($newPosition, $leaf);
249+
250+
print "AFTER:" . PHP_EOL;
251+
$tree = $this->nestedSet->getTree();
252+
$this->printTree($tree);
253+
254+
// Check leaf is in new position.
255+
$leaf = $this->nestedSet->getLeaf(7, 1);
256+
$this->assertEquals(2, $leaf->getLeft());
257+
$this->assertEquals(7, $leaf->getRight());
258+
$this->assertEquals(1, $leaf->getDepth());
259+
260+
// Check children are in new position.
261+
$leaf = $this->nestedSet->getLeaf(10, 1);
262+
$this->assertEquals(3, $leaf->getLeft());
263+
$this->assertEquals(4, $leaf->getRight());
264+
$this->assertEquals(2, $leaf->getDepth());
265+
266+
$leaf = $this->nestedSet->getLeaf(11, 1);
267+
$this->assertEquals(5, $leaf->getLeft());
268+
$this->assertEquals(6, $leaf->getRight());
269+
$this->assertEquals(2, $leaf->getDepth());
270+
271+
// Check old parent is updated.
272+
$leaf = $this->nestedSet->getLeaf(3, 1);
273+
$this->assertEquals(16, $leaf->getLeft());
274+
$this->assertEquals(21, $leaf->getRight());
275+
$this->assertEquals(1, $leaf->getDepth());
276+
277+
}
278+
237279
/**
238280
* Creates the table.
239281
*/
@@ -347,12 +389,12 @@ protected function loadTestData() {
347389
}
348390

349391
/**
350-
* Prints out a tree.
392+
* Prints out a tree to the console.
351393
*
352394
* @param array $tree
353395
* The tree to print.
354396
*/
355-
protected function printTree($tree) {
397+
public function printTree($tree) {
356398
$table = new Console_Table(CONSOLE_TABLE_ALIGN_RIGHT);
357399
$table->setHeaders(['ID', 'Rev', 'Left', 'Right', 'Depth']);
358400
$table->setAlign(0, CONSOLE_TABLE_ALIGN_LEFT);

0 commit comments

Comments
 (0)