From bc14cec16c4079650820d59619f0b584c617f127 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Wed, 2 Jul 2025 20:29:16 -0400 Subject: [PATCH 1/5] feat: Add cache invalidation method and tests for `NestedSetsBehavior`. --- composer.json | 1 + src/NestedSetsBehavior.php | 36 +++++- tests/NestedSetsBehaviorTest.php | 184 +++++++++++++++++++++++++++++++ 3 files changed, 215 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 35d3f24..01b9b17 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "ext-simplexml": "*", "infection/infection": "^0.27|^0.29", "maglnet/composer-require-checker": "^4.1", + "php-forge/support": "^0.1", "phpstan/extension-installer": "^1.4", "phpstan/phpstan-strict-rules": "^2.0.3", "phpunit/phpunit": "^10.2", diff --git a/src/NestedSetsBehavior.php b/src/NestedSetsBehavior.php index ea1bdf4..5b463db 100644 --- a/src/NestedSetsBehavior.php +++ b/src/NestedSetsBehavior.php @@ -193,9 +193,7 @@ public function afterDelete(): void } $this->shiftLeftRightAttribute($this->getRightValue(), $deltaValue); - - $this->operation = null; - $this->node = null; + $this->invalidateCache(); } /** @@ -235,8 +233,7 @@ public function afterInsert(): void ); } - $this->operation = null; - $this->node = null; + $this->invalidateCache(); } /** @@ -268,17 +265,44 @@ public function afterUpdate(): void if ($this->operation === self::OPERATION_MAKE_ROOT) { $this->moveNodeAsRoot($currentOwnerTreeValue); + $this->invalidateCache(); return; } if ($this->node === null) { + $this->invalidateCache(); + return; } $context = $this->createMoveContext($this->node, $this->operation); - $this->moveNode($context); + $this->invalidateCache(); + } + + /** + * Invalidates cached attribute values and resets internal state. + * + * Clears the cached depth, left, and right attribute values, forcing them to be re-fetched from the owner model + * on next access. + * + * This method should be called after operations that modify the owner model's attributes to ensure that cached + * values remain consistent with the actual model state. + * + * Usage example: + * ```php + * // After modifying the model's attributes externally + * $behavior->invalidateCache(); + * ``` + */ + public function invalidateCache(): void + { + $this->depthValue = null; + $this->leftValue = null; + $this->node = null; + $this->operation = null; + $this->rightValue = null; } /** diff --git a/tests/NestedSetsBehaviorTest.php b/tests/NestedSetsBehaviorTest.php index 2cf733e..e60c3c9 100644 --- a/tests/NestedSetsBehaviorTest.php +++ b/tests/NestedSetsBehaviorTest.php @@ -5,6 +5,7 @@ namespace yii2\extensions\nestedsets\tests; use LogicException; +use PHPForge\Support\Assert; use Throwable; use yii\base\NotSupportedException; use yii\db\{ActiveRecord, Exception, StaleObjectException}; @@ -2282,4 +2283,187 @@ public function testMakeRootRefreshIsNecessaryForCorrectAttributeValues(): void "Reloaded node should have 'depth=0'.", ); } + + public function testCacheInvalidationAfterMakeRoot(): void + { + $this->createDatabase(); + + $root = new MultipleTree(['name' => 'Original Root']); + + $root->makeRoot(); + + $child = new MultipleTree(['name' => 'Child']); + + $child->appendTo($root); + + $behavior = $child->getBehavior('nestedSetsBehavior'); + + self::assertNotNull($behavior, 'Behavior should be attached to the child node.'); + + $originalDepth = $child->getAttribute('depth'); + $originalLeft = $child->getAttribute('lft'); + $originalRight = $child->getAttribute('rgt'); + + $cachedDepth = Assert::invokeMethod($behavior, 'getDepthValue'); + $cachedLeft = Assert::invokeMethod($behavior, 'getLeftValue'); + $cachedRight = Assert::invokeMethod($behavior, 'getRightValue'); + + self::assertEquals($originalDepth, $cachedDepth, 'Initial cached depth value should match attribute.'); + self::assertEquals($originalLeft, $cachedLeft, 'Initial cached left value should match attribute.'); + self::assertEquals($originalRight, $cachedRight, 'Initial cached right value should match attribute.'); + + $child->makeRoot(); + + $depthValueProperty = Assert::inaccessibleProperty($behavior, 'depthValue'); + $leftValueProperty = Assert::inaccessibleProperty($behavior, 'leftValue'); + $nodeValueProperty = Assert::inaccessibleProperty($behavior, 'node'); + $operationProperty = Assert::inaccessibleProperty($behavior, 'operation'); + $rightValueProperty = Assert::inaccessibleProperty($behavior, 'rightValue'); + + self::assertNull($depthValueProperty, 'Depth value cache should be null after manual invalidation.'); + self::assertNull($leftValueProperty, 'Left value cache should be null after manual invalidation.'); + self::assertNull($nodeValueProperty, 'Node cache should be null after manual invalidation.'); + self::assertNull($operationProperty, 'Operation cache should be null after manual invalidation.'); + self::assertNull($rightValueProperty, 'Right value cache should be null after manual invalidation.'); + + $newDepth = Assert::invokeMethod($behavior, 'getDepthValue'); + $newLeft = Assert::invokeMethod($behavior, 'getLeftValue'); + $newRight = Assert::invokeMethod($behavior, 'getRightValue'); + + self::assertEquals(0, $newDepth, 'New cached depth value should be 0 for root.'); + self::assertEquals(1, $newLeft, 'New cached left value should be 1 for root.'); + self::assertEquals(2, $newRight, 'New cached right value should be 2 for root.'); + } + + public function testCacheInvalidationAfterAppendTo(): void + { + $this->createDatabase(); + + $root = new MultipleTree(['name' => 'Root']); + + $root->makeRoot(); + + $child1 = new MultipleTree(['name' => 'Child 1']); + + $child1->appendTo($root); + + $child2 = new MultipleTree(['name' => 'Child 2']); + + $behavior = $child2->getBehavior('nestedSetsBehavior'); + + self::assertNotNull($behavior, 'Behavior should be attached to the child node.'); + + $child2->appendTo($root); + + Assert::invokeMethod($behavior, 'getDepthValue'); + Assert::invokeMethod($behavior, 'getLeftValue'); + Assert::invokeMethod($behavior, 'getRightValue'); + + $depthValueProperty = Assert::inaccessibleProperty($behavior, 'depthValue'); + $leftValueProperty = Assert::inaccessibleProperty($behavior, 'leftValue'); + $rightValueProperty = Assert::inaccessibleProperty($behavior, 'rightValue'); + + self::assertNotNull($depthValueProperty, 'Depth value cache should be populated before movement.'); + self::assertNotNull($leftValueProperty, 'Left value cache should be populated before movement.'); + self::assertNotNull($rightValueProperty, 'Right value cache should be populated before movement.'); + + $child2->appendTo($child1); + + $depthValueProperty = Assert::inaccessibleProperty($behavior, 'depthValue'); + $leftValueProperty = Assert::inaccessibleProperty($behavior, 'leftValue'); + $rightValueProperty = Assert::inaccessibleProperty($behavior, 'rightValue'); + + self::assertNull($depthValueProperty, 'Depth value cache should be invalidated after appendTo.'); + self::assertNull($leftValueProperty, 'Left value cache should be invalidated after appendTo.'); + self::assertNull($rightValueProperty, 'Right value cache should be invalidated after appendTo.'); + } + + public function testCacheInvalidationAfterDeleteWithChildren(): void + { + $this->createDatabase(); + + $root = new MultipleTree(['name' => 'Root']); + + $root->makeRoot(); + + $child = new MultipleTree(['name' => 'Child']); + + $child->appendTo($root); + + $grandchild = new MultipleTree(['name' => 'Grandchild']); + + $grandchild->appendTo($child); + $behavior = $child->getBehavior('nestedSetsBehavior'); + + self::assertNotNull($behavior, 'Behavior should be attached to the child node.'); + + Assert::invokeMethod($behavior, 'getDepthValue'); + Assert::invokeMethod($behavior, 'getLeftValue'); + Assert::invokeMethod($behavior, 'getRightValue'); + + $depthValueProperty = Assert::inaccessibleProperty($behavior, 'depthValue'); + $leftValueProperty = Assert::inaccessibleProperty($behavior, 'leftValue'); + $rightValueProperty = Assert::inaccessibleProperty($behavior, 'rightValue'); + + self::assertNotNull($depthValueProperty, 'Depth value cache should be populated before deletion.'); + self::assertNotNull($leftValueProperty, 'Left value cache should be populated before deletion.'); + self::assertNotNull($rightValueProperty, 'Right value cache should be populated before deletion.'); + + $child->deleteWithChildren(); + + $depthValueProperty = Assert::inaccessibleProperty($behavior, 'depthValue'); + $leftValueProperty = Assert::inaccessibleProperty($behavior, 'leftValue'); + $rightValueProperty = Assert::inaccessibleProperty($behavior, 'rightValue'); + + self::assertNull($depthValueProperty, 'Depth value cache should be invalidated after deleteWithChildren.'); + self::assertNull($leftValueProperty, 'Left value cache should be invalidated after deleteWithChildren.'); + self::assertNull($rightValueProperty, 'Right value cache should be invalidated after deleteWithChildren.'); + } + + public function testManualCacheInvalidation(): void + { + $this->createDatabase(); + + $root = new MultipleTree(['name' => 'Root']); + + $root->makeRoot(); + + $behavior = $root->getBehavior('nestedSetsBehavior'); + + self::assertNotNull($behavior, 'Behavior should be attached to the root node.'); + + Assert::invokeMethod($behavior, 'getDepthValue'); + Assert::invokeMethod($behavior, 'getLeftValue'); + Assert::invokeMethod($behavior, 'getRightValue'); + + $depthValueProperty = Assert::inaccessibleProperty($behavior, 'depthValue'); + $leftValueProperty = Assert::inaccessibleProperty($behavior, 'leftValue'); + $rightValueProperty = Assert::inaccessibleProperty($behavior, 'rightValue'); + + self::assertNotNull($depthValueProperty, 'Depth value cache should be populated.'); + self::assertNotNull($leftValueProperty, 'Left value cache should be populated.'); + self::assertNotNull($rightValueProperty, 'Right value cache should be populated.'); + + $root->invalidateCache(); + + $depthValueProperty = Assert::inaccessibleProperty($behavior, 'depthValue'); + $leftValueProperty = Assert::inaccessibleProperty($behavior, 'leftValue'); + $nodeValueProperty = Assert::inaccessibleProperty($behavior, 'node'); + $operationProperty = Assert::inaccessibleProperty($behavior, 'operation'); + $rightValueProperty = Assert::inaccessibleProperty($behavior, 'rightValue'); + + self::assertNull($depthValueProperty, 'Depth value cache should be null after manual invalidation.'); + self::assertNull($leftValueProperty, 'Left value cache should be null after manual invalidation.'); + self::assertNull($nodeValueProperty, 'Node cache should be null after manual invalidation.'); + self::assertNull($operationProperty, 'Operation cache should be null after manual invalidation.'); + self::assertNull($rightValueProperty, 'Right value cache should be null after manual invalidation.'); + + $newDepth = Assert::invokeMethod($behavior, 'getDepthValue'); + $newLeft = Assert::invokeMethod($behavior, 'getLeftValue'); + $newRight = Assert::invokeMethod($behavior, 'getRightValue'); + + self::assertEquals(0, $newDepth, 'Depth value should be correctly retrieved after invalidation.'); + self::assertEquals(1, $newLeft, 'Left value should be correctly retrieved after invalidation.'); + self::assertEquals(2, $newRight, 'Right value should be correctly retrieved after invalidation.'); + } } From e62e75ba37a5fdd0f9c0f1efc1941cbf6dd2e886 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Thu, 3 Jul 2025 05:39:42 -0400 Subject: [PATCH 2/5] feat: Implement cache invalidation method in `ExtendableNestedSetsBehavior` class. --- tests/NestedSetsBehaviorTest.php | 524 +++++++++++++++--- .../stub/ExtendableNestedSetsBehavior.php | 10 +- 2 files changed, 445 insertions(+), 89 deletions(-) diff --git a/tests/NestedSetsBehaviorTest.php b/tests/NestedSetsBehaviorTest.php index e60c3c9..f6cfaf1 100644 --- a/tests/NestedSetsBehaviorTest.php +++ b/tests/NestedSetsBehaviorTest.php @@ -2298,41 +2298,64 @@ public function testCacheInvalidationAfterMakeRoot(): void $behavior = $child->getBehavior('nestedSetsBehavior'); - self::assertNotNull($behavior, 'Behavior should be attached to the child node.'); - - $originalDepth = $child->getAttribute('depth'); - $originalLeft = $child->getAttribute('lft'); - $originalRight = $child->getAttribute('rgt'); - - $cachedDepth = Assert::invokeMethod($behavior, 'getDepthValue'); - $cachedLeft = Assert::invokeMethod($behavior, 'getLeftValue'); - $cachedRight = Assert::invokeMethod($behavior, 'getRightValue'); + self::assertNotNull( + $behavior, + 'Behavior should be attached to the child node.', + ); - self::assertEquals($originalDepth, $cachedDepth, 'Initial cached depth value should match attribute.'); - self::assertEquals($originalLeft, $cachedLeft, 'Initial cached left value should match attribute.'); - self::assertEquals($originalRight, $cachedRight, 'Initial cached right value should match attribute.'); + self::assertEquals( + $child->getAttribute('depth'), + Assert::invokeMethod($behavior, 'getDepthValue'), + 'Initial cached depth value should match attribute.', + ); + self::assertEquals( + $child->getAttribute('lft'), + Assert::invokeMethod($behavior, 'getLeftValue'), + 'Initial cached left value should match attribute.', + ); + self::assertEquals( + $child->getAttribute('rgt'), + Assert::invokeMethod($behavior, 'getRightValue'), + 'Initial cached right value should match attribute.', + ); $child->makeRoot(); - $depthValueProperty = Assert::inaccessibleProperty($behavior, 'depthValue'); - $leftValueProperty = Assert::inaccessibleProperty($behavior, 'leftValue'); - $nodeValueProperty = Assert::inaccessibleProperty($behavior, 'node'); - $operationProperty = Assert::inaccessibleProperty($behavior, 'operation'); - $rightValueProperty = Assert::inaccessibleProperty($behavior, 'rightValue'); - - self::assertNull($depthValueProperty, 'Depth value cache should be null after manual invalidation.'); - self::assertNull($leftValueProperty, 'Left value cache should be null after manual invalidation.'); - self::assertNull($nodeValueProperty, 'Node cache should be null after manual invalidation.'); - self::assertNull($operationProperty, 'Operation cache should be null after manual invalidation.'); - self::assertNull($rightValueProperty, 'Right value cache should be null after manual invalidation.'); - - $newDepth = Assert::invokeMethod($behavior, 'getDepthValue'); - $newLeft = Assert::invokeMethod($behavior, 'getLeftValue'); - $newRight = Assert::invokeMethod($behavior, 'getRightValue'); - - self::assertEquals(0, $newDepth, 'New cached depth value should be 0 for root.'); - self::assertEquals(1, $newLeft, 'New cached left value should be 1 for root.'); - self::assertEquals(2, $newRight, 'New cached right value should be 2 for root.'); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'depthValue'), + "Depth value cache should be 'null' after manual invalidation.", + ); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'leftValue'), + "Left value cache should be 'null' after manual invalidation.", + ); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'node'), + "Node cache should be 'null' after manual invalidation.", + ); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'operation'), + "Operation cache should be 'null' after manual invalidation.", + ); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'rightValue'), + "Right value cache should be 'null' after manual invalidation.", + ); + self::assertEquals( + 0, + Assert::invokeMethod($behavior, 'getDepthValue'), + "New cached depth value should be '0' for root.", + ); + self::assertEquals( + 1, + Assert::invokeMethod($behavior, 'getLeftValue'), + "New cached left value should be '1' for root.", + ); + self::assertEquals( + 2, + Assert::invokeMethod($behavior, 'getRightValue'), + "New cached right value should be '2' for root.", + ); } public function testCacheInvalidationAfterAppendTo(): void @@ -2351,7 +2374,10 @@ public function testCacheInvalidationAfterAppendTo(): void $behavior = $child2->getBehavior('nestedSetsBehavior'); - self::assertNotNull($behavior, 'Behavior should be attached to the child node.'); + self::assertNotNull( + $behavior, + 'Behavior should be attached to the child node.', + ); $child2->appendTo($root); @@ -2359,23 +2385,33 @@ public function testCacheInvalidationAfterAppendTo(): void Assert::invokeMethod($behavior, 'getLeftValue'); Assert::invokeMethod($behavior, 'getRightValue'); - $depthValueProperty = Assert::inaccessibleProperty($behavior, 'depthValue'); - $leftValueProperty = Assert::inaccessibleProperty($behavior, 'leftValue'); - $rightValueProperty = Assert::inaccessibleProperty($behavior, 'rightValue'); - - self::assertNotNull($depthValueProperty, 'Depth value cache should be populated before movement.'); - self::assertNotNull($leftValueProperty, 'Left value cache should be populated before movement.'); - self::assertNotNull($rightValueProperty, 'Right value cache should be populated before movement.'); + self::assertNotNull( + Assert::inaccessibleProperty($behavior, 'depthValue'), + 'Depth value cache should be populated before movement.', + ); + self::assertNotNull( + Assert::inaccessibleProperty($behavior, 'leftValue'), + 'Left value cache should be populated before movement.', + ); + self::assertNotNull( + Assert::inaccessibleProperty($behavior, 'rightValue'), + 'Right value cache should be populated before movement.', + ); $child2->appendTo($child1); - $depthValueProperty = Assert::inaccessibleProperty($behavior, 'depthValue'); - $leftValueProperty = Assert::inaccessibleProperty($behavior, 'leftValue'); - $rightValueProperty = Assert::inaccessibleProperty($behavior, 'rightValue'); - - self::assertNull($depthValueProperty, 'Depth value cache should be invalidated after appendTo.'); - self::assertNull($leftValueProperty, 'Left value cache should be invalidated after appendTo.'); - self::assertNull($rightValueProperty, 'Right value cache should be invalidated after appendTo.'); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'depthValue'), + "Depth value cache should be invalidated after 'appendTo()'.", + ); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'leftValue'), + "Left value cache should be invalidated after 'appendTo()'.", + ); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'rightValue'), + "Right value cache should be invalidated after 'appendTo()'.", + ); } public function testCacheInvalidationAfterDeleteWithChildren(): void @@ -2395,29 +2431,42 @@ public function testCacheInvalidationAfterDeleteWithChildren(): void $grandchild->appendTo($child); $behavior = $child->getBehavior('nestedSetsBehavior'); - self::assertNotNull($behavior, 'Behavior should be attached to the child node.'); + self::assertNotNull( + $behavior, + 'Behavior should be attached to the child node.', + ); Assert::invokeMethod($behavior, 'getDepthValue'); Assert::invokeMethod($behavior, 'getLeftValue'); Assert::invokeMethod($behavior, 'getRightValue'); - $depthValueProperty = Assert::inaccessibleProperty($behavior, 'depthValue'); - $leftValueProperty = Assert::inaccessibleProperty($behavior, 'leftValue'); - $rightValueProperty = Assert::inaccessibleProperty($behavior, 'rightValue'); - - self::assertNotNull($depthValueProperty, 'Depth value cache should be populated before deletion.'); - self::assertNotNull($leftValueProperty, 'Left value cache should be populated before deletion.'); - self::assertNotNull($rightValueProperty, 'Right value cache should be populated before deletion.'); + self::assertNotNull( + Assert::inaccessibleProperty($behavior, 'depthValue'), + 'Depth value cache should be populated before deletion.', + ); + self::assertNotNull( + Assert::inaccessibleProperty($behavior, 'leftValue'), + 'Left value cache should be populated before deletion.', + ); + self::assertNotNull( + Assert::inaccessibleProperty($behavior, 'rightValue'), + 'Right value cache should be populated before deletion.', + ); $child->deleteWithChildren(); - $depthValueProperty = Assert::inaccessibleProperty($behavior, 'depthValue'); - $leftValueProperty = Assert::inaccessibleProperty($behavior, 'leftValue'); - $rightValueProperty = Assert::inaccessibleProperty($behavior, 'rightValue'); - - self::assertNull($depthValueProperty, 'Depth value cache should be invalidated after deleteWithChildren.'); - self::assertNull($leftValueProperty, 'Left value cache should be invalidated after deleteWithChildren.'); - self::assertNull($rightValueProperty, 'Right value cache should be invalidated after deleteWithChildren.'); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'depthValue'), + "Depth value cache should be invalidated after 'deleteWithChildren()'.", + ); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'leftValue'), + "Left value cache should be invalidated after 'deleteWithChildren()'.", + ); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'rightValue'), + "Right value cache should be invalidated after 'deleteWithChildren()'.", + ); } public function testManualCacheInvalidation(): void @@ -2430,40 +2479,339 @@ public function testManualCacheInvalidation(): void $behavior = $root->getBehavior('nestedSetsBehavior'); - self::assertNotNull($behavior, 'Behavior should be attached to the root node.'); + self::assertNotNull( + $behavior, + 'Behavior should be attached to the root node.', + ); Assert::invokeMethod($behavior, 'getDepthValue'); Assert::invokeMethod($behavior, 'getLeftValue'); Assert::invokeMethod($behavior, 'getRightValue'); - $depthValueProperty = Assert::inaccessibleProperty($behavior, 'depthValue'); - $leftValueProperty = Assert::inaccessibleProperty($behavior, 'leftValue'); - $rightValueProperty = Assert::inaccessibleProperty($behavior, 'rightValue'); - - self::assertNotNull($depthValueProperty, 'Depth value cache should be populated.'); - self::assertNotNull($leftValueProperty, 'Left value cache should be populated.'); - self::assertNotNull($rightValueProperty, 'Right value cache should be populated.'); + self::assertNotNull( + Assert::inaccessibleProperty($behavior, 'depthValue'), + 'Depth value cache should be populated.', + ); + self::assertNotNull( + Assert::inaccessibleProperty($behavior, 'leftValue'), + 'Left value cache should be populated.', + ); + self::assertNotNull( + Assert::inaccessibleProperty($behavior, 'rightValue'), + 'Right value cache should be populated.', + ); $root->invalidateCache(); - $depthValueProperty = Assert::inaccessibleProperty($behavior, 'depthValue'); - $leftValueProperty = Assert::inaccessibleProperty($behavior, 'leftValue'); - $nodeValueProperty = Assert::inaccessibleProperty($behavior, 'node'); - $operationProperty = Assert::inaccessibleProperty($behavior, 'operation'); - $rightValueProperty = Assert::inaccessibleProperty($behavior, 'rightValue'); - - self::assertNull($depthValueProperty, 'Depth value cache should be null after manual invalidation.'); - self::assertNull($leftValueProperty, 'Left value cache should be null after manual invalidation.'); - self::assertNull($nodeValueProperty, 'Node cache should be null after manual invalidation.'); - self::assertNull($operationProperty, 'Operation cache should be null after manual invalidation.'); - self::assertNull($rightValueProperty, 'Right value cache should be null after manual invalidation.'); - - $newDepth = Assert::invokeMethod($behavior, 'getDepthValue'); - $newLeft = Assert::invokeMethod($behavior, 'getLeftValue'); - $newRight = Assert::invokeMethod($behavior, 'getRightValue'); - - self::assertEquals(0, $newDepth, 'Depth value should be correctly retrieved after invalidation.'); - self::assertEquals(1, $newLeft, 'Left value should be correctly retrieved after invalidation.'); - self::assertEquals(2, $newRight, 'Right value should be correctly retrieved after invalidation.'); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'depthValue'), + "Depth value cache should be 'null' after manual invalidation.", + ); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'leftValue'), + "Left value cache should be 'null' after manual invalidation.", + ); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'node'), + "Node cache should be 'null' after manual invalidation.", + ); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'operation'), + "Operation cache should be 'null' after manual invalidation.", + ); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'rightValue'), + "Right value cache should be 'null' after manual invalidation.", + ); + self::assertEquals( + 0, + Assert::invokeMethod($behavior, 'getDepthValue'), + 'Depth value should be correctly retrieved after invalidation.', + ); + self::assertEquals( + 1, + Assert::invokeMethod($behavior, 'getLeftValue'), + 'Left value should be correctly retrieved after invalidation.', + ); + self::assertEquals( + 2, + Assert::invokeMethod($behavior, 'getRightValue'), + 'Right value should be correctly retrieved after invalidation.', + ); + } + + public function testCacheInvalidationAfterInsertWithTreeAttribute(): void + { + $this->createDatabase(); + + $node = new MultipleTree(['name' => 'Root Node']); + + $behavior = $node->getBehavior('nestedSetsBehavior'); + + self::assertNotNull( + $behavior, + 'Behavior should be attached to the node.', + ); + + $node->makeRoot(); + + Assert::invokeMethod($behavior, 'getDepthValue'); + Assert::invokeMethod($behavior, 'getLeftValue'); + Assert::invokeMethod($behavior, 'getRightValue'); + + self::assertNotNull( + Assert::inaccessibleProperty($behavior, 'depthValue'), + 'Depth value cache should be populated after access.', + ); + self::assertNotNull( + Assert::inaccessibleProperty($behavior, 'leftValue'), + 'Left value cache should be populated after access.', + ); + self::assertNotNull( + Assert::inaccessibleProperty($behavior, 'rightValue'), + 'Right value cache should be populated after access.', + ); + + $node->invalidateCache(); + + self::assertNull( + Assert::inaccessibleProperty($behavior, 'depthValue'), + "Depth value cache should be invalidated after 'invalidateCache()'.", + ); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'leftValue'), + "Left value cache should be invalidated after 'invalidateCache()'.", + ); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'rightValue'), + "Right value cache should be invalidated after 'invalidateCache()'.", + ); + self::assertEquals( + 0, + Assert::invokeMethod($behavior, 'getDepthValue'), + "New cached depth value should be '0' for root.", + ); + self::assertEquals( + 1, + Assert::invokeMethod($behavior, 'getLeftValue'), + "New cached left value should be '1' for root.", + ); + self::assertEquals( + 2, + Assert::invokeMethod($behavior, 'getRightValue'), + "New cached right value should be '2' for root.", + ); + self::assertNotFalse( + $node->treeAttribute, + 'Tree attribute should be set.', + ); + self::assertNotNull( + $node->getAttribute($node->treeAttribute), + "Tree attribute should be set after 'afterInsert()'.", + ); + self::assertNotNull( + $node->owner, + "Node owner should not be null after 'makeRoot()'.", + ); + self::assertEquals( + $node->owner->getPrimaryKey(), + $node->getAttribute($node->treeAttribute), + 'Tree attribute should equal primary key for root node.', + ); + } + + public function testCacheInvalidationAfterInsertWithoutTreeAttribute(): void + { + $this->createDatabase(); + + $node = new Tree(['name' => 'Root Node']); + + $behavior = $node->getBehavior('nestedSetsBehavior'); + + self::assertNotNull( + $behavior, + 'Behavior should be attached to the node.', + ); + + $node->makeRoot(); + + Assert::invokeMethod($behavior, 'getDepthValue'); + Assert::invokeMethod($behavior, 'getLeftValue'); + Assert::invokeMethod($behavior, 'getRightValue'); + + self::assertNotNull( + Assert::inaccessibleProperty($behavior, 'depthValue'), + 'Depth value cache should be populated after access.', + ); + self::assertNotNull( + Assert::inaccessibleProperty($behavior, 'leftValue'), + 'Left value cache should be populated after access.', + ); + self::assertNotNull( + Assert::inaccessibleProperty($behavior, 'rightValue'), + 'Right value cache should be populated after access.', + ); + + $node->invalidateCache(); + + self::assertNull( + Assert::inaccessibleProperty($behavior, 'depthValue'), + "Depth value cache should be invalidated after 'invalidateCache()'.", + ); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'leftValue'), + "Left value cache should be invalidated after 'invalidateCache()'.", + ); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'rightValue'), + "Right value cache should be invalidated after 'invalidateCache()'.", + ); + self::assertEquals( + 0, + Assert::invokeMethod($behavior, 'getDepthValue'), + "New cached depth value should be '0' for root.", + ); + self::assertEquals( + 1, + Assert::invokeMethod($behavior, 'getLeftValue'), + "New cached left value should be '1' for root.", + ); + self::assertEquals( + 2, + Assert::invokeMethod($behavior, 'getRightValue'), + "New cached right value should be '2' for root.", + ); + } + + public function testAfterInsertCallsInvalidateCache(): void + { + $this->createDatabase(); + + $node = new ExtendableMultipleTree(['name' => 'Root Node']); + + $behavior = $node->getBehavior('nestedSetsBehavior'); + + self::assertInstanceOf( + ExtendableNestedSetsBehavior::class, + $behavior, + "'ExtendableMultipleTree' should use 'ExtendableNestedSetsBehavior'.", + ); + + $node->makeRoot(); + + self::assertTrue( + $behavior->invalidateCacheCalled, + "'invalidateCache()' should be called during 'afterInsert()'.", + ); + self::assertNotFalse( + $node->treeAttribute, + 'Tree attribute should be set.', + ); + self::assertNotNull( + $node->getAttribute($node->treeAttribute), + "Tree attribute should be set after 'afterInsert()'.", + ); + self::assertEquals( + $node->getPrimaryKey(), + $node->getAttribute($node->treeAttribute), + 'Tree attribute should equal primary key for root node.', + ); + } + + public function testAfterInsertCacheInvalidationIntegration(): void + { + $this->createDatabase(); + + $root = new MultipleTree(['name' => 'Original Root']); + + $root->makeRoot(); + + $child = new MultipleTree(['name' => 'Child Node']); + + $child->appendTo($root); + + $behavior = $child->getBehavior('nestedSetsBehavior'); + + self::assertNotNull( + $behavior, + 'Behavior should be attached to the child node.', + ); + self::assertEquals( + 1, + $child->getAttribute('depth'), + "Child should start at depth '1'.", + ); + self::assertEquals( + 2, + $child->getAttribute('lft'), + "Child should start with 'lft=2'.", + ); + self::assertEquals( + 3, + $child->getAttribute('rgt'), + "Child should start with 'rgt=3'.", + ); + + Assert::invokeMethod($behavior, 'getDepthValue'); + Assert::invokeMethod($behavior, 'getLeftValue'); + Assert::invokeMethod($behavior, 'getRightValue'); + + self::assertNotNull( + Assert::inaccessibleProperty($behavior, 'depthValue'), + 'Depth value cache should be populated.', + ); + self::assertNotNull( + Assert::inaccessibleProperty($behavior, 'leftValue'), + 'Left value cache should be populated.', + ); + self::assertNotNull( + Assert::inaccessibleProperty($behavior, 'rightValue'), + 'Right value cache should be populated.', + ); + + $child->makeRoot(); + + self::assertNull( + Assert::inaccessibleProperty($behavior, 'depthValue'), + "Depth value cache should be invalidated after 'makeRoot()'/'afterInsert()'.", + ); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'leftValue'), + "Left value cache should be invalidated after 'makeRoot()'/'afterInsert()'.", + ); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'rightValue'), + "Right value cache should be invalidated after 'makeRoot()'/'afterInsert()'.", + ); + self::assertEquals( + 0, + $child->getAttribute('depth'), + "Child should be at depth '0' after becoming root.", + ); + self::assertEquals( + 1, + $child->getAttribute('lft'), + "Child should have 'lft=1' after becoming root.", + ); + self::assertEquals( + 2, + $child->getAttribute('rgt'), + "Child should have 'rgt=2' after becoming root.", + ); + self::assertEquals( + 0, + Assert::invokeMethod($behavior, 'getDepthValue'), + "New cached depth should be '0'.", + ); + self::assertEquals( + 1, + Assert::invokeMethod($behavior, 'getLeftValue'), + "New cached left should be '1'.", + ); + self::assertEquals( + 2, + Assert::invokeMethod($behavior, 'getRightValue'), + "New cached right should be '2'.", + ); } } diff --git a/tests/support/stub/ExtendableNestedSetsBehavior.php b/tests/support/stub/ExtendableNestedSetsBehavior.php index 67c4a29..97ce530 100644 --- a/tests/support/stub/ExtendableNestedSetsBehavior.php +++ b/tests/support/stub/ExtendableNestedSetsBehavior.php @@ -14,6 +14,8 @@ */ final class ExtendableNestedSetsBehavior extends NestedSetsBehavior { + public bool $invalidateCacheCalled = false; + /** * @phpstan-var array */ @@ -44,7 +46,6 @@ public function exposedMoveNode(ActiveRecord $node, int $value, int $depth): voi { $this->calledMethods['moveNode'] = true; - // Create a mock context for testing compatibility $context = new \yii2\extensions\nestedsets\NodeContext( $node, 0, @@ -84,4 +85,11 @@ public function getCalledMethods(): array { return $this->calledMethods; } + + public function invalidateCache(): void + { + $this->invalidateCacheCalled = true; + + parent::invalidateCache(); + } } From 229b75ad9ec825d976dd3b8ee6b82563665e7b48 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Thu, 3 Jul 2025 06:02:17 -0400 Subject: [PATCH 3/5] refactor: Simplify cache validation in `NestedSetsBehaviorTest` by introducing helper methods. --- tests/NestedSetsBehaviorTest.php | 266 ++++++++----------------------- 1 file changed, 69 insertions(+), 197 deletions(-) diff --git a/tests/NestedSetsBehaviorTest.php b/tests/NestedSetsBehaviorTest.php index f6cfaf1..c1466ce 100644 --- a/tests/NestedSetsBehaviorTest.php +++ b/tests/NestedSetsBehaviorTest.php @@ -7,7 +7,7 @@ use LogicException; use PHPForge\Support\Assert; use Throwable; -use yii\base\NotSupportedException; +use yii\base\{Behavior, NotSupportedException}; use yii\db\{ActiveRecord, Exception, StaleObjectException}; use yii\helpers\ArrayHelper; use yii2\extensions\nestedsets\NestedSetsBehavior; @@ -2321,26 +2321,8 @@ public function testCacheInvalidationAfterMakeRoot(): void $child->makeRoot(); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'depthValue'), - "Depth value cache should be 'null' after manual invalidation.", - ); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'leftValue'), - "Left value cache should be 'null' after manual invalidation.", - ); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'node'), - "Node cache should be 'null' after manual invalidation.", - ); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'operation'), - "Operation cache should be 'null' after manual invalidation.", - ); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'rightValue'), - "Right value cache should be 'null' after manual invalidation.", - ); + $this->verifyCacheInvalidation($behavior); + self::assertEquals( 0, Assert::invokeMethod($behavior, 'getDepthValue'), @@ -2381,37 +2363,11 @@ public function testCacheInvalidationAfterAppendTo(): void $child2->appendTo($root); - Assert::invokeMethod($behavior, 'getDepthValue'); - Assert::invokeMethod($behavior, 'getLeftValue'); - Assert::invokeMethod($behavior, 'getRightValue'); - - self::assertNotNull( - Assert::inaccessibleProperty($behavior, 'depthValue'), - 'Depth value cache should be populated before movement.', - ); - self::assertNotNull( - Assert::inaccessibleProperty($behavior, 'leftValue'), - 'Left value cache should be populated before movement.', - ); - self::assertNotNull( - Assert::inaccessibleProperty($behavior, 'rightValue'), - 'Right value cache should be populated before movement.', - ); + $this->populateAndVerifyCache($behavior); $child2->appendTo($child1); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'depthValue'), - "Depth value cache should be invalidated after 'appendTo()'.", - ); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'leftValue'), - "Left value cache should be invalidated after 'appendTo()'.", - ); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'rightValue'), - "Right value cache should be invalidated after 'appendTo()'.", - ); + $this->verifyCacheInvalidation($behavior); } public function testCacheInvalidationAfterDeleteWithChildren(): void @@ -2436,37 +2392,11 @@ public function testCacheInvalidationAfterDeleteWithChildren(): void 'Behavior should be attached to the child node.', ); - Assert::invokeMethod($behavior, 'getDepthValue'); - Assert::invokeMethod($behavior, 'getLeftValue'); - Assert::invokeMethod($behavior, 'getRightValue'); - - self::assertNotNull( - Assert::inaccessibleProperty($behavior, 'depthValue'), - 'Depth value cache should be populated before deletion.', - ); - self::assertNotNull( - Assert::inaccessibleProperty($behavior, 'leftValue'), - 'Left value cache should be populated before deletion.', - ); - self::assertNotNull( - Assert::inaccessibleProperty($behavior, 'rightValue'), - 'Right value cache should be populated before deletion.', - ); + $this->populateAndVerifyCache($behavior); $child->deleteWithChildren(); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'depthValue'), - "Depth value cache should be invalidated after 'deleteWithChildren()'.", - ); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'leftValue'), - "Left value cache should be invalidated after 'deleteWithChildren()'.", - ); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'rightValue'), - "Right value cache should be invalidated after 'deleteWithChildren()'.", - ); + $this->verifyCacheInvalidation($behavior); } public function testManualCacheInvalidation(): void @@ -2484,45 +2414,12 @@ public function testManualCacheInvalidation(): void 'Behavior should be attached to the root node.', ); - Assert::invokeMethod($behavior, 'getDepthValue'); - Assert::invokeMethod($behavior, 'getLeftValue'); - Assert::invokeMethod($behavior, 'getRightValue'); - - self::assertNotNull( - Assert::inaccessibleProperty($behavior, 'depthValue'), - 'Depth value cache should be populated.', - ); - self::assertNotNull( - Assert::inaccessibleProperty($behavior, 'leftValue'), - 'Left value cache should be populated.', - ); - self::assertNotNull( - Assert::inaccessibleProperty($behavior, 'rightValue'), - 'Right value cache should be populated.', - ); + $this->populateAndVerifyCache($behavior); $root->invalidateCache(); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'depthValue'), - "Depth value cache should be 'null' after manual invalidation.", - ); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'leftValue'), - "Left value cache should be 'null' after manual invalidation.", - ); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'node'), - "Node cache should be 'null' after manual invalidation.", - ); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'operation'), - "Operation cache should be 'null' after manual invalidation.", - ); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'rightValue'), - "Right value cache should be 'null' after manual invalidation.", - ); + $this->verifyCacheInvalidation($behavior); + self::assertEquals( 0, Assert::invokeMethod($behavior, 'getDepthValue'), @@ -2555,37 +2452,12 @@ public function testCacheInvalidationAfterInsertWithTreeAttribute(): void $node->makeRoot(); - Assert::invokeMethod($behavior, 'getDepthValue'); - Assert::invokeMethod($behavior, 'getLeftValue'); - Assert::invokeMethod($behavior, 'getRightValue'); - - self::assertNotNull( - Assert::inaccessibleProperty($behavior, 'depthValue'), - 'Depth value cache should be populated after access.', - ); - self::assertNotNull( - Assert::inaccessibleProperty($behavior, 'leftValue'), - 'Left value cache should be populated after access.', - ); - self::assertNotNull( - Assert::inaccessibleProperty($behavior, 'rightValue'), - 'Right value cache should be populated after access.', - ); + $this->populateAndVerifyCache($behavior); $node->invalidateCache(); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'depthValue'), - "Depth value cache should be invalidated after 'invalidateCache()'.", - ); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'leftValue'), - "Left value cache should be invalidated after 'invalidateCache()'.", - ); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'rightValue'), - "Right value cache should be invalidated after 'invalidateCache()'.", - ); + $this->verifyCacheInvalidation($behavior); + self::assertEquals( 0, Assert::invokeMethod($behavior, 'getDepthValue'), @@ -2635,37 +2507,12 @@ public function testCacheInvalidationAfterInsertWithoutTreeAttribute(): void $node->makeRoot(); - Assert::invokeMethod($behavior, 'getDepthValue'); - Assert::invokeMethod($behavior, 'getLeftValue'); - Assert::invokeMethod($behavior, 'getRightValue'); - - self::assertNotNull( - Assert::inaccessibleProperty($behavior, 'depthValue'), - 'Depth value cache should be populated after access.', - ); - self::assertNotNull( - Assert::inaccessibleProperty($behavior, 'leftValue'), - 'Left value cache should be populated after access.', - ); - self::assertNotNull( - Assert::inaccessibleProperty($behavior, 'rightValue'), - 'Right value cache should be populated after access.', - ); + $this->populateAndVerifyCache($behavior); $node->invalidateCache(); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'depthValue'), - "Depth value cache should be invalidated after 'invalidateCache()'.", - ); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'leftValue'), - "Left value cache should be invalidated after 'invalidateCache()'.", - ); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'rightValue'), - "Right value cache should be invalidated after 'invalidateCache()'.", - ); + $this->verifyCacheInvalidation($behavior); + self::assertEquals( 0, Assert::invokeMethod($behavior, 'getDepthValue'), @@ -2752,37 +2599,12 @@ public function testAfterInsertCacheInvalidationIntegration(): void "Child should start with 'rgt=3'.", ); - Assert::invokeMethod($behavior, 'getDepthValue'); - Assert::invokeMethod($behavior, 'getLeftValue'); - Assert::invokeMethod($behavior, 'getRightValue'); - - self::assertNotNull( - Assert::inaccessibleProperty($behavior, 'depthValue'), - 'Depth value cache should be populated.', - ); - self::assertNotNull( - Assert::inaccessibleProperty($behavior, 'leftValue'), - 'Left value cache should be populated.', - ); - self::assertNotNull( - Assert::inaccessibleProperty($behavior, 'rightValue'), - 'Right value cache should be populated.', - ); + $this->populateAndVerifyCache($behavior); $child->makeRoot(); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'depthValue'), - "Depth value cache should be invalidated after 'makeRoot()'/'afterInsert()'.", - ); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'leftValue'), - "Left value cache should be invalidated after 'makeRoot()'/'afterInsert()'.", - ); - self::assertNull( - Assert::inaccessibleProperty($behavior, 'rightValue'), - "Right value cache should be invalidated after 'makeRoot()'/'afterInsert()'.", - ); + $this->verifyCacheInvalidation($behavior); + self::assertEquals( 0, $child->getAttribute('depth'), @@ -2814,4 +2636,54 @@ public function testAfterInsertCacheInvalidationIntegration(): void "New cached right should be '2'.", ); } + + /** + * @phpstan-param Behavior $behavior + */ + private function populateAndVerifyCache(Behavior $behavior): void + { + Assert::invokeMethod($behavior, 'getDepthValue'); + Assert::invokeMethod($behavior, 'getLeftValue'); + Assert::invokeMethod($behavior, 'getRightValue'); + + self::assertNotNull( + Assert::inaccessibleProperty($behavior, 'depthValue'), + 'Depth value cache should be populated.', + ); + self::assertNotNull( + Assert::inaccessibleProperty($behavior, 'leftValue'), + 'Left value cache should be populated.', + ); + self::assertNotNull( + Assert::inaccessibleProperty($behavior, 'rightValue'), + 'Right value cache should be populated.', + ); + } + + /** + * @phpstan-param Behavior $behavior + */ + private function verifyCacheInvalidation(Behavior $behavior): void + { + self::assertNull( + Assert::inaccessibleProperty($behavior, 'depthValue'), + "Depth value cache should be invalidated after 'makeRoot()'/'afterInsert()'.", + ); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'leftValue'), + "Left value cache should be invalidated after 'makeRoot()'/'afterInsert()'.", + ); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'node'), + "Node cache should be 'null' after manual invalidation.", + ); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'operation'), + "Operation cache should be 'null' after manual invalidation.", + ); + self::assertNull( + Assert::inaccessibleProperty($behavior, 'rightValue'), + "Right value cache should be invalidated after 'makeRoot()'/'afterInsert()'.", + ); + } } From 73b3059334551a7e82bd96d007ef748aa6ef3941 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Thu, 3 Jul 2025 06:31:34 -0400 Subject: [PATCH 4/5] feat: Add cache invalidation test for root node creation and extend behavior with operation setter. --- src/NestedSetsBehavior.php | 30 +++++++++---------- tests/NestedSetsBehaviorTest.php | 28 +++++++++++++++++ .../stub/ExtendableNestedSetsBehavior.php | 5 ++++ 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/NestedSetsBehavior.php b/src/NestedSetsBehavior.php index 5b463db..7aadb6b 100644 --- a/src/NestedSetsBehavior.php +++ b/src/NestedSetsBehavior.php @@ -103,6 +103,11 @@ class NestedSetsBehavior extends Behavior */ public string $depthAttribute = 'depth'; + /** + * Stores the depth value for the current operation. + */ + protected int|null $depthValue = null; + /** * Name of the attribute that stores the left boundary value of the node in the nested set tree. * @@ -110,6 +115,11 @@ class NestedSetsBehavior extends Behavior */ public string $leftAttribute = 'lft'; + /** + * Stores the left value for the current operation. + */ + protected int|null $leftValue = null; + /** * Name of the attribute that stores the right boundary value of the node in the nested set tree. * @@ -117,6 +127,11 @@ class NestedSetsBehavior extends Behavior */ public string $rightAttribute = 'rgt'; + /** + * Stores the right value for the current operation. + */ + protected int|null $rightValue = null; + /** * Name of the attribute that stores the tree identifier for supporting multiple trees. */ @@ -130,21 +145,6 @@ 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. * diff --git a/tests/NestedSetsBehaviorTest.php b/tests/NestedSetsBehaviorTest.php index c1466ce..1f37e76 100644 --- a/tests/NestedSetsBehaviorTest.php +++ b/tests/NestedSetsBehaviorTest.php @@ -2637,6 +2637,34 @@ public function testAfterInsertCacheInvalidationIntegration(): void ); } + public function testAfterUpdateCacheInvalidationWhenMakeRoot(): void + { + $this->createDatabase(); + + $root = new ExtendableMultipleTree(['name' => 'Root']); + + $root->makeRoot(); + + $child = new ExtendableMultipleTree(['name' => 'Child']); + + $child->appendTo($root); + + $behavior = $child->getBehavior('nestedSetsBehavior'); + + self::assertInstanceOf( + ExtendableNestedSetsBehavior::class, + $behavior, + "'ExtendableMultipleTree' should use 'ExtendableNestedSetsBehavior'.", + ); + + $this->populateAndVerifyCache($behavior); + + $behavior->setOperation(NestedSetsBehavior::OPERATION_MAKE_ROOT); + $behavior->afterUpdate(); + + $this->verifyCacheInvalidation($behavior); + } + /** * @phpstan-param Behavior $behavior */ diff --git a/tests/support/stub/ExtendableNestedSetsBehavior.php b/tests/support/stub/ExtendableNestedSetsBehavior.php index 97ce530..64313c6 100644 --- a/tests/support/stub/ExtendableNestedSetsBehavior.php +++ b/tests/support/stub/ExtendableNestedSetsBehavior.php @@ -92,4 +92,9 @@ public function invalidateCache(): void parent::invalidateCache(); } + + public function setOperation(string|null $operation): void + { + $this->operation = $operation; + } } From b6daebcd533e579da51fdb10258762734887a4a4 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Thu, 3 Jul 2025 06:41:43 -0400 Subject: [PATCH 5/5] feat: Add test for cache invalidation after updating root node with null and implement `setNode` method in `ExtendableNestedSetsBehavior`. --- tests/NestedSetsBehaviorTest.php | 28 +++++++++++++++++++ .../stub/ExtendableNestedSetsBehavior.php | 5 ++++ 2 files changed, 33 insertions(+) diff --git a/tests/NestedSetsBehaviorTest.php b/tests/NestedSetsBehaviorTest.php index 1f37e76..c99007e 100644 --- a/tests/NestedSetsBehaviorTest.php +++ b/tests/NestedSetsBehaviorTest.php @@ -2665,6 +2665,34 @@ public function testAfterUpdateCacheInvalidationWhenMakeRoot(): void $this->verifyCacheInvalidation($behavior); } + public function testAfterUpdateCacheInvalidationWhenMakeRootAndNodeItsNull(): void + { + $this->createDatabase(); + + $root = new ExtendableMultipleTree(['name' => 'Root']); + + $root->makeRoot(); + + $child = new ExtendableMultipleTree(['name' => 'Child']); + + $child->appendTo($root); + + $behavior = $child->getBehavior('nestedSetsBehavior'); + + self::assertInstanceOf( + ExtendableNestedSetsBehavior::class, + $behavior, + "'ExtendableMultipleTree' should use 'ExtendableNestedSetsBehavior'.", + ); + + $this->populateAndVerifyCache($behavior); + + $behavior->setNode(null); + $behavior->afterUpdate(); + + $this->verifyCacheInvalidation($behavior); + } + /** * @phpstan-param Behavior $behavior */ diff --git a/tests/support/stub/ExtendableNestedSetsBehavior.php b/tests/support/stub/ExtendableNestedSetsBehavior.php index 64313c6..2396d69 100644 --- a/tests/support/stub/ExtendableNestedSetsBehavior.php +++ b/tests/support/stub/ExtendableNestedSetsBehavior.php @@ -93,6 +93,11 @@ public function invalidateCache(): void parent::invalidateCache(); } + public function setNode(ActiveRecord|null $node): void + { + $this->node = $node; + } + public function setOperation(string|null $operation): void { $this->operation = $operation;