55namespace yii2 \extensions \nestedsets ;
66
77use LogicException ;
8+ use RuntimeException ;
89use yii \base \{Behavior , NotSupportedException };
910use yii \db \{ActiveQuery , ActiveRecord , Connection , Exception , Expression };
1011
@@ -121,6 +122,12 @@ class NestedSetsBehavior extends Behavior
121122 */
122123 public string |false $ treeAttribute = false ;
123124
125+ /**
126+ * Database connection instance used for executing queries.
127+ *
128+ * This property is used to access the database connection associated with the attached {@see ActiveRecord} model,
129+ * allowing the behavior to perform database operations such as updates and queries.
130+ */
124131 private Connection |null $ db = null ;
125132
126133 /**
@@ -251,24 +258,23 @@ public function afterInsert(): void
251258 */
252259 public function afterUpdate (): void
253260 {
254- $ treeValue = $ this ->treeAttribute !== false ? $ this ->getOwner ()-> getAttribute ( $ this -> treeAttribute ) : null ;
261+ $ currentOwnerTreeValue = $ this ->getTreeValue ( $ this ->getOwner ()) ;
255262
256- match (true ) {
257- $ this ->operation === self ::OPERATION_APPEND_TO && $ this ->node !== null =>
258- $ this ->moveNode ($ this ->node , $ treeValue , $ this ->node ->getAttribute ($ this ->rightAttribute ), 1 ),
259- $ this ->operation === self ::OPERATION_INSERT_AFTER && $ this ->node !== null =>
260- $ this ->moveNode ($ this ->node , $ treeValue , $ this ->node ->getAttribute ($ this ->rightAttribute ) + 1 , 0 ),
261- $ this ->operation === self ::OPERATION_INSERT_BEFORE && $ this ->node !== null =>
262- $ this ->moveNode ($ this ->node , $ treeValue , $ this ->node ->getAttribute ($ this ->leftAttribute ), 0 ),
263- $ this ->operation === self ::OPERATION_MAKE_ROOT =>
264- $ this ->moveNodeAsRoot ($ treeValue ),
265- $ this ->operation === self ::OPERATION_PREPEND_TO && $ this ->node !== null =>
266- $ this ->moveNode ($ this ->node , $ treeValue , $ this ->node ->getAttribute ($ this ->leftAttribute ) + 1 , 1 ),
267- default => null ,
268- };
263+ if ($ this ->operation === self ::OPERATION_MAKE_ROOT ) {
264+ $ this ->moveNodeAsRoot ($ currentOwnerTreeValue );
265+ $ this ->resetOperationState ();
266+ return ;
267+ }
269268
270- $ this ->operation = null ;
271- $ this ->node = null ;
269+ if ($ this ->node === null ) {
270+ $ this ->resetOperationState ();
271+ return ;
272+ }
273+
274+ $ context = $ this ->createMoveContext ($ this ->node , $ this ->operation );
275+
276+ $ this ->moveNode ($ context );
277+ $ this ->resetOperationState ();
272278 }
273279
274280 /**
@@ -1165,67 +1171,54 @@ protected function deleteWithChildrenInternal(): bool|int
11651171 }
11661172
11671173 /**
1168- * Moves the current node to a new position within the nested set tree structure.
1169- *
1170- * Updates the left, right, and depth attributes of the node and its descendants to reflect the new position,
1171- * ensuring the integrity of the tree after the move operation.
1172- *
1173- * This method supports both single-tree and multi-tree configurations, handling node movement within the same tree
1174- * or across different trees as needed.
1174+ * Executes node movement using the provided context.
11751175 *
1176- * The method performs the following operations.
1177- * - Adjusts left and right boundaries for affected nodes.
1178- * - Handles tree attribute updates when moving nodes across trees.
1179- * - Shifts left and right attribute values to make space for the moved node and its subtree.
1180- * - Updates the depth of the node and its descendants based on the new position.
1181- *
1182- * This method is called internally during node movement operations such as append, prepend, insert before/after,
1183- * and supports both root and non-root node moves.
1176+ * Handles both same-tree and cross-tree movements, determining the strategy based on tree attribute configuration
1177+ * and tree values from the context.
11841178 *
1185- * @param ActiveRecord $node Node to be moved within the nested set tree.
1186- * @param mixed $treeValue Tree attribute value to which the node will be moved, or `false` if not applicable.
1187- * @param int $value Left attribute value indicating the new position for the node.
1188- * @param int $depth Depth offset to apply to the node and its descendants after the move.
1179+ * @param NodeContext $context Immutable context containing all movement data.
11891180 */
1190- protected function moveNode (ActiveRecord $ node , mixed $ treeValue , int $ value , int $ depth ): void
1181+ protected function moveNode (NodeContext $ context ): void
11911182 {
1192- $ db = $ this ->getOwner ():: getDb ( );
1193- $ depthAttribute = $ db -> quoteColumnName ($ this ->depthAttribute );
1194- $ depthValue = $ this -> getOwner ()-> getAttribute ($ this ->depthAttribute );
1195- $ leftValue = $ this ->getOwner ()->getAttribute ($ this ->leftAttribute );
1196- $ nodeDepthValue = $ node -> getAttribute ($ this ->depthAttribute );
1197- $ rightValue = $ this ->getOwner ()->getAttribute ($ this ->rightAttribute );
1183+ $ currentOwnerTreeValue = $ this ->getTreeValue ( $ this -> getOwner ());
1184+ $ targetNodeTreeValue = $ context -> getTargetTreeValue ($ this ->treeAttribute );
1185+ $ targetNodeDepthValue = $ context -> getTargetDepth ($ this ->depthAttribute );
1186+ $ ownerDepthValue = $ this ->getOwner ()->getAttribute ($ this ->depthAttribute );
1187+ $ ownerLeftValue = $ this -> getOwner ()-> getAttribute ($ this ->leftAttribute );
1188+ $ ownerRightValue = $ this ->getOwner ()->getAttribute ($ this ->rightAttribute );
11981189
1199- $ depthValue = $ nodeDepthValue - $ depthValue + $ depth ;
1190+ $ depthOffset = $ targetNodeDepthValue - $ ownerDepthValue + $ context -> depthLevelDelta ;
12001191
1201- if ($ this ->treeAttribute === false || $ treeValue === $ node -> getAttribute ( $ this -> treeAttribute ) ) {
1202- $ delta = $ rightValue - $ leftValue + 1 ;
1192+ if ($ this ->treeAttribute === false || $ targetNodeTreeValue === $ currentOwnerTreeValue ) {
1193+ $ subtreeSize = $ ownerRightValue - $ ownerLeftValue + 1 ;
12031194
1204- $ this ->shiftLeftRightAttribute ($ value , $ delta );
1195+ $ this ->shiftLeftRightAttribute ($ context -> targetPositionValue , $ subtreeSize );
12051196
1206- if ($ leftValue >= $ value ) {
1207- $ leftValue += $ delta ;
1208- $ rightValue += $ delta ;
1197+ if ($ ownerLeftValue >= $ context -> targetPositionValue ) {
1198+ $ ownerLeftValue += $ subtreeSize ;
1199+ $ ownerRightValue += $ subtreeSize ;
12091200 }
12101201
12111202 $ condition = [
12121203 'and ' ,
12131204 [
12141205 '>= ' ,
12151206 $ this ->leftAttribute ,
1216- $ leftValue ,
1207+ $ ownerLeftValue ,
12171208 ],
12181209 [
12191210 '<= ' ,
12201211 $ this ->rightAttribute ,
1221- $ rightValue ,
1212+ $ ownerRightValue ,
12221213 ],
12231214 ];
12241215
12251216 $ this ->applyTreeAttributeCondition ($ condition );
12261217 $ this ->getOwner ()::updateAll (
12271218 [
1228- $ this ->depthAttribute => new Expression ($ depthAttribute . sprintf ('%+d ' , $ depthValue )),
1219+ $ this ->depthAttribute => new Expression (
1220+ $ this ->getDb ()->quoteColumnName ($ this ->depthAttribute ) . sprintf ('%+d ' , $ depthOffset ),
1221+ ),
12291222 ],
12301223 $ condition ,
12311224 );
@@ -1235,59 +1228,57 @@ protected function moveNode(ActiveRecord $node, mixed $treeValue, int $value, in
12351228 'and ' ,
12361229 [
12371230 '>= ' ,
1238- $ attribute , $ leftValue ,
1231+ $ attribute , $ ownerLeftValue ,
12391232 ],
12401233 [
12411234 '<= ' ,
1242- $ attribute , $ rightValue ,
1235+ $ attribute , $ ownerRightValue ,
12431236 ],
12441237 ];
12451238
12461239 $ this ->applyTreeAttributeCondition ($ condition );
12471240 $ this ->getOwner ()::updateAll (
12481241 [
12491242 $ attribute => new Expression (
1250- $ db -> quoteColumnName ($ attribute ) . sprintf ('%+d ' , $ value - $ leftValue ),
1243+ $ this -> getDb ()-> quoteColumnName ($ attribute ) . sprintf ('%+d ' , $ context -> targetPositionValue - $ ownerLeftValue ),
12511244 ),
12521245 ],
12531246 $ condition ,
12541247 );
12551248 }
12561249
1257- $ this ->shiftLeftRightAttribute ($ rightValue , -$ delta );
1250+ $ this ->shiftLeftRightAttribute ($ ownerRightValue , -$ subtreeSize );
12581251 } else {
1259- $ nodeRootValue = $ node ->getAttribute ($ this ->treeAttribute );
1260-
12611252 foreach ([$ this ->leftAttribute , $ this ->rightAttribute ] as $ attribute ) {
12621253 $ this ->getOwner ()::updateAll (
12631254 [
12641255 $ attribute => new Expression (
1265- $ db -> quoteColumnName ($ attribute ) . sprintf ('%+d ' , $ rightValue - $ leftValue + 1 ),
1256+ $ this -> getDb ()-> quoteColumnName ($ attribute ) . sprintf ('%+d ' , $ ownerRightValue - $ ownerLeftValue + 1 ),
12661257 ),
12671258 ],
12681259 [
12691260 'and ' ,
12701261 [
12711262 '>= ' ,
12721263 $ attribute ,
1273- $ value ,
1264+ $ context -> targetPositionValue ,
12741265 ],
12751266 [
1276- $ this ->treeAttribute => $ nodeRootValue ,
1267+ $ this ->treeAttribute => $ targetNodeTreeValue ,
12771268 ],
12781269 ],
12791270 );
12801271 }
12811272
12821273 $ this ->moveSubtreeToTargetTree (
1283- $ nodeRootValue ,
1284- $ treeValue ,
1285- $ depthValue ,
1286- $ leftValue ,
1287- $ value - $ leftValue ,
1288- $ rightValue ,
1274+ $ targetNodeTreeValue ,
1275+ $ currentOwnerTreeValue ,
1276+ $ depthOffset ,
1277+ $ ownerLeftValue ,
1278+ $ context -> targetPositionValue - $ ownerLeftValue ,
1279+ $ ownerRightValue ,
12891280 );
1290- $ this ->shiftLeftRightAttribute ($ rightValue , $ leftValue - $ rightValue - 1 );
1281+ $ this ->shiftLeftRightAttribute ($ ownerRightValue , $ ownerLeftValue - $ ownerRightValue - 1 );
12911282 }
12921283 }
12931284
@@ -1346,21 +1337,40 @@ protected function moveNodeAsRoot(mixed $treeValue): void
13461337 */
13471338 protected function shiftLeftRightAttribute (int $ value , int $ delta ): void
13481339 {
1349- $ db = $ this ->getOwner ()::getDb ();
1350-
13511340 foreach ([$ this ->leftAttribute , $ this ->rightAttribute ] as $ attribute ) {
13521341 $ condition = ['>= ' , $ attribute , $ value ];
13531342
13541343 $ this ->applyTreeAttributeCondition ($ condition );
13551344 $ this ->getOwner ()::updateAll (
13561345 [
1357- $ attribute => new Expression ($ db ->quoteColumnName ($ attribute ) . sprintf ('%+d ' , $ delta )),
1346+ $ attribute => new Expression ($ this -> getDb () ->quoteColumnName ($ attribute ) . sprintf ('%+d ' , $ delta )),
13581347 ],
13591348 $ condition ,
13601349 );
13611350 }
13621351 }
13631352
1353+ /**
1354+ * Creates a typed movement context based on operation and target node.
1355+ *
1356+ * @param ActiveRecord $targetNode Target node for the operation.
1357+ * @param string|null $operation Operation type to perform.
1358+ *
1359+ * @throws RuntimeException if a runtime error prevents the operation from completing successfully.
1360+ *
1361+ * @return NodeContext New instance with the specified parameters for the operation.
1362+ */
1363+ private function createMoveContext (ActiveRecord $ targetNode , string |null $ operation ): NodeContext
1364+ {
1365+ return match ($ operation ) {
1366+ self ::OPERATION_APPEND_TO => NodeContext::forAppendTo ($ targetNode , $ this ->rightAttribute ),
1367+ self ::OPERATION_INSERT_AFTER => NodeContext::forInsertAfter ($ targetNode , $ this ->rightAttribute ),
1368+ self ::OPERATION_INSERT_BEFORE => NodeContext::forInsertBefore ($ targetNode , $ this ->leftAttribute ),
1369+ self ::OPERATION_PREPEND_TO => NodeContext::forPrependTo ($ targetNode , $ this ->leftAttribute ),
1370+ default => throw new RuntimeException ("Unsupported operation: {$ operation }" ),
1371+ };
1372+ }
1373+
13641374 /**
13651375 * Retrieves and caches the {@see Connection} object associated with the owner model.
13661376 *
@@ -1401,6 +1411,27 @@ private function getOwner(): ActiveRecord
14011411 return $ this ->owner ;
14021412 }
14031413
1414+ /**
1415+ * Retrieves the tree attribute value from the specified {@see ActiveRecord} instance.
1416+ *
1417+ * Extracts the tree identifier value from the given model instance when multi-tree support is enabled, providing
1418+ * a centralized method for accessing tree attribute values throughout the behavior.
1419+ *
1420+ * The method is used internally by movement operations, tree validation, and condition building to ensure proper
1421+ * tree scoping and maintain data integrity across different tree contexts.
1422+ *
1423+ * @param ActiveRecord|null $activeRecord Model instance from which to extract the tree value, or `null` if not
1424+ * available.
1425+ *
1426+ * @return mixed Tree attribute value if multi-tree support is enabled and the record exists, `null` otherwise.
1427+ */
1428+ private function getTreeValue (ActiveRecord |null $ activeRecord ): mixed
1429+ {
1430+ return $ activeRecord !== null && $ this ->treeAttribute !== false
1431+ ? $ activeRecord ->getAttribute ($ this ->treeAttribute )
1432+ : null ;
1433+ }
1434+
14041435 /**
14051436 * Moves a subtree to a different tree or position within a multi-tree nested set structure.
14061437 *
@@ -1413,16 +1444,16 @@ private function getOwner(): ActiveRecord
14131444 * This operation is essential for maintaining the integrity of the nested set structure when reorganizing nodes
14141445 * between trees or promoting a node to root in a multi-tree configuration.
14151446 *
1416- * @param mixed $newTreeValue Value to assign to the tree attribute for all nodes in the moved subtree.
1417- * @param mixed $currentTreeValue Current tree attribute value of the nodes being moved.
1447+ * @param mixed $targetNodeTreeValue Value to assign to the tree attribute for all nodes in the moved subtree.
1448+ * @param mixed $currentOwnerTreeValue Current tree attribute value of the nodes being moved.
14181449 * @param int $depth Depth offset to apply to all nodes in the subtree.
14191450 * @param int $leftValue Left boundary value of the subtree to move.
14201451 * @param int $positionOffset Amount to shift left and right attribute values for the subtree.
14211452 * @param int $rightValue Right boundary value of the subtree to move.
14221453 */
14231454 private function moveSubtreeToTargetTree (
1424- mixed $ newTreeValue ,
1425- mixed $ currentTreeValue ,
1455+ mixed $ targetNodeTreeValue ,
1456+ mixed $ currentOwnerTreeValue ,
14261457 int $ depth ,
14271458 int $ leftValue ,
14281459 int $ positionOffset ,
@@ -1439,7 +1470,7 @@ private function moveSubtreeToTargetTree(
14391470 $ this ->depthAttribute => new Expression (
14401471 $ this ->getDb ()->quoteColumnName ($ this ->depthAttribute ) . sprintf ('%+d ' , $ depth ),
14411472 ),
1442- $ this ->treeAttribute => $ newTreeValue ,
1473+ $ this ->treeAttribute => $ targetNodeTreeValue ,
14431474 ],
14441475 [
14451476 'and ' ,
@@ -1454,9 +1485,20 @@ private function moveSubtreeToTargetTree(
14541485 $ rightValue ,
14551486 ],
14561487 [
1457- $ this ->treeAttribute => $ currentTreeValue ,
1488+ $ this ->treeAttribute => $ currentOwnerTreeValue ,
14581489 ],
14591490 ],
14601491 );
14611492 }
1493+
1494+ /**
1495+ * Resets the internal operation state after completing a nested set operation.
1496+ *
1497+ * Clears the current operation type and target node reference to prepare for subsequent operations..
1498+ */
1499+ private function resetOperationState (): void
1500+ {
1501+ $ this ->operation = null ;
1502+ $ this ->node = null ;
1503+ }
14621504}
0 commit comments