Skip to content

Commit 958fb1c

Browse files
committed
feat: Introduce NodeContext class for improved node movement handling and refactor moveNode method to use context.
1 parent 0a4141f commit 958fb1c

3 files changed

Lines changed: 273 additions & 78 deletions

File tree

src/NestedSetsBehavior.php

Lines changed: 119 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace yii2\extensions\nestedsets;
66

77
use LogicException;
8+
use RuntimeException;
89
use yii\base\{Behavior, NotSupportedException};
910
use 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

Comments
 (0)