From 29b06e9ffacbe15012c17977c700104a233259a3 Mon Sep 17 00:00:00 2001 From: Armin Salihovic Date: Sat, 5 Apr 2025 21:06:18 +0200 Subject: [PATCH 1/2] Add UUID support --- composer.json | 3 +- phpunit.php | 8 +- src/NestedSet.php | 11 +- src/NestedSetServiceProvider.php | 6 +- tests/NodeTest.php | 996 +---------------------------- tests/NodeTestBase.php | 1021 ++++++++++++++++++++++++++++++ tests/NodeUuidTest.php | 30 + tests/ScopedNodeTest.php | 236 +------ tests/ScopedNodeTestBase.php | 251 ++++++++ tests/ScopedNodeUuidTest.php | 30 + tests/data/CategoryData.php | 42 ++ tests/data/MenuItemData.php | 36 ++ tests/data/categories.php | 15 - tests/data/menu_items.php | 8 - tests/models/CategoryUuid.php | 12 + tests/models/MenuItemUuid.php | 12 + 16 files changed, 1483 insertions(+), 1234 deletions(-) create mode 100644 tests/NodeTestBase.php create mode 100644 tests/NodeUuidTest.php create mode 100644 tests/ScopedNodeTestBase.php create mode 100644 tests/ScopedNodeUuidTest.php create mode 100644 tests/data/CategoryData.php create mode 100644 tests/data/MenuItemData.php delete mode 100644 tests/data/categories.php delete mode 100644 tests/data/menu_items.php create mode 100644 tests/models/CategoryUuid.php create mode 100644 tests/models/MenuItemUuid.php diff --git a/composer.json b/composer.json index 037244d..e3c4a6d 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,8 @@ } }, "require-dev": { - "phpunit/phpunit": "7.*|8.*|9.*|^10.5" + "phpunit/phpunit": "7.*|8.*|9.*|^10.5", + "ramsey/uuid": "^4.7" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/phpunit.php b/phpunit.php index 7f72aff..db5312b 100644 --- a/phpunit.php +++ b/phpunit.php @@ -9,4 +9,10 @@ $capsule->setAsGlobal(); include __DIR__.'/tests/models/Category.php'; -include __DIR__.'/tests/models/MenuItem.php'; \ No newline at end of file +include __DIR__.'/tests/models/MenuItem.php'; +include __DIR__.'/tests/ScopedNodeTestBase.php'; +include __DIR__.'/tests/NodeTestBase.php'; +include __DIR__.'/tests/models/CategoryUuid.php'; +include __DIR__.'/tests/models/MenuItemUuid.php'; +include __DIR__.'/tests/data/CategoryData.php'; +include __DIR__.'/tests/data/MenuItemData.php'; diff --git a/src/NestedSet.php b/src/NestedSet.php index 8ec8e02..fb826da 100644 --- a/src/NestedSet.php +++ b/src/NestedSet.php @@ -36,11 +36,16 @@ class NestedSet * * @param \Illuminate\Database\Schema\Blueprint $table */ - public static function columns(Blueprint $table) + public static function columns(Blueprint $table, bool $uuid = false) { $table->unsignedInteger(self::LFT)->default(0); $table->unsignedInteger(self::RGT)->default(0); - $table->unsignedInteger(self::PARENT_ID)->nullable(); + + if ($uuid) { + $table->uuid(self::PARENT_ID)->nullable()->index(); + } else { + $table->unsignedInteger(self::PARENT_ID)->nullable(); + } $table->index(static::getDefaultColumns()); } @@ -80,4 +85,4 @@ public static function isNode($node) return is_object($node) && in_array(NodeTrait::class, (array)$node); } -} \ No newline at end of file +} diff --git a/src/NestedSetServiceProvider.php b/src/NestedSetServiceProvider.php index b4516f7..ce4c1a9 100644 --- a/src/NestedSetServiceProvider.php +++ b/src/NestedSetServiceProvider.php @@ -13,8 +13,12 @@ public function register() NestedSet::columns($this); }); + Blueprint::macro('nestedSetWithUuid', function () { + NestedSet::columns($this, true); + }); + Blueprint::macro('dropNestedSet', function () { NestedSet::dropColumns($this); }); } -} \ No newline at end of file +} diff --git a/tests/NodeTest.php b/tests/NodeTest.php index 3b0831a..f3727a1 100644 --- a/tests/NodeTest.php +++ b/tests/NodeTest.php @@ -1,1000 +1,30 @@ dropIfExists('categories'); - - Capsule::disableQueryLog(); - - $schema->create('categories', function (\Illuminate\Database\Schema\Blueprint $table) { - $table->increments('id'); - $table->string('name'); - $table->softDeletes(); - NestedSet::columns($table); - }); - - Capsule::enableQueryLog(); - } - - public function setUp(): void - { - $data = include __DIR__.'/data/categories.php'; - - Capsule::table('categories')->insert($data); - - Capsule::flushQueryLog(); - - Category::resetActionsPerformed(); - - date_default_timezone_set('America/Denver'); - } - - public function tearDown(): void - { - Capsule::table('categories')->truncate(); - } - - // public static function tearDownAfterClass() - // { - // $log = Capsule::getQueryLog(); - // foreach ($log as $item) { - // echo $item['query']." with ".implode(', ', $item['bindings'])."\n"; - // } - // } - - public function assertTreeNotBroken($table = 'categories') - { - $checks = array(); - - $connection = Capsule::connection(); - - $table = $connection->getQueryGrammar()->wrapTable($table); - - // Check if lft and rgt values are ok - $checks[] = "from $table where _lft >= _rgt or (_rgt - _lft) % 2 = 0"; - - // Check if lft and rgt values are unique - $checks[] = "from $table c1, $table c2 where c1.id <> c2.id and ". - "(c1._lft=c2._lft or c1._rgt=c2._rgt or c1._lft=c2._rgt or c1._rgt=c2._lft)"; - - // Check if parent_id is set correctly - $checks[] = "from $table c, $table p, $table m where c.parent_id=p.id and m.id <> p.id and m.id <> c.id and ". - "(c._lft not between p._lft and p._rgt or c._lft between m._lft and m._rgt and m._lft between p._lft and p._rgt)"; - - foreach ($checks as $i => $check) { - $checks[$i] = 'select 1 as error '.$check; - } - - $sql = 'select max(error) as errors from ('.implode(' union ', $checks).') _'; - - $actual = $connection->selectOne($sql); - - $this->assertEquals(null, $actual->errors, "The tree structure of $table is broken!"); - $actual = (array)Capsule::connection()->selectOne($sql); - - $this->assertEquals(array('errors' => null), $actual, "The tree structure of $table is broken!"); - } - - public function dumpTree($items = null) - { - if ( ! $items) $items = Category::withTrashed()->defaultOrder()->get(); - - foreach ($items as $item) { - echo PHP_EOL.($item->trashed() ? '-' : '+').' '.$item->name." ".$item->getKey().' '.$item->getLft()." ".$item->getRgt().' '.$item->getParentId(); - } - } - - public function assertNodeReceivesValidValues($node) - { - $lft = $node->getLft(); - $rgt = $node->getRgt(); - $nodeInDb = $this->findCategory($node->name); - - $this->assertEquals( - [ $nodeInDb->getLft(), $nodeInDb->getRgt() ], - [ $lft, $rgt ], - 'Node is not synced with database after save.' - ); - } - - /** - * @param $name - * - * @return \Category - */ - public function findCategory($name, $withTrashed = false) - { - $q = new Category; - - $q = $withTrashed ? $q->withTrashed() : $q->newQuery(); - - return $q->whereName($name)->first(); - } - - public function testTreeNotBroken() - { - $this->assertTreeNotBroken(); - $this->assertFalse(Category::isBroken()); - } - - public function nodeValues($node) - { - return array($node->_lft, $node->_rgt, $node->parent_id); - } - - public function testGetsNodeData() - { - $data = Category::getNodeData(3); - - $this->assertEquals([ '_lft' => 3, '_rgt' => 4 ], $data); - } - - public function testGetsPlainNodeData() - { - $data = Category::getPlainNodeData(3); - - $this->assertEquals([ 3, 4 ], $data); - } - - public function testReceivesValidValuesWhenAppendedTo() - { - $node = new Category([ 'name' => 'test' ]); - $root = Category::root(); - - $accepted = array($root->_rgt, $root->_rgt + 1, $root->id); - - $root->appendNode($node); - - $this->assertTrue($node->hasMoved()); - $this->assertEquals($accepted, $this->nodeValues($node)); - $this->assertTreeNotBroken(); - $this->assertFalse($node->isDirty()); - $this->assertTrue($node->isDescendantOf($root)); - } - - public function testReceivesValidValuesWhenPrependedTo() - { - $root = Category::root(); - $node = new Category([ 'name' => 'test' ]); - $root->prependNode($node); - - $this->assertTrue($node->hasMoved()); - $this->assertEquals(array($root->_lft + 1, $root->_lft + 2, $root->id), $this->nodeValues($node)); - $this->assertTreeNotBroken(); - $this->assertTrue($node->isDescendantOf($root)); - $this->assertTrue($root->isAncestorOf($node)); - $this->assertTrue($node->isChildOf($root)); - } - - public function testReceivesValidValuesWhenInsertedAfter() - { - $target = $this->findCategory('apple'); - $node = new Category([ 'name' => 'test' ]); - $node->afterNode($target)->save(); - - $this->assertTrue($node->hasMoved()); - $this->assertEquals(array($target->_rgt + 1, $target->_rgt + 2, $target->parent->id), $this->nodeValues($node)); - $this->assertTreeNotBroken(); - $this->assertFalse($node->isDirty()); - $this->assertTrue($node->isSiblingOf($target)); - } - - public function testReceivesValidValuesWhenInsertedBefore() - { - $target = $this->findCategory('apple'); - $node = new Category([ 'name' => 'test' ]); - $node->beforeNode($target)->save(); - - $this->assertTrue($node->hasMoved()); - $this->assertEquals(array($target->_lft, $target->_lft + 1, $target->parent->id), $this->nodeValues($node)); - $this->assertTreeNotBroken(); - } - - public function testCategoryMovesDown() - { - $node = $this->findCategory('apple'); - $target = $this->findCategory('mobile'); - - $target->appendNode($node); - - $this->assertTrue($node->hasMoved()); - $this->assertNodeReceivesValidValues($node); - $this->assertTreeNotBroken(); - } - - public function testCategoryMovesUp() - { - $node = $this->findCategory('samsung'); - $target = $this->findCategory('notebooks'); - - $target->appendNode($node); - - $this->assertTrue($node->hasMoved()); - $this->assertTreeNotBroken(); - $this->assertNodeReceivesValidValues($node); - } - - public function testFailsToInsertIntoChild() - { - $this->expectException(Exception::class); - - $node = $this->findCategory('notebooks'); - $target = $node->children()->first(); - - $node->afterNode($target)->save(); - } - - public function testFailsToAppendIntoItself() - { - $this->expectException(Exception::class); - - $node = $this->findCategory('notebooks'); - - $node->appendToNode($node)->save(); - } - - public function testFailsToPrependIntoItself() - { - $this->expectException(Exception::class); - - $node = $this->findCategory('notebooks'); - - $node->prependTo($node)->save(); - } - - public function testWithoutRootWorks() - { - $result = Category::withoutRoot()->pluck('name'); - - $this->assertNotEquals('store', $result); - } - - public function testAncestorsReturnsAncestorsWithoutNodeItself() - { - $node = $this->findCategory('apple'); - $path = all($node->ancestors()->pluck('name')); - - $this->assertEquals(array('store', 'notebooks'), $path); - } - - public function testGetsAncestorsByStatic() - { - $path = all(Category::ancestorsOf(3)->pluck('name')); - - $this->assertEquals(array('store', 'notebooks'), $path); - } - - public function testGetsAncestorsDirect() - { - $path = all(Category::find(8)->getAncestors()->pluck('id')); - - $this->assertEquals(array(1, 5, 7), $path); - } - - public function testDescendants() - { - $node = $this->findCategory('mobile'); - $descendants = all($node->descendants()->pluck('name')); - $expected = array('nokia', 'samsung', 'galaxy', 'sony', 'lenovo'); - - $this->assertEquals($expected, $descendants); - - $descendants = all($node->getDescendants()->pluck('name')); - - $this->assertEquals(count($descendants), $node->getDescendantCount()); - $this->assertEquals($expected, $descendants); - - $descendants = all(Category::descendantsAndSelf(7)->pluck('name')); - $expected = [ 'samsung', 'galaxy' ]; - - $this->assertEquals($expected, $descendants); - } - - public function testWithDepthWorks() - { - $nodes = all(Category::withDepth()->limit(4)->pluck('depth')); - - $this->assertEquals(array(0, 1, 2, 2), $nodes); - } - - public function testWithDepthWithCustomKeyWorks() - { - $node = Category::whereIsRoot()->withDepth('level')->first(); - - $this->assertTrue(isset($node['level'])); + parent::__construct($name); + $this->categoryData = new CategoryData(); } - public function testWithDepthWorksAlongWithDefaultKeys() + protected function getTable(): string { - $node = Category::withDepth()->first(); - - $this->assertTrue(isset($node->name)); + return 'categories'; } - public function testParentIdAttributeAccessorAppendsNode() + protected function getModelClass(): string { - $node = new Category(array('name' => 'lg', 'parent_id' => 5)); - $node->save(); - - $this->assertEquals(5, $node->parent_id); - $this->assertEquals(5, $node->getParentId()); - - $node->parent_id = null; - $node->save(); - - $node->refreshNode(); - - $this->assertEquals(null, $node->parent_id); - $this->assertTrue($node->isRoot()); + return Category::class; } - public function testFailsToSaveNodeUntilNotInserted() + protected function createTable(\Illuminate\Database\Schema\Blueprint $table): void { - $this->expectException(Exception::class); - - $node = new Category; - $node->save(); - } - - public function testNodeIsDeletedWithDescendants() - { - $node = $this->findCategory('mobile'); - $node->forceDelete(); - - $this->assertTreeNotBroken(); - - $nodes = Category::whereIn('id', array(5, 6, 7, 8, 9))->count(); - $this->assertEquals(0, $nodes); - - $root = Category::root(); - $this->assertEquals(8, $root->getRgt()); + $table->increments('id'); + $table->string('name'); + $table->softDeletes(); + NestedSet::columns($table); } - - public function testNodeIsSoftDeleted() - { - $root = Category::root(); - - $samsung = $this->findCategory('samsung'); - $samsung->delete(); - - $this->assertTreeNotBroken(); - - $this->assertNull($this->findCategory('galaxy')); - - sleep(1); - - $node = $this->findCategory('mobile'); - $node->delete(); - - $nodes = Category::whereIn('id', array(5, 6, 7, 8, 9))->count(); - $this->assertEquals(0, $nodes); - - $originalRgt = $root->getRgt(); - $root->refreshNode(); - - $this->assertEquals($originalRgt, $root->getRgt()); - - $node = $this->findCategory('mobile', true); - - $node->restore(); - - $this->assertNull($this->findCategory('samsung')); - $this->assertNotNull($this->findCategory('nokia')); - } - - public function testSoftDeletedNodeisDeletedWhenParentIsDeleted() - { - $this->findCategory('samsung')->delete(); - - $this->findCategory('mobile')->forceDelete(); - - $this->assertTreeNotBroken(); - - $this->assertNull($this->findCategory('samsung', true)); - $this->assertNull($this->findCategory('sony')); - } - - public function testFailsToSaveNodeUntilParentIsSaved() - { - $this->expectException(Exception::class); - - $node = new Category(array('title' => 'Node')); - $parent = new Category(array('title' => 'Parent')); - - $node->appendTo($parent)->save(); - } - - public function testSiblings() - { - $node = $this->findCategory('samsung'); - $siblings = all($node->siblings()->pluck('id')); - $next = all($node->nextSiblings()->pluck('id')); - $prev = all($node->prevSiblings()->pluck('id')); - - $this->assertEquals(array(6, 9, 10), $siblings); - $this->assertEquals(array(9, 10), $next); - $this->assertEquals(array(6), $prev); - - $siblings = all($node->getSiblings()->pluck('id')); - $next = all($node->getNextSiblings()->pluck('id')); - $prev = all($node->getPrevSiblings()->pluck('id')); - - $this->assertEquals(array(6, 9, 10), $siblings); - $this->assertEquals(array(9, 10), $next); - $this->assertEquals(array(6), $prev); - - $next = $node->getNextSibling(); - $prev = $node->getPrevSibling(); - - $this->assertEquals(9, $next->id); - $this->assertEquals(6, $prev->id); - } - - public function testFetchesReversed() - { - $node = $this->findCategory('sony'); - $siblings = $node->prevSiblings()->reversed()->value('id'); - - $this->assertEquals(7, $siblings); - } - - public function testToTreeBuildsWithDefaultOrder() - { - $tree = Category::whereBetween('_lft', array(8, 17))->defaultOrder()->get()->toTree(); - - $this->assertEquals(1, count($tree)); - - $root = $tree->first(); - $this->assertEquals('mobile', $root->name); - $this->assertEquals(4, count($root->children)); - } - - public function testToTreeBuildsWithCustomOrder() - { - $tree = Category::whereBetween('_lft', array(8, 17)) - ->orderBy('title') - ->get() - ->toTree(); - - $this->assertEquals(1, count($tree)); - - $root = $tree->first(); - $this->assertEquals('mobile', $root->name); - $this->assertEquals(4, count($root->children)); - $this->assertEquals($root, $root->children->first()->parent); - } - - public function testToTreeWithSpecifiedRoot() - { - $node = $this->findCategory('mobile'); - $nodes = Category::whereBetween('_lft', array(8, 17))->get(); - - $tree1 = \Kalnoy\Nestedset\Collection::make($nodes)->toTree(5); - $tree2 = \Kalnoy\Nestedset\Collection::make($nodes)->toTree($node); - - $this->assertEquals(4, $tree1->count()); - $this->assertEquals(4, $tree2->count()); - } - - public function testToTreeBuildsWithDefaultOrderAndMultipleRootNodes() - { - $tree = Category::withoutRoot()->get()->toTree(); - - $this->assertEquals(2, count($tree)); - } - - public function testToTreeBuildsWithRootItemIdProvided() - { - $tree = Category::whereBetween('_lft', array(8, 17))->get()->toTree(5); - - $this->assertEquals(4, count($tree)); - - $root = $tree[1]; - $this->assertEquals('samsung', $root->name); - $this->assertEquals(1, count($root->children)); - } - - public function testRetrievesNextNode() - { - $node = $this->findCategory('apple'); - $next = $node->nextNodes()->first(); - - $this->assertEquals('lenovo', $next->name); - } - - public function testRetrievesPrevNode() - { - $node = $this->findCategory('apple'); - $next = $node->getPrevNode(); - - $this->assertEquals('notebooks', $next->name); - } - - public function testMultipleAppendageWorks() - { - $parent = $this->findCategory('mobile'); - - $child = new Category([ 'name' => 'test' ]); - - $parent->appendNode($child); - - $child->appendNode(new Category([ 'name' => 'sub' ])); - - $parent->appendNode(new Category([ 'name' => 'test2' ])); - - $this->assertTreeNotBroken(); - } - - public function testDefaultCategoryIsSavedAsRoot() - { - $node = new Category([ 'name' => 'test' ]); - $node->save(); - - $this->assertEquals(23, $node->_lft); - $this->assertTreeNotBroken(); - - $this->assertTrue($node->isRoot()); - } - - public function testExistingCategorySavedAsRoot() - { - $node = $this->findCategory('apple'); - $node->saveAsRoot(); - - $this->assertTreeNotBroken(); - $this->assertTrue($node->isRoot()); - } - - public function testNodeMovesDownSeveralPositions() - { - $node = $this->findCategory('nokia'); - - $this->assertTrue($node->down(2)); - - $this->assertEquals($node->_lft, 15); - } - - public function testNodeMovesUpSeveralPositions() - { - $node = $this->findCategory('sony'); - - $this->assertTrue($node->up(2)); - - $this->assertEquals($node->_lft, 9); - } - - public function testCountsTreeErrors() - { - $errors = Category::countErrors(); - - $this->assertEquals([ 'oddness' => 0, - 'duplicates' => 0, - 'wrong_parent' => 0, - 'missing_parent' => 0 ], $errors); - - Category::where('id', '=', 5)->update([ '_lft' => 14 ]); - Category::where('id', '=', 8)->update([ 'parent_id' => 2 ]); - Category::where('id', '=', 11)->update([ '_lft' => 20 ]); - Category::where('id', '=', 4)->update([ 'parent_id' => 24 ]); - - $errors = Category::countErrors(); - - $this->assertEquals(1, $errors['oddness']); - $this->assertEquals(2, $errors['duplicates']); - $this->assertEquals(1, $errors['missing_parent']); - } - - public function testCreatesNode() - { - $node = Category::create([ 'name' => 'test' ]); - - $this->assertEquals(23, $node->getLft()); - } - - public function testCreatesViaRelationship() - { - $node = $this->findCategory('apple'); - - $child = $node->children()->create([ 'name' => 'test' ]); - - $this->assertTreeNotBroken(); - } - - public function testCreatesTree() - { - $node = Category::create( - [ - 'name' => 'test', - 'children' => - [ - [ 'name' => 'test2' ], - [ 'name' => 'test3' ], - ], - ]); - - $this->assertTreeNotBroken(); - - $this->assertTrue(isset($node->children)); - - $node = $this->findCategory('test'); - - $this->assertCount(2, $node->children); - $this->assertEquals('test2', $node->children[0]->name); - } - - public function testDescendantsOfNonExistingNode() - { - $node = new Category; - - $this->assertTrue($node->getDescendants()->isEmpty()); - } - - public function testWhereDescendantsOf() - { - $this->expectException(\Illuminate\Database\Eloquent\ModelNotFoundException::class); - - Category::whereDescendantOf(124)->get(); - } - - public function testAncestorsByNode() - { - $category = $this->findCategory('apple'); - $ancestors = all(Category::whereAncestorOf($category)->pluck('id')); - - $this->assertEquals([ 1, 2 ], $ancestors); - } - - public function testDescendantsByNode() - { - $category = $this->findCategory('notebooks'); - $res = all(Category::whereDescendantOf($category)->pluck('id')); - - $this->assertEquals([ 3, 4 ], $res); - } - - public function testMultipleDeletionsDoNotBrakeTree() - { - $category = $this->findCategory('mobile'); - - foreach ($category->children()->take(2)->get() as $child) - { - $child->forceDelete(); - } - - $this->assertTreeNotBroken(); - } - - public function testTreeIsFixed() - { - Category::where('id', '=', 5)->update([ '_lft' => 14 ]); - Category::where('id', '=', 8)->update([ 'parent_id' => 2 ]); - Category::where('id', '=', 11)->update([ '_lft' => 20 ]); - Category::where('id', '=', 2)->update([ 'parent_id' => 24 ]); - - $fixed = Category::fixTree(); - - $this->assertTrue($fixed > 0); - $this->assertTreeNotBroken(); - - $node = Category::find(8); - - $this->assertEquals(2, $node->getParentId()); - - $node = Category::find(2); - - $this->assertEquals(null, $node->getParentId()); - } - - public function testSubtreeIsFixed() - { - Category::where('id', '=', 8)->update([ '_lft' => 11 ]); - - $fixed = Category::fixSubtree(Category::find(5)); - $this->assertEquals($fixed, 1); - $this->assertTreeNotBroken(); - $this->assertEquals(Category::find(8)->getLft(), 12); - } - - public function testParentIdDirtiness() - { - $node = $this->findCategory('apple'); - $node->parent_id = 5; - - $this->assertTrue($node->isDirty('parent_id')); - - $node = $this->findCategory('apple'); - $node->parent_id = null; - - $this->assertTrue($node->isDirty('parent_id')); - } - - public function testIsDirtyMovement() - { - $node = $this->findCategory('apple'); - $otherNode = $this->findCategory('samsung'); - - $this->assertFalse($node->isDirty()); - - $node->afterNode($otherNode); - - $this->assertTrue($node->isDirty()); - - $node = $this->findCategory('apple'); - $otherNode = $this->findCategory('samsung'); - - $this->assertFalse($node->isDirty()); - - $node->appendToNode($otherNode); - - $this->assertTrue($node->isDirty()); - } - - public function testRootNodesMoving() - { - $node = $this->findCategory('store'); - $node->down(); - - $this->assertEquals(3, $node->getLft()); - } - - public function testDescendantsRelation() - { - $node = $this->findCategory('notebooks'); - $result = $node->descendants; - - $this->assertEquals(2, $result->count()); - $this->assertEquals('apple', $result->first()->name); - } - - public function testDescendantsEagerlyLoaded() - { - $nodes = Category::whereIn('id', [ 2, 5 ])->get(); - - $nodes->load('descendants'); - - $this->assertEquals(2, $nodes->count()); - $this->assertTrue($nodes->first()->relationLoaded('descendants')); - } - - public function testDescendantsRelationQuery() - { - $nodes = Category::has('descendants')->whereIn('id', [ 2, 3 ])->get(); - - $this->assertEquals(1, $nodes->count()); - $this->assertEquals(2, $nodes->first()->getKey()); - - $nodes = Category::has('descendants', '>', 2)->get(); - - $this->assertEquals(2, $nodes->count()); - $this->assertEquals(1, $nodes[0]->getKey()); - $this->assertEquals(5, $nodes[1]->getKey()); - } - - public function testParentRelationQuery() - { - $nodes = Category::has('parent')->whereIn('id', [ 1, 2 ]); - - $this->assertEquals(1, $nodes->count()); - $this->assertEquals(2, $nodes->first()->getKey()); - } - - public function testRebuildTree() - { - $fixed = Category::rebuildTree([ - [ - 'id' => 1, - 'children' => [ - [ 'id' => 10 ], - [ 'id' => 3, 'name' => 'apple v2', 'children' => [ [ 'name' => 'new node' ] ] ], - [ 'id' => 2 ], - - ] - ] - ]); - - $this->assertTrue($fixed > 0); - $this->assertTreeNotBroken(); - - $node = Category::find(3); - - $this->assertEquals(1, $node->getParentId()); - $this->assertEquals('apple v2', $node->name); - $this->assertEquals(4, $node->getLft()); - - $node = $this->findCategory('new node'); - - $this->assertNotNull($node); - $this->assertEquals(3, $node->getParentId()); - } - - public function testRebuildSubtree() - { - $fixed = Category::rebuildSubtree(Category::find(7), [ - [ 'name' => 'new node' ], - [ 'id' => '8' ], - ]); - - $this->assertTrue($fixed > 0); - $this->assertTreeNotBroken(); - - $node = $this->findCategory('new node'); - - $this->assertNotNull($node); - $this->assertEquals($node->getLft(), 12); - } - - public function testRebuildTreeWithDeletion() - { - Category::rebuildTree([ [ 'name' => 'all deleted' ] ], true); - - $this->assertTreeNotBroken(); - - $nodes = Category::get(); - - $this->assertEquals(1, $nodes->count()); - $this->assertEquals('all deleted', $nodes->first()->name); - - $nodes = Category::withTrashed()->get(); - - $this->assertTrue($nodes->count() > 1); - } - - public function testRebuildFailsWithInvalidPK() - { - $this->expectException(\Illuminate\Database\Eloquent\ModelNotFoundException::class); - - Category::rebuildTree([ [ 'id' => 24 ] ]); - } - - public function testFlatTree() - { - $node = $this->findCategory('mobile'); - $tree = $node->descendants()->orderBy('name')->get()->toFlatTree(); - - $this->assertCount(5, $tree); - $this->assertEquals('samsung', $tree[2]->name); - $this->assertEquals('galaxy', $tree[3]->name); - } - - // Commented, cause there is no assertion here and otherwise the test is marked as risky in PHPUnit 7. - // What's the purpose of this method? @todo: remove/update? - /*public function testSeveralNodesModelWork() - { - $category = new Category; - - $category->name = 'test'; - - $category->saveAsRoot(); - - $duplicate = new DuplicateCategory; - - $duplicate->name = 'test'; - - $duplicate->saveAsRoot(); - }*/ - - public function testWhereIsLeaf() - { - $categories = Category::leaves(); - - $this->assertEquals(7, $categories->count()); - $this->assertEquals('apple', $categories->first()->name); - $this->assertTrue($categories->first()->isLeaf()); - - $category = Category::whereIsRoot()->first(); - - $this->assertFalse($category->isLeaf()); - } - - public function testEagerLoadAncestors() - { - $queryLogCount = count(Capsule::connection()->getQueryLog()); - $categories = Category::with('ancestors')->orderBy('name')->get(); - - $this->assertEquals($queryLogCount + 2, count(Capsule::connection()->getQueryLog())); - - $expectedShape = [ - 'apple (3)}' => 'store (1) > notebooks (2)', - 'galaxy (8)}' => 'store (1) > mobile (5) > samsung (7)', - 'lenovo (4)}' => 'store (1) > notebooks (2)', - 'lenovo (10)}' => 'store (1) > mobile (5)', - 'mobile (5)}' => 'store (1)', - 'nokia (6)}' => 'store (1) > mobile (5)', - 'notebooks (2)}' => 'store (1)', - 'samsung (7)}' => 'store (1) > mobile (5)', - 'sony (9)}' => 'store (1) > mobile (5)', - 'store (1)}' => '', - 'store_2 (11)}' => '' - ]; - - $output = []; - - foreach ($categories as $category) { - $output["{$category->name} ({$category->id})}"] = $category->ancestors->count() - ? implode(' > ', $category->ancestors->map(function ($cat) { return "{$cat->name} ({$cat->id})"; })->toArray()) - : ''; - } - - $this->assertEquals($expectedShape, $output); - } - - public function testLazyLoadAncestors() - { - $queryLogCount = count(Capsule::connection()->getQueryLog()); - $categories = Category::orderBy('name')->get(); - - $this->assertEquals($queryLogCount + 1, count(Capsule::connection()->getQueryLog())); - - $expectedShape = [ - 'apple (3)}' => 'store (1) > notebooks (2)', - 'galaxy (8)}' => 'store (1) > mobile (5) > samsung (7)', - 'lenovo (4)}' => 'store (1) > notebooks (2)', - 'lenovo (10)}' => 'store (1) > mobile (5)', - 'mobile (5)}' => 'store (1)', - 'nokia (6)}' => 'store (1) > mobile (5)', - 'notebooks (2)}' => 'store (1)', - 'samsung (7)}' => 'store (1) > mobile (5)', - 'sony (9)}' => 'store (1) > mobile (5)', - 'store (1)}' => '', - 'store_2 (11)}' => '' - ]; - - $output = []; - - foreach ($categories as $category) { - $output["{$category->name} ({$category->id})}"] = $category->ancestors->count() - ? implode(' > ', $category->ancestors->map(function ($cat) { return "{$cat->name} ({$cat->id})"; })->toArray()) - : ''; - } - - // assert that there is number of original query + 1 + number of rows to fulfill the relation - $this->assertEquals($queryLogCount + 12, count(Capsule::connection()->getQueryLog())); - - $this->assertEquals($expectedShape, $output); - } - - public function testWhereHasCountQueryForAncestors() - { - $categories = all(Category::has('ancestors', '>', 2)->pluck('name')); - - $this->assertEquals([ 'galaxy' ], $categories); - - $categories = all(Category::whereHas('ancestors', function ($query) { - $query->where('id', 5); - })->pluck('name')); - - $this->assertEquals([ 'nokia', 'samsung', 'galaxy', 'sony', 'lenovo' ], $categories); - } - - public function testReplication() - { - $category = $this->findCategory('nokia'); - $category = $category->replicate(); - $category->save(); - $category->refreshNode(); - - $this->assertNull($category->getParentId()); - - $category = $this->findCategory('nokia'); - $category = $category->replicate(); - $category->parent_id = 1; - $category->save(); - - $category->refreshNode(); - - $this->assertEquals(1, $category->getParentId()); - } - } - -function all($items) -{ - return is_array($items) ? $items : $items->all(); -} \ No newline at end of file diff --git a/tests/NodeTestBase.php b/tests/NodeTestBase.php new file mode 100644 index 0000000..a9799ca --- /dev/null +++ b/tests/NodeTestBase.php @@ -0,0 +1,1021 @@ +getTable(); + } + + public static function setUpBeforeClass(): void + { + $schema = Capsule::schema(); + $table = static::getTableName(); + + $schema->dropIfExists($table); + + Capsule::disableQueryLog(); + + $schema->create($table, function (\Illuminate\Database\Schema\Blueprint $table) { + $testClass = get_called_class(); + (new $testClass('dummy'))->createTable($table); + }); + + Capsule::enableQueryLog(); + } + + public function setUp(): void + { + $this->ids = $this->categoryData->getIds(); + Capsule::table($this->getTable())->insert($this->categoryData->getData()); + + Capsule::flushQueryLog(); + + $modelClass = $this->getModelClass(); + $modelClass::resetActionsPerformed(); + + date_default_timezone_set('America/Denver'); + } + + public function tearDown(): void + { + Capsule::table($this->getTable())->truncate(); + } + + protected function assertTreeNotBroken($table = null) + { + $table = $table ?? $this->getTable(); + $checks = array(); + + $connection = Capsule::connection(); + + $table = $connection->getQueryGrammar()->wrapTable($table); + + // Check if lft and rgt values are ok + $checks[] = "from $table where _lft >= _rgt or (_rgt - _lft) % 2 = 0"; + + // Check if lft and rgt values are unique + $checks[] = "from $table c1, $table c2 where c1.id <> c2.id and " . + "(c1._lft=c2._lft or c1._rgt=c2._rgt or c1._lft=c2._rgt or c1._rgt=c2._lft)"; + + // Check if parent_id is set correctly + $checks[] = "from $table c, $table p, $table m where c.parent_id=p.id and m.id <> p.id and m.id <> c.id and " . + "(c._lft not between p._lft and p._rgt or c._lft between m._lft and m._rgt and m._lft between p._lft and p._rgt)"; + + foreach ($checks as $i => $check) { + $checks[$i] = 'select 1 as error ' . $check; + } + + $sql = 'select max(error) as errors from (' . implode(' union ', $checks) . ') _'; + + $actual = $connection->selectOne($sql); + + $this->assertEquals(null, $actual->errors, "The tree structure of $table is broken!"); + $actual = (array)Capsule::connection()->selectOne($sql); + + $this->assertEquals(array('errors' => null), $actual, "The tree structure of $table is broken!"); + } + + public function dumpTree($items = null) + { + if (!$items) $items = $this->getModelClass()::withTrashed()->defaultOrder()->get(); + + foreach ($items as $item) { + echo PHP_EOL . ($item->trashed() ? '-' : '+') . ' ' . $item->name . " " . $item->getKey() . ' ' . $item->getLft() . " " . $item->getRgt() . ' ' . $item->getParentId(); + } + } + + public function assertNodeReceivesValidValues($node) + { + $lft = $node->getLft(); + $rgt = $node->getRgt(); + $nodeInDb = $this->findCategory($node->name); + + $this->assertEquals( + [$nodeInDb->getLft(), $nodeInDb->getRgt()], + [$lft, $rgt], + 'Node is not synced with database after save.' + ); + } + + /** + * @param $name + * + * @return \Category + */ + protected function findCategory($name, $withTrashed = false) + { + $modelClass = $this->getModelClass(); + $q = new $modelClass; + + $q = $withTrashed ? $q->withTrashed() : $q->newQuery(); + + return $q->whereName($name)->first(); + } + + public function testTreeNotBroken() + { + $this->assertTreeNotBroken(); + $this->assertFalse($this->getModelClass()::isBroken()); + } + + public function nodeValues($node) + { + return array($node->_lft, $node->_rgt, $node->parent_id); + } + + public function testGetsNodeData() + { + $data = $this->getModelClass()::getNodeData($this->ids[3]); + + $this->assertEquals(['_lft' => 3, '_rgt' => 4], $data); + } + + public function testGetsPlainNodeData() + { + $data = $this->getModelClass()::getPlainNodeData($this->ids[3]); + + $this->assertEquals([3, 4], $data); + } + + public function testReceivesValidValuesWhenAppendedTo() + { + $model = $this->getModelClass(); + $node = new $model(['name' => 'test']); + $root = $this->getModelClass()::root(); + + $accepted = array($root->_rgt, $root->_rgt + 1, $root->id); + + $root->appendNode($node); + + $this->assertTrue($node->hasMoved()); + $this->assertEquals($accepted, $this->nodeValues($node)); + $this->assertTreeNotBroken(); + $this->assertFalse($node->isDirty()); + $this->assertTrue($node->isDescendantOf($root)); + } + + public function testReceivesValidValuesWhenPrependedTo() + { + $root = $this->getModelClass()::root(); + $model = $this->getModelClass(); + $node = new $model(['name' => 'test']); + $root->prependNode($node); + + $this->assertTrue($node->hasMoved()); + $this->assertEquals(array($root->_lft + 1, $root->_lft + 2, $root->id), $this->nodeValues($node)); + $this->assertTreeNotBroken(); + $this->assertTrue($node->isDescendantOf($root)); + $this->assertTrue($root->isAncestorOf($node)); + $this->assertTrue($node->isChildOf($root)); + } + + public function testReceivesValidValuesWhenInsertedAfter() + { + $target = $this->findCategory('apple'); + $model = $this->getModelClass(); + $node = new $model(['name' => 'test']); + $node->afterNode($target)->save(); + + $this->assertTrue($node->hasMoved()); + $this->assertEquals(array($target->_rgt + 1, $target->_rgt + 2, $target->parent->id), $this->nodeValues($node)); + $this->assertTreeNotBroken(); + $this->assertFalse($node->isDirty()); + $this->assertTrue($node->isSiblingOf($target)); + } + + public function testReceivesValidValuesWhenInsertedBefore() + { + $target = $this->findCategory('apple'); + $model = $this->getModelClass(); + $node = new $model(['name' => 'test']); + $node->beforeNode($target)->save(); + + $this->assertTrue($node->hasMoved()); + $this->assertEquals(array($target->_lft, $target->_lft + 1, $target->parent->id), $this->nodeValues($node)); + $this->assertTreeNotBroken(); + } + + public function testCategoryMovesDown() + { + $node = $this->findCategory('apple'); + $target = $this->findCategory('mobile'); + + $target->appendNode($node); + + $this->assertTrue($node->hasMoved()); + $this->assertNodeReceivesValidValues($node); + $this->assertTreeNotBroken(); + } + + public function testCategoryMovesUp() + { + $node = $this->findCategory('samsung'); + $target = $this->findCategory('notebooks'); + + $target->appendNode($node); + + $this->assertTrue($node->hasMoved()); + $this->assertTreeNotBroken(); + $this->assertNodeReceivesValidValues($node); + } + + public function testFailsToInsertIntoChild() + { + $this->expectException(Exception::class); + + $node = $this->findCategory('notebooks'); + $target = $node->children()->first(); + + $node->afterNode($target)->save(); + } + + public function testFailsToAppendIntoItself() + { + $this->expectException(Exception::class); + + $node = $this->findCategory('notebooks'); + + $node->appendToNode($node)->save(); + } + + public function testFailsToPrependIntoItself() + { + $this->expectException(Exception::class); + + $node = $this->findCategory('notebooks'); + + $node->prependTo($node)->save(); + } + + public function testWithoutRootWorks() + { + $result = $this->getModelClass()::withoutRoot()->pluck('name'); + + $this->assertNotEquals('store', $result); + } + + public function testAncestorsReturnsAncestorsWithoutNodeItself() + { + $node = $this->findCategory('apple'); + $path = all($node->ancestors()->pluck('name')); + + $this->assertEquals(array('store', 'notebooks'), $path); + } + + public function testGetsAncestorsByStatic() + { + $path = all($this->getModelClass()::ancestorsOf($this->ids[3])->pluck('name')); + + $this->assertEquals(array('store', 'notebooks'), $path); + } + + public function testGetsAncestorsDirect() + { + $path = all($this->getModelClass()::find($this->ids[8])->getAncestors()->pluck('id')); + + $this->assertEquals(array($this->ids[1], $this->ids[5], $this->ids[7]), $path); + } + + public function testDescendants() + { + $node = $this->findCategory('mobile'); + $descendants = all($node->descendants()->pluck('name')); + $expected = array('nokia', 'samsung', 'galaxy', 'sony', 'lenovo'); + + $this->assertEquals($expected, $descendants); + + $descendants = all($node->getDescendants()->pluck('name')); + + $this->assertEquals(count($descendants), $node->getDescendantCount()); + $this->assertEquals($expected, $descendants); + + $descendants = all($this->getModelClass()::descendantsAndSelf($this->ids[7])->pluck('name')); + $expected = ['samsung', 'galaxy']; + + $this->assertEquals($expected, $descendants); + } + + public function testWithDepthWorks() + { + $nodes = all($this->getModelClass()::withDepth()->limit(4)->pluck('depth')); + + $this->assertEquals(array(0, 1, 2, 2), $nodes); + } + + public function testWithDepthWithCustomKeyWorks() + { + $node = $this->getModelClass()::whereIsRoot()->withDepth('level')->first(); + + $this->assertTrue(isset($node['level'])); + } + + public function testWithDepthWorksAlongWithDefaultKeys() + { + $node = $this->getModelClass()::withDepth()->first(); + + $this->assertTrue(isset($node->name)); + } + + public function testParentIdAttributeAccessorAppendsNode() + { + $model = $this->getModelClass(); + $node = new $model(array('name' => 'lg', 'parent_id' => $this->ids[5])); + $node->save(); + + $this->assertEquals($this->ids[5], $node->parent_id); + $this->assertEquals($this->ids[5], $node->getParentId()); + + $node->parent_id = null; + $node->save(); + + $node->refreshNode(); + + $this->assertEquals(null, $node->parent_id); + $this->assertTrue($node->isRoot()); + } + + public function testFailsToSaveNodeUntilNotInserted() + { + $this->expectException(Exception::class); + + $modelClass = $this->getModelClass(); + $node = new $modelClass(); + $node->save(); + } + + public function testNodeIsDeletedWithDescendants() + { + $node = $this->findCategory('mobile'); + $node->forceDelete(); + + $this->assertTreeNotBroken(); + + $nodes = $this->getModelClass()::whereIn('id', array($this->ids[5], $this->ids[6], $this->ids[7], $this->ids[8], $this->ids[9]))->count(); + $this->assertEquals(0, $nodes); + + $root = $this->getModelClass()::root(); + $this->assertEquals(8, $root->getRgt()); + } + + public function testNodeIsSoftDeleted() + { + $root = $this->getModelClass()::root(); + + $samsung = $this->findCategory('samsung'); + $samsung->delete(); + + $this->assertTreeNotBroken(); + + $this->assertNull($this->findCategory('galaxy')); + + sleep(1); + + $node = $this->findCategory('mobile'); + $node->delete(); + + $nodes = $this->getModelClass()::whereIn('id', array($this->ids[5], $this->ids[6], $this->ids[7], $this->ids[8], $this->ids[9]))->count(); + $this->assertEquals(0, $nodes); + + $originalRgt = $root->getRgt(); + $root->refreshNode(); + + $this->assertEquals($originalRgt, $root->getRgt()); + + $node = $this->findCategory('mobile', true); + + $node->restore(); + + $this->assertNull($this->findCategory('samsung')); + $this->assertNotNull($this->findCategory('nokia')); + } + + public function testSoftDeletedNodeisDeletedWhenParentIsDeleted() + { + $this->findCategory('samsung')->delete(); + + $this->findCategory('mobile')->forceDelete(); + + $this->assertTreeNotBroken(); + + $this->assertNull($this->findCategory('samsung', true)); + $this->assertNull($this->findCategory('sony')); + } + + public function testFailsToSaveNodeUntilParentIsSaved() + { + $this->expectException(Exception::class); + + $modelClass = $this->getModelClass(); + $node = new $modelClass(array('title' => 'Node')); + $parent = new $modelClass(array('title' => 'Parent')); + + $node->appendTo($parent)->save(); + } + + public function testSiblings() + { + $node = $this->findCategory('samsung'); + $siblings = all($node->siblings()->pluck('id')); + $next = all($node->nextSiblings()->pluck('id')); + $prev = all($node->prevSiblings()->pluck('id')); + + $this->assertEquals(array($this->ids[6], $this->ids[9], $this->ids[10]), $siblings); + $this->assertEquals(array($this->ids[9], $this->ids[10]), $next); + $this->assertEquals(array($this->ids[6]), $prev); + + $siblings = all($node->getSiblings()->pluck('id')); + $next = all($node->getNextSiblings()->pluck('id')); + $prev = all($node->getPrevSiblings()->pluck('id')); + + $this->assertEquals(array($this->ids[6], $this->ids[9], $this->ids[10]), $siblings); + $this->assertEquals(array($this->ids[9], $this->ids[10]), $next); + $this->assertEquals(array($this->ids[6]), $prev); + + $next = $node->getNextSibling(); + $prev = $node->getPrevSibling(); + + $this->assertEquals($this->ids[9], $next->id); + $this->assertEquals($this->ids[6], $prev->id); + } + + public function testFetchesReversed() + { + $node = $this->findCategory('sony'); + $siblings = $node->prevSiblings()->reversed()->value('id'); + + $this->assertEquals($this->ids[7], $siblings); + } + + public function testToTreeBuildsWithDefaultOrder() + { + $tree = $this->getModelClass()::whereBetween('_lft', array(8, 17))->defaultOrder()->get()->toTree(); + + $this->assertEquals(1, count($tree)); + + $root = $tree->first(); + $this->assertEquals('mobile', $root->name); + $this->assertEquals(4, count($root->children)); + } + + public function testToTreeBuildsWithCustomOrder() + { + $tree = $this->getModelClass()::whereBetween('_lft', array(8, 17)) + ->orderBy('title') + ->get() + ->toTree(); + + $this->assertEquals(1, count($tree)); + + $root = $tree->first(); + $this->assertEquals('mobile', $root->name); + $this->assertEquals(4, count($root->children)); + $this->assertEquals($root, $root->children->first()->parent); + } + + public function testToTreeWithSpecifiedRoot() + { + $node = $this->findCategory('mobile'); + $nodes = $this->getModelClass()::whereBetween('_lft', array(8, 17))->get(); + + $tree1 = \Kalnoy\Nestedset\Collection::make($nodes)->toTree($this->ids[5]); + $tree2 = \Kalnoy\Nestedset\Collection::make($nodes)->toTree($node); + + $this->assertEquals(4, $tree1->count()); + $this->assertEquals(4, $tree2->count()); + } + + public function testToTreeBuildsWithDefaultOrderAndMultipleRootNodes() + { + $tree = $this->getModelClass()::withoutRoot()->get()->toTree(); + + $this->assertEquals(2, count($tree)); + } + + public function testToTreeBuildsWithRootItemIdProvided() + { + $tree = $this->getModelClass()::whereBetween('_lft', array(8, 17))->get()->toTree($this->ids[5]); + + $this->assertEquals(4, count($tree)); + + $root = $tree[1]; + $this->assertEquals('samsung', $root->name); + $this->assertEquals(1, count($root->children)); + } + + public function testRetrievesNextNode() + { + $node = $this->findCategory('apple'); + $next = $node->nextNodes()->first(); + + $this->assertEquals('lenovo', $next->name); + } + + public function testRetrievesPrevNode() + { + $node = $this->findCategory('apple'); + $next = $node->getPrevNode(); + + $this->assertEquals('notebooks', $next->name); + } + + public function testMultipleAppendageWorks() + { + $parent = $this->findCategory('mobile'); + + $model = $this->getModelClass(); + $child = new $model(['name' => 'test']); + + $parent->appendNode($child); + + $model = $this->getModelClass(); + $child->appendNode(new $model(['name' => 'sub'])); + + $parent->appendNode(new $model(['name' => 'test2'])); + + $this->assertTreeNotBroken(); + } + + public function testDefaultCategoryIsSavedAsRoot() + { + $model = $this->getModelClass(); + $node = new $model(['name' => 'test']); + $node->save(); + + $this->assertEquals(23, $node->_lft); + $this->assertTreeNotBroken(); + + $this->assertTrue($node->isRoot()); + } + + public function testExistingCategorySavedAsRoot() + { + $node = $this->findCategory('apple'); + $node->saveAsRoot(); + + $this->assertTreeNotBroken(); + $this->assertTrue($node->isRoot()); + } + + public function testNodeMovesDownSeveralPositions() + { + $node = $this->findCategory('nokia'); + + $this->assertTrue($node->down(2)); + + $this->assertEquals($node->_lft, 15); + } + + public function testNodeMovesUpSeveralPositions() + { + $node = $this->findCategory('sony'); + + $this->assertTrue($node->up(2)); + + $this->assertEquals($node->_lft, 9); + } + + public function testCountsTreeErrors() + { + $errors = $this->getModelClass()::countErrors(); + + $this->assertEquals(['oddness' => 0, + 'duplicates' => 0, + 'wrong_parent' => 0, + 'missing_parent' => 0], $errors); + + $this->getModelClass()::where('id', '=', $this->ids[5])->update(['_lft' => 14]); + $this->getModelClass()::where('id', '=', $this->ids[8])->update(['parent_id' => $this->ids[2]]); + $this->getModelClass()::where('id', '=', $this->ids[11])->update(['_lft' => 20]); + $this->getModelClass()::where('id', '=', $this->ids[4])->update(['parent_id' => $this->ids[24]]); + + $errors = $this->getModelClass()::countErrors(); + + $this->assertEquals(1, $errors['oddness']); + $this->assertEquals(2, $errors['duplicates']); + $this->assertEquals(1, $errors['missing_parent']); + } + + public function testCreatesNode() + { + $node = $this->getModelClass()::create(['name' => 'test']); + + $this->assertEquals(23, $node->getLft()); + } + + public function testCreatesViaRelationship() + { + $node = $this->findCategory('apple'); + + $child = $node->children()->create(['name' => 'test']); + + $this->assertTreeNotBroken(); + } + + public function testCreatesTree() + { + $node = $this->getModelClass()::create( + [ + 'name' => 'test', + 'children' => + [ + ['name' => 'test2'], + ['name' => 'test3'], + ], + ]); + + $this->assertTreeNotBroken(); + + $this->assertTrue(isset($node->children)); + + $node = $this->findCategory('test'); + + $this->assertCount(2, $node->children); + $this->assertEquals('test2', $node->children[0]->name); + } + + public function testDescendantsOfNonExistingNode() + { + $modelClass = $this->getModelClass(); + $node = new $modelClass(); + + $this->assertTrue($node->getDescendants()->isEmpty()); + } + + public function testWhereDescendantsOf() + { + $this->expectException(\Illuminate\Database\Eloquent\ModelNotFoundException::class); + + $this->getModelClass()::whereDescendantOf($this->ids[124])->get(); + } + + public function testAncestorsByNode() + { + $category = $this->findCategory('apple'); + $ancestors = all($this->getModelClass()::whereAncestorOf($category)->pluck('id')); + + $this->assertEquals([$this->ids[1], $this->ids[2]], $ancestors); + } + + public function testDescendantsByNode() + { + $category = $this->findCategory('notebooks'); + $res = all($this->getModelClass()::whereDescendantOf($category)->pluck('id')); + + $this->assertEquals([$this->ids[3], $this->ids[4]], $res); + } + + public function testMultipleDeletionsDoNotBrakeTree() + { + $category = $this->findCategory('mobile'); + + foreach ($category->children()->take(2)->get() as $child) { + $child->forceDelete(); + } + + $this->assertTreeNotBroken(); + } + + public function testTreeIsFixed() + { + $this->getModelClass()::where('id', '=', $this->ids[5])->update(['_lft' => 14]); + $this->getModelClass()::where('id', '=', $this->ids[8])->update(['parent_id' => $this->ids[2]]); + $this->getModelClass()::where('id', '=', $this->ids[11])->update(['_lft' => 20]); + $this->getModelClass()::where('id', '=', $this->ids[2])->update(['parent_id' => $this->ids[24]]); + + $fixed = $this->getModelClass()::fixTree(); + + $this->assertTrue($fixed > 0); + $this->assertTreeNotBroken(); + + $node = $this->getModelClass()::find($this->ids[8]); + + $this->assertEquals($this->ids[2], $node->getParentId()); + + $node = $this->getModelClass()::find($this->ids[2]); + + $this->assertEquals(null, $node->getParentId()); + } + + public function testSubtreeIsFixed() + { + $this->getModelClass()::where('id', '=', $this->ids[8])->update(['_lft' => 11]); + + $fixed = $this->getModelClass()::fixSubtree($this->getModelClass()::find($this->ids[5])); + $this->assertEquals($fixed, 1); + $this->assertTreeNotBroken(); + $this->assertEquals($this->getModelClass()::find($this->ids[8])->getLft(), 12); + } + + public function testParentIdDirtiness() + { + $node = $this->findCategory('apple'); + $node->parent_id = $this->ids[5]; + + $this->assertTrue($node->isDirty('parent_id')); + + $node = $this->findCategory('apple'); + $node->parent_id = null; + + $this->assertTrue($node->isDirty('parent_id')); + } + + public function testIsDirtyMovement() + { + $node = $this->findCategory('apple'); + $otherNode = $this->findCategory('samsung'); + + $this->assertFalse($node->isDirty()); + + $node->afterNode($otherNode); + + $this->assertTrue($node->isDirty()); + + $node = $this->findCategory('apple'); + $otherNode = $this->findCategory('samsung'); + + $this->assertFalse($node->isDirty()); + + $node->appendToNode($otherNode); + + $this->assertTrue($node->isDirty()); + } + + public function testRootNodesMoving() + { + $node = $this->findCategory('store'); + $node->down(); + + $this->assertEquals(3, $node->getLft()); + } + + public function testDescendantsRelation() + { + $node = $this->findCategory('notebooks'); + $result = $node->descendants; + + $this->assertEquals(2, $result->count()); + $this->assertEquals('apple', $result->first()->name); + } + + public function testDescendantsEagerlyLoaded() + { + $nodes = $this->getModelClass()::whereIn('id', [$this->ids[2], $this->ids[5]])->get(); + + $nodes->load('descendants'); + + $this->assertEquals(2, $nodes->count()); + $this->assertTrue($nodes->first()->relationLoaded('descendants')); + } + + public function testDescendantsRelationQuery() + { + $nodes = $this->getModelClass()::has('descendants')->whereIn('id', [$this->ids[2], $this->ids[3]])->get(); + + $this->assertEquals(1, $nodes->count()); + $this->assertEquals($this->ids[2], $nodes->first()->getKey()); + + $nodes = $this->getModelClass()::has('descendants', '>', 2)->get(); + + $this->assertEquals(2, $nodes->count()); + $this->assertEquals($this->ids[1], $nodes[0]->getKey()); + $this->assertEquals($this->ids[5], $nodes[1]->getKey()); + } + + public function testParentRelationQuery() + { + $nodes = $this->getModelClass()::has('parent')->whereIn('id', [$this->ids[1], $this->ids[2]]); + + $this->assertEquals(1, $nodes->count()); + $this->assertEquals($this->ids[2], $nodes->first()->getKey()); + } + + public function testRebuildTree() + { + $fixed = $this->getModelClass()::rebuildTree([ + [ + 'id' => $this->ids[1], + 'children' => [ + ['id' => $this->ids[10]], + ['id' => $this->ids[3], 'name' => 'apple v2', 'children' => [['name' => 'new node']]], + ['id' => $this->ids[2]], + + ] + ] + ]); + + $this->assertTrue($fixed > 0); + $this->assertTreeNotBroken(); + + $node = $this->getModelClass()::find($this->ids[3]); + + $this->assertEquals($this->ids[1], $node->getParentId()); + $this->assertEquals('apple v2', $node->name); + $this->assertEquals(4, $node->getLft()); + + $node = $this->findCategory('new node'); + + $this->assertNotNull($node); + $this->assertEquals($this->ids[3], $node->getParentId()); + } + + public function testRebuildSubtree() + { + $fixed = $this->getModelClass()::rebuildSubtree($this->getModelClass()::find($this->ids[7]), [ + ['name' => 'new node'], + ['id' => strval($this->ids[8])], + ]); + + $this->assertTrue($fixed > 0); + $this->assertTreeNotBroken(); + + $node = $this->findCategory('new node'); + + $this->assertNotNull($node); + $this->assertEquals($node->getLft(), 12); + } + + public function testRebuildTreeWithDeletion() + { + $this->getModelClass()::rebuildTree([['name' => 'all deleted']], true); + + $this->assertTreeNotBroken(); + + $nodes = $this->getModelClass()::get(); + + $this->assertEquals(1, $nodes->count()); + $this->assertEquals('all deleted', $nodes->first()->name); + + $nodes = $this->getModelClass()::withTrashed()->get(); + + $this->assertTrue($nodes->count() > 1); + } + + public function testRebuildFailsWithInvalidPK() + { + $this->expectException(\Illuminate\Database\Eloquent\ModelNotFoundException::class); + + $this->getModelClass()::rebuildTree([['id' => $this->ids[24]]]); + } + + public function testFlatTree() + { + $node = $this->findCategory('mobile'); + $tree = $node->descendants()->orderBy('name')->get()->toFlatTree(); + + $this->assertCount(5, $tree); + $this->assertEquals('samsung', $tree[2]->name); + $this->assertEquals('galaxy', $tree[3]->name); + } + + // Commented, cause there is no assertion here and otherwise the test is marked as risky in PHPUnit 7. + // What's the purpose of this method? @todo: remove/update? + /*public function testSeveralNodesModelWork() + { + $category = new Category; + + $category->name = 'test'; + + $category->saveAsRoot(); + + $duplicate = new DuplicateCategory; + + $duplicate->name = 'test'; + + $duplicate->saveAsRoot(); + }*/ + + public function testWhereIsLeaf() + { + $categories = $this->getModelClass()::leaves(); + + $this->assertEquals(7, $categories->count()); + $this->assertEquals('apple', $categories->first()->name); + $this->assertTrue($categories->first()->isLeaf()); + + $category = $this->getModelClass()::whereIsRoot()->first(); + + $this->assertFalse($category->isLeaf()); + } + + public function testEagerLoadAncestors() + { + $queryLogCount = count(Capsule::connection()->getQueryLog()); + $categories = $this->getModelClass()::with('ancestors')->orderBy('name')->get(); + + $this->assertEquals($queryLogCount + 2, count(Capsule::connection()->getQueryLog())); + + + $expectedShape = [ + 'apple (' . $this->ids[3] . ')}' => 'store (' . $this->ids[1] . ') > notebooks (' . $this->ids[2] . ')', + 'galaxy (' . $this->ids[8] . ')}' => 'store (' . $this->ids[1] . ') > mobile (' . $this->ids[5] . ') > samsung (' . $this->ids[7] . ')', + 'lenovo (' . $this->ids[4] . ')}' => 'store (' . $this->ids[1] . ') > notebooks (' . $this->ids[2] . ')', + 'lenovo (' . $this->ids[10] . ')}' => 'store (' . $this->ids[1] . ') > mobile (' . $this->ids[5] . ')', + 'mobile (' . $this->ids[5] . ')}' => 'store (' . $this->ids[1] . ')', + 'nokia (' . $this->ids[6] . ')}' => 'store (' . $this->ids[1] . ') > mobile (' . $this->ids[5] . ')', + 'notebooks (' . $this->ids[2] . ')}' => 'store (' . $this->ids[1] . ')', + 'samsung (' . $this->ids[7] . ')}' => 'store (' . $this->ids[1] . ') > mobile (' . $this->ids[5] . ')', + 'sony (' . $this->ids[9] . ')}' => 'store (' . $this->ids[1] . ') > mobile (' . $this->ids[5] . ')', + 'store (' . $this->ids[1] . ')}' => '', + 'store_2 (' . $this->ids[11] . ')}' => '' + ]; + + $output = []; + + foreach ($categories as $category) { + $output["{$category->name} ({$category->id})}"] = $category->ancestors->count() + ? implode(' > ', $category->ancestors->map(function ($cat) { + return "{$cat->name} ({$cat->id})"; + })->toArray()) + : ''; + } + + $this->assertEquals($expectedShape, $output); + } + + public function testLazyLoadAncestors() + { + $queryLogCount = count(Capsule::connection()->getQueryLog()); + $categories = $this->getModelClass()::orderBy('name')->get(); + + $this->assertEquals($queryLogCount + 1, count(Capsule::connection()->getQueryLog())); + + $expectedShape = [ + 'apple (' . $this->ids[3] . ')}' => 'store (' . $this->ids[1] . ') > notebooks (' . $this->ids[2] . ')', + 'galaxy (' . $this->ids[8] . ')}' => 'store (' . $this->ids[1] . ') > mobile (' . $this->ids[5] . ') > samsung (' . $this->ids[7] . ')', + 'lenovo (' . $this->ids[4] . ')}' => 'store (' . $this->ids[1] . ') > notebooks (' . $this->ids[2] . ')', + 'lenovo (' . $this->ids[10] . ')}' => 'store (' . $this->ids[1] . ') > mobile (' . $this->ids[5] . ')', + 'mobile (' . $this->ids[5] . ')}' => 'store (' . $this->ids[1] . ')', + 'nokia (' . $this->ids[6] . ')}' => 'store (' . $this->ids[1] . ') > mobile (' . $this->ids[5] . ')', + 'notebooks (' . $this->ids[2] . ')}' => 'store (' . $this->ids[1] . ')', + 'samsung (' . $this->ids[7] . ')}' => 'store (' . $this->ids[1] . ') > mobile (' . $this->ids[5] . ')', + 'sony (' . $this->ids[9] . ')}' => 'store (' . $this->ids[1] . ') > mobile (' . $this->ids[5] . ')', + 'store (' . $this->ids[1] . ')}' => '', + 'store_2 (' . $this->ids[11] . ')}' => '' + ]; + + $output = []; + + foreach ($categories as $category) { + $output["{$category->name} ({$category->id})}"] = $category->ancestors->count() + ? implode(' > ', $category->ancestors->map(function ($cat) { + return "{$cat->name} ({$cat->id})"; + })->toArray()) + : ''; + } + + // assert that there is number of original query + 1 + number of rows to fulfill the relation + $this->assertEquals($queryLogCount + 12, count(Capsule::connection()->getQueryLog())); + + $this->assertEquals($expectedShape, $output); + } + + public function testWhereHasCountQueryForAncestors() + { + $categories = all($this->getModelClass()::has('ancestors', '>', 2)->pluck('name')); + + $this->assertEquals(['galaxy'], $categories); + + $categories = all($this->getModelClass()::whereHas('ancestors', function ($query) { + $query->where('id', $this->ids[5]); + })->pluck('name')); + + $this->assertEquals(['nokia', 'samsung', 'galaxy', 'sony', 'lenovo'], $categories); + } + + public function testReplication() + { + $category = $this->findCategory('nokia'); + $category = $category->replicate(); + $category->save(); + $category->refreshNode(); + + $this->assertNull($category->getParentId()); + + $category = $this->findCategory('nokia'); + $category = $category->replicate(); + $category->parent_id = $this->ids[1]; + $category->save(); + + $category->refreshNode(); + + $this->assertEquals($this->ids[1], $category->getParentId()); + } +} + +function all($items) +{ + return is_array($items) ? $items : $items->all(); +} diff --git a/tests/NodeUuidTest.php b/tests/NodeUuidTest.php new file mode 100644 index 0000000..ddbbdd4 --- /dev/null +++ b/tests/NodeUuidTest.php @@ -0,0 +1,30 @@ +categoryData = new CategoryData(); + } + + protected function getTable(): string + { + return 'uuid_categories'; + } + + protected function getModelClass(): string + { + return CategoryUuid::class; + } + + protected function createTable(\Illuminate\Database\Schema\Blueprint $table): void + { + $table->uuid('id')->primary(); + $table->string('name'); + $table->softDeletes(); + NestedSet::columns($table, true); + } +} diff --git a/tests/ScopedNodeTest.php b/tests/ScopedNodeTest.php index 9622fc9..a373625 100644 --- a/tests/ScopedNodeTest.php +++ b/tests/ScopedNodeTest.php @@ -1,238 +1,30 @@ dropIfExists('menu_items'); - - Capsule::disableQueryLog(); - - $schema->create('menu_items', function (\Illuminate\Database\Schema\Blueprint $table) { - $table->increments('id'); - $table->unsignedInteger('menu_id'); - $table->string('title')->nullable(); - NestedSet::columns($table); - }); - - Capsule::enableQueryLog(); - } - - public function setUp(): void - { - $data = include __DIR__.'/data/menu_items.php'; - - Capsule::table('menu_items')->insert($data); - - Capsule::flushQueryLog(); - - MenuItem::resetActionsPerformed(); - - date_default_timezone_set('America/Denver'); - } - - public function tearDown(): void - { - Capsule::table('menu_items')->truncate(); - } - - public function assertTreeNotBroken($menuId) - { - $this->assertFalse(MenuItem::scoped([ 'menu_id' => $menuId ])->isBroken()); - } - - public function testNotBroken() - { - $this->assertTreeNotBroken(1); - $this->assertTreeNotBroken(2); - } - - public function testMovingNodeNotAffectingOtherMenu() - { - $node = MenuItem::where('menu_id', '=', 1)->first(); - - $node->down(); - - $node = MenuItem::where('menu_id', '=', 2)->first(); - - $this->assertEquals(1, $node->getLft()); + parent::__construct($name); + $this->menuItemData = new MenuItemData(); } - public function testScoped() + protected function getTable(): string { - $node = MenuItem::scoped([ 'menu_id' => 2 ])->first(); - - $this->assertEquals(3, $node->getKey()); - } - - public function testSiblings() - { - $node = MenuItem::find(1); - - $result = $node->getSiblings(); - - $this->assertEquals(1, $result->count()); - $this->assertEquals(2, $result->first()->getKey()); - - $result = $node->getNextSiblings(); - - $this->assertEquals(2, $result->first()->getKey()); - - $node = MenuItem::find(2); - - $result = $node->getPrevSiblings(); - - $this->assertEquals(1, $result->first()->getKey()); - } - - public function testDescendants() - { - $node = MenuItem::find(2); - - $result = $node->getDescendants(); - - $this->assertEquals(1, $result->count()); - $this->assertEquals(5, $result->first()->getKey()); - - $node = MenuItem::scoped([ 'menu_id' => 1 ])->with('descendants')->find(2); - - $result = $node->descendants; - - $this->assertEquals(1, $result->count()); - $this->assertEquals(5, $result->first()->getKey()); + return 'menu_items'; } - public function testAncestors() + protected function getModelClass(): string { - $node = MenuItem::find(5); - - $result = $node->getAncestors(); - - $this->assertEquals(1, $result->count()); - $this->assertEquals(2, $result->first()->getKey()); - - $node = MenuItem::scoped([ 'menu_id' => 1 ])->with('ancestors')->find(5); - - $result = $node->ancestors; - - $this->assertEquals(1, $result->count()); - $this->assertEquals(2, $result->first()->getKey()); + return MenuItem::class; } - public function testDepth() + protected function createTable(\Illuminate\Database\Schema\Blueprint $table): void { - $node = MenuItem::scoped([ 'menu_id' => 1 ])->withDepth()->where('id', '=', 5)->first(); - - $this->assertEquals(1, $node->depth); - - $node = MenuItem::find(2); - - $result = $node->children()->withDepth()->get(); - - $this->assertEquals(1, $result->first()->depth); - } - - public function testSaveAsRoot() - { - $node = MenuItem::find(5); - - $node->saveAsRoot(); - - $this->assertEquals(5, $node->getLft()); - $this->assertEquals(null, $node->parent_id); - - $this->assertOtherScopeNotAffected(); - } - - public function testInsertion() - { - $node = MenuItem::create([ 'menu_id' => 1, 'parent_id' => 5 ]); - - $this->assertEquals(5, $node->parent_id); - $this->assertEquals(5, $node->getLft()); - - $this->assertOtherScopeNotAffected(); - } - - public function testInsertionToParentFromOtherScope() - { - $this->expectException(\Illuminate\Database\Eloquent\ModelNotFoundException::class); - - $node = MenuItem::create([ 'menu_id' => 2, 'parent_id' => 5 ]); - } - - public function testDeletion() - { - $node = MenuItem::find(2)->delete(); - - $node = MenuItem::find(1); - - $this->assertEquals(2, $node->getRgt()); - - $this->assertOtherScopeNotAffected(); - } - - public function testMoving() - { - $node = MenuItem::find(1); - $this->assertTrue($node->down()); - - $this->assertOtherScopeNotAffected(); - } - - protected function assertOtherScopeNotAffected() - { - $node = MenuItem::find(3); - - $this->assertEquals(1, $node->getLft()); - } - - // Commented, cause there is no assertion here and otherwise the test is marked as risky in PHPUnit 7. - // What's the purpose of this method? @todo: remove/update? - /*public function testRebuildsTree() - { - $data = []; - MenuItem::scoped([ 'menu_id' => 2 ])->rebuildTree($data); - }*/ - - public function testAppendingToAnotherScopeFails() - { - $this->expectException(LogicException::class); - - $a = MenuItem::find(1); - $b = MenuItem::find(3); - - $a->appendToNode($b)->save(); - } - - public function testInsertingBeforeAnotherScopeFails() - { - $this->expectException(LogicException::class); - - $a = MenuItem::find(1); - $b = MenuItem::find(3); - - $a->insertAfterNode($b); - } - - public function testEagerLoadingAncestorsWithScope() - { - $filteredNodes = MenuItem::where('title', 'menu item 3')->with(['ancestors'])->get(); - - $this->assertEquals(2, $filteredNodes->find(5)->ancestors[0]->id); - $this->assertEquals(4, $filteredNodes->find(6)->ancestors[0]->id); - } - - public function testEagerLoadingDescendantsWithScope() - { - $filteredNodes = MenuItem::where('title', 'menu item 2')->with(['descendants'])->get(); - - $this->assertEquals(5, $filteredNodes->find(2)->descendants[0]->id); - $this->assertEquals(6, $filteredNodes->find(4)->descendants[0]->id); + $table->increments('id'); + $table->unsignedInteger('menu_id'); + $table->string('title')->nullable(); + NestedSet::columns($table); } -} \ No newline at end of file +} diff --git a/tests/ScopedNodeTestBase.php b/tests/ScopedNodeTestBase.php new file mode 100644 index 0000000..b0a5654 --- /dev/null +++ b/tests/ScopedNodeTestBase.php @@ -0,0 +1,251 @@ +getTable(); + } + + public static function setUpBeforeClass(): void + { + $schema = Capsule::schema(); + $table = static::getTableName(); + + $schema->dropIfExists($table); + + Capsule::disableQueryLog(); + + $schema->create($table, function (\Illuminate\Database\Schema\Blueprint $table) { + $testClass = get_called_class(); + (new $testClass('dummy'))->createTable($table); + }); + + Capsule::enableQueryLog(); + } + + public function setUp(): void + { + $this->ids = $this->menuItemData->getIds(); + Capsule::table($this->getTable())->insert($this->menuItemData->getData()); + + Capsule::flushQueryLog(); + + $modelClass = $this->getModelClass(); + $modelClass::resetActionsPerformed(); + + date_default_timezone_set('America/Denver'); + } + + public function tearDown(): void + { + Capsule::table($this->getTable())->truncate(); + } + + public function assertTreeNotBroken($menuId) + { + $this->assertFalse($this->getModelClass()::scoped(['menu_id' => $menuId])->isBroken()); + } + + public function testNotBroken() + { + $this->assertTreeNotBroken(1); + $this->assertTreeNotBroken(2); + } + + public function testMovingNodeNotAffectingOtherMenu() + { + $node = $this->getModelClass()::where('menu_id', '=', 1)->first(); + + $node->down(); + + $node = $this->getModelClass()::where('menu_id', '=', 2)->first(); + + $this->assertEquals(1, $node->getLft()); + } + + public function testScoped() + { + $node = $this->getModelClass()::scoped(['menu_id' => 2])->first(); + + $this->assertEquals($this->ids[3], $node->getKey()); + } + + public function testSiblings() + { + $node = $this->getModelClass()::find($this->ids[1]); + + $result = $node->getSiblings(); + + $this->assertEquals(1, $result->count()); + $this->assertEquals($this->ids[2], $result->first()->getKey()); + + $result = $node->getNextSiblings(); + + $this->assertEquals($this->ids[2], $result->first()->getKey()); + + $node = $this->getModelClass()::find($this->ids[2]); + + $result = $node->getPrevSiblings(); + + $this->assertEquals($this->ids[1], $result->first()->getKey()); + } + + public function testDescendants() + { + $node = $this->getModelClass()::find($this->ids[2]); + + $result = $node->getDescendants(); + + $this->assertEquals(1, $result->count()); + $this->assertEquals($this->ids[5], $result->first()->getKey()); + + $node = $this->getModelClass()::scoped(['menu_id' => 1])->with('descendants')->find($this->ids[2]); + + $result = $node->descendants; + + $this->assertEquals(1, $result->count()); + $this->assertEquals($this->ids[5], $result->first()->getKey()); + } + + public function testAncestors() + { + $node = $this->getModelClass()::find($this->ids[5]); + + $result = $node->getAncestors(); + + $this->assertEquals(1, $result->count()); + $this->assertEquals($this->ids[2], $result->first()->getKey()); + + $node = $this->getModelClass()::scoped(['menu_id' => 1])->with('ancestors')->find($this->ids[5]); + + $result = $node->ancestors; + + $this->assertEquals(1, $result->count()); + $this->assertEquals($this->ids[2], $result->first()->getKey()); + } + + public function testDepth() + { + $node = $this->getModelClass()::scoped(['menu_id' => 1])->withDepth()->where('id', '=', $this->ids[5])->first(); + + $this->assertEquals(1, $node->depth); + + $node = $this->getModelClass()::find($this->ids[2]); + + $result = $node->children()->withDepth()->get(); + + $this->assertEquals(1, $result->first()->depth); + } + + public function testSaveAsRoot() + { + $node = $this->getModelClass()::find($this->ids[5]); + + $node->saveAsRoot(); + + $this->assertEquals(5, $node->getLft()); + $this->assertEquals(null, $node->parent_id); + + $this->assertOtherScopeNotAffected(); + } + + public function testInsertion() + { + $node = $this->getModelClass()::create(['menu_id' => 1, 'parent_id' => $this->ids[5]]); + + $this->assertEquals($this->ids[5], $node->parent_id); + $this->assertEquals(5, $node->getLft()); + + $this->assertOtherScopeNotAffected(); + } + + public function testInsertionToParentFromOtherScope() + { + $this->expectException(\Illuminate\Database\Eloquent\ModelNotFoundException::class); + + $node = $this->getModelClass()::create(['menu_id' => 2, 'parent_id' => $this->ids[5]]); + } + + public function testDeletion() + { + $node = $this->getModelClass()::find($this->ids[2])->delete(); + + $node = $this->getModelClass()::find($this->ids[1]); + + $this->assertEquals(2, $node->getRgt()); + + $this->assertOtherScopeNotAffected(); + } + + public function testMoving() + { + $node = $this->getModelClass()::find($this->ids[1]); + $this->assertTrue($node->down()); + + $this->assertOtherScopeNotAffected(); + } + + protected function assertOtherScopeNotAffected() + { + $node = $this->getModelClass()::find($this->ids[3]); + + $this->assertEquals(1, $node->getLft()); + } + + // Commented, cause there is no assertion here and otherwise the test is marked as risky in PHPUnit 7. + // What's the purpose of this method? @todo: remove/update? + /*public function testRebuildsTree() + { + $data = []; + $this->getModelClass()::scoped([ 'menu_id' => 2 ])->rebuildTree($data); + }*/ + + public function testAppendingToAnotherScopeFails() + { + $this->expectException(LogicException::class); + + $a = $this->getModelClass()::find($this->ids[1]); + $b = $this->getModelClass()::find($this->ids[3]); + + $a->appendToNode($b)->save(); + } + + public function testInsertingBeforeAnotherScopeFails() + { + $this->expectException(LogicException::class); + + $a = $this->getModelClass()::find($this->ids[1]); + $b = $this->getModelClass()::find($this->ids[3]); + + $a->insertAfterNode($b); + } + + public function testEagerLoadingAncestorsWithScope() + { + $filteredNodes = $this->getModelClass()::where('title', 'menu item 3')->with(['ancestors'])->get(); + + $this->assertEquals($this->ids[2], $filteredNodes->find($this->ids[5])->ancestors[0]->id); + $this->assertEquals($this->ids[4], $filteredNodes->find($this->ids[6])->ancestors[0]->id); + } + + public function testEagerLoadingDescendantsWithScope() + { + $filteredNodes = $this->getModelClass()::where('title', 'menu item 2')->with(['descendants'])->get(); + + $this->assertEquals($this->ids[5], $filteredNodes->find($this->ids[2])->descendants[0]->id); + $this->assertEquals($this->ids[6], $filteredNodes->find($this->ids[4])->descendants[0]->id); + } +} diff --git a/tests/ScopedNodeUuidTest.php b/tests/ScopedNodeUuidTest.php new file mode 100644 index 0000000..79c6e49 --- /dev/null +++ b/tests/ScopedNodeUuidTest.php @@ -0,0 +1,30 @@ +menuItemData = new MenuItemData(); + } + + protected function getTable(): string + { + return 'uuid_menu_items'; + } + + protected function getModelClass(): string + { + return MenuItemUuid::class; + } + + protected function createTable(\Illuminate\Database\Schema\Blueprint $table): void + { + $table->uuid('id')->primary(); + $table->unsignedInteger('menu_id'); + $table->string('title')->nullable(); + NestedSet::columns($table, true); + } +} diff --git a/tests/data/CategoryData.php b/tests/data/CategoryData.php new file mode 100644 index 0000000..218dd56 --- /dev/null +++ b/tests/data/CategoryData.php @@ -0,0 +1,42 @@ +ids[$i] = (string)\Illuminate\Support\Str::uuid(); + } + } else { + $this->ids = array_combine(range(1, 200), range(1, 200)); + } + } + + public function getData(): array + { + return array( + array('id' => $this->ids[1], 'name' => 'store', '_lft' => 1, '_rgt' => 20, 'parent_id' => null), + array('id' => $this->ids[2], 'name' => 'notebooks', '_lft' => 2, '_rgt' => 7, 'parent_id' => $this->ids[1]), + array('id' => $this->ids[3], 'name' => 'apple', '_lft' => 3, '_rgt' => 4, 'parent_id' => $this->ids[2]), + array('id' => $this->ids[4], 'name' => 'lenovo', '_lft' => 5, '_rgt' => 6, 'parent_id' => $this->ids[2]), + array('id' => $this->ids[5], 'name' => 'mobile', '_lft' => 8, '_rgt' => 19, 'parent_id' => $this->ids[1]), + array('id' => $this->ids[6], 'name' => 'nokia', '_lft' => 9, '_rgt' => 10, 'parent_id' => $this->ids[5]), + array('id' => $this->ids[7], 'name' => 'samsung', '_lft' => 11, '_rgt' => 14, 'parent_id' => $this->ids[5]), + array('id' => $this->ids[8], 'name' => 'galaxy', '_lft' => 12, '_rgt' => 13, 'parent_id' => $this->ids[7]), + array('id' => $this->ids[9], 'name' => 'sony', '_lft' => 15, '_rgt' => 16, 'parent_id' => $this->ids[5]), + array('id' => $this->ids[10], 'name' => 'lenovo', '_lft' => 17, '_rgt' => 18, 'parent_id' => $this->ids[5]), + array('id' => $this->ids[11], 'name' => 'store_2', '_lft' => 21, '_rgt' => 22, 'parent_id' => null), + ); + } + + public function getIds(): array + { + return $this->ids; + } +} + + + diff --git a/tests/data/MenuItemData.php b/tests/data/MenuItemData.php new file mode 100644 index 0000000..e6fb152 --- /dev/null +++ b/tests/data/MenuItemData.php @@ -0,0 +1,36 @@ +ids[$i] = (string) \Illuminate\Support\Str::uuid(); + } + } else { + $this->ids = array_combine(range(1, 10), range(1, 10)); + } + } + + public function getData(): array + { + return [ + array('id' => $this->ids[1], 'menu_id' => 1, '_lft' => 1, '_rgt' => 2, 'parent_id' => null, 'title' => 'menu item 1'), + array('id' => $this->ids[2], 'menu_id' => 1, '_lft' => 3, '_rgt' => 6, 'parent_id' => null, 'title' => 'menu item 2'), + array('id' => $this->ids[5], 'menu_id' => 1, '_lft' => 4, '_rgt' => 5, 'parent_id' => $this->ids[2], 'title' => 'menu item 3'), + array('id' => $this->ids[3], 'menu_id' => 2, '_lft' => 1, '_rgt' => 2, 'parent_id' => null, 'title' => 'menu item 1'), + array('id' => $this->ids[4], 'menu_id' => 2, '_lft' => 3, '_rgt' => 6, 'parent_id' => null, 'title' => 'menu item 2'), + array('id' => $this->ids[6], 'menu_id' => 2, '_lft' => 4, '_rgt' => 5, 'parent_id' => $this->ids[4], 'title' => 'menu item 3'), + ]; + } + + public function getIds(): array { + return $this->ids; + } +} + + + diff --git a/tests/data/categories.php b/tests/data/categories.php deleted file mode 100644 index 1f5b8ab..0000000 --- a/tests/data/categories.php +++ /dev/null @@ -1,15 +0,0 @@ - 1, 'name' => 'store', '_lft' => 1, '_rgt' => 20, 'parent_id' => null), - array('id' => 2, 'name' => 'notebooks', '_lft' => 2, '_rgt' => 7, 'parent_id' => 1), - array('id' => 3, 'name' => 'apple', '_lft' => 3, '_rgt' => 4, 'parent_id' => 2), - array('id' => 4, 'name' => 'lenovo', '_lft' => 5, '_rgt' => 6, 'parent_id' => 2), - array('id' => 5, 'name' => 'mobile', '_lft' => 8, '_rgt' => 19, 'parent_id' => 1), - array('id' => 6, 'name' => 'nokia', '_lft' => 9, '_rgt' => 10, 'parent_id' => 5), - array('id' => 7, 'name' => 'samsung', '_lft' => 11, '_rgt' => 14, 'parent_id' => 5), - array('id' => 8, 'name' => 'galaxy', '_lft' => 12, '_rgt' => 13, 'parent_id' => 7), - array('id' => 9, 'name' => 'sony', '_lft' => 15, '_rgt' => 16, 'parent_id' => 5), - array('id' => 10, 'name' => 'lenovo', '_lft' => 17, '_rgt' => 18, 'parent_id' => 5), - array('id' => 11, 'name' => 'store_2', '_lft' => 21, '_rgt' => 22, 'parent_id' => null), -); \ No newline at end of file diff --git a/tests/data/menu_items.php b/tests/data/menu_items.php deleted file mode 100644 index 5490f7d..0000000 --- a/tests/data/menu_items.php +++ /dev/null @@ -1,8 +0,0 @@ - 1, 'menu_id' => 1, '_lft' => 1, '_rgt' => 2, 'parent_id' => null, 'title' => 'menu item 1' ], - [ 'id' => 2, 'menu_id' => 1, '_lft' => 3, '_rgt' => 6, 'parent_id' => null, 'title' => 'menu item 2' ], - [ 'id' => 5, 'menu_id' => 1, '_lft' => 4, '_rgt' => 5, 'parent_id' => 2, 'title' => 'menu item 3' ], - [ 'id' => 3, 'menu_id' => 2, '_lft' => 1, '_rgt' => 2, 'parent_id' => null, 'title' => 'menu item 1' ], - [ 'id' => 4, 'menu_id' => 2, '_lft' => 3, '_rgt' => 6, 'parent_id' => null, 'title' => 'menu item 2' ], - [ 'id' => 6, 'menu_id' => 2, '_lft' => 4, '_rgt' => 5, 'parent_id' => 4, 'title' => 'menu item 3' ], -]; \ No newline at end of file diff --git a/tests/models/CategoryUuid.php b/tests/models/CategoryUuid.php new file mode 100644 index 0000000..2aaae9e --- /dev/null +++ b/tests/models/CategoryUuid.php @@ -0,0 +1,12 @@ + Date: Sat, 5 Apr 2025 22:19:37 +0200 Subject: [PATCH 2/2] Rename parameter uuid to withUuid --- src/NestedSet.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NestedSet.php b/src/NestedSet.php index fb826da..4a9d2bf 100644 --- a/src/NestedSet.php +++ b/src/NestedSet.php @@ -36,12 +36,12 @@ class NestedSet * * @param \Illuminate\Database\Schema\Blueprint $table */ - public static function columns(Blueprint $table, bool $uuid = false) + public static function columns(Blueprint $table, bool $withUuid = false) { $table->unsignedInteger(self::LFT)->default(0); $table->unsignedInteger(self::RGT)->default(0); - if ($uuid) { + if ($withUuid) { $table->uuid(self::PARENT_ID)->nullable()->index(); } else { $table->unsignedInteger(self::PARENT_ID)->nullable();