From e767e589fbb2da1bda20415352561bc4b9d9c7b2 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Wed, 2 Jul 2025 19:00:29 -0400 Subject: [PATCH 1/6] refactor: Centralize query condition building in `QueryConditionBuilder`. --- src/NestedSetsBehavior.php | 421 ++++++++-------- src/QueryConditionBuilder.php | 449 ++++++++++++++++++ tests/NestedSetsBehaviorTest.php | 26 - .../stub/ExtendableNestedSetsBehavior.php | 10 - 4 files changed, 638 insertions(+), 268 deletions(-) create mode 100644 src/QueryConditionBuilder.php diff --git a/src/NestedSetsBehavior.php b/src/NestedSetsBehavior.php index 6b2ed20..2c02896 100644 --- a/src/NestedSetsBehavior.php +++ b/src/NestedSetsBehavior.php @@ -130,6 +130,21 @@ class NestedSetsBehavior extends Behavior */ private Connection|null $db = null; + /** + * Stores the depth value for the current operation. + */ + private int|null $depthValue = null; + + /** + * Stores the left value for the current operation. + */ + private int|null $leftValue = null; + + /** + * Stores the right value for the current operation. + */ + private int|null $rightValue = null; + /** * Handles post-deletion updates for the nested set structure. * @@ -149,45 +164,36 @@ class NestedSetsBehavior extends Behavior */ public function afterDelete(): void { - $leftValue = $this->getOwner()->getAttribute($this->leftAttribute); - $rightValue = $this->getOwner()->getAttribute($this->rightAttribute); - if ($this->operation === self::OPERATION_DELETE_WITH_CHILDREN || $this->getOwner()->isLeaf()) { - $this->shiftLeftRightAttribute($rightValue, $leftValue - $rightValue - 1); + $deltaValue = $this->getLeftValue() - $this->getRightValue() - 1; } else { - $condition = [ - 'and', - [ - '>=', - $this->leftAttribute, - $this->getOwner()->getAttribute($this->leftAttribute), - ], - [ - '<=', - $this->rightAttribute, - $this->getOwner()->getAttribute($this->rightAttribute), - ], - ]; - - $this->applyTreeAttributeCondition($condition); - $db = $this->getOwner()::getDb(); + $deltaValue = -2; + $condition = QueryConditionBuilder::createRangeCondition( + $this->leftAttribute, + $this->getLeftValue(), + $this->rightAttribute, + $this->getRightValue(), + $this->treeAttribute, + $this->getTreeValue($this->getOwner()), + ); $this->getOwner()::updateAll( [ $this->leftAttribute => new Expression( - $db->quoteColumnName($this->leftAttribute) . sprintf('%+d', -1), + $this->getDb()->quoteColumnName($this->leftAttribute) . sprintf('%+d', -1), ), $this->rightAttribute => new Expression( - $db->quoteColumnName($this->rightAttribute) . sprintf('%+d', -1), + $this->getDb()->quoteColumnName($this->rightAttribute) . sprintf('%+d', -1), ), $this->depthAttribute => new Expression( - $db->quoteColumnName($this->depthAttribute) . sprintf('%+d', -1), + $this->getDb()->quoteColumnName($this->depthAttribute) . sprintf('%+d', -1), ), ], $condition, ); - $this->shiftLeftRightAttribute($rightValue, -2); } + $this->shiftLeftRightAttribute($this->getRightValue(), $deltaValue); + $this->operation = null; $this->node = null; } @@ -216,15 +222,15 @@ public function afterInsert(): void $primaryKey = $this->getOwner()::primaryKey(); if (isset($primaryKey[0]) === false) { - throw new Exception('"' . get_class($this->getOwner()) . '" must have a primary key.'); + throw new Exception('"' . $this->getOwner()::class . '" must have a primary key.'); } $this->getOwner()::updateAll( [ - $this->treeAttribute => $this->getOwner()->getAttribute($this->treeAttribute), + $this->treeAttribute => $this->getTreeValue($this->getOwner()), ], [ - $primaryKey[0] => $this->getOwner()->getAttribute($this->treeAttribute), + $primaryKey[0] => $this->getTreeValue($this->getOwner()), ], ); } @@ -262,6 +268,7 @@ public function afterUpdate(): void if ($this->operation === self::OPERATION_MAKE_ROOT) { $this->moveNodeAsRoot($currentOwnerTreeValue); + return; } @@ -341,7 +348,7 @@ public function beforeDelete(): void if ($this->operation !== self::OPERATION_DELETE_WITH_CHILDREN && $this->getOwner()->isRoot()) { throw new NotSupportedException( - 'Method "' . get_class($this->getOwner()) . '::delete" is not supported for deleting root nodes.', + 'Method "' . $this->getOwner()::class . '::delete" is not supported for deleting root nodes.', ); } } @@ -396,7 +403,7 @@ public function beforeInsert(): void NodeContext::forPrependTo($this->node, $this->leftAttribute), ), default => throw new NotSupportedException( - 'Method "' . get_class($this->getOwner()) . '::insert" is not supported for inserting new nodes.', + 'Method "' . $this->getOwner()::class . '::insert" is not supported for inserting new nodes.', ), }; } @@ -486,27 +493,17 @@ public function beforeUpdate(): void */ public function children(int|null $depth = null): ActiveQuery { - $condition = [ - 'and', - [ - '>', - $this->leftAttribute, $this->getOwner()->getAttribute($this->leftAttribute), - ], - [ - '<', - $this->rightAttribute, $this->getOwner()->getAttribute($this->rightAttribute), - ], - ]; - - if ($depth !== null) { - $condition[] = [ - '<=', - $this->depthAttribute, - $this->getOwner()->getAttribute($this->depthAttribute) + $depth, - ]; - } - - $this->applyTreeAttributeCondition($condition); + $condition = QueryConditionBuilder::createChildrenCondition( + $this->leftAttribute, + $this->getLeftValue(), + $this->rightAttribute, + $this->getRightValue(), + $this->treeAttribute, + $this->getTreeValue($this->getOwner()), + $depth !== null ? $this->depthAttribute : null, + $depth !== null ? $this->getDepthValue() : null, + $depth + ); return $this->getOwner()::find()->andWhere($condition)->addOrderBy([$this->leftAttribute => SORT_ASC]); } @@ -541,7 +538,7 @@ public function deleteWithChildren(): bool|int return $this->deleteWithChildrenInternal(); } - $transaction = $this->getOwner()::getDb()->beginTransaction(); + $transaction = $this->getDb()->beginTransaction(); try { match ($result = $this->deleteWithChildrenInternal()) { @@ -622,6 +619,10 @@ public function events(): array */ public function insertAfter(ActiveRecord $node, bool $runValidation = true, array|null $attributes = null): bool { + if ($node->getIsNewRecord() === true) { + throw new Exception('Can not move a node when the target node is new record.'); + } + $this->operation = self::OPERATION_INSERT_AFTER; $this->node = $node; @@ -694,19 +695,15 @@ public function insertBefore(ActiveRecord $node, bool $runValidation = true, arr */ public function isChildOf(ActiveRecord $node): bool { - $owner = $this->getOwner(); - - $currentLeft = $owner->getAttribute($this->leftAttribute); - $currentRight = $owner->getAttribute($this->rightAttribute); $nodeLeft = $node->getAttribute($this->leftAttribute); $nodeRight = $node->getAttribute($this->rightAttribute); - if ($currentLeft <= $nodeLeft || $currentRight >= $nodeRight) { + if ($this->getLeftValue() <= $nodeLeft || $this->getRightValue() >= $nodeRight) { return false; } if ($this->treeAttribute !== false) { - return $owner->getAttribute($this->treeAttribute) === $node->getAttribute($this->treeAttribute); + return $this->getTreeValue($this->getOwner()) === $this->getTreeValue($node); } return true; @@ -735,8 +732,7 @@ public function isChildOf(ActiveRecord $node): bool */ public function isLeaf(): bool { - return $this->getOwner() - ->getAttribute($this->rightAttribute) - $this->getOwner()->getAttribute($this->leftAttribute) === 1; + return ($this->getRightValue() - $this->getLeftValue()) === 1; } /** @@ -791,24 +787,14 @@ public function isRoot(): bool */ public function leaves(): ActiveQuery { - $condition = [ - 'and', - [ - '>', - $this->leftAttribute, $this->getOwner()->getAttribute($this->leftAttribute), - ], - [ - '<', - $this->rightAttribute, $this->getOwner()->getAttribute($this->rightAttribute), - ], - [ - $this->rightAttribute => new Expression( - $this->getOwner()::getDb()->quoteColumnName($this->leftAttribute) . '+ 1', - ), - ], - ]; - - $this->applyTreeAttributeCondition($condition); + $condition = QueryConditionBuilder::createLeavesCondition( + $this->leftAttribute, + $this->rightAttribute, + $this->treeAttribute, + $this->getTreeValue($this->getOwner()), + $this->getLeftValue(), + $this->getRightValue(), + ); return $this->getOwner()::find()->andWhere($condition)->addOrderBy([$this->leftAttribute => SORT_ASC]); } @@ -856,12 +842,10 @@ public function makeRoot(bool $runValidation = true, array|null $attributes = nu { $this->operation = self::OPERATION_MAKE_ROOT; - $owner = $this->getOwner(); - - $result = $owner->save($runValidation, $attributes); + $result = $this->getOwner()->save($runValidation, $attributes); if ($result === true) { - $owner->refresh(); + $this->getOwner()->refresh(); } return $result; @@ -892,8 +876,12 @@ public function makeRoot(bool $runValidation = true, array|null $attributes = nu */ public function next(): ActiveQuery { - $condition = [$this->leftAttribute => $this->getOwner()->getAttribute($this->rightAttribute) + 1]; - $this->applyTreeAttributeCondition($condition); + $condition = QueryConditionBuilder::createNextSiblingCondition( + $this->leftAttribute, + $this->getRightValue(), + $this->treeAttribute, + $this->getTreeValue($this->getOwner()), + ); return $this->getOwner()::find()->andWhere($condition); } @@ -932,26 +920,17 @@ public function next(): ActiveQuery */ public function parents(int|null $depth = null): ActiveQuery { - $condition = [ - 'and', - [ - '<', - $this->leftAttribute, $this->getOwner()->getAttribute($this->leftAttribute), - ], - [ - '>', - $this->rightAttribute, $this->getOwner()->getAttribute($this->rightAttribute), - ], - ]; - - if ($depth !== null) { - $condition[] = [ - '>=', - $this->depthAttribute, $this->getOwner()->getAttribute($this->depthAttribute) - $depth, - ]; - } - - $this->applyTreeAttributeCondition($condition); + $condition = QueryConditionBuilder::createParentsCondition( + $this->leftAttribute, + $this->getLeftValue(), + $this->rightAttribute, + $this->getRightValue(), + $this->treeAttribute, + $this->getTreeValue($this->getOwner()), + $depth !== null ? $this->depthAttribute : null, + $depth !== null ? $this->getDepthValue() : null, + $depth + ); return $this->getOwner()::find()->andWhere($condition)->addOrderBy([$this->leftAttribute => SORT_ASC]); } @@ -1022,41 +1001,16 @@ public function prependTo(ActiveRecord $node, bool $runValidation = true, array| */ public function prev(): ActiveQuery { - $condition = [$this->rightAttribute => $this->getOwner()->getAttribute($this->leftAttribute) - 1]; - $this->applyTreeAttributeCondition($condition); + $condition = QueryConditionBuilder::createPrevSiblingCondition( + $this->rightAttribute, + $this->getLeftValue(), + $this->treeAttribute, + $this->getTreeValue($this->getOwner()), + ); return $this->getOwner()::find()->andWhere($condition); } - /** - * Adds the tree attribute condition to the given query condition if multi-tree support is enabled. - * - * If the {@see treeAttribute} property is set (not `false`), this method augments the provided condition array with - * an additional constraint to ensure that queries are limited to the same tree as the current node. - * - * This is essential for supporting multiple independent trees within the same table, preventing cross-tree - * operations and ensuring data integrity when filtering or updating nodes. - * - * The method is used internally by query builders such as {@see leaves()}, {@see next()}, {@see parents()}, - * {@see prev()}, and {@see deleteWithChildrenInternal()} to automatically scope queries to the correct tree. - * - * @param array $condition Query condition to be modified by reference. - * - * @phpstan-param array $condition - */ - protected function applyTreeAttributeCondition(array &$condition): void - { - if ($this->treeAttribute !== false) { - $condition = [ - 'and', - $condition, - [ - $this->treeAttribute => $this->getOwner()->getAttribute($this->treeAttribute), - ], - ]; - } - } - /** * Prepares the current node for insertion as a child or sibling in the nested set tree. * @@ -1077,15 +1031,17 @@ protected function beforeInsertNode(int $value, int $depth): void throw new Exception('Can not create a node when the target node is root.'); } - $this->getOwner()->setAttribute($this->leftAttribute, $value); - $this->getOwner()->setAttribute($this->rightAttribute, $value + 1); + $owner = $this->getOwner(); + + $owner->setAttribute($this->leftAttribute, $value); + $owner->setAttribute($this->rightAttribute, $value + 1); $nodeDepthValue = $this->node?->getAttribute($this->depthAttribute) ?? 0; - $this->getOwner()->setAttribute($this->depthAttribute, $nodeDepthValue + $depth); + $owner->setAttribute($this->depthAttribute, $nodeDepthValue + $depth); if ($this->treeAttribute !== false && $this->node !== null) { - $this->getOwner()->setAttribute($this->treeAttribute, $this->node->getAttribute($this->treeAttribute)); + $owner->setAttribute($this->treeAttribute, $this->node->getAttribute($this->treeAttribute)); } $this->shiftLeftRightAttribute($value, 2); @@ -1108,13 +1064,15 @@ protected function beforeInsertNode(int $value, int $depth): void */ protected function beforeInsertRootNode(): void { - if ($this->treeAttribute === false && $this->getOwner()::find()->roots()->exists()) { + $owner = $this->getOwner(); + + if ($this->treeAttribute === false && $owner::find()->roots()->exists()) { throw new Exception('Can not create more than one root when "treeAttribute" is false.'); } - $this->getOwner()->setAttribute($this->leftAttribute, 1); - $this->getOwner()->setAttribute($this->rightAttribute, 2); - $this->getOwner()->setAttribute($this->depthAttribute, 0); + $owner->setAttribute($this->leftAttribute, 1); + $owner->setAttribute($this->rightAttribute, 2); + $owner->setAttribute($this->depthAttribute, 0); } /** @@ -1140,19 +1098,14 @@ protected function deleteWithChildrenInternal(): bool|int return false; } - $condition = [ - 'and', - [ - '>=', - $this->leftAttribute, $this->owner?->getAttribute($this->leftAttribute), - ], - [ - '<=', - $this->rightAttribute, $this->owner?->getAttribute($this->rightAttribute), - ], - ]; - - $this->applyTreeAttributeCondition($condition); + $condition = QueryConditionBuilder::createRangeCondition( + $this->leftAttribute, + $this->getLeftValue(), + $this->rightAttribute, + $this->getRightValue(), + $this->treeAttribute, + $this->getTreeValue($this->getOwner()), + ); $result = $this->getOwner()::deleteAll($condition); $this->getOwner()->setOldAttributes(null); $this->getOwner()->afterDelete(); @@ -1173,13 +1126,11 @@ protected function moveNode(NodeContext $context): void $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); - - $depthOffset = $targetNodeDepthValue - $ownerDepthValue + $context->depthLevelDelta; + $depthOffset = $targetNodeDepthValue - $this->getDepthValue() + $context->depthLevelDelta; if ($this->treeAttribute === false || $targetNodeTreeValue === $currentOwnerTreeValue) { + $ownerLeftValue = $this->getLeftValue(); + $ownerRightValue = $this->getRightValue(); $subtreeSize = $ownerRightValue - $ownerLeftValue + 1; $this->shiftLeftRightAttribute($context->targetPositionValue, $subtreeSize); @@ -1189,21 +1140,15 @@ protected function moveNode(NodeContext $context): void $ownerRightValue += $subtreeSize; } - $condition = [ - 'and', - [ - '>=', - $this->leftAttribute, - $ownerLeftValue, - ], - [ - '<=', - $this->rightAttribute, - $ownerRightValue, - ], - ]; + $condition = QueryConditionBuilder::createRangeCondition( + $this->leftAttribute, + $ownerLeftValue, + $this->rightAttribute, + $ownerRightValue, + $this->treeAttribute, + $currentOwnerTreeValue + ); - $this->applyTreeAttributeCondition($condition); $this->getOwner()::updateAll( [ $this->depthAttribute => new Expression( @@ -1214,19 +1159,15 @@ protected function moveNode(NodeContext $context): void ); foreach ([$this->leftAttribute, $this->rightAttribute] as $attribute) { - $condition = [ - 'and', - [ - '>=', - $attribute, $ownerLeftValue, - ], - [ - '<=', - $attribute, $ownerRightValue, - ], - ]; + $condition = QueryConditionBuilder::createRangeCondition( + $attribute, + $ownerLeftValue, + $attribute, + $ownerRightValue, + $this->treeAttribute, + $currentOwnerTreeValue + ); - $this->applyTreeAttributeCondition($condition); $this->getOwner()::updateAll( [ $attribute => new Expression( @@ -1241,23 +1182,21 @@ protected function moveNode(NodeContext $context): void $this->shiftLeftRightAttribute($ownerRightValue, -$subtreeSize); } else { foreach ([$this->leftAttribute, $this->rightAttribute] as $attribute) { + $condition = QueryConditionBuilder::createCrossTreeMoveCondition( + $attribute, + $context->targetPositionValue, + $this->treeAttribute, + $targetNodeTreeValue + ); + $this->getOwner()::updateAll( [ $attribute => new Expression( - $this->getDb()->quoteColumnName($attribute) . sprintf('%+d', $ownerRightValue - $ownerLeftValue + 1), + $this->getDb()->quoteColumnName($attribute) . + sprintf('%+d', $this->getRightValue() - $this->getLeftValue() + 1), ), ], - [ - 'and', - [ - '>=', - $attribute, - $context->targetPositionValue, - ], - [ - $this->treeAttribute => $targetNodeTreeValue, - ], - ], + $condition, ); } @@ -1265,11 +1204,11 @@ protected function moveNode(NodeContext $context): void $targetNodeTreeValue, $currentOwnerTreeValue, $depthOffset, - $ownerLeftValue, - $context->targetPositionValue - $ownerLeftValue, - $ownerRightValue, + $this->getLeftValue(), + $context->targetPositionValue - $this->getLeftValue(), + $this->getRightValue(), ); - $this->shiftLeftRightAttribute($ownerRightValue, $ownerLeftValue - $ownerRightValue - 1); + $this->shiftLeftRightAttribute($this->getRightValue(), $this->getLeftValue() - $this->getRightValue() - 1); } } @@ -1295,20 +1234,15 @@ protected function moveNode(NodeContext $context): void */ protected function moveNodeAsRoot(mixed $treeValue): void { - $depthValue = $this->getOwner()->getAttribute($this->depthAttribute); - $leftValue = $this->getOwner()->getAttribute($this->leftAttribute); - $nodeRootValue = $this->getOwner()->getPrimaryKey(); - $rightValue = $this->getOwner()->getAttribute($this->rightAttribute); - $this->moveSubtreeToTargetTree( - $nodeRootValue, + $this->getOwner()->getPrimaryKey(), $treeValue, - -$depthValue, - $leftValue, - 1 - $leftValue, - $rightValue, + -$this->getDepthValue(), + $this->getLeftValue(), + 1 - $this->getLeftValue(), + $this->getRightValue(), ); - $this->shiftLeftRightAttribute($rightValue, $leftValue - $rightValue - 1); + $this->shiftLeftRightAttribute($this->getRightValue(), $this->getLeftValue() - $this->getRightValue() - 1); } /** @@ -1329,9 +1263,12 @@ protected function moveNodeAsRoot(mixed $treeValue): void protected function shiftLeftRightAttribute(int $value, int $delta): void { foreach ([$this->leftAttribute, $this->rightAttribute] as $attribute) { - $condition = ['>=', $attribute, $value]; - - $this->applyTreeAttributeCondition($condition); + $condition = QueryConditionBuilder::createShiftCondition( + $attribute, + $value, + $this->treeAttribute, + $this->getTreeValue($this->getOwner()), + ); $this->getOwner()::updateAll( [ $attribute => new Expression($this->getDb()->quoteColumnName($attribute) . sprintf('%+d', $delta)), @@ -1396,6 +1333,24 @@ private function getDb(): Connection return $this->db ??= $this->getOwner()::getDb(); } + private function getDepthValue(): int + { + if ($this->depthValue === null) { + $this->depthValue = $this->getOwner()->getAttribute($this->depthAttribute); + } + + return $this->depthValue; + } + + private function getLeftValue(): int + { + if ($this->leftValue === null) { + $this->leftValue = $this->getOwner()->getAttribute($this->leftAttribute); + } + + return $this->leftValue; + } + /** * Returns the {@see ActiveRecord} instance to which this behavior is currently attached. * @@ -1420,6 +1375,15 @@ private function getOwner(): ActiveRecord return $this->owner; } + private function getRightValue(): int + { + if ($this->rightValue === null) { + $this->rightValue = $this->getOwner()->getAttribute($this->rightAttribute); + } + + return $this->rightValue; + } + /** * Retrieves the tree attribute value from the specified {@see ActiveRecord} instance. * @@ -1468,6 +1432,14 @@ private function moveSubtreeToTargetTree( int $positionOffset, int $rightValue, ): void { + $condition = QueryConditionBuilder::createSubtreeMoveCondition( + $this->leftAttribute, + $leftValue, + $this->rightAttribute, + $rightValue, + $this->treeAttribute, + $currentOwnerTreeValue + ); $this->getOwner()::updateAll( [ $this->leftAttribute => new Expression( @@ -1481,22 +1453,7 @@ private function moveSubtreeToTargetTree( ), $this->treeAttribute => $targetNodeTreeValue, ], - [ - 'and', - [ - '>=', - $this->leftAttribute, - $leftValue, - ], - [ - '<=', - $this->rightAttribute, - $rightValue, - ], - [ - $this->treeAttribute => $currentOwnerTreeValue, - ], - ], + $condition, ); } } diff --git a/src/QueryConditionBuilder.php b/src/QueryConditionBuilder.php new file mode 100644 index 0000000..7de59c1 --- /dev/null +++ b/src/QueryConditionBuilder.php @@ -0,0 +1,449 @@ +lft, 'rgt', $node->rgt, 'tree', $node->tree, 'depth', $node->depth, 2, + * ); + * $children = MyModel::find()->andWhere($condition)->all(); + * ``` + * + * @phpstan-return array + */ + public static function createChildrenCondition( + string $leftAttribute, + int $leftValue, + string $rightAttribute, + int $rightValue, + string|false $treeAttribute = false, + mixed $treeValue = null, + string|null $depthAttribute = null, + int|null $parentDepth = null, + int|null $maxRelativeDepth = null, + ): array { + $condition = [ + 'and', + ['>', $leftAttribute, $leftValue], + ['<', $rightAttribute, $rightValue], + ]; + + if ($depthAttribute !== null && $parentDepth !== null && $maxRelativeDepth !== null) { + $condition[] = ['<=', $depthAttribute, $parentDepth + $maxRelativeDepth]; + } + + if ($treeAttribute !== false && $treeValue !== null) { + $condition[] = [$treeAttribute => $treeValue]; + } + + return $condition; + } + + /** + * Creates a condition array for cross-tree movement operations. + * + * This method builds a condition for updating nodes when moving subtrees between different trees. + * + * @param string $attribute Name of the attribute to check (left or right). + * @param int $value Minimum value for the attribute. + * @param string $treeAttribute Name of tree attribute. + * @param mixed $treeValue Tree value to filter by. + * + * @return array Condition array for cross-tree operations. + * + * Usage example: + * ```php + * $condition = QueryConditionBuilder::createCrossTreeMoveCondition('lft', 5, 'tree', 2); + * MyModel::updateAll(['lft' => new Expression('lft + 10')], $condition); + * ``` + * + * @phpstan-return array + */ + public static function createCrossTreeMoveCondition( + string $attribute, + int $value, + string $treeAttribute, + mixed $treeValue, + ): array { + return [ + 'and', + ['>=', $attribute, $value], + [$treeAttribute => $treeValue], + ]; + } + + /** + * Creates a condition array for finding leaf nodes (nodes without children). + * + * This method builds a condition that identifies all nodes where the right attribute is exactly one greater than + * the left attribute, indicating that the node has no children. + * + * Optionally restricts the search to descendants of a specific node if parent boundaries are provided. + * + * @param string $leftAttribute Name of the left boundary attribute. + * @param string $rightAttribute Name of the right boundary attribute. + * @param string|false $treeAttribute Name of tree attribute or `false` if disabled. + * @param mixed $treeValue Tree value to filter by (ignored if treeAttribute is `false`). + * @param int|null $parentLeftValue Optional parent left value to restrict search to a subtree. + * @param int|null $parentRightValue Optional parent right value to restrict search to a subtree. + * + * @return array Condition array for finding leaf nodes. + * + * Usage example: + * ```php + * // Find all leaf nodes in the tree + * $condition = QueryConditionBuilder::createLeavesCondition('lft', 'rgt', 'tree', 1); + * $leaves = MyModel::find()->andWhere($condition)->all(); + * + * // Find leaf nodes only within a specific subtree + * $condition = QueryConditionBuilder::createLeavesCondition('lft', 'rgt', 'tree', 1, $node->lft, $node->rgt); + * ``` + * + * @phpstan-return array + */ + public static function createLeavesCondition( + string $leftAttribute, + string $rightAttribute, + string|false $treeAttribute = false, + mixed $treeValue = null, + int|null $parentLeftValue = null, + int|null $parentRightValue = null, + ): array { + $leafCondition = [ + $rightAttribute => new Expression("{{{$leftAttribute}}} + 1"), + ]; + + $condition = $leafCondition; + + if ($parentLeftValue !== null && $parentRightValue !== null) { + $condition = [ + 'and', + $leafCondition, + ['>', $leftAttribute, $parentLeftValue], + ['<', $rightAttribute, $parentRightValue], + ]; + } + + if ($treeAttribute !== false && $treeValue !== null) { + if (isset($condition[0]) && $condition[0] === 'and') { + $condition[] = [$treeAttribute => $treeValue]; + } + } + + return $condition; + } + + /** + * Creates a condition array for finding the next sibling of a node. + * + * This method builds a condition that identifies the node whose left attribute is exactly one greater than the + * right attribute of the reference node. + * + * @param string $leftAttribute Name of the left boundary attribute. + * @param int $rightValue Reference node right value. + * @param string|false $treeAttribute Name of tree attribute or `false` if disabled. + * @param mixed $treeValue Tree value to filter by (ignored if treeAttribute is `false`). + * + * @return array Condition array for finding the next sibling. + * + * Usage example: + * ```php + * $condition = QueryConditionBuilder::createNextSiblingCondition('lft', $node->rgt, 'tree', $node->tree); + * $nextSibling = MyModel::find()->andWhere($condition)->one(); + * ``` + * + * @phpstan-return array + */ + public static function createNextSiblingCondition( + string $leftAttribute, + int $rightValue, + string|false $treeAttribute = false, + mixed $treeValue = null, + ): array { + $condition = [$leftAttribute => $rightValue + 1]; + + if ($treeAttribute !== false && $treeValue !== null) { + $condition = ['and', $condition, [$treeAttribute => $treeValue]]; + } + + return $condition; + } + + /** + * Creates a condition array for finding parent nodes of a node with specific left and right values. + * + * This method builds a condition that identifies all nodes that are ancestors of a node with the specified + * boundaries. + * + * It requires the left attribute to be less than the child's left value and the right attribute to be greater than + * the child's right value. + * + * Optionally limits the depth of parents if the depth parameter is provided. + * + * @param string $leftAttribute Name of the left boundary attribute. + * @param int $leftValue Child node left value. + * @param string $rightAttribute Name of the right boundary attribute. + * @param int $rightValue Child node right value. + * @param string|false $treeAttribute Name of tree attribute or `false` if disabled. + * @param mixed $treeValue Tree value to filter by (ignored if treeAttribute is `false`). + * @param string|null $depthAttribute Name of the depth attribute, or `null` if depth filtering is not needed. + * @param int|null $childDepth Child node depth value, required if $maxRelativeDepth is provided. + * @param int|null $maxRelativeDepth Maximum relative depth from child, or `null` for all ancestors. + * + * @return array Condition array for finding parents of the specified node. + * + * Usage example: + * ```php + * // Find direct parent and grandparent only + * $condition = QueryConditionBuilder::createParentsCondition( + * 'lft', $node->lft, 'rgt', $node->rgt, 'tree', $node->tree, 'depth', $node->depth, 2, + * ); + * $parents = MyModel::find()->andWhere($condition)->all(); + * ``` + * + * @phpstan-return array + */ + public static function createParentsCondition( + string $leftAttribute, + int $leftValue, + string $rightAttribute, + int $rightValue, + string|false $treeAttribute = false, + mixed $treeValue = null, + string|null $depthAttribute = null, + int|null $childDepth = null, + int|null $maxRelativeDepth = null, + ): array { + $condition = [ + 'and', + ['<', $leftAttribute, $leftValue], + ['>', $rightAttribute, $rightValue], + ]; + + if ($depthAttribute !== null && $childDepth !== null && $maxRelativeDepth !== null) { + $condition[] = ['>=', $depthAttribute, $childDepth - $maxRelativeDepth]; + } + + if ($treeAttribute !== false && $treeValue !== null) { + $condition[] = [$treeAttribute => $treeValue]; + } + + return $condition; + } + + /** + * Creates a condition array for finding the previous sibling of a node. + * + * This method builds a condition that identifies the node whose right attribute is exactly one less than the left + * attribute of the reference node. + * + * @param string $rightAttribute Name of the right boundary attribute. + * @param int $leftValue Reference node left value. + * @param string|false $treeAttribute Name of tree attribute or `false` if disabled. + * @param mixed $treeValue Tree value to filter by (ignored if treeAttribute is `false`). + * + * @return array Condition array for finding the previous sibling. + * + * Usage example: + * ```php + * $condition = QueryConditionBuilder::createPrevSiblingCondition('rgt', $node->lft, 'tree', $node->tree); + * $prevSibling = MyModel::find()->andWhere($condition)->one(); + * ``` + * + * @phpstan-return array + */ + public static function createPrevSiblingCondition( + string $rightAttribute, + int $leftValue, + string|false $treeAttribute = false, + mixed $treeValue = null, + ): array { + $condition = [$rightAttribute => $leftValue - 1]; + + if ($treeAttribute !== false && $treeValue !== null) { + $condition = ['and', $condition, [$treeAttribute => $treeValue]]; + } + + return $condition; + } + + /** + * Creates a condition array for finding nodes within a range of left and right values. + * + * This is the core condition builder for nested sets operations, used to identify nodes within a specific subtree + * boundary. + * + * It creates an 'and' condition that requires both the left attribute to be greater than or equal to the left + * value, and the right attribute to be less than or equal to the right value. + * + * This condition is essential for operations like finding descendants, deleting subtrees, or moving nodes within + * the nested set structure. + * + * @param string $leftAttribute Name of the left boundary attribute. + * @param int $leftValue Left boundary value. + * @param string $rightAttribute Name of the right boundary attribute. + * @param int $rightValue Right boundary value. + * @param string|false $treeAttribute Name of tree attribute or `false` if disabled. + * @param mixed $treeValue Tree value to filter by (ignored if treeAttribute is `false`). + * + * @return array Condition array for finding nodes within the specified range. + * + * Usage example: + * ```php + * $condition = QueryConditionBuilder::createRangeCondition('lft', 5, 'rgt', 10, 'tree', 1); + * $descendants = MyModel::find()->andWhere($condition)->all(); + * ``` + * + * @phpstan-return array + */ + public static function createRangeCondition( + string $leftAttribute, + int $leftValue, + string $rightAttribute, + int $rightValue, + string|false $treeAttribute = false, + mixed $treeValue = null, + ): array { + $condition = [ + 'and', + ['>=', $leftAttribute, $leftValue], + ['<=', $rightAttribute, $rightValue], + ]; + + if ($treeAttribute !== false && $treeValue !== null) { + $condition[] = [$treeAttribute => $treeValue]; + } + + return $condition; + } + + /** + * Creates a condition array for shifting left/right attributes. + * + * This method builds a condition for nodes that need their left or right attributes updated during tree + * restructuring operations. + * + * @param string $attribute Name of the attribute to check (left or right). + * @param int $value Minimum value for the attribute. + * @param string|false $treeAttribute Name of tree attribute or `false` if disabled. + * @param mixed $treeValue Tree value to filter by (ignored if treeAttribute is `false`). + * + * @return array Condition array for shifting operations. + * + * Usage example: + * ```php + * $condition = QueryConditionBuilder::createShiftCondition('lft', 10, 'tree', 1); + * MyModel::updateAll(['lft' => new Expression('lft + 2')], $condition); + * ``` + * + * @phpstan-return array + */ + public static function createShiftCondition( + string $attribute, + int $value, + string|false $treeAttribute = false, + mixed $treeValue = null, + ): array { + $condition = ['>=', $attribute, $value]; + + if ($treeAttribute !== false && $treeValue !== null) { + $condition = ['and', $condition, [$treeAttribute => $treeValue]]; + } + + return $condition; + } + + /** + * Creates a condition array for moving a subtree to a different tree. + * + * This method builds a condition for identifying nodes in a subtree that need to be moved from one tree to another. + * + * @param string $leftAttribute Name of the left boundary attribute. + * @param int $leftValue Left boundary value of the subtree. + * @param string $rightAttribute Name of the right boundary attribute. + * @param int $rightValue Right boundary value of the subtree. + * @param string|false $treeAttribute Name of tree attribute or `false` if disabled. + * @param mixed $currentTreeValue Current tree value of the subtree. + * + * @return array Condition array for subtree movement. + * + * Usage example: + * ```php + * $condition = QueryConditionBuilder::createSubtreeMoveCondition('lft', 5, 'rgt', 10, 'tree', 1); + * MyModel::updateAll(['tree' => 2], $condition); + * ``` + * + */ + public static function createSubtreeMoveCondition( + string $leftAttribute, + int $leftValue, + string $rightAttribute, + int $rightValue, + string|false $treeAttribute, + mixed $currentTreeValue, + ): array { + $condition = [ + 'and', + ['>=', $leftAttribute, $leftValue], + ['<=', $rightAttribute, $rightValue], + ]; + + if ($treeAttribute !== false) { + $condition[] = [$treeAttribute => $currentTreeValue]; + } + + return $condition; + } +} diff --git a/tests/NestedSetsBehaviorTest.php b/tests/NestedSetsBehaviorTest.php index 2c3a5e3..bb8d3f6 100644 --- a/tests/NestedSetsBehaviorTest.php +++ b/tests/NestedSetsBehaviorTest.php @@ -229,18 +229,6 @@ public function testReturnTrueAndMatchXmlAfterInsertAfterNewForTreeAndMultipleTr ); } - public function testThrowExceptionWhenInsertAfterNewNodeTargetIsNewRecord(): void - { - $this->generateFixtureTree(); - - $node = new Tree(['name' => 'New node']); - - $this->expectException(Exception::class); - $this->expectExceptionMessage('Can not create a node when the target node is new record.'); - - $node->insertAfter(new Tree()); - } - public function testThrowExceptionWhenInsertAfterNewNodeTargetIsRoot(): void { $this->generateFixtureTree(); @@ -2022,20 +2010,6 @@ public function testProtectedApplyTreeAttributeConditionRemainsAccessibleToSubcl $extendableBehavior, "'ExtendableMultipleTree' should use 'ExtendableNestedSetsBehavior'.", ); - - $condition = ['name' => 'test']; - - $extendableBehavior->exposedApplyTreeAttributeCondition($condition); - - self::assertTrue( - $extendableBehavior->wasMethodCalled('applyTreeAttributeCondition'), - "'applyTreeAttributeCondition' method should remain protected to allow subclass access.", - ); - self::assertEquals( - ['and', ['name' => 'test'], ['tree' => 1]], - $condition, - "'Tree' attribute condition should be applied correctly when 'treeAttribute' is enabled.", - ); } public function testProtectedBeforeInsertNodeRemainsAccessibleToSubclasses(): void diff --git a/tests/support/stub/ExtendableNestedSetsBehavior.php b/tests/support/stub/ExtendableNestedSetsBehavior.php index 7f43a5f..67c4a29 100644 --- a/tests/support/stub/ExtendableNestedSetsBehavior.php +++ b/tests/support/stub/ExtendableNestedSetsBehavior.php @@ -19,16 +19,6 @@ final class ExtendableNestedSetsBehavior extends NestedSetsBehavior */ public array $calledMethods = []; - /** - * @phpstan-param array $condition - */ - public function exposedApplyTreeAttributeCondition(array &$condition): void - { - $this->calledMethods['applyTreeAttributeCondition'] = true; - - $this->applyTreeAttributeCondition($condition); - } - public function exposedBeforeInsertNode(int $value, int $depth): void { $this->calledMethods['beforeInsertNode'] = true; From 1e97d676a0252081bb442a3fe0a37b358b14fb29 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 2 Jul 2025 23:01:02 +0000 Subject: [PATCH 2/6] Apply fixes from StyleCI --- src/QueryConditionBuilder.php | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/QueryConditionBuilder.php b/src/QueryConditionBuilder.php index 7de59c1..08eac9e 100644 --- a/src/QueryConditionBuilder.php +++ b/src/QueryConditionBuilder.php @@ -44,7 +44,7 @@ final class QueryConditionBuilder * @param int $leftValue Parent node left value. * @param string $rightAttribute Name of the right boundary attribute. * @param int $rightValue Parent node right value. - * @param string|false $treeAttribute Name of tree attribute or `false` if disabled. + * @param false|string $treeAttribute Name of tree attribute or `false` if disabled. * @param mixed $treeValue Tree value to filter by (ignored if treeAttribute is `false`). * @param string|null $depthAttribute Name of the depth attribute, or `null` if depth filtering is not needed. * @param int|null $parentDepth Parent node depth value, required if $maxRelativeDepth is provided. @@ -134,7 +134,7 @@ public static function createCrossTreeMoveCondition( * * @param string $leftAttribute Name of the left boundary attribute. * @param string $rightAttribute Name of the right boundary attribute. - * @param string|false $treeAttribute Name of tree attribute or `false` if disabled. + * @param false|string $treeAttribute Name of tree attribute or `false` if disabled. * @param mixed $treeValue Tree value to filter by (ignored if treeAttribute is `false`). * @param int|null $parentLeftValue Optional parent left value to restrict search to a subtree. * @param int|null $parentRightValue Optional parent right value to restrict search to a subtree. @@ -193,7 +193,7 @@ public static function createLeavesCondition( * * @param string $leftAttribute Name of the left boundary attribute. * @param int $rightValue Reference node right value. - * @param string|false $treeAttribute Name of tree attribute or `false` if disabled. + * @param false|string $treeAttribute Name of tree attribute or `false` if disabled. * @param mixed $treeValue Tree value to filter by (ignored if treeAttribute is `false`). * * @return array Condition array for finding the next sibling. @@ -236,7 +236,7 @@ public static function createNextSiblingCondition( * @param int $leftValue Child node left value. * @param string $rightAttribute Name of the right boundary attribute. * @param int $rightValue Child node right value. - * @param string|false $treeAttribute Name of tree attribute or `false` if disabled. + * @param false|string $treeAttribute Name of tree attribute or `false` if disabled. * @param mixed $treeValue Tree value to filter by (ignored if treeAttribute is `false`). * @param string|null $depthAttribute Name of the depth attribute, or `null` if depth filtering is not needed. * @param int|null $childDepth Child node depth value, required if $maxRelativeDepth is provided. @@ -291,7 +291,7 @@ public static function createParentsCondition( * * @param string $rightAttribute Name of the right boundary attribute. * @param int $leftValue Reference node left value. - * @param string|false $treeAttribute Name of tree attribute or `false` if disabled. + * @param false|string $treeAttribute Name of tree attribute or `false` if disabled. * @param mixed $treeValue Tree value to filter by (ignored if treeAttribute is `false`). * * @return array Condition array for finding the previous sibling. @@ -335,7 +335,7 @@ public static function createPrevSiblingCondition( * @param int $leftValue Left boundary value. * @param string $rightAttribute Name of the right boundary attribute. * @param int $rightValue Right boundary value. - * @param string|false $treeAttribute Name of tree attribute or `false` if disabled. + * @param false|string $treeAttribute Name of tree attribute or `false` if disabled. * @param mixed $treeValue Tree value to filter by (ignored if treeAttribute is `false`). * * @return array Condition array for finding nodes within the specified range. @@ -377,7 +377,7 @@ public static function createRangeCondition( * * @param string $attribute Name of the attribute to check (left or right). * @param int $value Minimum value for the attribute. - * @param string|false $treeAttribute Name of tree attribute or `false` if disabled. + * @param false|string $treeAttribute Name of tree attribute or `false` if disabled. * @param mixed $treeValue Tree value to filter by (ignored if treeAttribute is `false`). * * @return array Condition array for shifting operations. @@ -414,7 +414,7 @@ public static function createShiftCondition( * @param int $leftValue Left boundary value of the subtree. * @param string $rightAttribute Name of the right boundary attribute. * @param int $rightValue Right boundary value of the subtree. - * @param string|false $treeAttribute Name of tree attribute or `false` if disabled. + * @param false|string $treeAttribute Name of tree attribute or `false` if disabled. * @param mixed $currentTreeValue Current tree value of the subtree. * * @return array Condition array for subtree movement. @@ -424,7 +424,6 @@ public static function createShiftCondition( * $condition = QueryConditionBuilder::createSubtreeMoveCondition('lft', 5, 'rgt', 10, 'tree', 1); * MyModel::updateAll(['tree' => 2], $condition); * ``` - * */ public static function createSubtreeMoveCondition( string $leftAttribute, From a48042eff1b977a20b52dd50d7eb14af84d3e142 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Wed, 2 Jul 2025 19:06:51 -0400 Subject: [PATCH 3/6] fix: Add `PHPStan` return type annotation for `createSubtreeMoveCondition()` method in `QueryConditionBuilder`. --- src/NestedSetsBehavior.php | 12 ++++++------ src/QueryConditionBuilder.php | 2 ++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/NestedSetsBehavior.php b/src/NestedSetsBehavior.php index 2c02896..24840df 100644 --- a/src/NestedSetsBehavior.php +++ b/src/NestedSetsBehavior.php @@ -502,7 +502,7 @@ public function children(int|null $depth = null): ActiveQuery $this->getTreeValue($this->getOwner()), $depth !== null ? $this->depthAttribute : null, $depth !== null ? $this->getDepthValue() : null, - $depth + $depth, ); return $this->getOwner()::find()->andWhere($condition)->addOrderBy([$this->leftAttribute => SORT_ASC]); @@ -929,7 +929,7 @@ public function parents(int|null $depth = null): ActiveQuery $this->getTreeValue($this->getOwner()), $depth !== null ? $this->depthAttribute : null, $depth !== null ? $this->getDepthValue() : null, - $depth + $depth, ); return $this->getOwner()::find()->andWhere($condition)->addOrderBy([$this->leftAttribute => SORT_ASC]); @@ -1146,7 +1146,7 @@ protected function moveNode(NodeContext $context): void $this->rightAttribute, $ownerRightValue, $this->treeAttribute, - $currentOwnerTreeValue + $currentOwnerTreeValue, ); $this->getOwner()::updateAll( @@ -1165,7 +1165,7 @@ protected function moveNode(NodeContext $context): void $attribute, $ownerRightValue, $this->treeAttribute, - $currentOwnerTreeValue + $currentOwnerTreeValue, ); $this->getOwner()::updateAll( @@ -1186,7 +1186,7 @@ protected function moveNode(NodeContext $context): void $attribute, $context->targetPositionValue, $this->treeAttribute, - $targetNodeTreeValue + $targetNodeTreeValue, ); $this->getOwner()::updateAll( @@ -1438,7 +1438,7 @@ private function moveSubtreeToTargetTree( $this->rightAttribute, $rightValue, $this->treeAttribute, - $currentOwnerTreeValue + $currentOwnerTreeValue, ); $this->getOwner()::updateAll( [ diff --git a/src/QueryConditionBuilder.php b/src/QueryConditionBuilder.php index 08eac9e..fda32b6 100644 --- a/src/QueryConditionBuilder.php +++ b/src/QueryConditionBuilder.php @@ -424,6 +424,8 @@ public static function createShiftCondition( * $condition = QueryConditionBuilder::createSubtreeMoveCondition('lft', 5, 'rgt', 10, 'tree', 1); * MyModel::updateAll(['tree' => 2], $condition); * ``` + * + * @phpstan-return array */ public static function createSubtreeMoveCondition( string $leftAttribute, From 5dbcb34833e2ceb2a64343b224ab9b6cd538502c Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Wed, 2 Jul 2025 19:12:32 -0400 Subject: [PATCH 4/6] fix: Remove unnecessary `null` checks for `treeValue` in query condition methods. --- src/QueryConditionBuilder.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/QueryConditionBuilder.php b/src/QueryConditionBuilder.php index fda32b6..e16372d 100644 --- a/src/QueryConditionBuilder.php +++ b/src/QueryConditionBuilder.php @@ -84,7 +84,7 @@ public static function createChildrenCondition( $condition[] = ['<=', $depthAttribute, $parentDepth + $maxRelativeDepth]; } - if ($treeAttribute !== false && $treeValue !== null) { + if ($treeAttribute !== false) { $condition[] = [$treeAttribute => $treeValue]; } @@ -176,7 +176,7 @@ public static function createLeavesCondition( ]; } - if ($treeAttribute !== false && $treeValue !== null) { + if ($treeAttribute !== false) { if (isset($condition[0]) && $condition[0] === 'and') { $condition[] = [$treeAttribute => $treeValue]; } @@ -214,7 +214,7 @@ public static function createNextSiblingCondition( ): array { $condition = [$leftAttribute => $rightValue + 1]; - if ($treeAttribute !== false && $treeValue !== null) { + if ($treeAttribute !== false) { $condition = ['and', $condition, [$treeAttribute => $treeValue]]; } @@ -276,7 +276,7 @@ public static function createParentsCondition( $condition[] = ['>=', $depthAttribute, $childDepth - $maxRelativeDepth]; } - if ($treeAttribute !== false && $treeValue !== null) { + if ($treeAttribute !== false) { $condition[] = [$treeAttribute => $treeValue]; } @@ -312,7 +312,7 @@ public static function createPrevSiblingCondition( ): array { $condition = [$rightAttribute => $leftValue - 1]; - if ($treeAttribute !== false && $treeValue !== null) { + if ($treeAttribute !== false) { $condition = ['and', $condition, [$treeAttribute => $treeValue]]; } @@ -362,7 +362,7 @@ public static function createRangeCondition( ['<=', $rightAttribute, $rightValue], ]; - if ($treeAttribute !== false && $treeValue !== null) { + if ($treeAttribute !== false) { $condition[] = [$treeAttribute => $treeValue]; } @@ -398,7 +398,7 @@ public static function createShiftCondition( ): array { $condition = ['>=', $attribute, $value]; - if ($treeAttribute !== false && $treeValue !== null) { + if ($treeAttribute !== false) { $condition = ['and', $condition, [$treeAttribute => $treeValue]]; } From 11d8354ffbf5398e59582e3ff5cd7dec8b7e1ee7 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Wed, 2 Jul 2025 19:18:48 -0400 Subject: [PATCH 5/6] refactor: Simplify `createLeavesCondition` method by consolidating condition logic and removing redundant checks. --- src/QueryConditionBuilder.php | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/QueryConditionBuilder.php b/src/QueryConditionBuilder.php index e16372d..8ca58ab 100644 --- a/src/QueryConditionBuilder.php +++ b/src/QueryConditionBuilder.php @@ -161,25 +161,15 @@ public static function createLeavesCondition( int|null $parentLeftValue = null, int|null $parentRightValue = null, ): array { - $leafCondition = [ - $rightAttribute => new Expression("{{{$leftAttribute}}} + 1"), + $condition = [ + 'and', + [$rightAttribute => new Expression("{{{$leftAttribute}}} + 1")], + ['>', $leftAttribute, $parentLeftValue], + ['<', $rightAttribute, $parentRightValue], ]; - $condition = $leafCondition; - - if ($parentLeftValue !== null && $parentRightValue !== null) { - $condition = [ - 'and', - $leafCondition, - ['>', $leftAttribute, $parentLeftValue], - ['<', $rightAttribute, $parentRightValue], - ]; - } - if ($treeAttribute !== false) { - if (isset($condition[0]) && $condition[0] === 'and') { - $condition[] = [$treeAttribute => $treeValue]; - } + $condition[] = [$treeAttribute => $treeValue]; } return $condition; From 0854586cd6d6c8f43d59f00ea344f7430aac7953 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Wed, 2 Jul 2025 19:23:41 -0400 Subject: [PATCH 6/6] fix: Remove check for new record in `insertAfter` method to allow moving nodes correctly. --- src/NestedSetsBehavior.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/NestedSetsBehavior.php b/src/NestedSetsBehavior.php index 24840df..a488be1 100644 --- a/src/NestedSetsBehavior.php +++ b/src/NestedSetsBehavior.php @@ -619,10 +619,6 @@ public function events(): array */ public function insertAfter(ActiveRecord $node, bool $runValidation = true, array|null $attributes = null): bool { - if ($node->getIsNewRecord() === true) { - throw new Exception('Can not move a node when the target node is new record.'); - } - $this->operation = self::OPERATION_INSERT_AFTER; $this->node = $node;