diff --git a/src/NestedSetsBehavior.php b/src/NestedSetsBehavior.php index 3ae9c10..c2a6698 100644 --- a/src/NestedSetsBehavior.php +++ b/src/NestedSetsBehavior.php @@ -809,15 +809,17 @@ public function leaves(): ActiveQuery * Sets the internal operation state to {@see self::OPERATION_MAKE_ROOT} and triggers the save process on the owner * model. * - * - If the attached {@see ActiveRecord} is a new record, this method creates it as the root node of a new tree. + * - If the attached {@see ActiveRecord} is a new record, this method creates it as the root node of a new tree, + * setting `left=1`, `right=2`, and `depth=0`. * - If the record already exists, it moves the node to become the root node, updating the nested set structure - * accordingly. + * accordingly and adjusting all affected nodes in the tree. * * This operation is essential for initializing or reorganizing hierarchical data structures, such as category * trees, where nodes can be promoted to root status or new trees can be started. * * The actual creation or movement is performed by saving the owner model, which triggers the appropriate lifecycle - * events and updates the tree structure. + * events and updates the tree structure. Upon successful save, the model is automatically refreshed to ensure all + * nested set attributes reflect their current database values. * * @param bool $runValidation Whether to perform validation before saving the record. * @param array|null $attributes List of attributes that need to be saved. Defaults to `null`, meaning all @@ -829,7 +831,13 @@ public function leaves(): ActiveQuery * * Usage example: * ```php + * // Create a new root node + * $category = new Category(['name' => 'Electronics']); * $category->makeRoot(); + * + * // Move existing node to become root + * $existingNode = Category::findOne(5); + * $existingNode->makeRoot(); * ``` * * @phpstan-param array|null $attributes @@ -838,7 +846,15 @@ public function makeRoot(bool $runValidation = true, array|null $attributes = nu { $this->operation = self::OPERATION_MAKE_ROOT; - return $this->getOwner()->save($runValidation, $attributes); + $owner = $this->getOwner(); + + $result = $owner->save($runValidation, $attributes); + + if ($result === true) { + $owner->refresh(); + } + + return $result; } /** diff --git a/tests/NestedSetsBehaviorTest.php b/tests/NestedSetsBehaviorTest.php index 6909613..5028c95 100644 --- a/tests/NestedSetsBehaviorTest.php +++ b/tests/NestedSetsBehaviorTest.php @@ -2322,4 +2322,135 @@ public function testChildrenMethodRequiresOrderByForCorrectTreeTraversal(): void } } } + + public function testMakeRootRefreshIsNecessaryForCorrectAttributeValues(): void + { + $this->createDatabase(); + + $root = new MultipleTree(['name' => 'Original Root']); + + $root->makeRoot(); + + $child1 = new MultipleTree(['name' => 'Child 1']); + + $child1->appendTo($root); + + $child2 = new MultipleTree(['name' => 'Child 2']); + + $child2->appendTo($root); + + $grandchild = new MultipleTree(['name' => 'Grandchild']); + + $grandchild->appendTo($child1); + + $nodeToPromote = MultipleTree::findOne($child1->id); + + self::assertNotNull( + $nodeToPromote, + 'Child node should exist before promoting to root.', + ); + self::assertFalse( + $nodeToPromote->isRoot(), + "Node should not be root before 'makeRoot()' operation.", + ); + + $originalLeft = $nodeToPromote->getAttribute('lft'); + $originalRight = $nodeToPromote->getAttribute('rgt'); + $originalDepth = $nodeToPromote->getAttribute('depth'); + $originalTree = $nodeToPromote->getAttribute('tree'); + + $result = $nodeToPromote->makeRoot(); + + self::assertTrue( + $result, + "'makeRoot()' should return 'true' when converting node to root.", + ); + self::assertTrue( + $nodeToPromote->isRoot(), + "Node should be identified as root after 'makeRoot()' - this requires 'refresh()' to work.", + ); + self::assertEquals( + 1, + $nodeToPromote->getAttribute('lft'), + "Root node left value should be '1' after 'makeRoot()' - requires 'refresh()' to see updated value.", + ); + self::assertEquals( + 4, + $nodeToPromote->getAttribute('rgt'), + "Root node right value should be '4' after 'makeRoot()' - requires 'refresh()' to see updated value.", + ); + self::assertEquals( + 0, + $nodeToPromote->getAttribute('depth'), + "Root node depth should be '0' after 'makeRoot()' - requires 'refresh()' to see updated value.", + ); + self::assertEquals( + $nodeToPromote->getAttribute('id'), + $nodeToPromote->getAttribute('tree'), + "Tree attribute should equal node ID for new root - requires 'refresh()' to see updated value.", + ); + self::assertNotEquals( + $originalLeft, + $nodeToPromote->getAttribute('lft'), + "Left value should have changed from original after 'makeRoot()'.", + ); + self::assertNotEquals( + $originalRight, + $nodeToPromote->getAttribute('rgt'), + "Right value should have changed from original after 'makeRoot()'.", + ); + self::assertNotEquals( + $originalDepth, + $nodeToPromote->getAttribute('depth'), + "Depth should have changed from original after 'makeRoot()'.", + ); + self::assertNotEquals( + $originalTree, + $nodeToPromote->getAttribute('tree'), + "Tree should have changed from original after 'makeRoot()'.", + ); + + $grandchildAfter = MultipleTree::findOne($grandchild->id); + + self::assertNotNull( + $grandchildAfter, + "'Grandchild' should still exist after parent became root.", + ); + self::assertEquals( + $nodeToPromote->getAttribute('tree'), + $grandchildAfter->getAttribute('tree'), + "'Grandchild' should be in the same tree as the new root.", + ); + self::assertEquals( + 1, + $grandchildAfter->getAttribute('depth'), + "'Grandchild' depth should be recalculated relative to new root.", + ); + + $reloadedNode = MultipleTree::findOne($nodeToPromote->id); + + self::assertNotNull( + $reloadedNode, + "Node should exist in database after 'makeRoot()'.", + ); + self::assertTrue( + $reloadedNode->isRoot(), + 'Reloaded node should be root.', + ); + self::assertEquals( + 1, + $reloadedNode->getAttribute('lft'), + "Reloaded node should have 'left=1'.", + ); + self::assertEquals( + 4, + $reloadedNode->getAttribute('rgt'), + "Reloaded node should have 'right=4'.", + ); + self::assertEquals( + 0, + $reloadedNode->getAttribute('depth'), + "Reloaded node should have 'depth=0'.", + ); + } }