diff --git a/.styleci.yml b/.styleci.yml index 9fae20e..ac75a55 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -85,3 +85,5 @@ enabled: disabled: - function_declaration - new_with_parentheses + - psr12_braces + - psr12_class_definition diff --git a/src/NestedSetsBehavior.php b/src/NestedSetsBehavior.php index 5f12921..6b2ed20 100644 --- a/src/NestedSetsBehavior.php +++ b/src/NestedSetsBehavior.php @@ -5,8 +5,11 @@ namespace yii2\extensions\nestedsets; use LogicException; +use RuntimeException; use yii\base\{Behavior, NotSupportedException}; -use yii\db\{ActiveQuery, ActiveRecord, Exception, Expression}; +use yii\db\{ActiveQuery, ActiveRecord, Connection, Exception, Expression}; + +use function sprintf; /** * Nested set behavior for managing hierarchical data in {@see ActiveRecord} models. @@ -119,6 +122,14 @@ class NestedSetsBehavior extends Behavior */ public string|false $treeAttribute = false; + /** + * Database connection instance used for executing queries. + * + * This property is used to access the database connection associated with the attached {@see ActiveRecord} model, + * allowing the behavior to perform database operations such as updates and queries. + */ + private Connection|null $db = null; + /** * Handles post-deletion updates for the nested set structure. * @@ -247,22 +258,20 @@ public function afterInsert(): void */ public function afterUpdate(): void { - match (true) { - $this->operation === self::OPERATION_APPEND_TO && $this->node !== null => - $this->moveNode($this->node, $this->node->getAttribute($this->rightAttribute), 1), - $this->operation === self::OPERATION_INSERT_AFTER && $this->node !== null => - $this->moveNode($this->node, $this->node->getAttribute($this->rightAttribute) + 1, 0), - $this->operation === self::OPERATION_INSERT_BEFORE && $this->node !== null => - $this->moveNode($this->node, $this->node->getAttribute($this->leftAttribute), 0), - $this->operation === self::OPERATION_MAKE_ROOT => - $this->moveNodeAsRoot(), - $this->operation === self::OPERATION_PREPEND_TO && $this->node !== null => - $this->moveNode($this->node, $this->node->getAttribute($this->leftAttribute) + 1, 1), - default => null, - }; + $currentOwnerTreeValue = $this->getTreeValue($this->getOwner()); - $this->operation = null; - $this->node = null; + if ($this->operation === self::OPERATION_MAKE_ROOT) { + $this->moveNodeAsRoot($currentOwnerTreeValue); + return; + } + + if ($this->node === null) { + return; + } + + $context = $this->createMoveContext($this->node, $this->operation); + + $this->moveNode($context); } /** @@ -289,6 +298,7 @@ public function afterUpdate(): void * $category->appendTo($parentCategory); * ``` * + * @phpstan-param T $node * @phpstan-param array|null $attributes */ public function appendTo(ActiveRecord $node, bool $runValidation = true, array|null $attributes = null): bool @@ -367,23 +377,23 @@ public function beforeDelete(): void */ public function beforeInsert(): void { + if ($this->node?->getIsNewRecord() === true) { + throw new Exception('Can not create a node when the target node is new record.'); + } + match (true) { - $this->operation === self::OPERATION_APPEND_TO && $this->node !== null => $this->beforeInsertNode( - $this->node->getAttribute($this->rightAttribute), - 1, + $this->operation === self::OPERATION_APPEND_TO && $this->node !== null => $this->beforeInsertNodeWithContext( + NodeContext::forAppendTo($this->node, $this->rightAttribute), ), - $this->operation === self::OPERATION_INSERT_AFTER && $this->node !== null => $this->beforeInsertNode( - $this->node->getAttribute($this->rightAttribute) + 1, - 0, + $this->operation === self::OPERATION_INSERT_AFTER && $this->node !== null => $this->beforeInsertNodeWithContext( + NodeContext::forInsertAfter($this->node, $this->rightAttribute), ), - $this->operation === self::OPERATION_INSERT_BEFORE && $this->node !== null => $this->beforeInsertNode( - $this->node->getAttribute($this->leftAttribute), - 0, + $this->operation === self::OPERATION_INSERT_BEFORE && $this->node !== null => $this->beforeInsertNodeWithContext( + NodeContext::forInsertBefore($this->node, $this->leftAttribute), ), $this->operation === self::OPERATION_MAKE_ROOT => $this->beforeInsertRootNode(), - $this->operation === self::OPERATION_PREPEND_TO && $this->node !== null => $this->beforeInsertNode( - $this->node->getAttribute($this->leftAttribute) + 1, - 1, + $this->operation === self::OPERATION_PREPEND_TO && $this->node !== null => $this->beforeInsertNodeWithContext( + NodeContext::forPrependTo($this->node, $this->leftAttribute), ), default => throw new NotSupportedException( 'Method "' . get_class($this->getOwner()) . '::insert" is not supported for inserting new nodes.', @@ -1056,25 +1066,17 @@ protected function applyTreeAttributeCondition(array &$condition): void * This method is called internally before inserting a node as a child or sibling, ensuring the nested set * attributes are correctly initialized. * - * @param int|null $value Left attribute value for the new node, or `null` if not applicable. + * @param int $value Left attribute value for the new node, or `null` if not applicable. * @param int $depth Depth offset relative to the target node (`0` for sibling, `1` for child). * * @throws Exception if an unexpected error occurs during execution. */ - protected function beforeInsertNode(int|null $value, int $depth): void + protected function beforeInsertNode(int $value, int $depth): void { - if ($this->node?->getIsNewRecord() === true) { - throw new Exception('Can not create a node when the target node is new record.'); - } - if ($depth === 0 && $this->node?->isRoot() === true) { throw new Exception('Can not create a node when the target node is root.'); } - if ($value === null) { - throw new Exception('Value cannot be \'null\' in \'beforeInsertNode()\' method.'); - } - $this->getOwner()->setAttribute($this->leftAttribute, $value); $this->getOwner()->setAttribute($this->rightAttribute, $value + 1); @@ -1159,49 +1161,32 @@ protected function deleteWithChildrenInternal(): bool|int } /** - * Moves the current node to a new position within the nested set tree structure. - * - * Updates the left, right, and depth attributes of the node and its descendants to reflect the new position, - * ensuring the integrity of the tree after the move operation. - * - * This method supports both single-tree and multi-tree configurations, handling node movement within the same tree - * or across different trees as needed. - * - * The method performs the following operations. - * - Adjusts left and right boundaries for affected nodes. - * - Handles tree attribute updates when moving nodes across trees. - * - Shifts left and right attribute values to make space for the moved node and its subtree. - * - Updates the depth of the node and its descendants based on the new position. + * Executes node movement using the provided context. * - * This method is called internally during node movement operations such as append, prepend, insert before/after, - * and supports both root and non-root node moves. + * Handles both same-tree and cross-tree movements, determining the strategy based on tree attribute configuration + * and tree values from the context. * - * @param ActiveRecord $node Node to be moved within the nested set tree. - * @param int $value Left attribute value indicating the new position for the node. - * @param int $depth Depth offset to apply to the node and its descendants after the move. + * @param NodeContext $context Immutable context containing all movement data. */ - protected function moveNode(ActiveRecord $node, int $value, int $depth): void + protected function moveNode(NodeContext $context): void { - $db = $this->getOwner()::getDb(); - $leftValue = $this->getOwner()->getAttribute($this->leftAttribute); - $rightValue = $this->getOwner()->getAttribute($this->rightAttribute); - $depthValue = $this->getOwner()->getAttribute($this->depthAttribute); - $depthAttribute = $db->quoteColumnName($this->depthAttribute); - $nodeDepthValue = $node->getAttribute($this->depthAttribute); + $currentOwnerTreeValue = $this->getTreeValue($this->getOwner()); + $targetNodeTreeValue = $context->getTargetTreeValue($this->treeAttribute); + $targetNodeDepthValue = $context->getTargetDepth($this->depthAttribute); + $ownerDepthValue = $this->getOwner()->getAttribute($this->depthAttribute); + $ownerLeftValue = $this->getOwner()->getAttribute($this->leftAttribute); + $ownerRightValue = $this->getOwner()->getAttribute($this->rightAttribute); - $depth = $nodeDepthValue - $depthValue + $depth; + $depthOffset = $targetNodeDepthValue - $ownerDepthValue + $context->depthLevelDelta; - if ( - $this->treeAttribute === false || - $this->getOwner()->getAttribute($this->treeAttribute) === $node->getAttribute($this->treeAttribute) - ) { - $delta = $rightValue - $leftValue + 1; + if ($this->treeAttribute === false || $targetNodeTreeValue === $currentOwnerTreeValue) { + $subtreeSize = $ownerRightValue - $ownerLeftValue + 1; - $this->shiftLeftRightAttribute($value, $delta); + $this->shiftLeftRightAttribute($context->targetPositionValue, $subtreeSize); - if ($leftValue >= $value) { - $leftValue += $delta; - $rightValue += $delta; + if ($ownerLeftValue >= $context->targetPositionValue) { + $ownerLeftValue += $subtreeSize; + $ownerRightValue += $subtreeSize; } $condition = [ @@ -1209,19 +1194,21 @@ protected function moveNode(ActiveRecord $node, int $value, int $depth): void [ '>=', $this->leftAttribute, - $leftValue, + $ownerLeftValue, ], [ '<=', $this->rightAttribute, - $rightValue, + $ownerRightValue, ], ]; $this->applyTreeAttributeCondition($condition); $this->getOwner()::updateAll( [ - $this->depthAttribute => new Expression($depthAttribute . sprintf('%+d', $depth)), + $this->depthAttribute => new Expression( + $this->getDb()->quoteColumnName($this->depthAttribute) . sprintf('%+d', $depthOffset), + ), ], $condition, ); @@ -1231,11 +1218,11 @@ protected function moveNode(ActiveRecord $node, int $value, int $depth): void 'and', [ '>=', - $attribute, $leftValue, + $attribute, $ownerLeftValue, ], [ '<=', - $attribute, $rightValue, + $attribute, $ownerRightValue, ], ]; @@ -1243,24 +1230,21 @@ protected function moveNode(ActiveRecord $node, int $value, int $depth): void $this->getOwner()::updateAll( [ $attribute => new Expression( - $db->quoteColumnName($attribute) . sprintf('%+d', $value - $leftValue), + $this->getDb()->quoteColumnName($attribute) . + sprintf('%+d', $context->targetPositionValue - $ownerLeftValue), ), ], $condition, ); } - $this->shiftLeftRightAttribute($rightValue, -$delta); + $this->shiftLeftRightAttribute($ownerRightValue, -$subtreeSize); } else { - $leftAttribute = $db->quoteColumnName($this->leftAttribute); - $rightAttribute = $db->quoteColumnName($this->rightAttribute); - $nodeRootValue = $node->getAttribute($this->treeAttribute); - foreach ([$this->leftAttribute, $this->rightAttribute] as $attribute) { $this->getOwner()::updateAll( [ $attribute => new Expression( - $db->quoteColumnName($attribute) . sprintf('%+d', $rightValue - $leftValue + 1), + $this->getDb()->quoteColumnName($attribute) . sprintf('%+d', $ownerRightValue - $ownerLeftValue + 1), ), ], [ @@ -1268,42 +1252,24 @@ protected function moveNode(ActiveRecord $node, int $value, int $depth): void [ '>=', $attribute, - $value, + $context->targetPositionValue, ], [ - $this->treeAttribute => $nodeRootValue, + $this->treeAttribute => $targetNodeTreeValue, ], ], ); } - $delta = $value - $leftValue; - - $this->getOwner()::updateAll( - [ - $this->leftAttribute => new Expression($leftAttribute . sprintf('%+d', $delta)), - $this->rightAttribute => new Expression($rightAttribute . sprintf('%+d', $delta)), - $this->depthAttribute => new Expression($depthAttribute . sprintf('%+d', $depth)), - $this->treeAttribute => $nodeRootValue, - ], - [ - 'and', - [ - '>=', - $this->leftAttribute, - $leftValue, - ], - [ - '<=', - $this->rightAttribute, - $rightValue, - ], - [ - $this->treeAttribute => $this->getOwner()->getAttribute($this->treeAttribute), - ], - ], + $this->moveSubtreeToTargetTree( + $targetNodeTreeValue, + $currentOwnerTreeValue, + $depthOffset, + $ownerLeftValue, + $context->targetPositionValue - $ownerLeftValue, + $ownerRightValue, ); - $this->shiftLeftRightAttribute($rightValue, $leftValue - $rightValue - 1); + $this->shiftLeftRightAttribute($ownerRightValue, $ownerLeftValue - $ownerRightValue - 1); } } @@ -1324,40 +1290,23 @@ protected function moveNode(ActiveRecord $node, int $value, int $depth): void * - Shifts left and right boundaries of the node and its descendants to start from 1. * - Shifts left/right values of remaining nodes to close the gap left by the moved subtree. * - Updates the tree attribute to the new root identifier if multi-tree is enabled. + * + * @param mixed $treeValue Tree attribute value to which the node will be moved, or `false` if not applicable. */ - protected function moveNodeAsRoot(): void + protected function moveNodeAsRoot(mixed $treeValue): void { - $db = $this->getOwner()::getDb(); + $depthValue = $this->getOwner()->getAttribute($this->depthAttribute); $leftValue = $this->getOwner()->getAttribute($this->leftAttribute); + $nodeRootValue = $this->getOwner()->getPrimaryKey(); $rightValue = $this->getOwner()->getAttribute($this->rightAttribute); - $depthValue = $this->getOwner()->getAttribute($this->depthAttribute); - $treeValue = $this->treeAttribute !== false ? $this->getOwner()->getAttribute($this->treeAttribute) : null; - $leftAttribute = $db->quoteColumnName($this->leftAttribute); - $rightAttribute = $db->quoteColumnName($this->rightAttribute); - $depthAttribute = $db->quoteColumnName($this->depthAttribute); - $this->getOwner()::updateAll( - [ - $this->leftAttribute => new Expression($leftAttribute . sprintf('%+d', 1 - $leftValue)), - $this->rightAttribute => new Expression($rightAttribute . sprintf('%+d', 1 - $leftValue)), - $this->depthAttribute => new Expression($depthAttribute . sprintf('%+d', -$depthValue)), - $this->treeAttribute => $this->getOwner()->getPrimaryKey(), - ], - [ - 'and', - [ - '>=', - $this->leftAttribute, - $leftValue, - ], - [ - '<=', - $this->rightAttribute, - $rightValue, - ], - [ - $this->treeAttribute => $treeValue, - ], - ], + + $this->moveSubtreeToTargetTree( + $nodeRootValue, + $treeValue, + -$depthValue, + $leftValue, + 1 - $leftValue, + $rightValue, ); $this->shiftLeftRightAttribute($rightValue, $leftValue - $rightValue - 1); } @@ -1379,21 +1328,74 @@ protected function moveNodeAsRoot(): void */ protected function shiftLeftRightAttribute(int $value, int $delta): void { - $db = $this->getOwner()::getDb(); - foreach ([$this->leftAttribute, $this->rightAttribute] as $attribute) { $condition = ['>=', $attribute, $value]; $this->applyTreeAttributeCondition($condition); $this->getOwner()::updateAll( [ - $attribute => new Expression($db->quoteColumnName($attribute) . sprintf('%+d', $delta)), + $attribute => new Expression($this->getDb()->quoteColumnName($attribute) . sprintf('%+d', $delta)), ], $condition, ); } } + /** + * Prepares the current node for insertion using a NodeContext. + * + * Sets the left, right, and depth attributes of the node to be inserted, based on the provided context which + * encapsulates the target node, operation type, and calculated values. + * + * This method delegates to {@see beforeInsertNode()} using the values from the NodeContext, ensuring consistency + * and reducing code duplication. + * + * @param NodeContext $context Immutable context containing all necessary data for node insertion. + * + * @throws Exception if an unexpected error occurs during execution. + */ + private function beforeInsertNodeWithContext(NodeContext $context): void + { + $this->beforeInsertNode($context->targetPositionValue, $context->depthLevelDelta); + } + + /** + * Creates a typed movement context based on operation and target node. + * + * @param ActiveRecord $targetNode Target node for the operation. + * @param string|null $operation Operation type to perform. + * + * @throws RuntimeException if a runtime error prevents the operation from completing successfully. + * + * @return NodeContext New instance with the specified parameters for the operation. + */ + private function createMoveContext(ActiveRecord $targetNode, string|null $operation): NodeContext + { + return match ($operation) { + self::OPERATION_APPEND_TO => NodeContext::forAppendTo($targetNode, $this->rightAttribute), + self::OPERATION_INSERT_AFTER => NodeContext::forInsertAfter($targetNode, $this->rightAttribute), + self::OPERATION_INSERT_BEFORE => NodeContext::forInsertBefore($targetNode, $this->leftAttribute), + self::OPERATION_PREPEND_TO => NodeContext::forPrependTo($targetNode, $this->leftAttribute), + default => throw new RuntimeException("Unsupported operation: {$operation}"), + }; + } + + /** + * Retrieves and caches the {@see Connection} object associated with the owner model. + * + * The connection is resolved on first access and stored for subsequent calls to improve performance and avoid + * redundant lookups. + * + * This method is used internally by operations that require direct database access, such as bulk updates or + * structural modifications to the nested set tree. + * + * @return Connection Database connection instance for the owner model. + */ + private function getDb(): Connection + { + return $this->db ??= $this->getOwner()::getDb(); + } + /** * Returns the {@see ActiveRecord} instance to which this behavior is currently attached. * @@ -1417,4 +1419,84 @@ private function getOwner(): ActiveRecord return $this->owner; } + + /** + * Retrieves the tree attribute value from the specified {@see ActiveRecord} instance. + * + * Extracts the tree identifier value from the given model instance when multi-tree support is enabled, providing + * a centralized method for accessing tree attribute values throughout the behavior. + * + * The method is used internally by movement operations, tree validation, and condition building to ensure proper + * tree scoping and maintain data integrity across different tree contexts. + * + * @param ActiveRecord|null $activeRecord Model instance from which to extract the tree value, or `null` if not + * available. + * + * @return mixed Tree attribute value if multi-tree support is enabled and the record exists, `null` otherwise. + */ + private function getTreeValue(ActiveRecord|null $activeRecord): mixed + { + return $activeRecord !== null && $this->treeAttribute !== false + ? $activeRecord->getAttribute($this->treeAttribute) + : null; + } + + /** + * Moves a subtree to a different tree or position within a multi-tree nested set structure. + * + * Updates the left, right, and depth attributes, as well as the tree attribute, for all nodes in the specified + * subtree. + * + * This method is used internally when moving a node and its descendants across trees or to a new root, ensuring + * that all affected nodes are updated in a single bulk operation. + * + * This operation is essential for maintaining the integrity of the nested set structure when reorganizing nodes + * between trees or promoting a node to root in a multi-tree configuration. + * + * @param mixed $targetNodeTreeValue Value to assign to the tree attribute for all nodes in the moved subtree. + * @param mixed $currentOwnerTreeValue Current tree attribute value of the nodes being moved. + * @param int $depth Depth offset to apply to all nodes in the subtree. + * @param int $leftValue Left boundary value of the subtree to move. + * @param int $positionOffset Amount to shift left and right attribute values for the subtree. + * @param int $rightValue Right boundary value of the subtree to move. + */ + private function moveSubtreeToTargetTree( + mixed $targetNodeTreeValue, + mixed $currentOwnerTreeValue, + int $depth, + int $leftValue, + int $positionOffset, + int $rightValue, + ): void { + $this->getOwner()::updateAll( + [ + $this->leftAttribute => new Expression( + $this->getDb()->quoteColumnName($this->leftAttribute) . sprintf('%+d', $positionOffset), + ), + $this->rightAttribute => new Expression( + $this->getDb()->quoteColumnName($this->rightAttribute) . sprintf('%+d', $positionOffset), + ), + $this->depthAttribute => new Expression( + $this->getDb()->quoteColumnName($this->depthAttribute) . sprintf('%+d', $depth), + ), + $this->treeAttribute => $targetNodeTreeValue, + ], + [ + 'and', + [ + '>=', + $this->leftAttribute, + $leftValue, + ], + [ + '<=', + $this->rightAttribute, + $rightValue, + ], + [ + $this->treeAttribute => $currentOwnerTreeValue, + ], + ], + ); + } } diff --git a/src/NodeContext.php b/src/NodeContext.php new file mode 100644 index 0000000..211d79a --- /dev/null +++ b/src/NodeContext.php @@ -0,0 +1,142 @@ +getAttribute($rightAttribute), + depthLevelDelta: 1, + ); + } + + /** + * Creates context for insert-after operation (next sibling). + * + * @param ActiveRecord $targetNode Target node to insert after. + * @param string $rightAttribute Name of the right attribute. + * + * @return self New instance with the specified parameters for insert-after operation. + * + * @phpstan-param 'rgt' $rightAttribute + */ + public static function forInsertAfter(ActiveRecord $targetNode, string $rightAttribute): self + { + $rightValue = $targetNode->getAttribute($rightAttribute); + + return new self( + targetNode: $targetNode, + targetPositionValue: $rightValue + 1, + depthLevelDelta: 0, + ); + } + + /** + * Creates context for insert-before operation (previous sibling). + * + * @param ActiveRecord $targetNode Target node to insert before. + * @param string $leftAttribute Name of the left attribute. + * + * @return self New instance with the specified parameters for insert-before operation. + * + * @phpstan-param 'lft' $leftAttribute + */ + public static function forInsertBefore(ActiveRecord $targetNode, string $leftAttribute): self + { + $leftValue = $targetNode->getAttribute($leftAttribute); + + return new self( + targetNode: $targetNode, + targetPositionValue: $leftValue, + depthLevelDelta: 0, + ); + } + + /** + * Creates context for prepend-to operation (first child). + * + * @param ActiveRecord $targetNode Target node to prepend to. + * @param string $leftAttribute Name of the left attribute. + * + * @return self New instance with the specified parameters for prepend-to operation. + * + * @phpstan-param 'lft' $leftAttribute + */ + public static function forPrependTo(ActiveRecord $targetNode, string $leftAttribute): self + { + $leftValue = $targetNode->getAttribute($leftAttribute); + + return new self( + targetNode: $targetNode, + targetPositionValue: $leftValue + 1, + depthLevelDelta: 1, + ); + } + + /** + * Returns the depth value of the target node. + * + * @param string $depthAttribute Name of the depth attribute. + * + * @return int Depth value of the target node. + * + * @phpstan-param 'depth' $depthAttribute + */ + public function getTargetDepth(string $depthAttribute): int + { + return $this->targetNode->getAttribute($depthAttribute); + } + + /** + * Returns the tree attribute value of the target node. + * + * @param false|string $treeAttribute Name of the tree attribute or `false` if disabled. + * + * @return mixed Tree attribute value or `null` if tree attribute is disabled. + */ + public function getTargetTreeValue(string|false $treeAttribute): mixed + { + return $treeAttribute !== false + ? $this->targetNode->getAttribute($treeAttribute) + : null; + } +} diff --git a/tests/NestedSetsBehaviorTest.php b/tests/NestedSetsBehaviorTest.php index 59a5c42..2c3a5e3 100644 --- a/tests/NestedSetsBehaviorTest.php +++ b/tests/NestedSetsBehaviorTest.php @@ -1601,7 +1601,7 @@ public function testSetNodeToNullAndCallBeforeInsertNodeSetsLftRgtAndDepth(): vo $this->createDatabase(); $behavior = new class extends NestedSetsBehavior { - public function callBeforeInsertNode(int|null $value, int $depth): void + public function callBeforeInsertNode(int $value, int $depth): void { $this->beforeInsertNode($value, $depth); } @@ -1741,23 +1741,6 @@ public function testReturnShiftedLeftRightAttributesWhenChildAppendedToRoot(): v ); } - public function testThrowExceptionWhenAppendToParentWithNullRightValue(): void - { - $this->createDatabase(); - - $parentNode = new Tree(['name' => 'Parent Node']); - - $parentNode->makeRoot(); - $parentNode->setAttribute('rgt', null); - - $childNode = new Tree(['name' => 'Child Node']); - - $this->expectException(Exception::class); - $this->expectExceptionMessage("Value cannot be 'null' in 'beforeInsertNode()' method."); - - $childNode->appendTo($parentNode); - } - public function testAppendToWithRunValidationParameterUsingStrictValidation(): void { $this->generateFixtureTree(); diff --git a/tests/support/stub/ExtendableNestedSetsBehavior.php b/tests/support/stub/ExtendableNestedSetsBehavior.php index 9f56be0..7f43a5f 100644 --- a/tests/support/stub/ExtendableNestedSetsBehavior.php +++ b/tests/support/stub/ExtendableNestedSetsBehavior.php @@ -29,7 +29,7 @@ public function exposedApplyTreeAttributeCondition(array &$condition): void $this->applyTreeAttributeCondition($condition); } - public function exposedBeforeInsertNode(int|null $value, int $depth): void + public function exposedBeforeInsertNode(int $value, int $depth): void { $this->calledMethods['beforeInsertNode'] = true; @@ -54,14 +54,20 @@ public function exposedMoveNode(ActiveRecord $node, int $value, int $depth): voi { $this->calledMethods['moveNode'] = true; - $this->moveNode($node, $value, $depth); + // Create a mock context for testing compatibility + $context = new \yii2\extensions\nestedsets\NodeContext( + $node, + 0, + 0, + ); + $this->moveNode($context); } public function exposedMoveNodeAsRoot(): void { $this->calledMethods['moveNodeAsRoot'] = true; - $this->moveNodeAsRoot(); + $this->moveNodeAsRoot(null); } public function exposedShiftLeftRightAttribute(int $value, int $delta): void