From 088fdee4e0d9d2aee50e43c9b31cd41a2c8f2dbc Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Thu, 21 Mar 2019 20:16:06 +0100 Subject: [PATCH 01/62] Update dependencies to newest frameworks. --- composer.json | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 49505a5b..729e4fa2 100644 --- a/composer.json +++ b/composer.json @@ -12,12 +12,10 @@ } ], "require": { - "php": ">=5.4.0", - "illuminate/console": "5.*", - "illuminate/database": "5.*", - "illuminate/events": "5.*", - "illuminate/filesystem": "5.*", - "illuminate/support": "5.*" + "php": "^7.1.3", + "illuminate/database": "5.5.*|5.6.*|5.7.*|5.8.*|5.9.*", + "illuminate/events": "5.5.*|5.6.*|5.7.*|5.8.*|5.9.*", + "doctrine/dbal": "^2.9" }, "require-dev": { "phpunit/phpunit": "~4.0", From e24d82806c6c6dbcb4e2b0cbe21ba489ff2ed500 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Thu, 21 Mar 2019 20:16:44 +0100 Subject: [PATCH 02/62] Add vscode workspace settings to .gitignore. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8b7ef350..a439bd57 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /vendor composer.lock +.vscode From c2d6cf4a7c2f9d4f87a5f34c03ccd2ca63d291d7 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Thu, 21 Mar 2019 20:46:16 +0100 Subject: [PATCH 03/62] Fix compatibility issues with newer frameworks. Theses changes should allow to make it work with newer laravel versions 5.5+. --- src/Baum/Extensions/Eloquent/Model.php | 8 ++++---- src/Baum/Extensions/Query/Builder.php | 25 +++++++++++++++++++++---- src/Baum/Move.php | 2 +- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/Baum/Extensions/Eloquent/Model.php b/src/Baum/Extensions/Eloquent/Model.php index 6a124629..946960cc 100644 --- a/src/Baum/Extensions/Eloquent/Model.php +++ b/src/Baum/Extensions/Eloquent/Model.php @@ -50,8 +50,8 @@ public function getObservableEvents() { * @param Closure|string $callback * @return void */ - public static function moving($callback, $priority = 0) { - static::registerModelEvent('moving', $callback, $priority); + public static function moving($callback) { + static::registerModelEvent('moving', $callback); } /** @@ -60,8 +60,8 @@ public static function moving($callback, $priority = 0) { * @param Closure|string $callback * @return void */ - public static function moved($callback, $priority = 0) { - static::registerModelEvent('moved', $callback, $priority); + public static function moved($callback) { + static::registerModelEvent('moved', $callback); } /** diff --git a/src/Baum/Extensions/Query/Builder.php b/src/Baum/Extensions/Query/Builder.php index 46ad758d..801a9209 100644 --- a/src/Baum/Extensions/Query/Builder.php +++ b/src/Baum/Extensions/Query/Builder.php @@ -13,10 +13,12 @@ class Builder extends BaseBuilder { * @param string $direction * @return \Illuminate\Database\Query\Builder|static */ - public function reOrderBy($column, $direction = 'asc') { - $this->orders = null; + public function reOrderBy($column = null, $direction = 'asc') { + $this->{$this->unions ? 'unionOrders' : 'orders'} = null; - if ( !is_null($column) ) return $this->orderBy($column, $direction); + if (!is_null($column)) { + return parent::orderBy($column, $direction); + } return $this; } @@ -30,10 +32,25 @@ public function reOrderBy($column, $direction = 'asc') { */ public function aggregate($function, $columns = array('*')) { // Postgres doesn't like ORDER BY when there's no GROUP BY clause - if ( !isset($this->groups) ) + if (!isset($this->groups)) { $this->reOrderBy(null); + } return parent::aggregate($function, $columns); } + /** + * Determine if any rows exist for the current query. + * + * @return bool + */ + public function exists() + { + if (!isset($this->groups)) { + $this->reOrderBy(null); + } + + return parent::exists(); + } + } diff --git a/src/Baum/Move.php b/src/Baum/Move.php index 8abed059..5bd0f35f 100644 --- a/src/Baum/Move.php +++ b/src/Baum/Move.php @@ -352,7 +352,7 @@ protected function fireMoveEvent($event, $halt = true) { // but we relay the event into the node instance. $event = "eloquent.{$event}: ".get_class($this->node); - $method = $halt ? 'until' : 'fire'; + $method = $halt ? 'until' : 'dispatch'; return static::$dispatcher->$method($event, $this->node); } From 79d9cade8393b5ec2e55ee1fb9f9027d610875e1 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Thu, 21 Mar 2019 20:46:30 +0100 Subject: [PATCH 04/62] Fix tests. --- .../Category/CategoryCustomEventsTest.php | 15 ++++++----- .../suite/Category/CategoryHierarchyTest.php | 24 +++++++++--------- tests/suite/Category/CategoryMovementTest.php | 6 ++--- .../suite/Category/CategoryRelationsTest.php | 8 +++--- .../Category/CategorySoftDeletesTest.php | 8 +++--- .../Category/CategoryTreeRebuildingTest.php | 8 +++--- tests/suite/Cluster/ClusterHierarchyTest.php | 24 +++++++++--------- tests/suite/Cluster/ClusterMovementTest.php | 6 ++--- tests/suite/NodeModelExtensionsTest.php | 16 +++++++----- tests/suite/QueryBuilderExtensionTest.php | 25 +++++++++++-------- 10 files changed, 78 insertions(+), 62 deletions(-) diff --git a/tests/suite/Category/CategoryCustomEventsTest.php b/tests/suite/Category/CategoryCustomEventsTest.php index 7f39f611..ec2ada15 100644 --- a/tests/suite/Category/CategoryCustomEventsTest.php +++ b/tests/suite/Category/CategoryCustomEventsTest.php @@ -9,15 +9,18 @@ public function tearDown() { } public function testMovementEventsFire() { + $child1 = $this->categories('Child 1'); + $child3 = $this->categories('Child 3'); + $dispatcher = Category::getEventDispatcher(); - Category::setEventDispatcher($events = m::mock('Illuminate\Events\Dispatcher')); - $child = $this->categories('Child 1'); + Category::setEventDispatcher($events = m::mock('\Illuminate\Events\Dispatcher')->makePartial()); + + $events->shouldReceive('until')->once()->with('eloquent.moving: '.get_class($child1), $child1)->andReturn(true); - $events->shouldReceive('until')->once()->with('eloquent.moving: '.get_class($child), $child)->andReturn(true); - $events->shouldReceive('fire')->once()->with('eloquent.moved: '.get_class($child), $child)->andReturn(true); + $events->shouldReceive('dispatch')->once()->with('eloquent.moved: '.get_class($child1), $child1)->andReturn(true); - $child->moveToRightOf($this->categories('Child 3')); + $child1->moveToRightOf($child3); Category::unsetEventDispatcher(); Category::setEventDispatcher($dispatcher); @@ -28,7 +31,7 @@ public function testMovementHaltsWhenReturningFalseFromMoving() { $dispatcher = Category::getEventDispatcher(); - Category::setEventDispatcher($events = m::mock('Illuminate\Events\Dispatcher[until]')); + Category::setEventDispatcher($events = m::mock('Illuminate\Events\Dispatcher')->makePartial()); $events->shouldReceive('until')->once()->with('eloquent.moving: '.get_class($unchanged), $unchanged)->andReturn(false); // Force "moving" to return false diff --git a/tests/suite/Category/CategoryHierarchyTest.php b/tests/suite/Category/CategoryHierarchyTest.php index 74a3a551..bad299c6 100644 --- a/tests/suite/Category/CategoryHierarchyTest.php +++ b/tests/suite/Category/CategoryHierarchyTest.php @@ -24,8 +24,8 @@ public function testRootsStatic() { $this->assertEquals($query->count(), $roots->count()); $this->assertCount(2, $roots); - foreach ($query->lists('id') as $node) - $this->assertContains($node, $roots->lists('id')); + foreach($query->pluck('id')as $node) + $this->assertContains($node, $roots->pluck('id')); } public function testRootsStaticWithCustomOrder() { @@ -35,7 +35,7 @@ public function testRootsStaticWithCustomOrder() { $roots = OrderedCategory::roots()->get(); $this->assertCount(3, $roots); - $this->assertEquals($category, $roots->first()); + $this->assertEquals($category->getAttributes(), $roots->first()->getAttributes()); } public function testRootStatic() { @@ -47,7 +47,7 @@ public function testAllLeavesStatic() { $this->assertCount(4, $allLeaves); - $leaves = $allLeaves->lists('name'); + $leaves = $allLeaves->pluck('name'); $this->assertContains('Child 1' , $leaves); $this->assertContains('Child 2.1' , $leaves); @@ -60,7 +60,7 @@ public function testAllTrunksStatic() { $this->assertCount(1, $allTrunks); - $trunks = $allTrunks->lists('name'); + $trunks = $allTrunks->pluck('name'); $this->assertContains('Child 2', $trunks); } @@ -184,16 +184,16 @@ public function testLimitDepthScope() { $node = $this->categories('Child 2'); - $descendancy = $node->descendants()->lists('id'); + $descendancy = $node->descendants()->pluck('id')->all(); - $this->assertEmpty($node->descendants()->limitDepth(0)->lists('id')); - $this->assertEquals($node, $node->descendantsAndSelf()->limitDepth(0)->first()); + $this->assertEmpty($node->descendants()->limitDepth(0)->pluck('id')->all()); + $this->assertEquals($node->getAttributes(), $node->descendantsAndSelf()->limitDepth(0)->first()->getAttributes()); - $this->assertEquals(array_slice($descendancy, 0, 3), $node->descendants()->limitDepth(3)->lists('id')); - $this->assertEquals(array_slice($descendancy, 0, 5), $node->descendants()->limitDepth(5)->lists('id')); - $this->assertEquals(array_slice($descendancy, 0, 7), $node->descendants()->limitDepth(7)->lists('id')); + $this->assertEquals(array_slice($descendancy, 0, 3), $node->descendants()->limitDepth(3)->pluck('id')->all()); + $this->assertEquals(array_slice($descendancy, 0, 5), $node->descendants()->limitDepth(5)->pluck('id')->all()); + $this->assertEquals(array_slice($descendancy, 0, 7), $node->descendants()->limitDepth(7)->pluck('id')->all()); - $this->assertEquals($descendancy, $node->descendants()->limitDepth(1000)->lists('id')); + $this->assertEquals($descendancy, $node->descendants()->limitDepth(1000)->pluck('id')->all()); } public function testGetAncestorsAndSelf() { diff --git a/tests/suite/Category/CategoryMovementTest.php b/tests/suite/Category/CategoryMovementTest.php index 68dc61c4..ddb821fd 100644 --- a/tests/suite/Category/CategoryMovementTest.php +++ b/tests/suite/Category/CategoryMovementTest.php @@ -246,7 +246,7 @@ public function testMakeChildOfAppendsAtTheEnd() { $newChild->makeChildOf($this->categories('Root 1')); $lastChild = $this->categories('Root 1')->children()->get()->last(); - $this->assertEquals($newChild, $lastChild); + $this->assertEquals($newChild->getAttributes(), $lastChild->getAttributes()); $this->assertTrue(Category::isValidNestedSet()); } @@ -314,7 +314,7 @@ public function testMakeFirstChildOfAppendsAtTheBeginning() { $newChild->makeFirstChildOf($this->categories('Root 1')); $lastChild = $this->categories('Root 1')->children()->get()->first(); - $this->assertEquals($newChild, $lastChild); + $this->assertEquals($newChild->getAttributes(), $lastChild->getAttributes()); $this->assertTrue(Category::isValidNestedSet()); } @@ -382,7 +382,7 @@ public function testMakeLastChildOfAppendsAtTheEnd() { $newChild->makeLastChildOf($this->categories('Root 1')); $lastChild = $this->categories('Root 1')->children()->get()->last(); - $this->assertEquals($newChild, $lastChild); + $this->assertEquals($newChild->getAttributes(), $lastChild->getAttributes()); $this->assertTrue(Category::isValidNestedSet()); } diff --git a/tests/suite/Category/CategoryRelationsTest.php b/tests/suite/Category/CategoryRelationsTest.php index 42f2e100..5c767ce3 100644 --- a/tests/suite/Category/CategoryRelationsTest.php +++ b/tests/suite/Category/CategoryRelationsTest.php @@ -17,9 +17,9 @@ public function testParentRelationIsSelfReferential() { public function testParentRelationRefersToCorrectField() { $category = new Category; - $this->assertEquals($category->getParentColumnName(), $category->parent()->getForeignKey()); + $this->assertEquals($category->getParentColumnName(), $category->parent()->getForeignKeyName()); - $this->assertEquals($category->getQualifiedParentColumnName(), $category->parent()->getQualifiedForeignKey()); + $this->assertEquals($category->getQualifiedParentColumnName(), $category->parent()->getQualifiedForeignKeyName()); } public function testParentRelation() { @@ -43,9 +43,9 @@ public function testChildrenRelationIsSelfReferential() { public function testChildrenRelationReferesToCorrectField() { $category = new Category; - $this->assertEquals($category->getParentColumnName(), $category->children()->getPlainForeignKey()); + $this->assertEquals($category->getParentColumnName(), $category->children()->getForeignKeyName()); - $this->assertEquals($category->getQualifiedParentColumnName(), $category->children()->getForeignKey()); + $this->assertEquals($category->getQualifiedParentColumnName(), $category->children()->getQualifiedForeignKeyName()); } public function testChildrenRelation() { diff --git a/tests/suite/Category/CategorySoftDeletesTest.php b/tests/suite/Category/CategorySoftDeletesTest.php index ae1f8f7b..62ccbe55 100644 --- a/tests/suite/Category/CategorySoftDeletesTest.php +++ b/tests/suite/Category/CategorySoftDeletesTest.php @@ -5,10 +5,12 @@ class CategorySoftDeletesTest extends CategoryTestCase { public function testReload() { $node = $this->categories('Child 3', 'SoftCategory'); + $this->assertTrue($node->exists); + $this->assertFalse($node->trashed()); + $node->delete(); $this->assertTrue($node->trashed()); - $this->assertFalse($node->exists); $node->reload(); @@ -243,7 +245,7 @@ public function testRestoreUnshiftsIndexesFullSubtree() { public function testAllStatic() { $expected = array('Root 1', 'Child 1', 'Child 2', 'Child 2.1', 'Child 3', 'Root 2'); - $this->assertArraysAreEqual($expected, SoftCategory::all()->lists('name')); + $this->assertArraysAreEqual($expected, SoftCategory::all()->pluck('name')->all()); } public function testAllStaticWithSoftDeletes() { @@ -251,7 +253,7 @@ public function testAllStaticWithSoftDeletes() { $this->categories('Child 3', 'SoftCategory')->delete(); $expected = array('Root 1', 'Child 2', 'Child 2.1', 'Root 2'); - $this->assertArraysAreEqual($expected, SoftCategory::all()->lists('name')); + $this->assertArraysAreEqual($expected, SoftCategory::all()->pluck('name')->all()); } } diff --git a/tests/suite/Category/CategoryTreeRebuildingTest.php b/tests/suite/Category/CategoryTreeRebuildingTest.php index ac7fc382..8e951d58 100644 --- a/tests/suite/Category/CategoryTreeRebuildingTest.php +++ b/tests/suite/Category/CategoryTreeRebuildingTest.php @@ -59,10 +59,12 @@ public function testRebuildWithScope() { MultiscopedCategory::rebuild(); $this->assertTrue(MultiscopedCategory::isValidNestedSet()); - $this->assertEquals($root, $this->categories('A', 'MultiScopedCategory')); + $this->assertEquals($root->getAttributes(), $this->categories('A', 'MultiScopedCategory')->getAttributes()); - $expected = array($child1, $child2); - $this->assertEquals($expected, $this->categories('A', 'MultiScopedCategory')->children()->get()->all()); + $expected = array($child1->getAttributes(), $child2->getAttributes()); + $actual = array_map(function ($ch) { return $ch->getAttributes(); }, $this->categories('A', 'MultiScopedCategory')->children()->get()->all()); + + $this->assertEquals($expected, $actual); } public function testRebuildWithMultipleScopes() { diff --git a/tests/suite/Cluster/ClusterHierarchyTest.php b/tests/suite/Cluster/ClusterHierarchyTest.php index d1de5b44..c660ee73 100644 --- a/tests/suite/Cluster/ClusterHierarchyTest.php +++ b/tests/suite/Cluster/ClusterHierarchyTest.php @@ -24,8 +24,8 @@ public function testRootsStatic() { $this->assertEquals($query->count(), $roots->count()); $this->assertCount(2, $roots); - foreach ($query->lists('id') as $node) - $this->assertContains($node, $roots->lists('id')); + foreach ($query->pluck('id') as $node) + $this->assertContains($node, $roots->pluck('id')); } public function testRootsStaticWithCustomOrder() { @@ -35,7 +35,7 @@ public function testRootsStaticWithCustomOrder() { $roots = OrderedCluster::roots()->get(); $this->assertCount(3, $roots); - $this->assertEquals($cluster, $roots->first()); + $this->assertEquals($cluster->getAttributes(), $roots->first()->getAttributes()); } public function testRootStatic() { @@ -47,7 +47,7 @@ public function testAllLeavesStatic() { $this->assertCount(4, $allLeaves); - $leaves = $allLeaves->lists('name'); + $leaves = $allLeaves->pluck('name'); $this->assertContains('Child 1' , $leaves); $this->assertContains('Child 2.1' , $leaves); @@ -60,7 +60,7 @@ public function testAllTrunksStatic() { $this->assertCount(1, $allTrunks); - $trunks = $allTrunks->lists('name'); + $trunks = $allTrunks->pluck('name'); $this->assertContains('Child 2', $trunks); } @@ -188,16 +188,16 @@ public function testLimitDepthScope() { $node = $this->clusters('Child 2'); - $descendancy = $node->descendants()->lists('id'); + $descendancy = $node->descendants()->pluck('id')->all(); - $this->assertEmpty($node->descendants()->limitDepth(0)->lists('id')); - $this->assertEquals($node, $node->descendantsAndSelf()->limitDepth(0)->first()); + $this->assertEmpty($node->descendants()->limitDepth(0)->pluck('id')->all()); + $this->assertEquals($node->getAttributes(), $node->descendantsAndSelf()->limitDepth(0)->first()->getAttributes()); - $this->assertEquals(array_slice($descendancy, 0, 3), $node->descendants()->limitDepth(3)->lists('id')); - $this->assertEquals(array_slice($descendancy, 0, 5), $node->descendants()->limitDepth(5)->lists('id')); - $this->assertEquals(array_slice($descendancy, 0, 7), $node->descendants()->limitDepth(7)->lists('id')); + $this->assertEquals(array_slice($descendancy, 0, 3), $node->descendants()->limitDepth(3)->pluck('id')->all()); + $this->assertEquals(array_slice($descendancy, 0, 5), $node->descendants()->limitDepth(5)->pluck('id')->all()); + $this->assertEquals(array_slice($descendancy, 0, 7), $node->descendants()->limitDepth(7)->pluck('id')->all()); - $this->assertEquals($descendancy, $node->descendants()->limitDepth(1000)->lists('id')); + $this->assertEquals($descendancy, $node->descendants()->limitDepth(1000)->pluck('id')->all()); } public function testGetAncestorsAndSelf() { diff --git a/tests/suite/Cluster/ClusterMovementTest.php b/tests/suite/Cluster/ClusterMovementTest.php index 746aa1a3..967a68ea 100644 --- a/tests/suite/Cluster/ClusterMovementTest.php +++ b/tests/suite/Cluster/ClusterMovementTest.php @@ -246,7 +246,7 @@ public function testMakeChildOfAppendsAtTheEnd() { $newChild->makeChildOf($this->clusters('Root 1')); $lastChild = $this->clusters('Root 1')->children()->get()->last(); - $this->assertEquals($newChild, $lastChild); + $this->assertEquals($newChild->getAttributes(), $lastChild->getAttributes()); $this->assertTrue(Cluster::isValidNestedSet()); } @@ -314,7 +314,7 @@ public function testMakeFirstChildOfAppendsAtTheBeginning() { $newChild->makeFirstChildOf($this->clusters('Root 1')); $lastChild = $this->clusters('Root 1')->children()->get()->first(); - $this->assertEquals($newChild, $lastChild); + $this->assertEquals($newChild->getAttributes(), $lastChild->getAttributes()); $this->assertTrue(Cluster::isValidNestedSet()); } @@ -382,7 +382,7 @@ public function testMakeLastChildOfAppendsAtTheEnd() { $newChild->makeLastChildOf($this->clusters('Root 1')); $lastChild = $this->clusters('Root 1')->children()->get()->last(); - $this->assertEquals($newChild, $lastChild); + $this->assertEquals($newChild->getAttributes(), $lastChild->getAttributes()); $this->assertTrue(Cluster::isValidNestedSet()); } diff --git a/tests/suite/NodeModelExtensionsTest.php b/tests/suite/NodeModelExtensionsTest.php index 2b5d6aad..d3dd542f 100644 --- a/tests/suite/NodeModelExtensionsTest.php +++ b/tests/suite/NodeModelExtensionsTest.php @@ -54,7 +54,9 @@ public function testMoving() { Category::setEventDispatcher($events = m::mock('Illuminate\Contracts\Events\Dispatcher')); $closure = function() {}; - $events->shouldReceive('listen')->once()->with('eloquent.moving: '.get_class(new Category), $closure, 0); + + $events->shouldReceive('listen')->once()->with('eloquent.moving: ' . Category::class, $closure); + Category::moving($closure); Category::unsetEventDispatcher(); @@ -68,7 +70,9 @@ public function testMoved() { Category::setEventDispatcher($events = m::mock('Illuminate\Contracts\Events\Dispatcher')); $closure = function() {}; - $events->shouldReceive('listen')->once()->with('eloquent.moved: '.get_class(new Category), $closure, 0); + + $events->shouldReceive('listen')->once()->with('eloquent.moved: ' . Category::class, $closure); + Category::moved($closure); Category::unsetEventDispatcher(); @@ -92,7 +96,7 @@ public function testReloadResetsChangesOnPersistedNodes() { $node->lft = 10; $node->reload(); - $this->assertEquals($this->categories('Some node'), $node); + $this->assertEquals($this->categories('Some node')->getAttributes(), $node->getAttributes()); } public function testReloadResetsChangesOnDeletedNodes() { @@ -137,7 +141,7 @@ public function testNewNestedSetQueryIsOrderedByDefault() { $builder = $category->newNestedSetQuery(); $query = $builder->getQuery(); - $this->assertNull($query->wheres); + $this->assertEmpty($query->wheres); $this->assertNotEmpty($query->orders); $this->assertEquals($category->getLeftColumnName(), $category->getOrderColumnName()); $this->assertEquals($category->getQualifiedLeftColumnName(), $category->getQualifiedOrderColumnName()); @@ -149,7 +153,7 @@ public function testNewNestedSetQueryIsOrderedByCustom() { $builder = $category->newNestedSetQuery(); $query = $builder->getQuery(); - $this->assertNull($query->wheres); + $this->assertEmpty($query->wheres); $this->assertNotEmpty($query->orders); $this->assertEquals('name', $category->getOrderColumnName()); $this->assertEquals('categories.name', $category->getQualifiedOrderColumnName()); @@ -159,7 +163,7 @@ public function testNewNestedSetQueryIsOrderedByCustom() { public function testNewNestedSetQueryIncludesScopedColumns() { $category = new Category; $simpleQuery = $category->newNestedSetQuery()->getQuery(); - $this->assertNull($simpleQuery->wheres); + $this->assertEmpty($simpleQuery->wheres); $scopedCategory = new ScopedCategory; $scopedQuery = $scopedCategory->newNestedSetQuery()->getQuery(); diff --git a/tests/suite/QueryBuilderExtensionTest.php b/tests/suite/QueryBuilderExtensionTest.php index 4398ab8d..92bc4d77 100644 --- a/tests/suite/QueryBuilderExtensionTest.php +++ b/tests/suite/QueryBuilderExtensionTest.php @@ -1,7 +1,10 @@ getBuilder(); $builder->select('*')->from('users')->orderBy('email')->orderBy('age', 'desc')->reOrderBy('full_name', 'asc'); + $this->assertEquals('select * from "users" order by "full_name" asc', $builder->toSql()); } public function testAggregatesRemoveOrderBy() { $builder = $this->getBuilder(); - $builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as aggregate from "users"', array())->andReturn(array(array('aggregate' => 1))); + $builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as aggregate from "users"', [], true)->andReturn([['aggregate' => 1]]); $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function($builder, $results) { return $results; }); $results = $builder->from('users')->orderBy('age', 'desc')->count(); $this->assertEquals(1, $results); $builder = $this->getBuilder(); - $builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as aggregate from "users" limit 1', array())->andReturn(array(array('aggregate' => 1))); - $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function($builder, $results) { return $results; }); + + $builder->getConnection()->shouldReceive('select')->once()->with('select exists(select * from "users") as "exists"', [], true)->andReturn([['exists' => 1]]); $results = $builder->from('users')->orderBy('age', 'desc')->exists(); $this->assertTrue($results); $builder = $this->getBuilder(); - $builder->getConnection()->shouldReceive('select')->once()->with('select max("id") as aggregate from "users"', array())->andReturn(array(array('aggregate' => 1))); + $builder->getConnection()->shouldReceive('select')->once()->with('select max("id") as aggregate from "users"', [], true)->andReturn([['aggregate' => 1]]); $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function($builder, $results) { return $results; }); $results = $builder->from('users')->orderBy('age', 'desc')->max('id'); $this->assertEquals(1, $results); $builder = $this->getBuilder(); - $builder->getConnection()->shouldReceive('select')->once()->with('select min("id") as aggregate from "users"', array())->andReturn(array(array('aggregate' => 1))); + $builder->getConnection()->shouldReceive('select')->once()->with('select min("id") as aggregate from "users"', [], true)->andReturn([['aggregate' => 1]]); $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function($builder, $results) { return $results; }); $results = $builder->from('users')->orderBy('age', 'desc')->min('id'); $this->assertEquals(1, $results); $builder = $this->getBuilder(); - $builder->getConnection()->shouldReceive('select')->once()->with('select sum("id") as aggregate from "users"', array())->andReturn(array(array('aggregate' => 1))); + $builder->getConnection()->shouldReceive('select')->once()->with('select sum("id") as aggregate from "users"', [], true)->andReturn([['aggregate' => 1]]); $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function($builder, $results) { return $results; }); $results = $builder->from('users')->orderBy('age', 'desc')->sum('id'); $this->assertEquals(1, $results); From 1521e708217d219f106010e7c95880eb2e97ff94 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Thu, 21 Mar 2019 21:21:47 +0100 Subject: [PATCH 05/62] PSR-4 changes. --- composer.json | 4 ++-- src/{Baum => }/Console/BaumCommand.php | 0 src/{Baum => }/Console/InstallCommand.php | 0 src/{Baum => }/Extensions/Eloquent/Collection.php | 0 src/{Baum => }/Extensions/Eloquent/Model.php | 0 src/{Baum => }/Extensions/Query/Builder.php | 0 src/{Baum => }/Generators/Generator.php | 0 src/{Baum => }/Generators/MigrationGenerator.php | 0 src/{Baum => }/Generators/ModelGenerator.php | 0 src/{Baum => }/Generators/stubs/migration.php | 0 src/{Baum => }/Generators/stubs/model.php | 0 src/{Baum => }/Move.php | 0 src/{Baum => }/MoveNotPossibleException.php | 0 src/{Baum => }/Node.php | 0 src/{Baum => }/Providers/BaumServiceProvider.php | 0 src/{Baum => }/SetBuilder.php | 0 src/{Baum => }/SetMapper.php | 0 src/{Baum => }/SetValidator.php | 0 18 files changed, 2 insertions(+), 2 deletions(-) rename src/{Baum => }/Console/BaumCommand.php (100%) rename src/{Baum => }/Console/InstallCommand.php (100%) rename src/{Baum => }/Extensions/Eloquent/Collection.php (100%) rename src/{Baum => }/Extensions/Eloquent/Model.php (100%) rename src/{Baum => }/Extensions/Query/Builder.php (100%) rename src/{Baum => }/Generators/Generator.php (100%) rename src/{Baum => }/Generators/MigrationGenerator.php (100%) rename src/{Baum => }/Generators/ModelGenerator.php (100%) rename src/{Baum => }/Generators/stubs/migration.php (100%) rename src/{Baum => }/Generators/stubs/model.php (100%) rename src/{Baum => }/Move.php (100%) rename src/{Baum => }/MoveNotPossibleException.php (100%) rename src/{Baum => }/Node.php (100%) rename src/{Baum => }/Providers/BaumServiceProvider.php (100%) rename src/{Baum => }/SetBuilder.php (100%) rename src/{Baum => }/SetMapper.php (100%) rename src/{Baum => }/SetValidator.php (100%) diff --git a/composer.json b/composer.json index 729e4fa2..c6ba090e 100644 --- a/composer.json +++ b/composer.json @@ -23,8 +23,8 @@ "d11wtq/boris": "~1.0.10" }, "autoload": { - "psr-0": { - "Baum": "src/" + "psr-4": { + "Baum\\": "src/" } } } diff --git a/src/Baum/Console/BaumCommand.php b/src/Console/BaumCommand.php similarity index 100% rename from src/Baum/Console/BaumCommand.php rename to src/Console/BaumCommand.php diff --git a/src/Baum/Console/InstallCommand.php b/src/Console/InstallCommand.php similarity index 100% rename from src/Baum/Console/InstallCommand.php rename to src/Console/InstallCommand.php diff --git a/src/Baum/Extensions/Eloquent/Collection.php b/src/Extensions/Eloquent/Collection.php similarity index 100% rename from src/Baum/Extensions/Eloquent/Collection.php rename to src/Extensions/Eloquent/Collection.php diff --git a/src/Baum/Extensions/Eloquent/Model.php b/src/Extensions/Eloquent/Model.php similarity index 100% rename from src/Baum/Extensions/Eloquent/Model.php rename to src/Extensions/Eloquent/Model.php diff --git a/src/Baum/Extensions/Query/Builder.php b/src/Extensions/Query/Builder.php similarity index 100% rename from src/Baum/Extensions/Query/Builder.php rename to src/Extensions/Query/Builder.php diff --git a/src/Baum/Generators/Generator.php b/src/Generators/Generator.php similarity index 100% rename from src/Baum/Generators/Generator.php rename to src/Generators/Generator.php diff --git a/src/Baum/Generators/MigrationGenerator.php b/src/Generators/MigrationGenerator.php similarity index 100% rename from src/Baum/Generators/MigrationGenerator.php rename to src/Generators/MigrationGenerator.php diff --git a/src/Baum/Generators/ModelGenerator.php b/src/Generators/ModelGenerator.php similarity index 100% rename from src/Baum/Generators/ModelGenerator.php rename to src/Generators/ModelGenerator.php diff --git a/src/Baum/Generators/stubs/migration.php b/src/Generators/stubs/migration.php similarity index 100% rename from src/Baum/Generators/stubs/migration.php rename to src/Generators/stubs/migration.php diff --git a/src/Baum/Generators/stubs/model.php b/src/Generators/stubs/model.php similarity index 100% rename from src/Baum/Generators/stubs/model.php rename to src/Generators/stubs/model.php diff --git a/src/Baum/Move.php b/src/Move.php similarity index 100% rename from src/Baum/Move.php rename to src/Move.php diff --git a/src/Baum/MoveNotPossibleException.php b/src/MoveNotPossibleException.php similarity index 100% rename from src/Baum/MoveNotPossibleException.php rename to src/MoveNotPossibleException.php diff --git a/src/Baum/Node.php b/src/Node.php similarity index 100% rename from src/Baum/Node.php rename to src/Node.php diff --git a/src/Baum/Providers/BaumServiceProvider.php b/src/Providers/BaumServiceProvider.php similarity index 100% rename from src/Baum/Providers/BaumServiceProvider.php rename to src/Providers/BaumServiceProvider.php diff --git a/src/Baum/SetBuilder.php b/src/SetBuilder.php similarity index 100% rename from src/Baum/SetBuilder.php rename to src/SetBuilder.php diff --git a/src/Baum/SetMapper.php b/src/SetMapper.php similarity index 100% rename from src/Baum/SetMapper.php rename to src/SetMapper.php diff --git a/src/Baum/SetValidator.php b/src/SetValidator.php similarity index 100% rename from src/Baum/SetValidator.php rename to src/SetValidator.php From bc36c6ae3474050a07927ae14a5bf2372fe27fb8 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Thu, 21 Mar 2019 21:28:13 +0100 Subject: [PATCH 06/62] Add L5.5+ package auto-discovery. --- composer.json | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index c6ba090e..fc89ba19 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ } ], "require": { - "php": "^7.1.3", + "php": ">=7.1.0", "illuminate/database": "5.5.*|5.6.*|5.7.*|5.8.*|5.9.*", "illuminate/events": "5.5.*|5.6.*|5.7.*|5.8.*|5.9.*", "doctrine/dbal": "^2.9" @@ -26,5 +26,17 @@ "psr-4": { "Baum\\": "src/" } - } + }, + "extra": { + "laravel": { + "providers": [ + "Baum\\Providers\\BaumServiceProvider" + ] + } + }, + "config": { + "sort-packages": true + }, + "minimum-stability": "dev", + "prefer-stable": true } From 37e0ea5ef91984f0d85b841743d10520272f6acb Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Fri, 22 Mar 2019 16:40:34 +0100 Subject: [PATCH 07/62] Update phpunit library version. --- composer.json | 14 +++++++------- phpunit.xml | 13 +++++++------ tests/suite/BaumTestCase.php | 4 +++- tests/suite/NodeModelExtensionsTest.php | 3 ++- tests/suite/QueryBuilderExtensionTest.php | 3 ++- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/composer.json b/composer.json index fc89ba19..12259801 100644 --- a/composer.json +++ b/composer.json @@ -12,15 +12,15 @@ } ], "require": { - "php": ">=7.1.0", - "illuminate/database": "5.5.*|5.6.*|5.7.*|5.8.*|5.9.*", - "illuminate/events": "5.5.*|5.6.*|5.7.*|5.8.*|5.9.*", - "doctrine/dbal": "^2.9" + "php": ">=7.0.0", + "illuminate/database": "~5.5.0|~5.6.0|~5.7.0|~5.8.0", + "illuminate/events": "~5.5.0|~5.6.0|~5.7.0|~5.8.0" }, "require-dev": { - "phpunit/phpunit": "~4.0", - "mockery/mockery": "~0.9", - "d11wtq/boris": "~1.0.10" + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.0", + "d11wtq/boris": "~1.0.10", + "doctrine/dbal": "^2.9" }, "autoload": { "psr-4": { diff --git a/phpunit.xml b/phpunit.xml index 5764142c..6dacd3e8 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,20 +1,21 @@ + verbose="true"> - tests/suite/QueryBuilderExtensionTest.php - tests/suite/NodeModelExtensionsTest.php - tests/suite/Category - tests/suite/Cluster + ./tests diff --git a/tests/suite/BaumTestCase.php b/tests/suite/BaumTestCase.php index 66eb3d8c..68b6e445 100644 --- a/tests/suite/BaumTestCase.php +++ b/tests/suite/BaumTestCase.php @@ -1,6 +1,8 @@ up(); diff --git a/tests/suite/QueryBuilderExtensionTest.php b/tests/suite/QueryBuilderExtensionTest.php index 92bc4d77..06aa2b89 100644 --- a/tests/suite/QueryBuilderExtensionTest.php +++ b/tests/suite/QueryBuilderExtensionTest.php @@ -5,8 +5,9 @@ use Illuminate\Database\Query\Grammars\Grammar; use Illuminate\Database\Query\Processors\Processor; use Baum\Extensions\Query\Builder; +use PHPUnit\Framework\TestCase; -class QueryBuilderExtensionTest extends PHPUnit_Framework_TestCase { +class QueryBuilderExtensionTest extends TestCase { public function tearDown() { m::close(); From 7a78dc61921578ad9881066b4bfad450c1d55788 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Fri, 22 Mar 2019 16:41:03 +0100 Subject: [PATCH 08/62] Update Travis CI config. --- .travis.yml | 35 ++++++--- src/NestedSet/Columns.php | 156 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+), 9 deletions(-) create mode 100644 src/NestedSet/Columns.php diff --git a/.travis.yml b/.travis.yml index 14cf1c41..6e7bcfa0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,31 @@ language: php php: - - 5.4 - - 5.5 - - 5.6 - - hhvm +- '7.0' +- '7.1' +- '7.2' +- '7.3' -before_script: - - travis_retry composer self-update - - travis_retry composer install --no-interaction --prefer-source --dev +env: +- LARAVEL=5.5.* +- LARAVEL=5.6.* +- LARAVEL=5.7.* +- LARAVEL=5.8.* -script: - - vendor/bin/phpunit --verbose +matrix: + exclude: + - php: '7.0' + env: LARAVEL=5.6.* + - php: '7.0' + env: LARAVEL=5.7.* + - php: '7.0' + env: LARAVEL=5.8.* + fast_finish: true + +sudo: false + +install: + - composer require "laravel/framework=${LARAVEL}" --dev --prefer-dist --no-interaction --no-suggest + - travis_retry composer update --prefer-dist --no-interaction --prefer-stable --no-suggest + +script: vendor/bin/phpunit --verbose diff --git a/src/NestedSet/Columns.php b/src/NestedSet/Columns.php new file mode 100644 index 00000000..ce5ceded --- /dev/null +++ b/src/NestedSet/Columns.php @@ -0,0 +1,156 @@ +parentColumn; + } + + /** + * Get the table qualified parent column name. + * + * @return string + */ + public function getQualifiedParentColumnName() { + $this->qualifyColumn($this->getParentColumnName()); + } + + /** + * Get the "left" field column name. + * + * @return string + */ + public function getLeftColumnName() { + return $this->leftColumn; + } + + /** + * Get the table qualified "left" field column name. + * + * @return string + */ + public function getQualifiedLeftColumnName() { + return $this->qualifyColumn($this->getLeftColumnName()); + } + + /** + * Get the value of the model's "left" field. + * + * @return int + */ + public function getLeft() { + return $this->getAttribute($this->getLeftColumnName()); + } + + /** + * Get the "right" field column name. + * + * @return string + */ + public function getRightColumnName() { + return $this->rightColumn; + } + + /** + * Get the table qualified "right" field column name. + * + * @return string + */ + public function getQualifiedRightColumnName() { + return $this->getTable() . '.' . $this->getRightColumnName(); + } + + /** + * Get the value of the model's "right" field. + * + * @return int + */ + public function getRight() { + return $this->getAttribute($this->getRightColumnName()); + } + + /** + * Get the "depth" field column name. + * + * @return string + */ + public function getDepthColumnName() { + return $this->depthColumn; + } + + /** + * Get the table qualified "depth" field column name. + * + * @return string + */ + public function getQualifiedDepthColumnName() { + return $this->getTable() . '.' . $this->getDepthColumnName(); + } + + /** + * Get the model's "depth" value. + * + * @return int + */ + public function getDepth() { + return $this->getAttribute($this->getDepthColumnName()); + } + + /** + * Get the "order" field column name. + * + * @return string + */ + public function getOrderColumnName() { + return is_null($this->orderColumn) ? $this->getLeftColumnName() : $this->orderColumn; + } + + /** + * Get the table qualified "order" field column name. + * + * @return string + */ + public function getQualifiedOrderColumnName() { + return $this->getTable() . '.' . $this->getOrderColumnName(); + } + + /** + * Get the model's "order" value. + * + * @return mixed + */ + public function getOrder() { + return $this->getAttribute($this->getOrderColumnName()); + } + + /** + * Get the column names which define our scope + * + * @return array + */ + public function getScopedColumns() { + return (array) $this->scoped; + } + + /** + * Get the qualified column names which define our scope + * + * @return array + */ + public function getQualifiedScopedColumns() { + if ( !$this->isScoped() ) + return $this->getScopedColumns(); + + $prefix = $this->getTable() . '.'; + + return array_map(function($c) use ($prefix) { + return $prefix . $c; }, $this->getScopedColumns()); + } + +} From 1e56290825b6d00e0d9ec891043be43275d33df1 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Fri, 22 Mar 2019 17:18:08 +0100 Subject: [PATCH 09/62] Add .editorconfig file. --- .editorconfig | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..56f3b6bf --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false From f248ae9a640e2e74846df242531c4c47fe86902e Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Tue, 26 Mar 2019 10:31:09 +0100 Subject: [PATCH 10/62] Fix test for Laravel 5.5 --- composer.json | 10 ++++++---- tests/suite/Category/CategoryRelationsTest.php | 11 ++++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 12259801..562372c5 100644 --- a/composer.json +++ b/composer.json @@ -13,14 +13,16 @@ ], "require": { "php": ">=7.0.0", - "illuminate/database": "~5.5.0|~5.6.0|~5.7.0|~5.8.0", - "illuminate/events": "~5.5.0|~5.6.0|~5.7.0|~5.8.0" + "illuminate/database": "~5.5.0|~5.6.0|~5.7.0|~5.8.0|~5.9.0", + "illuminate/events": "~5.5.0|~5.6.0|~5.7.0|~5.8.0|~5.9.0", + "illuminate/support": "~5.5.0|~5.6.0|~5.7.0|~5.8.0|~5.9.0" }, "require-dev": { + "d11wtq/boris": "~1.0.10", + "doctrine/dbal": "*", "mockery/mockery": "^1.0", "phpunit/phpunit": "^7.0", - "d11wtq/boris": "~1.0.10", - "doctrine/dbal": "^2.9" + "squizlabs/php_codesniffer": "^3.4" }, "autoload": { "psr-4": { diff --git a/tests/suite/Category/CategoryRelationsTest.php b/tests/suite/Category/CategoryRelationsTest.php index 5c767ce3..7f3e008e 100644 --- a/tests/suite/Category/CategoryRelationsTest.php +++ b/tests/suite/Category/CategoryRelationsTest.php @@ -17,9 +17,14 @@ public function testParentRelationIsSelfReferential() { public function testParentRelationRefersToCorrectField() { $category = new Category; - $this->assertEquals($category->getParentColumnName(), $category->parent()->getForeignKeyName()); - - $this->assertEquals($category->getQualifiedParentColumnName(), $category->parent()->getQualifiedForeignKeyName()); + if (method_exists($category->parent(), 'getForeignKeyName')) { + // For Laravel 5.6+ + $this->assertEquals($category->getParentColumnName(), $category->parent()->getForeignKeyName()); + $this->assertEquals($category->getQualifiedParentColumnName(), $category->parent()->getQualifiedForeignKeyName()); + } else { + $this->assertEquals($category->getParentColumnName(), $category->parent()->getForeignKey()); + $this->assertEquals($category->getQualifiedParentColumnName(), $category->parent()->getQualifiedForeignKey()); + } } public function testParentRelation() { From db53aff90bf1364ff703e9a7db6ff7eb7eb524e2 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Wed, 8 May 2019 11:54:24 +0200 Subject: [PATCH 11/62] Revamp implementation to a trait-based one. --- .php_cs.cache | 1 + .travis.yml | 4 + bootstrap.php | 75 +- boris | 32 - composer.json | 22 +- console | 21 + phpcs.xml | 11 + phpunit.xml | 2 +- src/BaumServiceProvider.php | 67 + src/Console/BaumCommand.php | 34 - src/Console/InstallCommand.php | 123 -- src/Console/MakeMigrationCommand.php | 39 + src/Console/MakeModelCommand.php | 83 + src/Console/MigrationCreator.php | 24 + src/Console/VersionCommand.php | 37 + src/Console/stubs/migration.stub | 47 + src/Console/stubs/model.stub | 87 + src/Extensions/Eloquent/Collection.php | 41 - src/Extensions/Eloquent/Model.php | 119 -- src/Extensions/Query/Builder.php | 56 - src/Generators/Generator.php | 93 - src/Generators/MigrationGenerator.php | 65 - src/Generators/ModelGenerator.php | 37 - src/Generators/stubs/migration.php | 42 - src/Generators/stubs/model.php | 102 -- src/Mixins/Blueprint.php | 23 + src/Mixins/Collection.php | 40 + src/Move.php | 392 ----- src/MoveNotPossibleException.php | 4 - src/NestedSet/Builder.php | 182 ++ src/NestedSet/Columns.php | 156 -- src/NestedSet/Concerns/CanBeScoped.php | 34 + src/NestedSet/Concerns/HasAttributes.php | 56 + src/NestedSet/Concerns/HasColumns.php | 192 +++ src/NestedSet/Concerns/HasEvents.php | 50 + src/NestedSet/Concerns/Mappable.php | 34 + src/NestedSet/Concerns/Movable.php | 135 ++ src/NestedSet/Concerns/Rebuildable.php | 21 + src/NestedSet/Concerns/Relatable.php | 167 ++ src/NestedSet/Concerns/Validatable.php | 20 + .../Concerns/WorksWithSoftDeletes.php | 84 + src/NestedSet/Mapper.php | 183 ++ src/NestedSet/Move.php | 428 +++++ src/NestedSet/MoveNotPossibleException.php | 10 + src/NestedSet/Node.php | 675 ++++++++ src/NestedSet/NodeObserver.php | 78 + src/NestedSet/Scopes/OrderingScope.php | 56 + src/NestedSet/Scopes/ScopedByScope.php | 74 + src/NestedSet/Validator.php | 243 +++ src/Node.php | 1273 +------------- src/Playground/Caster.php | 87 + src/Playground/Console.php | 109 ++ src/Providers/BaumServiceProvider.php | 76 - src/SetBuilder.php | 175 -- src/SetMapper.php | 164 -- src/SetValidator.php | 224 --- tests/config/database.php | 50 +- tests/migrators/CategoryMigrator.php | 34 - tests/migrators/ClusterMigrator.php | 34 - tests/models/Category.php | 48 - tests/models/Cluster.php | 67 - tests/seeders/CategorySeeder.php | 167 -- tests/seeders/ClusterSeeder.php | 48 - tests/suite/BaumTestCase.php | 14 - tests/suite/Category/CategoryColumnsTest.php | 131 -- .../Category/CategoryCustomEventsTest.php | 56 - .../suite/Category/CategoryHierarchyTest.php | 705 -------- tests/suite/Category/CategoryMovementTest.php | 528 ------ .../suite/Category/CategoryRelationsTest.php | 121 -- tests/suite/Category/CategoryScopedTest.php | 320 ---- .../Category/CategorySoftDeletesTest.php | 259 --- .../suite/Category/CategoryTreeMapperTest.php | 210 --- .../Category/CategoryTreeRebuildingTest.php | 96 -- .../Category/CategoryTreeValidationTest.php | 84 - tests/suite/CategoryTestCase.php | 17 - tests/suite/Cluster/ClusterColumnsTest.php | 19 - tests/suite/Cluster/ClusterHierarchyTest.php | 708 -------- tests/suite/Cluster/ClusterMovementTest.php | 528 ------ tests/suite/ClusterTestCase.php | 17 - tests/suite/ColumnsTest.php | 172 ++ tests/suite/Concerns/MigratesDatabase.php | 15 + tests/suite/Concerns/SeedsDatabase.php | 15 + tests/suite/CustomEventsTest.php | 109 ++ tests/suite/HierarchyTest.php | 1532 +++++++++++++++++ tests/suite/MovementTest.php | 1151 +++++++++++++ tests/suite/NodeModelExtensionsTest.php | 182 -- tests/suite/QueryBuilderExtensionTest.php | 64 - tests/suite/RelationsTest.php | 150 ++ tests/suite/ScopingTest.php | 346 ++++ tests/suite/SoftDeletesTest.php | 282 +++ .../Support/Migrators/CategoryMigrator.php | 37 + .../Support/Migrators/ClusterMigrator.php | 38 + tests/suite/Support/Models/Category.php | 14 + tests/suite/Support/Models/Cluster.php | 49 + .../Support/Models/MultiScopedCategory.php | 10 + .../Support/Models/MultiScopedCluster.php | 10 + .../suite/Support/Models/OrderedCategory.php | 8 + tests/suite/Support/Models/OrderedCluster.php | 8 + .../Support/Models/OrderedScopedCategory.php | 12 + tests/suite/Support/Models/ScopedCategory.php | 10 + tests/suite/Support/Models/ScopedCluster.php | 10 + tests/suite/Support/Models/SoftCategory.php | 12 + tests/suite/Support/Models/SoftCluster.php | 12 + .../suite/Support/Seeders/CategorySeeder.php | 44 + tests/suite/Support/Seeders/ClusterSeeder.php | 36 + .../Seeders/MultiScopedCategorySeeder.php | 46 + .../Support/Seeders/OrderedCategorySeeder.php | 32 + .../Support/Seeders/OrderedClusterSeeder.php | 25 + .../Seeders/OrderedScopedCategorySeeder.php | 36 + .../Support/Seeders/ScopedCategorySeeder.php | 37 + tests/suite/Support/helpers.php | 59 + tests/suite/TestCase.php | 49 + tests/suite/TreeMapperTest.php | 266 +++ tests/suite/TreeRebuildingTest.php | 109 ++ tests/suite/TreeValidationTest.php | 96 ++ tests/suite/support.php | 59 - 116 files changed, 8338 insertions(+), 7774 deletions(-) create mode 100644 .php_cs.cache delete mode 100755 boris create mode 100755 console create mode 100644 phpcs.xml create mode 100644 src/BaumServiceProvider.php delete mode 100644 src/Console/BaumCommand.php delete mode 100644 src/Console/InstallCommand.php create mode 100644 src/Console/MakeMigrationCommand.php create mode 100644 src/Console/MakeModelCommand.php create mode 100644 src/Console/MigrationCreator.php create mode 100644 src/Console/VersionCommand.php create mode 100644 src/Console/stubs/migration.stub create mode 100644 src/Console/stubs/model.stub delete mode 100644 src/Extensions/Eloquent/Collection.php delete mode 100644 src/Extensions/Eloquent/Model.php delete mode 100644 src/Extensions/Query/Builder.php delete mode 100644 src/Generators/Generator.php delete mode 100644 src/Generators/MigrationGenerator.php delete mode 100644 src/Generators/ModelGenerator.php delete mode 100644 src/Generators/stubs/migration.php delete mode 100644 src/Generators/stubs/model.php create mode 100644 src/Mixins/Blueprint.php create mode 100644 src/Mixins/Collection.php delete mode 100644 src/Move.php delete mode 100644 src/MoveNotPossibleException.php create mode 100644 src/NestedSet/Builder.php delete mode 100644 src/NestedSet/Columns.php create mode 100644 src/NestedSet/Concerns/CanBeScoped.php create mode 100644 src/NestedSet/Concerns/HasAttributes.php create mode 100644 src/NestedSet/Concerns/HasColumns.php create mode 100644 src/NestedSet/Concerns/HasEvents.php create mode 100644 src/NestedSet/Concerns/Mappable.php create mode 100644 src/NestedSet/Concerns/Movable.php create mode 100644 src/NestedSet/Concerns/Rebuildable.php create mode 100644 src/NestedSet/Concerns/Relatable.php create mode 100644 src/NestedSet/Concerns/Validatable.php create mode 100644 src/NestedSet/Concerns/WorksWithSoftDeletes.php create mode 100644 src/NestedSet/Mapper.php create mode 100644 src/NestedSet/Move.php create mode 100644 src/NestedSet/MoveNotPossibleException.php create mode 100644 src/NestedSet/Node.php create mode 100644 src/NestedSet/NodeObserver.php create mode 100644 src/NestedSet/Scopes/OrderingScope.php create mode 100644 src/NestedSet/Scopes/ScopedByScope.php create mode 100644 src/NestedSet/Validator.php create mode 100644 src/Playground/Caster.php create mode 100644 src/Playground/Console.php delete mode 100644 src/Providers/BaumServiceProvider.php delete mode 100644 src/SetBuilder.php delete mode 100644 src/SetMapper.php delete mode 100644 src/SetValidator.php delete mode 100644 tests/migrators/CategoryMigrator.php delete mode 100644 tests/migrators/ClusterMigrator.php delete mode 100644 tests/models/Category.php delete mode 100644 tests/models/Cluster.php delete mode 100644 tests/seeders/CategorySeeder.php delete mode 100644 tests/seeders/ClusterSeeder.php delete mode 100644 tests/suite/BaumTestCase.php delete mode 100644 tests/suite/Category/CategoryColumnsTest.php delete mode 100644 tests/suite/Category/CategoryCustomEventsTest.php delete mode 100644 tests/suite/Category/CategoryHierarchyTest.php delete mode 100644 tests/suite/Category/CategoryMovementTest.php delete mode 100644 tests/suite/Category/CategoryRelationsTest.php delete mode 100644 tests/suite/Category/CategoryScopedTest.php delete mode 100644 tests/suite/Category/CategorySoftDeletesTest.php delete mode 100644 tests/suite/Category/CategoryTreeMapperTest.php delete mode 100644 tests/suite/Category/CategoryTreeRebuildingTest.php delete mode 100644 tests/suite/Category/CategoryTreeValidationTest.php delete mode 100644 tests/suite/CategoryTestCase.php delete mode 100644 tests/suite/Cluster/ClusterColumnsTest.php delete mode 100644 tests/suite/Cluster/ClusterHierarchyTest.php delete mode 100644 tests/suite/Cluster/ClusterMovementTest.php delete mode 100644 tests/suite/ClusterTestCase.php create mode 100644 tests/suite/ColumnsTest.php create mode 100644 tests/suite/Concerns/MigratesDatabase.php create mode 100644 tests/suite/Concerns/SeedsDatabase.php create mode 100644 tests/suite/CustomEventsTest.php create mode 100644 tests/suite/HierarchyTest.php create mode 100644 tests/suite/MovementTest.php delete mode 100644 tests/suite/NodeModelExtensionsTest.php delete mode 100644 tests/suite/QueryBuilderExtensionTest.php create mode 100644 tests/suite/RelationsTest.php create mode 100644 tests/suite/ScopingTest.php create mode 100644 tests/suite/SoftDeletesTest.php create mode 100644 tests/suite/Support/Migrators/CategoryMigrator.php create mode 100644 tests/suite/Support/Migrators/ClusterMigrator.php create mode 100644 tests/suite/Support/Models/Category.php create mode 100644 tests/suite/Support/Models/Cluster.php create mode 100644 tests/suite/Support/Models/MultiScopedCategory.php create mode 100644 tests/suite/Support/Models/MultiScopedCluster.php create mode 100644 tests/suite/Support/Models/OrderedCategory.php create mode 100644 tests/suite/Support/Models/OrderedCluster.php create mode 100644 tests/suite/Support/Models/OrderedScopedCategory.php create mode 100644 tests/suite/Support/Models/ScopedCategory.php create mode 100644 tests/suite/Support/Models/ScopedCluster.php create mode 100644 tests/suite/Support/Models/SoftCategory.php create mode 100644 tests/suite/Support/Models/SoftCluster.php create mode 100644 tests/suite/Support/Seeders/CategorySeeder.php create mode 100644 tests/suite/Support/Seeders/ClusterSeeder.php create mode 100644 tests/suite/Support/Seeders/MultiScopedCategorySeeder.php create mode 100644 tests/suite/Support/Seeders/OrderedCategorySeeder.php create mode 100644 tests/suite/Support/Seeders/OrderedClusterSeeder.php create mode 100644 tests/suite/Support/Seeders/OrderedScopedCategorySeeder.php create mode 100644 tests/suite/Support/Seeders/ScopedCategorySeeder.php create mode 100644 tests/suite/Support/helpers.php create mode 100644 tests/suite/TestCase.php create mode 100644 tests/suite/TreeMapperTest.php create mode 100644 tests/suite/TreeRebuildingTest.php create mode 100644 tests/suite/TreeValidationTest.php delete mode 100644 tests/suite/support.php diff --git a/.php_cs.cache b/.php_cs.cache new file mode 100644 index 00000000..93af1cc0 --- /dev/null +++ b/.php_cs.cache @@ -0,0 +1 @@ +{"php":"7.2.17-1+ubuntu18.04.1+deb.sury.org+3","version":"2.14.2","rules":{"array_syntax":{"syntax":"short"}},"hashes":{"tests\/suite\/QueryBuilderExtensionTest.php":2401011848,"tests\/suite\/CategoryTestCase.php":1459149732,"tests\/suite\/Support\/helpers.php":2887944969,"tests\/suite\/Support\/Seeders\/ClusterSeeder.php":1677258117,"tests\/suite\/Support\/Seeders\/OrderedCategorySeeder.php":3399630089,"tests\/suite\/Support\/Seeders\/OrderedClusterSeeder.php":3534917921,"tests\/suite\/Support\/Seeders\/MultiScopedCategorySeeder.php":2379713817,"tests\/suite\/Support\/Seeders\/OrderedScopedCategorySeeder.php":1122912534,"tests\/suite\/Support\/Seeders\/ScopedCategorySeeder.php":1867462824,"tests\/suite\/Support\/Seeders\/CategorySeeder.php":4060707526,"tests\/suite\/Support\/Models\/OrderedCategory.php":1127747670,"tests\/suite\/Support\/Models\/OrderedScopedCategory.php":3079717558,"tests\/suite\/Support\/Models\/ScopedCluster.php":3122942005,"tests\/suite\/Support\/Models\/Cluster.php":2187538680,"tests\/suite\/Support\/Models\/OrderedCluster.php":200827106,"tests\/suite\/Support\/Models\/MultiScopedCategory.php":2608465521,"tests\/suite\/Support\/Models\/Category.php":891317858,"tests\/suite\/Support\/Models\/MultiScopedCluster.php":1919760292,"tests\/suite\/Support\/Models\/SoftCluster.php":3352690176,"tests\/suite\/Support\/Models\/SoftCategory.php":3796899808,"tests\/suite\/Support\/Models\/ScopedCategory.php":1579628681,"tests\/suite\/Support\/Migrators\/ClusterMigrator.php":1940264335,"tests\/suite\/Support\/Migrators\/CategoryMigrator.php":314973804,"tests\/suite\/Category\/CategorySoftDeletesTest.php":2524683052,"tests\/suite\/Category\/CategoryRelationsTest.php":702111616,"tests\/suite\/Category\/CategoryScopedTest.php":1926983763,"tests\/suite\/Category\/CategoryTreeMapperTest.php":2492303857,"tests\/suite\/Category\/CategoryHierarchyTest.php":990981592,"tests\/suite\/Category\/CategoryTreeRebuildingTest.php":3068289855,"tests\/suite\/Category\/CategoryTreeValidationTest.php":3527788738,"tests\/suite\/Category\/CategoryMovementTest.php":2090494941,"tests\/suite\/Category\/CategoryColumnsTest.php":2084740264,"tests\/suite\/Category\/CategoryCustomEventsTest.php":715441256,"tests\/suite\/ClusterTestCase.php":1139337425,"tests\/suite\/BaumTestCase.php":3604825002,"tests\/suite\/Cluster\/ClusterHierarchyTest.php":2678987013,"tests\/suite\/Cluster\/ClusterColumnsTest.php":4212176167,"tests\/suite\/Cluster\/ClusterMovementTest.php":681419415,"tests\/suite\/NodeModelExtensionsTest.php":4270767412}} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 6e7bcfa0..341f0200 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,10 +7,12 @@ php: - '7.3' env: +- LARAVEL=5.4.* - LARAVEL=5.5.* - LARAVEL=5.6.* - LARAVEL=5.7.* - LARAVEL=5.8.* +- LARAVEL=5.9.* matrix: exclude: @@ -20,6 +22,8 @@ matrix: env: LARAVEL=5.7.* - php: '7.0' env: LARAVEL=5.8.* + - php: '7.0' + env: LARAVEL=5.9.* fast_finish: true sudo: false diff --git a/bootstrap.php b/bootstrap.php index e00b7e52..0283d8c1 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -1,38 +1,55 @@ addConnection(require(__DIR__.'/tests/config/database.php')); - -$capsule->setEventDispatcher(new Illuminate\Events\Dispatcher(new Illuminate\Container\Container)); - +$capsule->setEventDispatcher(new \Illuminate\Events\Dispatcher(new \Illuminate\Container\Container)); $capsule->bootEloquent(); $capsule->setAsGlobal(); - -/** - * Autoload required libraries - */ -$__autoload_paths = array('models', 'migrators', 'seeders'); - -foreach($__autoload_paths as $path) { - foreach(glob(__DIR__ . "/tests/$path/*.php") as $dep) { - require_once $dep; - } -} - -/** - * Require test helpers - */ -require __DIR__ . '/tests/suite/support.php'; -require __DIR__ . '/tests/suite/BaumTestCase.php'; -require __DIR__ . '/tests/suite/CategoryTestCase.php'; -require __DIR__ . '/tests/suite/ClusterTestCase.php'; diff --git a/boris b/boris deleted file mode 100755 index 0a739fa4..00000000 --- a/boris +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env php -up(); -with(new ClusterMigrator)->up(); - -// Seed the database with some data -// ... - -// Set up a custom inspector for our models -class BaumNodeInspector extends \Boris\ColoredInspector { - public function objectVars($value) { - if ( $value instanceof Baum\Node ) - return array_merge($value->getAttributes(), get_object_vars($value)); - - return parent::objectVars($value); - } -} - -// Start-up boris REPL and import any defined var (in scope) which happens to be -// a Baum\Node instance into the context -$boris = new \Boris\Boris('> '); - -$boris->setInspector(new BaumNodeInspector); - -$boris->setLocal(array_filter(get_defined_vars(), function($var) { - return $var instanceof Baum\Node; })); - -$boris->start(); diff --git a/composer.json b/composer.json index 562372c5..f1b3e8b2 100644 --- a/composer.json +++ b/composer.json @@ -13,15 +13,15 @@ ], "require": { "php": ">=7.0.0", - "illuminate/database": "~5.5.0|~5.6.0|~5.7.0|~5.8.0|~5.9.0", - "illuminate/events": "~5.5.0|~5.6.0|~5.7.0|~5.8.0|~5.9.0", - "illuminate/support": "~5.5.0|~5.6.0|~5.7.0|~5.8.0|~5.9.0" + "illuminate/database": "~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0|~5.9.0", + "illuminate/events": "~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0|~5.9.0", + "illuminate/support": "~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0|~5.9.0" }, "require-dev": { - "d11wtq/boris": "~1.0.10", - "doctrine/dbal": "*", + "doctrine/dbal": "~2.9.2", "mockery/mockery": "^1.0", - "phpunit/phpunit": "^7.0", + "phpunit/phpunit": "~6.5.14|^7.0", + "psy/psysh": "@stable", "squizlabs/php_codesniffer": "^3.4" }, "autoload": { @@ -29,10 +29,18 @@ "Baum\\": "src/" } }, + "autoload-dev": { + "files": [ + "tests/suite/Support/helpers.php" + ], + "psr-4": { + "Baum\\Tests\\": "tests/suite/" + } + }, "extra": { "laravel": { "providers": [ - "Baum\\Providers\\BaumServiceProvider" + "Baum\\BaumServiceProvider" ] } }, diff --git a/console b/console new file mode 100755 index 00000000..ecaec210 --- /dev/null +++ b/console @@ -0,0 +1,21 @@ +#!/usr/bin/env php + + */ + + require __DIR__ . '/bootstrap.php'; + +class Test extends Illuminate\Database\Eloquent\Model { + use Baum\NestedSet\Node; + + protected $tableName = 'forest'; +} + +with(new \Baum\Tests\Support\Migrators\CategoryMigrator)->up(); + +exit($status = Baum\Playground\Console::start()); diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 00000000..ac3706d1 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,11 @@ + + + The coding standard for Baum. + + + + + + + + diff --git a/phpunit.xml b/phpunit.xml index 6dacd3e8..12d26272 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -15,7 +15,7 @@ verbose="true"> - ./tests + ./tests/suite diff --git a/src/BaumServiceProvider.php b/src/BaumServiceProvider.php new file mode 100644 index 00000000..161e4f0c --- /dev/null +++ b/src/BaumServiceProvider.php @@ -0,0 +1,67 @@ +registerMacros(); + + $this->registerCommands(); + } + + /** + * Setup "mixed-in" functionality for some objects. + * + * @return void + */ + public function registerMacros() + { + Collection::mixin(new Mixins\Collection); + + Blueprint::mixin(new Mixins\Blueprint); + } + + /** + * Register the Horizon Artisan commands. + * + * @return void + */ + protected function registerCommands() + { + if ($this->app->runningInConsole()) { + $this->commands([ + Console\MakeMigrationCommand::class, + Console\MakeModelCommand::class, + Console\VersionCommand::class + ]); + } + } +} diff --git a/src/Console/BaumCommand.php b/src/Console/BaumCommand.php deleted file mode 100644 index fdcd1c9e..00000000 --- a/src/Console/BaumCommand.php +++ /dev/null @@ -1,34 +0,0 @@ -line('Baum version ' . Baum::VERSION . ''); - $this->line('A Nested Set pattern implementation for the Eloquent ORM.'); - $this->line('Copyright (c) 2013 Estanislau Trepat'); - } - -} diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php deleted file mode 100644 index 8c273b7b..00000000 --- a/src/Console/InstallCommand.php +++ /dev/null @@ -1,123 +0,0 @@ -migrator = $migrator; - $this->modeler = $modeler; - } - - /** - * Execute the console command. - * - * Basically, we'll write the migration and model stubs out to disk inflected - * with the name provided. Once its done, we'll `dump-autoload` for the entire - * framework to make sure that the new classes are registered by the class - * loaders. - * - * @return void - */ - public function fire() { - $name = $this->input->getArgument('name'); - - $this->writeMigration($name); - - $this->writeModel($name); - - } - - /** - * Get the command arguments - * - * @return array - */ - protected function getArguments() { - return array( - array('name', InputArgument::REQUIRED, 'Name to use for the scaffolding of the migration and model.') - ); - } - - /** - * Write the migration file to disk. - * - * @param string $name - * @return string - */ - protected function writeMigration($name) { - $output = pathinfo($this->migrator->create($name, $this->getMigrationsPath()), PATHINFO_FILENAME); - - $this->line(" create $output"); - } - - /** - * Write the model file to disk. - * - * @param string $name - * @return string - */ - protected function writeModel($name) { - $output = pathinfo($this->modeler->create($name, $this->getModelsPath()), PATHINFO_FILENAME); - - $this->line(" create $output"); - } - - /** - * Get the path to the migrations directory. - * - * @return string - */ - protected function getMigrationsPath() { - return $this->laravel['path.database'].'/migrations'; - } - - /** - * Get the path to the models directory. - * - * @return string - */ - protected function getModelsPath() { - return $this->laravel['path.base']; - } - -} diff --git a/src/Console/MakeMigrationCommand.php b/src/Console/MakeMigrationCommand.php new file mode 100644 index 00000000..2db811a4 --- /dev/null +++ b/src/Console/MakeMigrationCommand.php @@ -0,0 +1,39 @@ +option('force')) { + return false; + } + + if ($this->option('migration')) { + $this->createMigration(); + } + } + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getStub() + { + return __DIR__.'/stubs/model.stub'; + } + + /** + * Create a migration file for the model. + * + * @return void + */ + protected function createMigration() + { + $table = Str::snake(Str::pluralStudly(class_basename($this->argument('name')))); + + $this->call('baum:make-migration', ['name' => "create_{$table}_table", '--create' => $table]); + } + + /** + * Get the console command options. + * + * @return array + */ + protected function getOptions() + { + return [ + ['force', null, InputOption::VALUE_NONE, 'Create the class even if the model already exists'], + + ['migration', 'm', InputOption::VALUE_NONE, 'Create a new migration file for the model'], + ]; + } +} diff --git a/src/Console/MigrationCreator.php b/src/Console/MigrationCreator.php new file mode 100644 index 00000000..52ebaa18 --- /dev/null +++ b/src/Console/MigrationCreator.php @@ -0,0 +1,24 @@ +files->get(__DIR__.'/stubs/migration.stub'); + } + + return parent::getStub($table, $create); + } +} diff --git a/src/Console/VersionCommand.php b/src/Console/VersionCommand.php new file mode 100644 index 00000000..7cf562b0 --- /dev/null +++ b/src/Console/VersionCommand.php @@ -0,0 +1,37 @@ +line('Baum v'.BaumServiceProvider::VERSION.''); + $this->line('A Nested Set pattern implementation for Laravel\'s Eloquent ORM.'); + $this->line(''); + $this->line('Licensed under the terms of the MIT License .'); + $this->line('Coded by Estanislau Trepat .'); + } +} diff --git a/src/Console/stubs/migration.stub b/src/Console/stubs/migration.stub new file mode 100644 index 00000000..c859ee75 --- /dev/null +++ b/src/Console/stubs/migration.stub @@ -0,0 +1,47 @@ +increments('id'); + + // We declare here the nested set structure columns/fields + $table->nestedSet(); + + // The previous `nestedSet` blueprint helper is equivalent to + // the following column/field declarations: + // + // $table->integer('parent_id')->unsigned()->nullable()->index(); + // $table->foreign('parent_id')->references('id')->on($table->getTable()); + // $table->integer('left')->unsigned()->nullable()->index(); + // $table->integer('right')->unsgined()->nullable()->index(); + // $table->integer('depth')->unsigned()->nullable()->index(); + // + // Feel free to modify at your own will but note that all columns + // *must be present* and initialized on the model accordingly. + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('DummyTable'); + } +} diff --git a/src/Console/stubs/model.stub b/src/Console/stubs/model.stub new file mode 100644 index 00000000..1031a268 --- /dev/null +++ b/src/Console/stubs/model.stub @@ -0,0 +1,87 @@ +getDictionary(); - - // Enforce sorting by $orderColumn setting in Baum\Node instance - uasort($dict, function($a, $b){ - return ($a->getOrder() >= $b->getOrder()) ? 1 : -1; - }); - - return new BaseCollection($this->hierarchical($dict)); - } - - protected function hierarchical($result) { - foreach($result as $key => $node) - $node->setRelation('children', new BaseCollection); - - $nestedKeys = array(); - - foreach($result as $key => $node) { - $parentKey = $node->getParentId(); - - if ( !is_null($parentKey) && array_key_exists($parentKey, $result) ) { - $result[$parentKey]->children[] = $node; - - $nestedKeys[] = $node->getKey(); - } - } - - foreach($nestedKeys as $key) - unset($result[$key]); - - return $result; - } - -} diff --git a/src/Extensions/Eloquent/Model.php b/src/Extensions/Eloquent/Model.php deleted file mode 100644 index 946960cc..00000000 --- a/src/Extensions/Eloquent/Model.php +++ /dev/null @@ -1,119 +0,0 @@ -exists || ($this->areSoftDeletesEnabled() && $this->trashed()) ) { - $fresh = $this->getFreshInstance(); - - if ( is_null($fresh) ) - throw with(new ModelNotFoundException)->setModel(get_called_class()); - - $this->setRawAttributes($fresh->getAttributes(), true); - - $this->setRelations($fresh->getRelations()); - - $this->exists = $fresh->exists; - } else { - // Revert changes if model is not persisted - $this->attributes = $this->original; - } - - return $this; - } - - /** - * Get the observable event names. - * - * @return array - */ - public function getObservableEvents() { - return array_merge(array('moving', 'moved'), parent::getObservableEvents()); - } - - /** - * Register a moving model event with the dispatcher. - * - * @param Closure|string $callback - * @return void - */ - public static function moving($callback) { - static::registerModelEvent('moving', $callback); - } - - /** - * Register a moved model event with the dispatcher. - * - * @param Closure|string $callback - * @return void - */ - public static function moved($callback) { - static::registerModelEvent('moved', $callback); - } - - /** - * Get a new query builder instance for the connection. - * - * @return \Baum\Extensions\Query\Builder - */ - protected function newBaseQueryBuilder() { - $conn = $this->getConnection(); - - $grammar = $conn->getQueryGrammar(); - - return new QueryBuilder($conn, $grammar, $conn->getPostProcessor()); - } - - /** - * Returns a fresh instance from the database. - * - * @return \Baum\Node - */ - protected function getFreshInstance() { - if ( $this->areSoftDeletesEnabled() ) - return static::withTrashed()->find($this->getKey()); - - return static::find($this->getKey()); - } - - /** - * Returns wether soft delete functionality is enabled on the model or not. - * - * @return boolean - */ - public function areSoftDeletesEnabled() { - // To determine if there's a global soft delete scope defined we must - // first determine if there are any, to workaround a non-existent key error. - $globalScopes = $this->getGlobalScopes(); - - if ( count($globalScopes) === 0 ) return false; - - // Now that we're sure that the calling class has some kind of global scope - // we check for the SoftDeletingScope existance - return static::hasGlobalScope(new SoftDeletingScope); - } - - /** - * Static method which returns wether soft delete functionality is enabled - * on the model. - * - * @return boolean - */ - public static function softDeletesEnabled() { - return with(new static)->areSoftDeletesEnabled(); - } - -} diff --git a/src/Extensions/Query/Builder.php b/src/Extensions/Query/Builder.php deleted file mode 100644 index 801a9209..00000000 --- a/src/Extensions/Query/Builder.php +++ /dev/null @@ -1,56 +0,0 @@ -{$this->unions ? 'unionOrders' : 'orders'} = null; - - if (!is_null($column)) { - return parent::orderBy($column, $direction); - } - - return $this; - } - - /** - * Execute an aggregate function on the database. - * - * @param string $function - * @param array $columns - * @return mixed - */ - public function aggregate($function, $columns = array('*')) { - // Postgres doesn't like ORDER BY when there's no GROUP BY clause - if (!isset($this->groups)) { - $this->reOrderBy(null); - } - - return parent::aggregate($function, $columns); - } - - /** - * Determine if any rows exist for the current query. - * - * @return bool - */ - public function exists() - { - if (!isset($this->groups)) { - $this->reOrderBy(null); - } - - return parent::exists(); - } - -} diff --git a/src/Generators/Generator.php b/src/Generators/Generator.php deleted file mode 100644 index de02fab4..00000000 --- a/src/Generators/Generator.php +++ /dev/null @@ -1,93 +0,0 @@ -files = $files; - } - - /** - * Get the path to the stubs. - * - * @return string - */ - public function getStubPath() { - return __DIR__.'/stubs'; - } - - /** - * Get the filesystem instance. - * - * @return \Illuminate\Filesystem\Filesystem - */ - public function getFilesystem() { - return $this->files; - } - - /** - * Get the given stub by name. - * - * @param string $table - * @return void - */ - protected function getStub($name) { - if ( stripos($name, '.php') === FALSE ) - $name = $name . '.php'; - - return $this->files->get($this->getStubPath() . '/' . $name); - } - - /** - * Parse the provided stub and replace via the array given. - * - * @param string $stub - * @param string $replacements - * @return string - */ - protected function parseStub($stub, $replacements=array()) { - $output = $stub; - - foreach ($replacements as $key => $replacement) { - $search = '{{'.$key.'}}'; - $output = str_replace($search, $replacement, $output); - } - - return $output; - } - - /** - * Inflect to a class name - * - * @param string $input - * @return string - */ - protected function classify($input) { - return studly_case(str_singular($input)); - } - - /** - * Inflect to table name - * - * @param string $input - * @return string - */ - protected function tableize($input) { - return snake_case(str_plural($input)); - } -} diff --git a/src/Generators/MigrationGenerator.php b/src/Generators/MigrationGenerator.php deleted file mode 100644 index c7a052e9..00000000 --- a/src/Generators/MigrationGenerator.php +++ /dev/null @@ -1,65 +0,0 @@ -getPath($name, $path); - - $stub = $this->getStub('migration'); - - $this->files->put($path, $this->parseStub($stub, array( - 'table' => $this->tableize($name), - 'class' => $this->getMigrationClassName($name) - ))); - - return $path; - } - - /** - * Get the migration name. - * - * @param string $name - * @return string - */ - protected function getMigrationName($name) { - return 'create_' . $this->tableize($name) . '_table'; - } - - /** - * Get the name for the migration class. - * - * @param string $name - */ - protected function getMigrationClassName($name) { - return $this->classify($this->getMigrationName($name)); - } - - /** - * Get the full path name to the migration. - * - * @param string $name - * @param string $path - * @return string - */ - protected function getPath($name, $path) { - return $path . '/' . $this->getDatePrefix() . '_' . $this->getMigrationName($name) . '.php'; - } - - /** - * Get the date prefix for the migration. - * - * @return int - */ - protected function getDatePrefix() { - return date('Y_m_d_His'); - } - -} diff --git a/src/Generators/ModelGenerator.php b/src/Generators/ModelGenerator.php deleted file mode 100644 index b5ebf5dd..00000000 --- a/src/Generators/ModelGenerator.php +++ /dev/null @@ -1,37 +0,0 @@ -getPath($name, $path); - - $stub = $this->getStub('model'); - - $this->files->put($path, $this->parseStub($stub, array( - 'table' => $this->tableize($name), - 'class' => $this->classify($name) - ))); - - return $path; - } - - /** - * Get the full path name to the migration. - * - * @param string $name - * @param string $path - * @return string - */ - protected function getPath($name, $path) { - return $path . '/' . $this->classify($name) . '.php'; - } - -} diff --git a/src/Generators/stubs/migration.php b/src/Generators/stubs/migration.php deleted file mode 100644 index 1ad33068..00000000 --- a/src/Generators/stubs/migration.php +++ /dev/null @@ -1,42 +0,0 @@ -increments('id'); - $table->integer('parent_id')->nullable()->index(); - $table->integer('lft')->nullable()->index(); - $table->integer('rgt')->nullable()->index(); - $table->integer('depth')->nullable(); - - // Add needed columns here (f.ex: name, slug, path, etc.) - // $table->string('name', 255); - - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() { - Schema::drop('{{table}}'); - } - -} diff --git a/src/Generators/stubs/model.php b/src/Generators/stubs/model.php deleted file mode 100644 index 0574e787..00000000 --- a/src/Generators/stubs/model.php +++ /dev/null @@ -1,102 +0,0 @@ -integer('parent_id')->unsigned()->nullable()->index(); + $this->foreign('parent_id')->references('id')->on($this->getTable()); + $this->integer('left')->unsigned()->nullable()->index(); + $this->integer('right')->unsgined()->nullable()->index(); + $this->integer('depth')->unsigned()->nullable()->index(); + }; + } +} diff --git a/src/Mixins/Collection.php b/src/Mixins/Collection.php new file mode 100644 index 00000000..d2369285 --- /dev/null +++ b/src/Mixins/Collection.php @@ -0,0 +1,40 @@ +sortBy(function ($node) { + return $node->getOrder(); + })->each(function ($node) { + $node->setRelation('children', new static); + })->getDictionary(); + + $nestedKeys = []; + + foreach ($dict as $key => $node) { + $parentKey = $node->getParentKey(); + + if (!is_null($parentKey) && array_key_exists($parentKey, $dict)) { + $dict[$parentKey]->children[] = $node; + + $nestedKeys[] = $node->getKey(); + } + } + + foreach ($nestedKeys as $key) { + unset($dict[$key]); + } + + return new static($dict); + }; + } +} diff --git a/src/Move.php b/src/Move.php deleted file mode 100644 index 5bd0f35f..00000000 --- a/src/Move.php +++ /dev/null @@ -1,392 +0,0 @@ -node = $node; - $this->target = $this->resolveNode($target); - $this->position = $position; - - $this->setEventDispatcher($node->getEventDispatcher()); - } - - /** - * Easy static accessor for performing a move operation. - * - * @param \Baum\Node $node - * @param \Baum\Node|int $target - * @param string $position - * @return \Baum\Node - */ - public static function to($node, $target, $position) { - $instance = new static($node, $target, $position); - - return $instance->perform(); - } - - /** - * Perform the move operation. - * - * @return \Baum\Node - */ - public function perform() { - $this->guardAgainstImpossibleMove(); - - if ( $this->fireMoveEvent('moving') === false ) - return $this->node; - - if ( $this->hasChange() ) { - $self = $this; - - $this->node->getConnection()->transaction(function() use ($self) { - $self->updateStructure(); - }); - - $this->target->reload(); - - $this->node->setDepthWithSubtree(); - - $this->node->reload(); - } - - $this->fireMoveEvent('moved', false); - - return $this->node; - } - - /** - * Runs the SQL query associated with the update of the indexes affected - * by the move operation. - * - * @return int - */ - public function updateStructure() { - list($a, $b, $c, $d) = $this->boundaries(); - - // select the rows between the leftmost & the rightmost boundaries and apply a lock - $this->applyLockBetween($a, $d); - - $connection = $this->node->getConnection(); - $grammar = $connection->getQueryGrammar(); - - $currentId = $this->quoteIdentifier($this->node->getKey()); - $parentId = $this->quoteIdentifier($this->parentId()); - $leftColumn = $this->node->getLeftColumnName(); - $rightColumn = $this->node->getRightColumnName(); - $parentColumn = $this->node->getParentColumnName(); - $wrappedLeft = $grammar->wrap($leftColumn); - $wrappedRight = $grammar->wrap($rightColumn); - $wrappedParent = $grammar->wrap($parentColumn); - $wrappedId = $grammar->wrap($this->node->getKeyName()); - - $lftSql = "CASE - WHEN $wrappedLeft BETWEEN $a AND $b THEN $wrappedLeft + $d - $b - WHEN $wrappedLeft BETWEEN $c AND $d THEN $wrappedLeft + $a - $c - ELSE $wrappedLeft END"; - - $rgtSql = "CASE - WHEN $wrappedRight BETWEEN $a AND $b THEN $wrappedRight + $d - $b - WHEN $wrappedRight BETWEEN $c AND $d THEN $wrappedRight + $a - $c - ELSE $wrappedRight END"; - - $parentSql = "CASE - WHEN $wrappedId = $currentId THEN $parentId - ELSE $wrappedParent END"; - - $updateConditions = array( - $leftColumn => $connection->raw($lftSql), - $rightColumn => $connection->raw($rgtSql), - $parentColumn => $connection->raw($parentSql) - ); - - if ( $this->node->timestamps ) - $updateConditions[$this->node->getUpdatedAtColumn()] = $this->node->freshTimestamp(); - - return $this->node - ->newNestedSetQuery() - ->where(function($query) use ($leftColumn, $rightColumn, $a, $d) { - $query->whereBetween($leftColumn, array($a, $d)) - ->orWhereBetween($rightColumn, array($a, $d)); - }) - ->update($updateConditions); - } - - /** - * Resolves suplied node. Basically returns the node unchanged if - * supplied parameter is an instance of \Baum\Node. Otherwise it will try - * to find the node in the database. - * - * @param \Baum\node|int - * @return \Baum\Node - */ - protected function resolveNode($node) { - if ( $node instanceof \Baum\Node ) return $node->reload(); - - return $this->node->newNestedSetQuery()->find($node); - } - - /** - * Check wether the current move is possible and if not, rais an exception. - * - * @return void - */ - protected function guardAgainstImpossibleMove() { - if ( !$this->node->exists ) - throw new MoveNotPossibleException('A new node cannot be moved.'); - - if ( array_search($this->position, array('child', 'left', 'right', 'root')) === FALSE ) - throw new MoveNotPossibleException("Position should be one of ['child', 'left', 'right'] but is {$this->position}."); - - if ( !$this->promotingToRoot() ) { - if ( is_null($this->target) ) { - if ( $this->position === 'left' || $this->position === 'right' ) - throw new MoveNotPossibleException("Could not resolve target node. This node cannot move any further to the {$this->position}."); - else - throw new MoveNotPossibleException('Could not resolve target node.'); - } - - if ( $this->node->equals($this->target) ) - throw new MoveNotPossibleException('A node cannot be moved to itself.'); - - if ( $this->target->insideSubtree($this->node) ) - throw new MoveNotPossibleException('A node cannot be moved to a descendant of itself (inside moved tree).'); - - if ( !$this->node->inSameScope($this->target) ) - throw new MoveNotPossibleException('A node cannot be moved to a different scope.'); - } - } - - /** - * Computes the boundary. - * - * @return int - */ - protected function bound1() { - if ( !is_null($this->_bound1) ) return $this->_bound1; - - switch ( $this->position ) { - case 'child': - $this->_bound1 = $this->target->getRight(); - break; - - case 'left': - $this->_bound1 = $this->target->getLeft(); - break; - - case 'right': - $this->_bound1 = $this->target->getRight() + 1; - break; - - case 'root': - $this->_bound1 = $this->node->newNestedSetQuery()->max($this->node->getRightColumnName()) + 1; - break; - } - - $this->_bound1 = (($this->_bound1 > $this->node->getRight()) ? $this->_bound1 - 1 : $this->_bound1); - return $this->_bound1; - } - - /** - * Computes the other boundary. - * TODO: Maybe find a better name for this... ¿? - * - * @return int - */ - protected function bound2() { - if ( !is_null($this->_bound2) ) return $this->_bound2; - - $this->_bound2 = (($this->bound1() > $this->node->getRight()) ? $this->node->getRight() + 1 : $this->node->getLeft() - 1); - return $this->_bound2; - } - - /** - * Computes the boundaries array. - * - * @return array - */ - protected function boundaries() { - if ( !is_null($this->_boundaries) ) return $this->_boundaries; - - // we have defined the boundaries of two non-overlapping intervals, - // so sorting puts both the intervals and their boundaries in order - $this->_boundaries = array( - $this->node->getLeft() , - $this->node->getRight() , - $this->bound1() , - $this->bound2() - ); - sort($this->_boundaries); - - return $this->_boundaries; - } - - /** - * Computes the new parent id for the node being moved. - * - * @return int - */ - protected function parentId() { - switch( $this->position ) { - case 'root': - return NULL; - - case 'child': - return $this->target->getKey(); - - default: - return $this->target->getParentId(); - } - } - - /** - * Check wether there should be changes in the downward tree structure. - * - * @return boolean - */ - protected function hasChange() { - return !( $this->bound1() == $this->node->getRight() || $this->bound1() == $this->node->getLeft() ); - } - - /** - * Check if we are promoting the provided instance to a root node. - * - * @return boolean - */ - protected function promotingToRoot() { - return ($this->position == 'root'); - } - - /** - * Get the event dispatcher instance. - * - * @return \Illuminate\Events\Dispatcher - */ - public static function getEventDispatcher() { - return static::$dispatcher; - } - - /** - * Set the event dispatcher instance. - * - * @param \Illuminate\Events\Dispatcher - * @return void - */ - public static function setEventDispatcher(Dispatcher $dispatcher) { - static::$dispatcher = $dispatcher; - } - - /** - * Fire the given move event for the model. - * - * @param string $event - * @param bool $halt - * @return mixed - */ - protected function fireMoveEvent($event, $halt = true) { - if ( !isset(static::$dispatcher) ) return true; - - // Basically the same as \Illuminate\Database\Eloquent\Model->fireModelEvent - // but we relay the event into the node instance. - $event = "eloquent.{$event}: ".get_class($this->node); - - $method = $halt ? 'until' : 'dispatch'; - - return static::$dispatcher->$method($event, $this->node); - } - - /** - * Quotes an identifier for being used in a database query. - * - * @param mixed $value - * @return string - */ - protected function quoteIdentifier($value) { - if ( is_null($value) ) - return 'NULL'; - - $connection = $this->node->getConnection(); - - $pdo = $connection->getPdo(); - - return $pdo->quote($value); - } - - /** - * Applies a lock to the rows between the supplied index boundaries. - * - * @param int $lft - * @param int $rgt - * @return void - */ - protected function applyLockBetween($lft, $rgt) { - $this->node->newQuery() - ->where($this->node->getLeftColumnName(), '>=', $lft) - ->where($this->node->getRightColumnName(), '<=', $rgt) - ->select($this->node->getKeyName()) - ->lockForUpdate() - ->get(); - } -} diff --git a/src/MoveNotPossibleException.php b/src/MoveNotPossibleException.php deleted file mode 100644 index 4832ec2c..00000000 --- a/src/MoveNotPossibleException.php +++ /dev/null @@ -1,4 +0,0 @@ -node = $node; + } + + /** + * Perform the re-calculation of the left and right indexes of the whole + * nested set tree structure. + * + * @param bool $force + * @return void + */ + public function rebuild($force = false) + { + $alreadyValid = forward_static_call([get_class($this->node), 'isValidNestedSet']); + + // Do not rebuild a valid Nested Set tree structure + if (!$force && $alreadyValid) { + return true; + } + + // Rebuild lefts and rights for each root node and its children (recursively). + // We go by setting left (and keep track of the current left bound), then + // search for each children and recursively set the left index (while + // incrementing that index). When going back up the recursive chain we start + // setting the right indexes and saving the nodes... + $roots = $this->roots(); + + $this->node->getConnection()->transaction(function () use ($roots) { + foreach ($roots as $r) { + $this->rebuildBounds($r, 0); + } + }); + } + + /** + * Return all root nodes for the current database table appropiately sorted. + * + * @return Illuminate\Database\Eloquent\Collection + */ + public function roots() + { + return $this->node->newQueryWithoutNestedSetScopes() + ->whereNull($this->node->getQualifiedParentColumnName()) + ->orderBy($this->node->getQualifiedLeftColumnName()) + ->orderBy($this->node->getQualifiedRightColumnName()) + ->orderBy($this->node->getQualifiedKeyName()) + ->get(); + } + + /** + * Recompute left and right index bounds for the specified node and its + * children (recursive call). Fill the depth column too. + */ + public function rebuildBounds($node, $depth = 0) + { + $k = $this->scopedKey($node); + + $node->setAttribute($node->getLeftColumnName(), $this->getNextBound($k)); + $node->setAttribute($node->getDepthColumnName(), $depth); + + foreach ($this->children($node) as $child) { + $this->rebuildBounds($child, $depth + 1); + } + + $node->setAttribute($node->getRightColumnName(), $this->getNextBound($k)); + + $node->save(); + } + + /** + * Return all children for the specified node. + * + * @param Baum\Node $node + * @return Illuminate\Database\Eloquent\Collection + */ + public function children($node) + { + // $query = $this->node->newQuery(); + $query = $this->node->newQueryWithoutNestedSetScopes(); + + $query->where($this->node->getQualifiedParentColumnName(), '=', $node->getKey()); + + // We must also add the scoped column values to the query to compute valid + // left and right indexes. + foreach ($this->scopedAttributes($node) as $fld => $value) { + $query->where($this->node->qualifyColumn($fld), '=', $value); + } + + $query->orderBy($this->node->getQualifiedLeftColumnName()); + $query->orderBy($this->node->getQualifiedRightColumnName()); + $query->orderBy($this->node->getQualifiedKeyName()); + + return $query->get(); + } + + /** + * Return an array of the scoped attributes of the supplied node. + * + * @param Baum\Node $node + * @return array + */ + protected function scopedAttributes($node) + { + $keys = $this->node->getScopedColumnNames(); + + if (count($keys) == 0) { + return []; + } + + $values = array_map(function ($column) use ($node) { + return $node->getAttribute($column); + }, $keys); + + return array_combine($keys, $values); + } + + /** + * Return a string-key for the current scoped attributes. Used for index + * computing when a scope is defined (acsts as an scope identifier). + * + * @param Baum\Node $node + * @return string + */ + protected function scopedKey($node) + { + $attributes = $this->scopedAttributes($node); + + $output = []; + + foreach ($attributes as $fld => $value) { + $output[] = $this->node->qualifyColumn($fld).'='.(is_null($value) ? 'NULL' : $value); + } + + // NOTE: Maybe an md5 or something would be better. Should be unique though. + return implode(',', $output); + } + + /** + * Return next index bound value for the given key (current scope identifier) + * + * @param string $key + * @return integer + */ + protected function getNextBound($key) + { + if (false === array_key_exists($key, $this->bounds)) { + $this->bounds[$key] = 0; + } + + $this->bounds[$key] = $this->bounds[$key] + 1; + + return $this->bounds[$key]; + } +} diff --git a/src/NestedSet/Columns.php b/src/NestedSet/Columns.php deleted file mode 100644 index ce5ceded..00000000 --- a/src/NestedSet/Columns.php +++ /dev/null @@ -1,156 +0,0 @@ -parentColumn; - } - - /** - * Get the table qualified parent column name. - * - * @return string - */ - public function getQualifiedParentColumnName() { - $this->qualifyColumn($this->getParentColumnName()); - } - - /** - * Get the "left" field column name. - * - * @return string - */ - public function getLeftColumnName() { - return $this->leftColumn; - } - - /** - * Get the table qualified "left" field column name. - * - * @return string - */ - public function getQualifiedLeftColumnName() { - return $this->qualifyColumn($this->getLeftColumnName()); - } - - /** - * Get the value of the model's "left" field. - * - * @return int - */ - public function getLeft() { - return $this->getAttribute($this->getLeftColumnName()); - } - - /** - * Get the "right" field column name. - * - * @return string - */ - public function getRightColumnName() { - return $this->rightColumn; - } - - /** - * Get the table qualified "right" field column name. - * - * @return string - */ - public function getQualifiedRightColumnName() { - return $this->getTable() . '.' . $this->getRightColumnName(); - } - - /** - * Get the value of the model's "right" field. - * - * @return int - */ - public function getRight() { - return $this->getAttribute($this->getRightColumnName()); - } - - /** - * Get the "depth" field column name. - * - * @return string - */ - public function getDepthColumnName() { - return $this->depthColumn; - } - - /** - * Get the table qualified "depth" field column name. - * - * @return string - */ - public function getQualifiedDepthColumnName() { - return $this->getTable() . '.' . $this->getDepthColumnName(); - } - - /** - * Get the model's "depth" value. - * - * @return int - */ - public function getDepth() { - return $this->getAttribute($this->getDepthColumnName()); - } - - /** - * Get the "order" field column name. - * - * @return string - */ - public function getOrderColumnName() { - return is_null($this->orderColumn) ? $this->getLeftColumnName() : $this->orderColumn; - } - - /** - * Get the table qualified "order" field column name. - * - * @return string - */ - public function getQualifiedOrderColumnName() { - return $this->getTable() . '.' . $this->getOrderColumnName(); - } - - /** - * Get the model's "order" value. - * - * @return mixed - */ - public function getOrder() { - return $this->getAttribute($this->getOrderColumnName()); - } - - /** - * Get the column names which define our scope - * - * @return array - */ - public function getScopedColumns() { - return (array) $this->scoped; - } - - /** - * Get the qualified column names which define our scope - * - * @return array - */ - public function getQualifiedScopedColumns() { - if ( !$this->isScoped() ) - return $this->getScopedColumns(); - - $prefix = $this->getTable() . '.'; - - return array_map(function($c) use ($prefix) { - return $prefix . $c; }, $this->getScopedColumns()); - } - -} diff --git a/src/NestedSet/Concerns/CanBeScoped.php b/src/NestedSet/Concerns/CanBeScoped.php new file mode 100644 index 00000000..7bc3267f --- /dev/null +++ b/src/NestedSet/Concerns/CanBeScoped.php @@ -0,0 +1,34 @@ +getScopedColumnNames()) > 0); + } + + /** + * Checkes if the given node is in the same scope as the current one. + * + * @param \Baum\Node + * @return boolean + */ + public function inSameScope($other) + { + foreach ($this->getScopedColumnNames() as $fld) { + if ($this->getAttribute($fld) != $other->getAttribute($fld)) { + return false; + } + } + + return true; + } +} diff --git a/src/NestedSet/Concerns/HasAttributes.php b/src/NestedSet/Concerns/HasAttributes.php new file mode 100644 index 00000000..54a45a79 --- /dev/null +++ b/src/NestedSet/Concerns/HasAttributes.php @@ -0,0 +1,56 @@ +getAttribute($this->getParentColumnName()); + } + + /** + * Get the value of the model's "left" field. + * + * @return int + */ + public function getLeft() + { + return $this->getAttribute($this->getLeftColumnName()); + } + + /** + * Get the value of the model's "right" field. + * + * @return int + */ + public function getRight() + { + return $this->getAttribute($this->getRightColumnName()); + } + + /** + * Get the model's "depth" value. + * + * @return int + */ + public function getDepth() + { + return $this->getAttribute($this->getDepthColumnName()); + } + + /** + * Get the model's "order" value. + * + * @return mixed + */ + public function getOrder() + { + return $this->getAttribute($this->getOrderColumnName()); + } +} diff --git a/src/NestedSet/Concerns/HasColumns.php b/src/NestedSet/Concerns/HasColumns.php new file mode 100644 index 00000000..b5cc3e9f --- /dev/null +++ b/src/NestedSet/Concerns/HasColumns.php @@ -0,0 +1,192 @@ +getDefaultParentColumnName(); + } + + return $this->parentColumnName; + } + + /** + * Get the "default" parent column name. + * + * @return string + */ + public function getDefaultParentColumnName() + { + return 'parent_id'; + } + + /** + * Get the table qualified parent column name. + * + * @return string + */ + public function getQualifiedParentColumnName() + { + return $this->qualifyColumn($this->getParentColumnName()); + } + + /** + * Get the "left" field column name. + * + * @return string + */ + public function getLeftColumnName() + { + if (!property_exists($this, 'leftColumnName')) { + return $this->getDefaultLeftColumnName(); + } + + return $this->leftColumnName; + } + + /** + * Get the "default" left column name. + * + * @return string + */ + public function getDefaultLeftColumnName() + { + return 'left'; + } + + /** + * Get the table qualified "left" field column name. + * + * @return string + */ + public function getQualifiedLeftColumnName() + { + return $this->qualifyColumn($this->getLeftColumnName()); + } + + /** + * Get the "right" field column name. + * + * @return string + */ + public function getRightColumnName() + { + if (!property_exists($this, 'rightColumnName')) { + return $this->getDefaultRightColumnName(); + } + + return $this->rightColumnName; + } + + /** + * Get the "default" right column name. + * + * @return string + */ + public function getDefaultRightColumnName() + { + return 'right'; + } + + /** + * Get the table qualified "right" field column name. + * + * @return string + */ + public function getQualifiedRightColumnName() + { + return $this->qualifyColumn($this->getRightColumnName()); + } + + /** + * Get the "depth" field column name. + * + * @return string + */ + public function getDepthColumnName() + { + if (!property_exists($this, 'depthColumnName')) { + return $this->getDefaultDepthColumnName(); + } + + return $this->depthColumnName; + } + + /** + * Get the "default" depth column name. + * + * @return string + */ + public function getDefaultDepthColumnName() + { + return 'depth'; + } + + /** + * Get the table qualified "depth" field column name. + * + * @return string + */ + public function getQualifiedDepthColumnName() + { + return $this->qualifyColumn($this->getDepthColumnName()); + } + + /** + * Get the "order" field column name. + * + * @return string + */ + public function getOrderColumnName() + { + if (!property_exists($this, 'orderColumnName')) { + return $this->getLeftColumnName(); + } + + return $this->orderColumnName ?? $this->getLeftColumnName(); + } + + /** + * Get the table qualified "order" field column name. + * + * @return string + */ + public function getQualifiedOrderColumnName() + { + return $this->qualifyColumn($this->getOrderColumnName()); + } + + /** + * Get the column names which define our scope + * + * @return array + */ + public function getScopedColumnNames() + { + if (!property_exists($this, 'scopeColumnNames')) { + return []; + } + + return (array) $this->scopeColumnNames; + } + + /** + * Get the qualified column names which define our scope + * + * @return array + */ + public function getQualifiedScopedColumnNames() + { + return array_map(function ($c) { + return $this->qualifyColumn($c); + }, $this->getScopedColumnNames()); + } +} diff --git a/src/NestedSet/Concerns/HasEvents.php b/src/NestedSet/Concerns/HasEvents.php new file mode 100644 index 00000000..e9bc11ec --- /dev/null +++ b/src/NestedSet/Concerns/HasEvents.php @@ -0,0 +1,50 @@ +addObservableEvents('moving', 'moved'); + } + + /** + * Register a moving model event with the dispatcher. + * + * @param Closure|string $callback + * @return void + */ + public static function moving($callback) + { + static::registerModelEvent('moving', $callback); + } + + /** + * Register a moved model event with the dispatcher. + * + * @param Closure|string $callback + * @return void + */ + public static function moved($callback) + { + static::registerModelEvent('moved', $callback); + } +} diff --git a/src/NestedSet/Concerns/Mappable.php b/src/NestedSet/Concerns/Mappable.php new file mode 100644 index 00000000..3f3e8185 --- /dev/null +++ b/src/NestedSet/Concerns/Mappable.php @@ -0,0 +1,34 @@ +map($nodeList); + } + + /** + * Maps the provided tree structure into the database. + * + * @param array|\Illuminate\Support\Contracts\ArrayableInterface + * @return boolean + */ + public static function buildTree($nodeList) + { + return with(new static)->makeTree($nodeList); + } +} diff --git a/src/NestedSet/Concerns/Movable.php b/src/NestedSet/Concerns/Movable.php new file mode 100644 index 00000000..fdbf6d03 --- /dev/null +++ b/src/NestedSet/Concerns/Movable.php @@ -0,0 +1,135 @@ +moveToLeftOf($this->getLeftSibling()); + } + + /** + * Find the right sibling and move to the right of it. + * + * @return \Baum\Node + */ + public function moveRight() + { + return $this->moveToRightOf($this->getRightSibling()); + } + + /** + * Move to the node to the left of ... + * + * @return \Baum\Node + */ + public function moveToLeftOf($node) + { + return $this->moveTo($node, 'left'); + } + + /** + * Move to the node to the right of ... + * + * @return \Baum\Node + */ + public function moveToRightOf($node) + { + return $this->moveTo($node, 'right'); + } + + /** + * Alias for moveToRightOf + * + * @return \Baum\Node + */ + public function makeNextSiblingOf($node) + { + return $this->moveToRightOf($node); + } + + /** + * Alias for moveToRightOf + * + * @return \Baum\Node + */ + public function makeSiblingOf($node) + { + return $this->moveToRightOf($node); + } + + /** + * Alias for moveToLeftOf + * + * @return \Baum\Node + */ + public function makePreviousSiblingOf($node) + { + return $this->moveToLeftOf($node); + } + + /** + * Make the node a child of ... + * + * @return \Baum\Node + */ + public function makeChildOf($node) + { + return $this->moveTo($node, 'child'); + } + + /** + * Make the node the first child of ... + * + * @return \Baum\Node + */ + public function makeFirstChildOf($node) + { + if ($node->children()->count() == 0) { + return $this->makeChildOf($node); + } + + return $this->moveToLeftOf($node->children()->first()); + } + + /** + * Make the node the last child of ... + * + * @return \Baum\Node + */ + public function makeLastChildOf($node) + { + return $this->makeChildOf($node); + } + + /** + * Make current node a root node. + * + * @return \Baum\Node + */ + public function makeRoot() + { + return $this->moveTo($this, 'root'); + } +} diff --git a/src/NestedSet/Concerns/Rebuildable.php b/src/NestedSet/Concerns/Rebuildable.php new file mode 100644 index 00000000..92478e2a --- /dev/null +++ b/src/NestedSet/Concerns/Rebuildable.php @@ -0,0 +1,21 @@ +rebuild($force); + } +} diff --git a/src/NestedSet/Concerns/Relatable.php b/src/NestedSet/Concerns/Relatable.php new file mode 100644 index 00000000..85b501d0 --- /dev/null +++ b/src/NestedSet/Concerns/Relatable.php @@ -0,0 +1,167 @@ +belongsTo(get_class($this), $this->getParentColumnName()); + } + + /** + * Children relation (self-referential) 1-N. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function children() + { + return $this->hasMany(get_class($this), $this->getParentColumnName()); + } + + // /** + // * Inmmediate descendants relation. Alias for "children". + // * + // * @return \Illuminate\Database\Eloquent\Relations\HasMany + // */ + // public function immediateDescendants() + // { + // return $this->children(); + // } + + // /** + // * Attribute alias so as to eager-load the proper relationship. + // * + // * @return mixed + // */ + // public function getImmediateDescendantsAttribute() + // { + // return $this->getRelationValue('children'); + // } + + /** + * Retrive all of its "immediate" descendants. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getImmediateDescendants($columns = ['*']) + { + // return $this->children()->get($columns); + return $this->getRelationValue('children'); + } + + + /** + * Returns true if this is a root node. + * + * @return boolean + */ + public function isRoot() + { + return is_null($this->getParentKey()); + } + + /** + * Returns true if this is a leaf node (end of a branch). + * + * @return boolean + */ + public function isLeaf() + { + return $this->exists && ($this->getRight() - $this->getLeft() == 1); + } + + /** + * Returns true if this is a child node. + * + * @return boolean + */ + public function isChild() + { + return !$this->isRoot(); + } + + /** + * Returns true if node is a descendant. + * + * @param NestedSet + * @return boolean + */ + public function isDescendantOf($other) + { + return ( + $this->getLeft() > $other->getLeft() && + $this->getLeft() < $other->getRight() && + $this->inSameScope($other) + ); + } + + /** + * Returns true if node is self or a descendant. + * + * @param NestedSet + * @return boolean + */ + public function isSelfOrDescendantOf($other) + { + return ( + $this->getLeft() >= $other->getLeft() && + $this->getLeft() < $other->getRight() && + $this->inSameScope($other) + ); + } + + /** + * Returns true if node is an ancestor. + * + * @param NestedSet + * @return boolean + */ + public function isAncestorOf($other) + { + return ( + $this->getLeft() < $other->getLeft() && + $this->getRight() > $other->getLeft() && + $this->inSameScope($other) + ); + } + + /** + * Returns true if node is self or an ancestor. + * + * @param NestedSet + * @return boolean + */ + public function isSelfOrAncestorOf($other) + { + return ( + $this->getLeft() <= $other->getLeft() && + $this->getRight() > $other->getLeft() && + $this->inSameScope($other) + ); + } + + /** + * Checks wether the given node is a descendant of itself. Basically, whether + * its in the subtree defined by the left and right indices. + * + * @param \Baum\Node + * @return boolean + */ + public function insideSubtree($node) + { + return ( + $this->getLeft() >= $node->getLeft() && + $this->getLeft() <= $node->getRight() && + $this->getRight() >= $node->getLeft() && + $this->getRight() <= $node->getRight() && + $this->inSameScope($node) + ); + } +} diff --git a/src/NestedSet/Concerns/Validatable.php b/src/NestedSet/Concerns/Validatable.php new file mode 100644 index 00000000..61a01dc2 --- /dev/null +++ b/src/NestedSet/Concerns/Validatable.php @@ -0,0 +1,20 @@ +passes(); + } +} diff --git a/src/NestedSet/Concerns/WorksWithSoftDeletes.php b/src/NestedSet/Concerns/WorksWithSoftDeletes.php new file mode 100644 index 00000000..25e8f374 --- /dev/null +++ b/src/NestedSet/Concerns/WorksWithSoftDeletes.php @@ -0,0 +1,84 @@ +getGlobalScopes(); + + if (count($globalScopes) === 0) { + return false; + } + + // Now that we're sure that the calling class has some kind of global scope + // we check for the SoftDeletingScope existance + return static::hasGlobalScope(new SoftDeletingScope); + } + + /** + * "Makes room" for the the current node between its siblings. + * + * @return void + */ + public function shiftSiblingsForRestore() + { + if (!$this->hasSoftDeletes()) { + return; + } + + if (is_null($this->getRight()) || is_null($this->getLeft())) { + return; + } + + $this->getConnection()->transaction(function () { + $lftCol = $this->getLeftColumnName(); + $rgtCol = $this->getRightColumnName(); + $lft = $this->getLeft(); + $rgt = $this->getRight(); + + $diff = $rgt - $lft + 1; + + $this->newQuery()->where($lftCol, '>=', $lft)->increment($lftCol, $diff); + $this->newQuery()->where($rgtCol, '>=', $lft)->increment($rgtCol, $diff); + }); + } + + /** + * Restores all of the current node's descendants. + * + * @return void + */ + public function restoreDescendants() + { + if (!$this->hasSoftDeletes()) { + return; + } + + if (is_null($this->getRight()) || is_null($this->getLeft())) { + return; + } + + $this->getConnection()->transaction(function () { + $this->newQuery() + ->withTrashed() + ->where($this->getLeftColumnName(), '>', $this->getLeft()) + ->where($this->getRightColumnName(), '<', $this->getRight()) + ->update([ + $this->getDeletedAtColumn() => null, + $this->getUpdatedAtColumn() => $this->{$this->getUpdatedAtColumn()} + ]); + }); + } +} diff --git a/src/NestedSet/Mapper.php b/src/NestedSet/Mapper.php new file mode 100644 index 00000000..086917f2 --- /dev/null +++ b/src/NestedSet/Mapper.php @@ -0,0 +1,183 @@ +node = $node; + + $this->childrenKeyName = $childrenKeyName; + } + + /** + * Maps a tree structure into the database. Unguards & wraps in transaction. + * + * @param array|\Illuminate\Support\Contracts\ArrayableInterface + * @return boolean + */ + public function map($nodeList) + { + $self = $this; + + return $this->wrapInTransaction(function () use ($self, $nodeList) { + forward_static_call([get_class($self->node), 'unguard']); + + $result = $self->mapTree($nodeList); + + forward_static_call([get_class($self->node), 'reguard']); + + return $result; + }); + } + + /** + * Maps a tree structure into the database without unguarding nor wrapping + * inside a transaction. + * + * @param array|\Illuminate\Support\Contracts\ArrayableInterface + * @return boolean + */ + public function mapTree($nodeList) + { + $tree = $nodeList instanceof ArrayableInterface ? $nodeList->toArray() : $nodeList; + + $affectedKeys = []; + + $result = $this->mapTreeRecursive($tree, $this->node->getKey(), $affectedKeys); + + if ($result && count($affectedKeys) > 0) { + $this->deleteUnaffected($affectedKeys); + } + + return $result; + } + + /** + * Returns the children key name to use on the mapping array + * + * @return string + */ + public function getChildrenKeyName() + { + return $this->childrenKeyName; + } + + /** + * Maps a tree structure into the database + * + * @param array $tree + * @param mixed $parent + * @return boolean + */ + protected function mapTreeRecursive(array $tree, $parentKey = null, &$affectedKeys = []) + { + // For every attribute entry: We'll need to instantiate a new node either + // from the database (if the primary key was supplied) or a new instance. Then, + // append all the remaining data attributes (including the `parent_id` if + // present) and save it. Finally, tail-recurse performing the same + // operations for any child node present. Setting the `parent_id` property at + // each level will take care of the nesting work for us. + foreach ($tree as $attributes) { + $node = $this->firstOrNew($this->getSearchAttributes($attributes)); + + $data = $this->getDataAttributes($attributes); + if (!is_null($parentKey)) { + $data[$node->getParentColumnName()] = $parentKey; + } + + $node->fill($data); + + $result = $node->save(); + + if (! $result) { + return false; + } + + $affectedKeys[] = $node->getKey(); + + if (array_key_exists($this->getChildrenKeyName(), $attributes)) { + $children = $attributes[$this->getChildrenKeyName()]; + + if (count($children) > 0) { + $result = $this->mapTreeRecursive($children, $node->getKey(), $affectedKeys); + + if (! $result) { + return false; + } + } + } + } + + return true; + } + + protected function getSearchAttributes($attributes) + { + $searchable = [$this->node->getKeyName()]; + + return array_only($attributes, $searchable); + } + + protected function getDataAttributes($attributes) + { + $exceptions = [$this->node->getKeyName(), $this->getChildrenKeyName()]; + + return array_except($attributes, $exceptions); + } + + protected function firstOrNew($attributes) + { + $className = get_class($this->node); + + if (count($attributes) === 0) { + return new $className; + } + + return forward_static_call([$className, 'firstOrNew'], $attributes); + } + + protected function pruneScope() + { + if ($this->node->exists) { + return $this->node->descendants(); + } + + return $this->node->newQuery(); + } + + protected function deleteUnaffected($keys = []) + { + return $this->pruneScope()->whereNotIn($this->node->getKeyName(), $keys)->delete(); + } + + protected function wrapInTransaction(Closure $callback) + { + return $this->node->getConnection()->transaction($callback); + } +} diff --git a/src/NestedSet/Move.php b/src/NestedSet/Move.php new file mode 100644 index 00000000..0069a35f --- /dev/null +++ b/src/NestedSet/Move.php @@ -0,0 +1,428 @@ +node = $node; + $this->target = $this->resolveNode($target); + $this->position = $position; + + $this->setEventDispatcher($dispatcher ?? $node->getEventDispatcher()); + } + + /** + * Easy static accessor for performing a move operation. + * + * @param \Baum\Node $node + * @param \Baum\Node|int $target + * @param string $position + * @return \Baum\Node + */ + public static function to($node, $target, $position) + { + $instance = new static($node, $target, $position); + + return $instance->perform(); + } + + /** + * Perform the move operation. + * + * @return \Baum\Node + */ + public function perform() + { + $this->guardAgainstImpossibleMove(); + + if ($this->fireMoveEvent('moving') === false) { + return $this->node; + } + + if ($this->hasChange()) { + $this->node->getConnection()->transaction(function () { + $this->updateStructure(); + }); + + $this->target->refresh(); + + $this->node->setDepthWithSubtree(); + + $this->node->refresh(); + } + + $this->fireMoveEvent('moved', false); + + return $this->node; + } + + /** + * Runs the SQL query associated with the update of the indexes affected + * by the move operation. + * + * @return int + */ + public function updateStructure() + { + list($a, $b, $c, $d) = $this->boundaries(); + + // select the rows between the leftmost & the rightmost boundaries and apply a lock + $this->applyLockBetween($a, $d); + + $connection = $this->node->getConnection(); + $grammar = $connection->getQueryGrammar(); + + $currentId = $this->quoteIdentifier($this->node->getKey()); + $parentId = $this->quoteIdentifier($this->parentId()); + $leftColumn = $this->node->getLeftColumnName(); + $rightColumn = $this->node->getRightColumnName(); + $parentColumn = $this->node->getParentColumnName(); + $wrappedLeft = $grammar->wrap($leftColumn); + $wrappedRight = $grammar->wrap($rightColumn); + $wrappedParent = $grammar->wrap($parentColumn); + $wrappedId = $grammar->wrap($this->node->getKeyName()); + + $lftSql = "CASE + WHEN $wrappedLeft BETWEEN $a AND $b THEN $wrappedLeft + $d - $b + WHEN $wrappedLeft BETWEEN $c AND $d THEN $wrappedLeft + $a - $c + ELSE $wrappedLeft END"; + + $rgtSql = "CASE + WHEN $wrappedRight BETWEEN $a AND $b THEN $wrappedRight + $d - $b + WHEN $wrappedRight BETWEEN $c AND $d THEN $wrappedRight + $a - $c + ELSE $wrappedRight END"; + + $parentSql = "CASE + WHEN $wrappedId = $currentId THEN $parentId + ELSE $wrappedParent END"; + + $updateConditions = [ + $leftColumn => $connection->raw($lftSql), + $rightColumn => $connection->raw($rgtSql), + $parentColumn => $connection->raw($parentSql) + ]; + + if ($this->node->timestamps) { + $updateConditions[$this->node->getUpdatedAtColumn()] = $this->node->freshTimestamp(); + } + + return $this->node + ->newQuery() + ->where(function ($query) use ($leftColumn, $rightColumn, $a, $d) { + $query->whereBetween($leftColumn, [$a, $d])->orWhereBetween($rightColumn, [$a, $d]); + }) + ->update($updateConditions); + } + + /** + * Resolves suplied node. Basically returns the node unchanged if + * supplied parameter is an instance of \Baum\Node. Otherwise it will try + * to find the node in the database. + * + * @param \Baum\node|int + * @return \Baum\Node + */ + protected function resolveNode($node) + { + if ($node instanceof \Baum\Node) { + return $node->refresh(); + } + + return $this->node->newQuery()->find($node); + } + + /** + * Check wether the current move is possible and if not, rais an exception. + * + * @return void + */ + protected function guardAgainstImpossibleMove() + { + if (!$this->node->exists) { + throw new MoveNotPossibleException('A new node cannot be moved.'); + } + + if (array_search($this->position, ['child', 'left', 'right', 'root']) === false) { + throw new MoveNotPossibleException("Position should be one of ['child', 'left', 'right'] but is {$this->position}."); + } + + if (!$this->promotingToRoot()) { + if (is_null($this->target)) { + if ($this->position === 'left' || $this->position === 'right') { + throw new MoveNotPossibleException("Could not resolve target node. This node cannot move any further to the {$this->position}."); + } else { + throw new MoveNotPossibleException('Could not resolve target node.'); + } + } + + if ($this->node->equals($this->target)) { + throw new MoveNotPossibleException('A node cannot be moved to itself.'); + } + + if ($this->target->insideSubtree($this->node)) { + throw new MoveNotPossibleException('A node cannot be moved to a descendant of itself (inside moved tree).'); + } + + if (!$this->node->inSameScope($this->target)) { + throw new MoveNotPossibleException('A node cannot be moved to a different scope.'); + } + } + } + + /** + * Computes the boundary. + * + * @return int + */ + protected function bound1() + { + if (!is_null($this->_bound1)) { + return $this->_bound1; + } + + switch ($this->position) { + case 'child': + $this->_bound1 = $this->target->getRight(); + break; + + case 'left': + $this->_bound1 = $this->target->getLeft(); + break; + + case 'right': + $this->_bound1 = $this->target->getRight() + 1; + break; + + case 'root': + $this->_bound1 = $this->node->newQuery()->max($this->node->getRightColumnName()) + 1; + break; + } + + $this->_bound1 = (($this->_bound1 > $this->node->getRight()) ? $this->_bound1 - 1 : $this->_bound1); + + return $this->_bound1; + } + + /** + * Computes the other boundary. + * TODO: Maybe find a better name for this... ¿? + * + * @return int + */ + protected function bound2() + { + if (!is_null($this->_bound2)) { + return $this->_bound2; + } + + $this->_bound2 = (($this->bound1() > $this->node->getRight()) ? $this->node->getRight() + 1 : $this->node->getLeft() - 1); + return $this->_bound2; + } + + /** + * Computes the boundaries array. + * + * @return array + */ + protected function boundaries() + { + if (!is_null($this->_boundaries)) { + return $this->_boundaries; + } + + // we have defined the boundaries of two non-overlapping intervals, + // so sorting puts both the intervals and their boundaries in order + $this->_boundaries = [ + $this->node->getLeft(), + $this->node->getRight(), + $this->bound1(), + $this->bound2() + ]; + sort($this->_boundaries); + + return $this->_boundaries; + } + + /** + * Computes the new parent id for the node being moved. + * + * @return int + */ + protected function parentId() + { + switch ($this->position) { + case 'root': + return null; + + case 'child': + return $this->target->getKey(); + + default: + return $this->target->getParentKey(); + } + } + + /** + * Check wether there should be changes in the downward tree structure. + * + * @return boolean + */ + protected function hasChange() + { + return !($this->bound1() == $this->node->getRight() || $this->bound1() == $this->node->getLeft()); + } + + /** + * Check if we are promoting the provided instance to a root node. + * + * @return boolean + */ + protected function promotingToRoot() + { + return ($this->position == 'root'); + } + + /** + * Get the event dispatcher instance. + * + * @return \Illuminate\Events\Dispatcher + */ + public static function getEventDispatcher() + { + return static::$dispatcher; + } + + /** + * Set the event dispatcher instance. + * + * @param \Illuminate\Events\Dispatcher + * @return void + */ + public static function setEventDispatcher(Dispatcher $dispatcher) + { + static::$dispatcher = $dispatcher; + } + + /** + * Fire the given move event for the model. + * + * @param string $event + * @param bool $halt + * @return mixed + */ + protected function fireMoveEvent($event, $halt = true) + { + if (!isset(static::$dispatcher)) { + return true; + } + + // Basically the same as \Illuminate\Database\Eloquent\Model->fireModelEvent + // but we relay the event into the node instance. + $event = "eloquent.{$event}: ".get_class($this->node); + + $method = $halt ? 'until' : 'dispatch'; + + return static::$dispatcher->$method($event, $this->node); + } + + /** + * Quotes an identifier for being used in a database query. + * + * @param mixed $value + * @return string + */ + protected function quoteIdentifier($value) + { + if (is_null($value)) { + return 'NULL'; + } + + $connection = $this->node->getConnection(); + + $pdo = $connection->getPdo(); + + return $pdo->quote($value); + } + + /** + * Applies a lock to the rows between the supplied index boundaries. + * + * @param int $lft + * @param int $rgt + * @return void + */ + protected function applyLockBetween($lft, $rgt) + { + $this->node->newQuery() + ->where($this->node->getLeftColumnName(), '>=', $lft) + ->where($this->node->getRightColumnName(), '<=', $rgt) + ->select($this->node->getKeyName()) + ->lockForUpdate() + ->get(); + } +} diff --git a/src/NestedSet/MoveNotPossibleException.php b/src/NestedSet/MoveNotPossibleException.php new file mode 100644 index 00000000..0aa45632 --- /dev/null +++ b/src/NestedSet/MoveNotPossibleException.php @@ -0,0 +1,10 @@ +newQuery() + ->withoutGlobalScope(Scopes\OrderingScope::class) + ->withoutGlobalScope(Scopes\ScopedByScope::class); + } + + /** + * Indicates whether we should move to a new parent. + * + * @var int + */ + protected static $moveToNewParentId = null; + + /** + * Equals? + * + * @param \Baum\NestedSet\Node + * @return boolean + */ + public function equals($node) + { + return ($this == $node); + } + + /** + * Returns the first root node. + * + * @return \Baum\NestedSet\Node + */ + public static function root() + { + return static::roots()->first(); + } + + /** + * Static query scope. Returns a query scope with all root nodes. + * + * @return \Illuminate\Database\Query\Builder + */ + public static function roots() + { + $instance = new static; + + return $instance->newQuery()->whereNull($instance->getParentColumnName()); + } + + /** + * Returns the level of this node in the tree. + * Root level is 0. + * + * @return int + */ + public function getLevel() + { + if (is_null($this->getParentKey())) { + return 0; + } + + return $this->computeLevel(); + } + + /** + * Compute current node level. If could not move past ourseleves return + * our ancestor count, otherwhise get the first parent level + the computed + * nesting. + * + * @return integer + */ + protected function computeLevel() + { + list($node, $nesting) = $this->determineDepth($this); + + if ($node->equals($this)) { + return $this->ancestors()->count(); + } + + return $node->getLevel() + $nesting; + } + + /** + * Return an array with the last node we could reach and its nesting level + * + * @param Baum\Node $node + * @param integer $nesting + * @return array + */ + protected function determineDepth($node, $nesting = 0) + { + // Traverse back up the ancestry chain and add to the nesting level count + while ($parent = $node->parent()->first()) { + $nesting = $nesting + 1; + + $node = $parent; + } + + return [$node, $nesting]; + } + + /** + * Sets default values for left and right fields. + * + * @return void + */ + public function setDefaultLeftAndRight() + { + $withHighestRight = $this->newQuery() + ->scopedBy() + ->reOrderBy($this->getRightColumnName(), 'desc') + ->take(1) + ->sharedLock() + ->first(); + + $maxRgt = 0; + if (!is_null($withHighestRight)) { + $maxRgt = $withHighestRight->getRight(); + } + + $this->setAttribute($this->getLeftColumnName(), $maxRgt + 1); + $this->setAttribute($this->getRightColumnName(), $maxRgt + 2); + } + + /** + * Store the parent_id if the attribute is modified so as we are able to move + * the node to this new parent after saving. + * + * @return void + */ + public function storeNewParent() + { + if ($this->isDirty($this->getParentColumnName()) && ($this->exists || !$this->isRoot())) { + static::$moveToNewParentId = $this->getParentKey(); + } else { + static::$moveToNewParentId = false; + } + } + + /** + * Move to the new parent if appropiate. + * + * @return void + */ + public function moveToNewParent() + { + $pid = static::$moveToNewParentId; + + if (is_null($pid)) { + $this->makeRoot(); + } elseif ($pid !== false) { + $this->makeChildOf($pid); + } + } + + /** + * Sets the depth attribute + * + * @return \Baum\Node + */ + public function setDepth() + { + $this->getConnection()->transaction(function () { + $this->refresh(); + + $level = $this->getLevel(); + + $this->newQuery()->where($this->getKeyName(), '=', $this->getKey())->update([$this->getDepthColumnName() => $level]); + $this->setAttribute($this->getDepthColumnName(), $level); + }); + + return $this; + } + + /** + * Prunes a branch off the tree, shifting all the elements on the right + * back to the left so the counts work. + * + * @return void; + */ + public function destroyDescendants() + { + if (is_null($this->getRight()) || is_null($this->getLeft())) { + return; + } + + $this->getConnection()->transaction(function () { + $this->refresh(); + + $lftCol = $this->getLeftColumnName(); + $rgtCol = $this->getRightColumnName(); + $lft = $this->getLeft(); + $rgt = $this->getRight(); + + // Apply a lock to the rows which fall past the deletion point + $this->newQuery()->where($lftCol, '>=', $lft)->select($this->getKeyName())->lockForUpdate()->get(); + + // Prune children + $this->newQuery()->where($lftCol, '>', $lft)->where($rgtCol, '<', $rgt)->delete(); + + // Update left and right indexes for the remaining nodes + $diff = $rgt - $lft + 1; + + $this->newQuery()->where($lftCol, '>', $rgt)->decrement($lftCol, $diff); + $this->newQuery()->where($rgtCol, '>', $rgt)->decrement($rgtCol, $diff); + }); + } + + /** + * Static query scope. Returns a query scope with all nodes which are at + * the end of a branch. + * + * @return \Illuminate\Database\Query\Builder + */ + public static function allLeaves() + { + $instance = new static; + + $grammar = $instance->getConnection()->getQueryGrammar(); + + $rgtCol = $grammar->wrap($instance->getQualifiedRightColumnName()); + $lftCol = $grammar->wrap($instance->getQualifiedLeftColumnName()); + + return $instance->newQuery() + ->whereRaw($rgtCol . ' - ' . $lftCol . ' = 1') + ->orderBy($instance->getQualifiedOrderColumnName()); + } + + /** + * Static query scope. Returns a query scope with all nodes which are at + * the middle of a branch (not root and not leaves). + * + * @return \Illuminate\Database\Query\Builder + */ + public static function allTrunks() + { + $instance = new static; + + $grammar = $instance->getConnection()->getQueryGrammar(); + + $rgtCol = $grammar->wrap($instance->getQualifiedRightColumnName()); + $lftCol = $grammar->wrap($instance->getQualifiedLeftColumnName()); + + return $instance->newQuery() + ->whereNotNull($instance->getParentColumnName()) + ->whereRaw($rgtCol . ' - ' . $lftCol . ' != 1') + ->orderBy($instance->getQualifiedOrderColumnName()); + } + + /** + * Query scope which extracts a certain node object from the current query + * expression. + * + * @return \Illuminate\Database\Query\Builder + */ + public function scopeWithoutNode($query, $node) + { + return $query->where($node->getKeyName(), '!=', $node->getKey()); + } + + /** + * Extracts current node (self) from current query expression. + * + * @return \Illuminate\Database\Query\Builder + */ + public function scopeWithoutSelf($query) + { + return $this->scopeWithoutNode($query, $this); + } + + /** + * Extracts first root (from the current node p-o-v) from current query + * expression. + * + * @return \Illuminate\Database\Query\Builder + */ + public function scopeWithoutRoot($query) + { + return $this->scopeWithoutNode($query, $this->getRoot()); + } + + /** + * Provides a depth level limit for the query. + * + * @param query \Illuminate\Database\Query\Builder + * @param limit integer + * @return \Illuminate\Database\Query\Builder + */ + public function scopeLimitDepth($query, $limit) + { + $depth = $this->exists ? $this->getDepth() : $this->getLevel(); + $max = $depth + $limit; + $scopes = [$depth, $max]; + + return $query->whereBetween($this->getDepthColumnName(), [min($scopes), max($scopes)]); + } + + /** + * Returns true if this is a trunk node (not root or leaf). + * + * @return boolean + */ + public function isTrunk() + { + return !$this->isRoot() && !$this->isLeaf(); + } + + /** + * Returns the root node starting at the current node. + * + * @return NestedSet + */ + public function getRoot() + { + if ($this->exists) { + return $this->ancestorsAndSelf()->whereNull($this->getParentColumnName())->first(); + } else { + $parentId = $this->getParentKey(); + + if (!is_null($parentId) && $currentParent = static::find($parentId)) { + return $currentParent->getRoot(); + } else { + return $this; + } + } + } + + /** + * Instance scope which targes all the ancestor chain nodes including + * the current one. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function ancestorsAndSelf() + { + return $this->newQuery() + ->where($this->getLeftColumnName(), '<=', $this->getLeft()) + ->where($this->getRightColumnName(), '>=', $this->getRight()); + } + + /** + * Get all the ancestor chain from the database including the current node. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getAncestorsAndSelf($columns = ['*']) + { + return $this->ancestorsAndSelf()->get($columns); + } + + /** + * Get all the ancestor chain from the database including the current node + * but without the root node. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getAncestorsAndSelfWithoutRoot($columns = ['*']) + { + return $this->ancestorsAndSelf()->withoutRoot()->get($columns); + } + + /** + * Instance scope which targets all the ancestor chain nodes excluding + * the current one. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function ancestors() + { + return $this->ancestorsAndSelf()->withoutSelf(); + } + + /** + * Get all the ancestor chain from the database excluding the current node. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getAncestors($columns = ['*']) + { + return $this->ancestors()->get($columns); + } + + /** + * Get all the ancestor chain from the database excluding the current node + * and the root node (from the current node's perspective). + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getAncestorsWithoutRoot($columns = ['*']) + { + return $this->ancestors()->withoutRoot()->get($columns); + } + + /** + * Instance scope which targets all children of the parent, including self. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function siblingsAndSelf() + { + return $this->newQuery() + ->where($this->getParentColumnName(), $this->getParentKey()); + } + + /** + * Get all children of the parent, including self. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getSiblingsAndSelf($columns = ['*']) + { + return $this->siblingsAndSelf()->get($columns); + } + + /** + * Instance scope targeting all children of the parent, except self. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function siblings() + { + return $this->siblingsAndSelf()->withoutSelf(); + } + + /** + * Return all children of the parent, except self. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getSiblings($columns = ['*']) + { + return $this->siblings()->get($columns); + } + + /** + * Instance scope targeting all of its nested children which do not have + * children. + * + * @return \Illuminate\Database\Query\Builder + */ + public function leaves() + { + $grammar = $this->getConnection()->getQueryGrammar(); + + $rgtCol = $grammar->wrap($this->getQualifiedRightColumnName()); + $lftCol = $grammar->wrap($this->getQualifiedLeftColumnName()); + + return $this->descendants() + ->whereRaw($rgtCol . ' - ' . $lftCol . ' = 1'); + } + + /** + * Return all of its nested children which do not have children. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getLeaves($columns = ['*']) + { + return $this->leaves()->get($columns); + } + + /** + * Instance scope targeting all of its nested children which are between the + * root and the leaf nodes (middle branch). + * + * @return \Illuminate\Database\Query\Builder + */ + public function trunks() + { + $grammar = $this->getConnection()->getQueryGrammar(); + + $rgtCol = $grammar->wrap($this->getQualifiedRightColumnName()); + $lftCol = $grammar->wrap($this->getQualifiedLeftColumnName()); + + return $this->descendants() + ->whereNotNull($this->getQualifiedParentColumnName()) + ->whereRaw($rgtCol . ' - ' . $lftCol . ' != 1'); + } + + /** + * Return all of its nested children which are trunks. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getTrunks($columns = ['*']) + { + return $this->trunks()->get($columns); + } + + /** + * Scope targeting itself and all of its nested children. + * + * @return \Illuminate\Database\Query\Builder + */ + public function descendantsAndSelf() + { + return $this->newQuery() + ->where($this->getLeftColumnName(), '>=', $this->getLeft()) + ->where($this->getLeftColumnName(), '<', $this->getRight()); + } + + /** + * Retrieve all nested children an self. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getDescendantsAndSelf($columns = ['*']) + { + if (is_array($columns)) { + return $this->descendantsAndSelf()->get($columns); + } + + $arguments = func_get_args(); + + $limit = intval(array_shift($arguments)); + $columns = array_shift($arguments) ?: ['*']; + + return $this->descendantsAndSelf()->limitDepth($limit)->get($columns); + } + + /** + * Set of all children & nested children. + * + * @return \Illuminate\Database\Query\Builder + */ + public function descendants() + { + return $this->descendantsAndSelf()->withoutSelf(); + } + + /** + * Retrieve all of its children & nested children. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getDescendants($columns = ['*']) + { + if (is_array($columns)) { + return $this->descendants()->get($columns); + } + + $arguments = func_get_args(); + + $limit = intval(array_shift($arguments)); + $columns = array_shift($arguments) ?: ['*']; + + return $this->descendants()->limitDepth($limit)->get($columns); + } + + /** + * Returns the first sibling to the left. + * + * @return NestedSet + */ + public function getLeftSibling() + { + return $this->siblings() + ->where($this->getLeftColumnName(), '<', $this->getLeft()) + ->orderBy($this->getOrderColumnName(), 'desc') + ->get() + ->first(); + // ->last(); + } + + /** + * Returns the first sibling to the right. + * + * @return NestedSet + */ + public function getRightSibling() + { + return $this->siblings() + ->where($this->getLeftColumnName(), '>', $this->getLeft()) + ->first(); + } + + /** + * Sets the depth attribute for the current node and all of its descendants. + * + * @return \Baum\Node + */ + public function setDepthWithSubtree() + { + $this->getConnection()->transaction(function () { + $this->refresh(); + + $this->descendantsAndSelf()->select($this->getKeyName())->lockForUpdate()->get(); + + $oldDepth = !is_null($this->getDepth()) ? $this->getDepth() : 0; + + $newDepth = $this->getLevel(); + + $this->newQuery()->where($this->getKeyName(), '=', $this->getKey())->update([$this->getDepthColumnName() => $newDepth]); + $this->setAttribute($this->getDepthColumnName(), $newDepth); + + $diff = $newDepth - $oldDepth; + if (!$this->isLeaf() && $diff != 0) { + $this->descendants()->increment($this->getDepthColumnName(), $diff); + } + }); + + return $this; + } + + /** + * Return an key-value array indicating the node's depth with $seperator + * + * @return Array + */ + public static function getNestedList($column, $key = null, $seperator = ' ') + { + $instance = new static; + + $key = $key ?: $instance->getKeyName(); + $depthColumn = $instance->getDepthColumnName(); + + $nodes = $instance->newQuery()->get()->toArray(); + + return array_combine(array_map(function ($node) use ($key) { + return $node[$key]; + }, $nodes), array_map(function ($node) use ($seperator, $depthColumn, $column) { + return str_repeat($seperator, $node[$depthColumn]) . $node[$column]; + }, $nodes)); + } +} diff --git a/src/NestedSet/NodeObserver.php b/src/NestedSet/NodeObserver.php new file mode 100644 index 00000000..46ae9771 --- /dev/null +++ b/src/NestedSet/NodeObserver.php @@ -0,0 +1,78 @@ +setDefaultLeftAndRight(); + } + + /** + * Executed *before* saving a nested set node instance. It checks if we + * have to perform a move operation to another parent. + * + * @param mixed $node + * @return void + */ + public function saving($node) + { + $node->storeNewParent(); + } + + /** + * Executed *after* saving a nested set node instance. It will move to + * a new parent (if needed) and recomputes the depth attribute. + * + * @param mixed $node + * @return void + */ + public function saved($node) + { + $node->moveToNewParent(); + $node->setDepth(); + } + + /** + * Executed *before* deleting a nested set node instance. Will delete + * all of the node's descendants. + * + * @param mixed $node + * @return void + */ + public function deleting($node) + { + $node->destroyDescendants(); + } + + /** + * Executed *before* a soft-delete restore operation. Shifts its siblings. + * + * @param mixed $node + * @return void + */ + public function restoring($node) + { + $node->shiftSiblingsForRestore(); + } + + /** + * *After* having restored a soft-deleted nested set node instance, restore + * all of its descendants. + * + * @param mixed $node + * @return void + */ + public function restored($node) + { + $node->restoreDescendants(); + } +} diff --git a/src/NestedSet/Scopes/OrderingScope.php b/src/NestedSet/Scopes/OrderingScope.php new file mode 100644 index 00000000..bafb0bd0 --- /dev/null +++ b/src/NestedSet/Scopes/OrderingScope.php @@ -0,0 +1,56 @@ +orderBy($model->getQualifiedOrderColumnName()); + } + + /** + * Extend the query builder with the needed functions. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return void + */ + public function extend(Builder $builder) + { + $this->addReOrderBy($builder); + } + + /** + * Add a 'reOrderBy' macro to easily reset or restart an order by sequence. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return void + */ + public function addReOrderBy(Builder $builder) + { + $builder->macro('reOrderBy', function ($builder, $column = null, $direction = 'asc') { + $query = $builder->getQuery(); + + $query->{$query->unions ? 'unionOrders' : 'orders'} = null; + + if (!is_null($column)) { + $query->orderBy($column, $direction); + } + + $builder->setQuery($query); + + return $builder->withoutGlobalScope($this); + }); + } +} diff --git a/src/NestedSet/Scopes/ScopedByScope.php b/src/NestedSet/Scopes/ScopedByScope.php new file mode 100644 index 00000000..52df942a --- /dev/null +++ b/src/NestedSet/Scopes/ScopedByScope.php @@ -0,0 +1,74 @@ +isScoped() && $model->exists) { + foreach ($model->getScopedColumnNames() as $fld) { + $builder->where($model->qualifyColumn($fld), '=', $model->getAttribute($fld)); + } + } + } + + /** + * Extend the query builder with the needed functions. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return void + */ + public function extend(Builder $builder) + { + $this->addUnscoped($builder); + + $this->addScopedBy($builder); + } + + /** + * 'unscoped' macro. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return void + */ + public function addUnscoped(Builder $builder) + { + $builder->macro('unscoped', function (Builder $builder) { + return $builder->withoutGlobalScope($this); + }); + } + + /** + * 'scopedBy' macro. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return void + */ + public function addScopedBy(Builder $builder) + { + $builder->macro('scopedBy', function (Builder $builder, $scopedBy = []) { + $model = $builder->getModel(); + + $scopeColumns = array_merge($scopedBy, $model->isScoped() ? $model->getScopedColumnNames() : []); + + return array_reduce($scopeColumns, function ($builder, $column) use ($model) { + return $builder->where( + $model->qualifyColumn($column), + $model->getAttribute($column) + ); + }, $builder->unscoped()); + }); + } +} diff --git a/src/NestedSet/Validator.php b/src/NestedSet/Validator.php new file mode 100644 index 00000000..93629e42 --- /dev/null +++ b/src/NestedSet/Validator.php @@ -0,0 +1,243 @@ +node = $node; + } + + /** + * Determine if the validation passes. + * + * @return boolean + */ + public function passes() + { + return $this->validateBounds() && $this->validateDuplicates() && $this->validateRoots(); + } + + /** + * Determine if validation fails. + * + * @return boolean + */ + public function fails() + { + return !$this->passes(); + } + + /** + * Validates bounds of the nested tree structure. It will perform checks on + * the `lft`, `rgt` and `parent_id` columns. Mainly that they're not null, + * rights greater than lefts, and that they're within the bounds of the parent. + * + * @return boolean + */ + protected function validateBounds() + { + $connection = $this->node->getConnection(); + $grammar = $connection->getQueryGrammar(); + + $tableName = $this->node->getTable(); + $primaryKeyName = $this->node->getKeyName(); + $parentColumn = $this->node->getQualifiedParentColumnName(); + + $lftCol = $grammar->wrap($this->node->getLeftColumnName()); + $rgtCol = $grammar->wrap($this->node->getRightColumnName()); + + $qualifiedLftCol = $grammar->wrap($this->node->getQualifiedLeftColumnName()); + $qualifiedRgtCol = $grammar->wrap($this->node->getQualifiedRightColumnName()); + $qualifiedParentCol = $grammar->wrap($this->node->getQualifiedParentColumnName()); + + $whereStm = "($qualifiedLftCol IS NULL OR + $qualifiedRgtCol IS NULL OR + $qualifiedLftCol >= $qualifiedRgtCol OR + ($qualifiedParentCol IS NOT NULL AND + ($qualifiedLftCol <= parent.$lftCol OR + $qualifiedRgtCol >= parent.$rgtCol)))"; + + $query = $this->node->newQuery() + ->join( + $connection->raw($grammar->wrapTable($tableName).' AS parent'), + $parentColumn, + '=', + $connection->raw('parent.'.$grammar->wrap($primaryKeyName)), + 'left outer' + ) + ->whereRaw($whereStm); + + return ($query->count() == 0); + } + + /** + * Checks that there are no duplicates for the `lft` and `rgt` columns. + * + * @return boolean + */ + protected function validateDuplicates() + { + return ( + !$this->duplicatesExistForColumn($this->node->getQualifiedLeftColumnName()) && + !$this->duplicatesExistForColumn($this->node->getQualifiedRightColumnName()) + ); + } + + /** + * For each root of the whole nested set tree structure, checks that their + * `lft` and `rgt` bounds are properly set. + * + * @return boolean + */ + protected function validateRoots() + { + $roots = forward_static_call([get_class($this->node), 'roots'])->get(); + + // If a scope is defined in the model we should check that the roots are + // valid *for each* value in the scope columns. + if ($this->node->isScoped()) { + return $this->validateRootsByScope($roots); + } + + return $this->isEachRootValid($roots); + } + + /** + * Checks if duplicate values for the column specified exist. Takes + * the Nested Set scope columns into account (if appropiate). + * + * @param string $column + * @return boolean + */ + protected function duplicatesExistForColumn($column) + { + $connection = $this->node->getConnection(); + $grammar = $connection->getQueryGrammar(); + + $columns = array_merge($this->node->isScoped() ? $this->node->getQualifiedScopedColumnNames() : [], [$column]); + + $columnsForSelect = implode(', ', array_map(function ($col) use ($grammar) { + return $grammar->wrap($col); + }, $columns)); + + $wrappedColumn = $grammar->wrap($column); + + $query = $this->node->newQueryWithoutNestedSetScopes() + ->select($connection->raw("$columnsForSelect, COUNT($wrappedColumn)")) + ->havingRaw("COUNT($wrappedColumn) > 1"); + + foreach ($columns as $col) { + $query->groupBy($col); + } + + $result = $query->first(); + + return !is_null($result); + } + + /** + * Check that each root node in the list supplied satisfies that its bounds + * values (lft, rgt indexes) are less than the next. + * + * @param mixed $roots + * @return boolean + */ + protected function isEachRootValid($roots) + { + $left = $right = 0; + + foreach ($roots as $root) { + $rootLeft = $root->getLeft(); + $rootRight = $root->getRight(); + + if (!($rootLeft > $left && $rootRight > $right)) { + return false; + } + + $left = $rootLeft; + $right = $rootRight; + } + + return true; + } + + /** + * Check that each root node in the list supplied satisfies that its bounds + * values (lft, rgt indexes) are less than the next *within each scope*. + * + * @param mixed $roots + * @return boolean + */ + protected function validateRootsByScope($roots) + { + foreach ($this->groupRootsByScope($roots) as $scope => $groupedRoots) { + $valid = $this->isEachRootValid($groupedRoots); + + if (!$valid) { + return false; + } + } + + return true; + } + + /** + * Given a list of root nodes, it returns an array in which the keys are the + * array of the actual scope column values and the values are the root nodes + * inside that scope themselves + * + * @param mixed $roots + * @return array + */ + protected function groupRootsByScope($roots) + { + $rootsGroupedByScope = []; + + foreach ($roots as $root) { + $key = $this->keyForScope($root); + + if (!isset($rootsGroupedByScope[$key])) { + $rootsGroupedByScope[$key] = []; + } + + $rootsGroupedByScope[$key][] = $root; + } + + return $rootsGroupedByScope; + } + + /** + * Builds a single string for the given scope columns values. Useful for + * making array keys for grouping. + * + * @param Baum\Node $node + * @return string + */ + protected function keyForScope($node) + { + return implode('-', array_map(function ($column) use ($node) { + $value = $node->getAttribute($column); + + if (is_null($value)) { + return 'NULL'; + } + + return $value; + }, $node->getScopedColumnNames())); + } +} diff --git a/src/Node.php b/src/Node.php index 8dcab130..002d1aac 100644 --- a/src/Node.php +++ b/src/Node.php @@ -1,8 +1,10 @@ setDefaultLeftAndRight(); - }); - - static::saving(function($node) { - $node->storeNewParent(); - }); - - static::saved(function($node) { - $node->moveToNewParent(); - $node->setDepth(); - }); - - static::deleting(function($node) { - $node->destroyDescendants(); - }); - - if ( static::softDeletesEnabled() ) { - static::restoring(function($node) { - $node->shiftSiblingsForRestore(); - }); - - static::restored(function($node) { - $node->restoreDescendants(); - }); - } - } - - /** - * Get the parent column name. - * - * @return string - */ - public function getParentColumnName() { - return $this->parentColumn; - } - - /** - * Get the table qualified parent column name. - * - * @return string - */ - public function getQualifiedParentColumnName() { - return $this->getTable(). '.' .$this->getParentColumnName(); - } - - /** - * Get the value of the models "parent_id" field. - * - * @return int - */ - public function getParentId() { - return $this->getAttribute($this->getparentColumnName()); - } - - /** - * Get the "left" field column name. - * - * @return string - */ - public function getLeftColumnName() { - return $this->leftColumn; - } - - /** - * Get the table qualified "left" field column name. - * - * @return string - */ - public function getQualifiedLeftColumnName() { - return $this->getTable() . '.' . $this->getLeftColumnName(); - } - - /** - * Get the value of the model's "left" field. - * - * @return int - */ - public function getLeft() { - return $this->getAttribute($this->getLeftColumnName()); - } - - /** - * Get the "right" field column name. - * - * @return string - */ - public function getRightColumnName() { - return $this->rightColumn; - } - - /** - * Get the table qualified "right" field column name. - * - * @return string - */ - public function getQualifiedRightColumnName() { - return $this->getTable() . '.' . $this->getRightColumnName(); - } - - /** - * Get the value of the model's "right" field. - * - * @return int - */ - public function getRight() { - return $this->getAttribute($this->getRightColumnName()); - } - - /** - * Get the "depth" field column name. - * - * @return string - */ - public function getDepthColumnName() { - return $this->depthColumn; - } - - /** - * Get the table qualified "depth" field column name. - * - * @return string - */ - public function getQualifiedDepthColumnName() { - return $this->getTable() . '.' . $this->getDepthColumnName(); - } - - /** - * Get the model's "depth" value. - * - * @return int - */ - public function getDepth() { - return $this->getAttribute($this->getDepthColumnName()); - } - - /** - * Get the "order" field column name. - * - * @return string - */ - public function getOrderColumnName() { - return is_null($this->orderColumn) ? $this->getLeftColumnName() : $this->orderColumn; - } - - /** - * Get the table qualified "order" field column name. - * - * @return string - */ - public function getQualifiedOrderColumnName() { - return $this->getTable() . '.' . $this->getOrderColumnName(); - } - - /** - * Get the model's "order" value. - * - * @return mixed - */ - public function getOrder() { - return $this->getAttribute($this->getOrderColumnName()); - } - - /** - * Get the column names which define our scope - * - * @return array - */ - public function getScopedColumns() { - return (array) $this->scoped; - } - - /** - * Get the qualified column names which define our scope - * - * @return array - */ - public function getQualifiedScopedColumns() { - if ( !$this->isScoped() ) - return $this->getScopedColumns(); - - $prefix = $this->getTable() . '.'; - - return array_map(function($c) use ($prefix) { - return $prefix . $c; }, $this->getScopedColumns()); - } - - /** - * Returns wether this particular node instance is scoped by certain fields - * or not. - * - * @return boolean - */ - public function isScoped() { - return !!(count($this->getScopedColumns()) > 0); - } - - /** - * Parent relation (self-referential) 1-1. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function parent() { - return $this->belongsTo(get_class($this), $this->getParentColumnName()); - } - - /** - * Children relation (self-referential) 1-N. - * - * @return \Illuminate\Database\Eloquent\Relations\HasMany - */ - public function children() { - return $this->hasMany(get_class($this), $this->getParentColumnName()) - ->orderBy($this->getOrderColumnName()); - } - - /** - * Get a new "scoped" query builder for the Node's model. - * - * @param bool $excludeDeleted - * @return \Illuminate\Database\Eloquent\Builder|static - */ - public function newNestedSetQuery($excludeDeleted = true) { - $builder = $this->newQuery($excludeDeleted)->orderBy($this->getQualifiedOrderColumnName()); - - if ( $this->isScoped() ) { - foreach($this->scoped as $scopeFld) - $builder->where($scopeFld, '=', $this->$scopeFld); - } - - return $builder; - } - - /** - * Overload new Collection - * - * @param array $models - * @return \Baum\Extensions\Eloquent\Collection - */ - public function newCollection(array $models = array()) { - return new Collection($models); - } - - /** - * Get all of the nodes from the database. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection|static[] - */ - public static function all($columns = array('*')) { - $instance = new static; - - return $instance->newQuery() - ->orderBy($instance->getQualifiedOrderColumnName()) - ->get(); - } - - /** - * Returns the first root node. - * - * @return NestedSet - */ - public static function root() { - return static::roots()->first(); - } - - /** - * Static query scope. Returns a query scope with all root nodes. - * - * @return \Illuminate\Database\Query\Builder - */ - public static function roots() { - $instance = new static; - - return $instance->newQuery() - ->whereNull($instance->getParentColumnName()) - ->orderBy($instance->getQualifiedOrderColumnName()); - } - - /** - * Static query scope. Returns a query scope with all nodes which are at - * the end of a branch. - * - * @return \Illuminate\Database\Query\Builder - */ - public static function allLeaves() { - $instance = new static; - - $grammar = $instance->getConnection()->getQueryGrammar(); - - $rgtCol = $grammar->wrap($instance->getQualifiedRightColumnName()); - $lftCol = $grammar->wrap($instance->getQualifiedLeftColumnName()); - - return $instance->newQuery() - ->whereRaw($rgtCol . ' - ' . $lftCol . ' = 1') - ->orderBy($instance->getQualifiedOrderColumnName()); - } - - /** - * Static query scope. Returns a query scope with all nodes which are at - * the middle of a branch (not root and not leaves). - * - * @return \Illuminate\Database\Query\Builder - */ - public static function allTrunks() { - $instance = new static; - - $grammar = $instance->getConnection()->getQueryGrammar(); - - $rgtCol = $grammar->wrap($instance->getQualifiedRightColumnName()); - $lftCol = $grammar->wrap($instance->getQualifiedLeftColumnName()); - - return $instance->newQuery() - ->whereNotNull($instance->getParentColumnName()) - ->whereRaw($rgtCol . ' - ' . $lftCol . ' != 1') - ->orderBy($instance->getQualifiedOrderColumnName()); - } - - /** - * Checks wether the underlying Nested Set structure is valid. - * - * @return boolean - */ - public static function isValidNestedSet() { - $validator = new SetValidator(new static); - - return $validator->passes(); - } - - /** - * Rebuilds the structure of the current Nested Set. - * - * @param bool $force - * @return void - */ - public static function rebuild($force = false) { - $builder = new SetBuilder(new static); - - $builder->rebuild($force); - } - - /** - * Maps the provided tree structure into the database. - * - * @param array|\Illuminate\Support\Contracts\ArrayableInterface - * @return boolean - */ - public static function buildTree($nodeList) { - return with(new static)->makeTree($nodeList); - } - - /** - * Query scope which extracts a certain node object from the current query - * expression. - * - * @return \Illuminate\Database\Query\Builder - */ - public function scopeWithoutNode($query, $node) { - return $query->where($node->getKeyName(), '!=', $node->getKey()); - } - - /** - * Extracts current node (self) from current query expression. - * - * @return \Illuminate\Database\Query\Builder - */ - public function scopeWithoutSelf($query) { - return $this->scopeWithoutNode($query, $this); - } - - /** - * Extracts first root (from the current node p-o-v) from current query - * expression. - * - * @return \Illuminate\Database\Query\Builder - */ - public function scopeWithoutRoot($query) { - return $this->scopeWithoutNode($query, $this->getRoot()); - } - - /** - * Provides a depth level limit for the query. - * - * @param query \Illuminate\Database\Query\Builder - * @param limit integer - * @return \Illuminate\Database\Query\Builder - */ - public function scopeLimitDepth($query, $limit) { - $depth = $this->exists ? $this->getDepth() : $this->getLevel(); - $max = $depth + $limit; - $scopes = array($depth, $max); - - return $query->whereBetween($this->getDepthColumnName(), array(min($scopes), max($scopes))); - } - - /** - * Returns true if this is a root node. - * - * @return boolean - */ - public function isRoot() { - return is_null($this->getParentId()); - } - - /** - * Returns true if this is a leaf node (end of a branch). - * - * @return boolean - */ - public function isLeaf() { - return $this->exists && ($this->getRight() - $this->getLeft() == 1); - } - - /** - * Returns true if this is a trunk node (not root or leaf). - * - * @return boolean - */ - public function isTrunk() { - return !$this->isRoot() && !$this->isLeaf(); - } - - /** - * Returns true if this is a child node. - * - * @return boolean - */ - public function isChild() { - return !$this->isRoot(); - } - - /** - * Returns the root node starting at the current node. - * - * @return NestedSet - */ - public function getRoot() { - if ( $this->exists ) { - return $this->ancestorsAndSelf()->whereNull($this->getParentColumnName())->first(); - } else { - $parentId = $this->getParentId(); - - if ( !is_null($parentId) && $currentParent = static::find($parentId) ) { - return $currentParent->getRoot(); - } else { - return $this; - } - } - } - - /** - * Instance scope which targes all the ancestor chain nodes including - * the current one. - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function ancestorsAndSelf() { - return $this->newNestedSetQuery() - ->where($this->getLeftColumnName(), '<=', $this->getLeft()) - ->where($this->getRightColumnName(), '>=', $this->getRight()); - } - - /** - * Get all the ancestor chain from the database including the current node. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getAncestorsAndSelf($columns = array('*')) { - return $this->ancestorsAndSelf()->get($columns); - } - - /** - * Get all the ancestor chain from the database including the current node - * but without the root node. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getAncestorsAndSelfWithoutRoot($columns = array('*')) { - return $this->ancestorsAndSelf()->withoutRoot()->get($columns); - } - - /** - * Instance scope which targets all the ancestor chain nodes excluding - * the current one. - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function ancestors() { - return $this->ancestorsAndSelf()->withoutSelf(); - } - - /** - * Get all the ancestor chain from the database excluding the current node. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getAncestors($columns = array('*')) { - return $this->ancestors()->get($columns); - } - - /** - * Get all the ancestor chain from the database excluding the current node - * and the root node (from the current node's perspective). - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getAncestorsWithoutRoot($columns = array('*')) { - return $this->ancestors()->withoutRoot()->get($columns); - } - - /** - * Instance scope which targets all children of the parent, including self. - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function siblingsAndSelf() { - return $this->newNestedSetQuery() - ->where($this->getParentColumnName(), $this->getParentId()); - } - - /** - * Get all children of the parent, including self. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getSiblingsAndSelf($columns = array('*')) { - return $this->siblingsAndSelf()->get($columns); - } - - /** - * Instance scope targeting all children of the parent, except self. - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function siblings() { - return $this->siblingsAndSelf()->withoutSelf(); - } - - /** - * Return all children of the parent, except self. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getSiblings($columns = array('*')) { - return $this->siblings()->get($columns); - } - - /** - * Instance scope targeting all of its nested children which do not have - * children. - * - * @return \Illuminate\Database\Query\Builder - */ - public function leaves() { - $grammar = $this->getConnection()->getQueryGrammar(); - - $rgtCol = $grammar->wrap($this->getQualifiedRightColumnName()); - $lftCol = $grammar->wrap($this->getQualifiedLeftColumnName()); - - return $this->descendants() - ->whereRaw($rgtCol . ' - ' . $lftCol . ' = 1'); - } - - /** - * Return all of its nested children which do not have children. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getLeaves($columns = array('*')) { - return $this->leaves()->get($columns); - } - - /** - * Instance scope targeting all of its nested children which are between the - * root and the leaf nodes (middle branch). - * - * @return \Illuminate\Database\Query\Builder - */ - public function trunks() { - $grammar = $this->getConnection()->getQueryGrammar(); - - $rgtCol = $grammar->wrap($this->getQualifiedRightColumnName()); - $lftCol = $grammar->wrap($this->getQualifiedLeftColumnName()); - - return $this->descendants() - ->whereNotNull($this->getQualifiedParentColumnName()) - ->whereRaw($rgtCol . ' - ' . $lftCol . ' != 1'); - } - - /** - * Return all of its nested children which are trunks. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getTrunks($columns = array('*')) { - return $this->trunks()->get($columns); - } - - /** - * Scope targeting itself and all of its nested children. - * - * @return \Illuminate\Database\Query\Builder - */ - public function descendantsAndSelf() { - return $this->newNestedSetQuery() - ->where($this->getLeftColumnName(), '>=', $this->getLeft()) - ->where($this->getLeftColumnName(), '<', $this->getRight()); - } - - /** - * Retrieve all nested children an self. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getDescendantsAndSelf($columns = array('*')) { - if ( is_array($columns) ) - return $this->descendantsAndSelf()->get($columns); - - $arguments = func_get_args(); - - $limit = intval(array_shift($arguments)); - $columns = array_shift($arguments) ?: array('*'); - - return $this->descendantsAndSelf()->limitDepth($limit)->get($columns); - } - - /** - * Set of all children & nested children. - * - * @return \Illuminate\Database\Query\Builder - */ - public function descendants() { - return $this->descendantsAndSelf()->withoutSelf(); - } - - /** - * Retrieve all of its children & nested children. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getDescendants($columns = array('*')) { - if ( is_array($columns) ) - return $this->descendants()->get($columns); - - $arguments = func_get_args(); - - $limit = intval(array_shift($arguments)); - $columns = array_shift($arguments) ?: array('*'); - - return $this->descendants()->limitDepth($limit)->get($columns); - } - - /** - * Set of "immediate" descendants (aka children), alias for the children relation. - * - * @return \Illuminate\Database\Query\Builder - */ - public function immediateDescendants() { - return $this->children(); - } - - /** - * Retrive all of its "immediate" descendants. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getImmediateDescendants($columns = array('*')) { - return $this->children()->get($columns); - } - - /** - * Returns the level of this node in the tree. - * Root level is 0. - * - * @return int - */ - public function getLevel() { - if ( is_null($this->getParentId()) ) - return 0; - - return $this->computeLevel(); - } - - /** - * Returns true if node is a descendant. - * - * @param NestedSet - * @return boolean - */ - public function isDescendantOf($other) { - return ( - $this->getLeft() > $other->getLeft() && - $this->getLeft() < $other->getRight() && - $this->inSameScope($other) - ); - } - - /** - * Returns true if node is self or a descendant. - * - * @param NestedSet - * @return boolean - */ - public function isSelfOrDescendantOf($other) { - return ( - $this->getLeft() >= $other->getLeft() && - $this->getLeft() < $other->getRight() && - $this->inSameScope($other) - ); - } - - /** - * Returns true if node is an ancestor. - * - * @param NestedSet - * @return boolean - */ - public function isAncestorOf($other) { - return ( - $this->getLeft() < $other->getLeft() && - $this->getRight() > $other->getLeft() && - $this->inSameScope($other) - ); - } - - /** - * Returns true if node is self or an ancestor. - * - * @param NestedSet - * @return boolean - */ - public function isSelfOrAncestorOf($other) { - return ( - $this->getLeft() <= $other->getLeft() && - $this->getRight() > $other->getLeft() && - $this->inSameScope($other) - ); - } - - /** - * Returns the first sibling to the left. - * - * @return NestedSet - */ - public function getLeftSibling() { - return $this->siblings() - ->where($this->getLeftColumnName(), '<', $this->getLeft()) - ->orderBy($this->getOrderColumnName(), 'desc') - ->get() - ->last(); - } - - /** - * Returns the first sibling to the right. - * - * @return NestedSet - */ - public function getRightSibling() { - return $this->siblings() - ->where($this->getLeftColumnName(), '>', $this->getLeft()) - ->first(); - } - - /** - * Find the left sibling and move to left of it. - * - * @return \Baum\Node - */ - public function moveLeft() { - return $this->moveToLeftOf($this->getLeftSibling()); - } - - /** - * Find the right sibling and move to the right of it. - * - * @return \Baum\Node - */ - public function moveRight() { - return $this->moveToRightOf($this->getRightSibling()); - } - - /** - * Move to the node to the left of ... - * - * @return \Baum\Node - */ - public function moveToLeftOf($node) { - return $this->moveTo($node, 'left'); - } - - /** - * Move to the node to the right of ... - * - * @return \Baum\Node - */ - public function moveToRightOf($node) { - return $this->moveTo($node, 'right'); - } - - /** - * Alias for moveToRightOf - * - * @return \Baum\Node - */ - public function makeNextSiblingOf($node) { - return $this->moveToRightOf($node); - } - - /** - * Alias for moveToRightOf - * - * @return \Baum\Node - */ - public function makeSiblingOf($node) { - return $this->moveToRightOf($node); - } - - /** - * Alias for moveToLeftOf - * - * @return \Baum\Node - */ - public function makePreviousSiblingOf($node) { - return $this->moveToLeftOf($node); - } - - /** - * Make the node a child of ... - * - * @return \Baum\Node - */ - public function makeChildOf($node) { - return $this->moveTo($node, 'child'); - } - - /** - * Make the node the first child of ... - * - * @return \Baum\Node - */ - public function makeFirstChildOf($node) { - if ( $node->children()->count() == 0 ) - return $this->makeChildOf($node); - - return $this->moveToLeftOf($node->children()->first()); - } - - /** - * Make the node the last child of ... - * - * @return \Baum\Node - */ - public function makeLastChildOf($node) { - return $this->makeChildOf($node); - } - - /** - * Make current node a root node. - * - * @return \Baum\Node - */ - public function makeRoot() { - return $this->moveTo($this, 'root'); - } - - /** - * Equals? - * - * @param \Baum\Node - * @return boolean - */ - public function equals($node) { - return ($this == $node); - } - - /** - * Checkes if the given node is in the same scope as the current one. - * - * @param \Baum\Node - * @return boolean - */ - public function inSameScope($other) { - foreach($this->getScopedColumns() as $fld) { - if ( $this->$fld != $other->$fld ) return false; - } - - return true; - } - - /** - * Checks wether the given node is a descendant of itself. Basically, whether - * its in the subtree defined by the left and right indices. - * - * @param \Baum\Node - * @return boolean - */ - public function insideSubtree($node) { - return ( - $this->getLeft() >= $node->getLeft() && - $this->getLeft() <= $node->getRight() && - $this->getRight() >= $node->getLeft() && - $this->getRight() <= $node->getRight() - ); - } - - /** - * Sets default values for left and right fields. - * - * @return void - */ - public function setDefaultLeftAndRight() { - $withHighestRight = $this->newNestedSetQuery()->reOrderBy($this->getRightColumnName(), 'desc')->take(1)->sharedLock()->first(); - - $maxRgt = 0; - if ( !is_null($withHighestRight) ) $maxRgt = $withHighestRight->getRight(); - - $this->setAttribute($this->getLeftColumnName() , $maxRgt + 1); - $this->setAttribute($this->getRightColumnName() , $maxRgt + 2); - } - - /** - * Store the parent_id if the attribute is modified so as we are able to move - * the node to this new parent after saving. - * - * @return void - */ - public function storeNewParent() { - if ( $this->isDirty($this->getParentColumnName()) && ($this->exists || !$this->isRoot()) ) - static::$moveToNewParentId = $this->getParentId(); - else - static::$moveToNewParentId = FALSE; - } - - /** - * Move to the new parent if appropiate. - * - * @return void - */ - public function moveToNewParent() { - $pid = static::$moveToNewParentId; - - if ( is_null($pid) ) - $this->makeRoot(); - else if ( $pid !== FALSE ) - $this->makeChildOf($pid); - } - - /** - * Sets the depth attribute - * - * @return \Baum\Node - */ - public function setDepth() { - $self = $this; - - $this->getConnection()->transaction(function() use ($self) { - $self->reload(); - - $level = $self->getLevel(); - - $self->newNestedSetQuery()->where($self->getKeyName(), '=', $self->getKey())->update(array($self->getDepthColumnName() => $level)); - $self->setAttribute($self->getDepthColumnName(), $level); - }); - - return $this; - } - - /** - * Sets the depth attribute for the current node and all of its descendants. - * - * @return \Baum\Node - */ - public function setDepthWithSubtree() { - $self = $this; - - $this->getConnection()->transaction(function() use ($self) { - $self->reload(); - - $self->descendantsAndSelf()->select($self->getKeyName())->lockForUpdate()->get(); - - $oldDepth = !is_null($self->getDepth()) ? $self->getDepth() : 0; - - $newDepth = $self->getLevel(); - - $self->newNestedSetQuery()->where($self->getKeyName(), '=', $self->getKey())->update(array($self->getDepthColumnName() => $newDepth)); - $self->setAttribute($self->getDepthColumnName(), $newDepth); - - $diff = $newDepth - $oldDepth; - if ( !$self->isLeaf() && $diff != 0 ) - $self->descendants()->increment($self->getDepthColumnName(), $diff); - }); - - return $this; - } - - /** - * Prunes a branch off the tree, shifting all the elements on the right - * back to the left so the counts work. - * - * @return void; - */ - public function destroyDescendants() { - if ( is_null($this->getRight()) || is_null($this->getLeft()) ) return; - - $self = $this; - - $this->getConnection()->transaction(function() use ($self) { - $self->reload(); - - $lftCol = $self->getLeftColumnName(); - $rgtCol = $self->getRightColumnName(); - $lft = $self->getLeft(); - $rgt = $self->getRight(); - - // Apply a lock to the rows which fall past the deletion point - $self->newNestedSetQuery()->where($lftCol, '>=', $lft)->select($self->getKeyName())->lockForUpdate()->get(); - - // Prune children - $self->newNestedSetQuery()->where($lftCol, '>', $lft)->where($rgtCol, '<', $rgt)->delete(); - - // Update left and right indexes for the remaining nodes - $diff = $rgt - $lft + 1; - - $self->newNestedSetQuery()->where($lftCol, '>', $rgt)->decrement($lftCol, $diff); - $self->newNestedSetQuery()->where($rgtCol, '>', $rgt)->decrement($rgtCol, $diff); - }); - } - - /** - * "Makes room" for the the current node between its siblings. - * - * @return void - */ - public function shiftSiblingsForRestore() { - if ( is_null($this->getRight()) || is_null($this->getLeft()) ) return; - - $self = $this; - - $this->getConnection()->transaction(function() use ($self) { - $lftCol = $self->getLeftColumnName(); - $rgtCol = $self->getRightColumnName(); - $lft = $self->getLeft(); - $rgt = $self->getRight(); - - $diff = $rgt - $lft + 1; - - $self->newNestedSetQuery()->where($lftCol, '>=', $lft)->increment($lftCol, $diff); - $self->newNestedSetQuery()->where($rgtCol, '>=', $lft)->increment($rgtCol, $diff); - }); - } - - /** - * Restores all of the current node's descendants. - * - * @return void - */ - public function restoreDescendants() { - if ( is_null($this->getRight()) || is_null($this->getLeft()) ) return; - - $self = $this; - - $this->getConnection()->transaction(function() use ($self) { - $self->newNestedSetQuery() - ->withTrashed() - ->where($self->getLeftColumnName(), '>', $self->getLeft()) - ->where($self->getRightColumnName(), '<', $self->getRight()) - ->update(array( - $self->getDeletedAtColumn() => null, - $self->getUpdatedAtColumn() => $self->{$self->getUpdatedAtColumn()} - )); - }); - } - - /** - * Return an key-value array indicating the node's depth with $seperator - * - * @return Array - */ - public static function getNestedList($column, $key = null, $seperator = ' ') { - $instance = new static; - - $key = $key ?: $instance->getKeyName(); - $depthColumn = $instance->getDepthColumnName(); - - $nodes = $instance->newNestedSetQuery()->get()->toArray(); - - return array_combine(array_map(function($node) use($key) { - return $node[$key]; - }, $nodes), array_map(function($node) use($seperator, $depthColumn, $column) { - return str_repeat($seperator, $node[$depthColumn]) . $node[$column]; - }, $nodes)); - } - - /** - * Maps the provided tree structure into the database using the current node - * as the parent. The provided tree structure will be inserted/updated as the - * descendancy subtree of the current node instance. - * - * @param array|\Illuminate\Support\Contracts\ArrayableInterface - * @return boolean - */ - public function makeTree($nodeList) { - $mapper = new SetMapper($this); - - return $mapper->map($nodeList); - } - - /** - * Main move method. Here we handle all node movements with the corresponding - * lft/rgt index updates. - * - * @param Baum\Node|int $target - * @param string $position - * @return \Baum\Node - */ - protected function moveTo($target, $position) { - return Move::to($this, $target, $position); - } - - /** - * Compute current node level. If could not move past ourseleves return - * our ancestor count, otherwhise get the first parent level + the computed - * nesting. - * - * @return integer - */ - protected function computeLevel() { - list($node, $nesting) = $this->determineDepth($this); - - if ( $node->equals($this) ) - return $this->ancestors()->count(); - - return $node->getLevel() + $nesting; - } - - /** - * Return an array with the last node we could reach and its nesting level - * - * @param Baum\Node $node - * @param integer $nesting - * @return array - */ - protected function determineDepth($node, $nesting = 0) { - // Traverse back up the ancestry chain and add to the nesting level count - while( $parent = $node->parent()->first() ) { - $nesting = $nesting + 1; - - $node = $parent; - } - - return array($node, $nesting); - } - +abstract class Node extends Model +{ + use HasNestedSetProperties; } diff --git a/src/Playground/Caster.php b/src/Playground/Caster.php new file mode 100644 index 00000000..1c6fdc58 --- /dev/null +++ b/src/Playground/Caster.php @@ -0,0 +1,87 @@ + 'Baum\Playground\Caster::castCollection', + 'Illuminate\Database\Eloquent\Model' => 'Baum\Playground\Caster::castModel', + 'Illuminate\Database\Eloquent\Builder' => 'Baum\Playground\Caster::castEloquentBuilder' + ]; + + /** + * Available casters accessor. + * + * @return array + */ + public static function availableCasters() + { + return static::$availableCasters; + } + + /** + * Get an array representing the properties of a collection. + * + * @param \Illuminate\Support\Collection $collection + * @return array + */ + public static function castCollection($collection) + { + return [ + BaseCaster::PREFIX_VIRTUAL.'all' => $collection->all(), + ]; + } + + /** + * Get an array representing the properties of a model. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return array + */ + public static function castModel($model) + { + $attributes = array_merge($model->getAttributes(), $model->getRelations()); + + $visible = array_flip( + $model->getVisible() ?: array_diff(array_keys($attributes), $model->getHidden()) + ); + + $results = []; + + foreach (array_intersect_key($attributes, $visible) as $key => $value) { + $results[(isset($visible[$key]) ? BaseCaster::PREFIX_VIRTUAL : BaseCaster::PREFIX_PROTECTED).$key] = $value; + } + + return $results; + } + + /** + * Get the raw query string for a builder. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return array + */ + public static function castEloquentBuilder($builder) + { + $sql = $builder->toSql(); + + $bindings = $builder->getBindings(); + + return [ + BaseCaster::PREFIX_VIRTUAL.'sql' => vsprintf( + str_replace('?', '%s', $sql), + array_map(function ($v) { + return is_int($v) || is_float($v) ? (string) $v : "'{$v}'"; + }, $bindings) + ), + ]; + } +} diff --git a/src/Playground/Console.php b/src/Playground/Console.php new file mode 100644 index 00000000..ffa56b3a --- /dev/null +++ b/src/Playground/Console.php @@ -0,0 +1,109 @@ +shell = $shell; + } + + /** + * Static instantiator and executor. + * + * @return int + */ + public static function start() + { + $instance = new static; + + return $instance->run(); + } + + /** + * Run the shell + * + * @return int + */ + public function run() + { + if (is_null($this->shell)) { + $this->shell = $this->newShell(); + } + + return $this->shell->run(); + } + + /** + * Return the actual shell instance. + * + * @return \Psy\Shell + */ + public function getShell() + { + return $this->shell; + } + + /** + * Set the actual shell instance. + * + * @param \Psy\Shell $shell + * @return void + */ + public function setShell($shell) + { + $this->shell = $shell; + } + + /** + * Builds a new shell instance with default options. + * + * @return \Psy\Shell + */ + protected function newShell() + { + $shell = new Shell($this->getShellConfig()); + + return $shell; + } + + /** + * Build a new Psy\Shell configuration object instance. + * + * @return \Psy\Configuration + */ + protected function getShellConfig() + { + $config = new Configuration(['updateCheck' => 'never']); + + $config->getPresenter()->addCasters($this->getShellCasters()); + + return $config; + } + + /** + * Get the object casters to use. + * + * @return array + */ + protected function getShellCasters() + { + return Caster::availableCasters(); + } +} diff --git a/src/Providers/BaumServiceProvider.php b/src/Providers/BaumServiceProvider.php deleted file mode 100644 index 5c6f18c5..00000000 --- a/src/Providers/BaumServiceProvider.php +++ /dev/null @@ -1,76 +0,0 @@ -registerCommands(); - } - - /** - * Register the commands. - * - * @return void - */ - public function registerCommands() { - $this->registerBaumCommand(); - $this->registerInstallCommand(); - - // Resolve the commands with Artisan by attaching the event listener to Artisan's - // startup. This allows us to use the commands from our terminal. - $this->commands('command.baum', 'command.baum.install'); - } - - /** - * Register the 'baum' command. - * - * @return void - */ - protected function registerBaumCommand() { - $this->app->singleton('command.baum', function($app) { - return new BaumCommand; - }); - } - - /** - * Register the 'baum:install' command. - * - * @return void - */ - protected function registerInstallCommand() { - $this->app->singleton('command.baum.install', function($app) { - $migrator = new MigrationGenerator($app['files']); - $modeler = new ModelGenerator($app['files']); - - return new InstallCommand($migrator, $modeler); - }); - } - - /** - * Get the services provided by the provider. - * - * @return array - */ - public function provides() { - return array('command.baum', 'command.baum.install'); - } - -} diff --git a/src/SetBuilder.php b/src/SetBuilder.php deleted file mode 100644 index 873c238e..00000000 --- a/src/SetBuilder.php +++ /dev/null @@ -1,175 +0,0 @@ -node = $node; - } - - /** - * Perform the re-calculation of the left and right indexes of the whole - * nested set tree structure. - * - * @param bool $force - * @return void - */ - public function rebuild($force = false) { - $alreadyValid = forward_static_call(array(get_class($this->node), 'isValidNestedSet')); - - // Do not rebuild a valid Nested Set tree structure - if ( !$force && $alreadyValid ) return true; - - // Rebuild lefts and rights for each root node and its children (recursively). - // We go by setting left (and keep track of the current left bound), then - // search for each children and recursively set the left index (while - // incrementing that index). When going back up the recursive chain we start - // setting the right indexes and saving the nodes... - $self = $this; - - $this->node->getConnection()->transaction(function() use ($self) { - foreach($self->roots() as $root) - $self->rebuildBounds($root, 0); - }); - } - - /** - * Return all root nodes for the current database table appropiately sorted. - * - * @return Illuminate\Database\Eloquent\Collection - */ - public function roots() { - return $this->node->newQuery() - ->whereNull($this->node->getQualifiedParentColumnName()) - ->orderBy($this->node->getQualifiedLeftColumnName()) - ->orderBy($this->node->getQualifiedRightColumnName()) - ->orderBy($this->node->getQualifiedKeyName()) - ->get(); - } - - /** - * Recompute left and right index bounds for the specified node and its - * children (recursive call). Fill the depth column too. - */ - public function rebuildBounds($node, $depth = 0) { - $k = $this->scopedKey($node); - - $node->setAttribute($node->getLeftColumnName(), $this->getNextBound($k)); - $node->setAttribute($node->getDepthColumnName(), $depth); - - foreach($this->children($node) as $child) - $this->rebuildBounds($child, $depth + 1); - - $node->setAttribute($node->getRightColumnName(), $this->getNextBound($k)); - - $node->save(); - } - - /** - * Return all children for the specified node. - * - * @param Baum\Node $node - * @return Illuminate\Database\Eloquent\Collection - */ - public function children($node) { - $query = $this->node->newQuery(); - - $query->where($this->node->getQualifiedParentColumnName(), '=', $node->getKey()); - - // We must also add the scoped column values to the query to compute valid - // left and right indexes. - foreach($this->scopedAttributes($node) as $fld => $value) - $query->where($this->qualify($fld), '=', $value); - - $query->orderBy($this->node->getQualifiedLeftColumnName()); - $query->orderBy($this->node->getQualifiedRightColumnName()); - $query->orderBy($this->node->getQualifiedKeyName()); - - return $query->get(); - } - - /** - * Return an array of the scoped attributes of the supplied node. - * - * @param Baum\Node $node - * @return array - */ - protected function scopedAttributes($node) { - $keys = $this->node->getScopedColumns(); - - if ( count($keys) == 0 ) - return array(); - - $values = array_map(function($column) use ($node) { - return $node->getAttribute($column); }, $keys); - - return array_combine($keys, $values); - } - - /** - * Return a string-key for the current scoped attributes. Used for index - * computing when a scope is defined (acsts as an scope identifier). - * - * @param Baum\Node $node - * @return string - */ - protected function scopedKey($node) { - $attributes = $this->scopedAttributes($node); - - $output = array(); - - foreach($attributes as $fld => $value) - $output[] = $this->qualify($fld).'='.(is_null($value) ? 'NULL' : $value); - - // NOTE: Maybe an md5 or something would be better. Should be unique though. - return implode(',', $output); - } - - /** - * Return next index bound value for the given key (current scope identifier) - * - * @param string $key - * @return integer - */ - protected function getNextBound($key) { - if ( false === array_key_exists($key, $this->bounds) ) - $this->bounds[$key] = 0; - - $this->bounds[$key] = $this->bounds[$key] + 1; - - return $this->bounds[$key]; - } - - /** - * Get the fully qualified value for the specified column. - * - * @return string - */ - protected function qualify($column) { - return $this->node->getTable() . '.' . $column; - } - -} diff --git a/src/SetMapper.php b/src/SetMapper.php deleted file mode 100644 index a44dabab..00000000 --- a/src/SetMapper.php +++ /dev/null @@ -1,164 +0,0 @@ -node = $node; - - $this->childrenKeyName = $childrenKeyName; - } - - /** - * Maps a tree structure into the database. Unguards & wraps in transaction. - * - * @param array|\Illuminate\Support\Contracts\ArrayableInterface - * @return boolean - */ - public function map($nodeList) { - $self = $this; - - return $this->wrapInTransaction(function() use ($self, $nodeList) { - forward_static_call(array(get_class($self->node), 'unguard')); - - $result = $self->mapTree($nodeList); - - forward_static_call(array(get_class($self->node), 'reguard')); - - return $result; - }); - } - - /** - * Maps a tree structure into the database without unguarding nor wrapping - * inside a transaction. - * - * @param array|\Illuminate\Support\Contracts\ArrayableInterface - * @return boolean - */ - public function mapTree($nodeList) { - $tree = $nodeList instanceof ArrayableInterface ? $nodeList->toArray() : $nodeList; - - $affectedKeys = array(); - - $result = $this->mapTreeRecursive($tree, $this->node->getKey(), $affectedKeys); - - if ( $result && count($affectedKeys) > 0 ) - $this->deleteUnaffected($affectedKeys); - - return $result; - } - - /** - * Returns the children key name to use on the mapping array - * - * @return string - */ - public function getChildrenKeyName() { - return $this->childrenKeyName; - } - - /** - * Maps a tree structure into the database - * - * @param array $tree - * @param mixed $parent - * @return boolean - */ - protected function mapTreeRecursive(array $tree, $parentKey = null, &$affectedKeys = array()) { - // For every attribute entry: We'll need to instantiate a new node either - // from the database (if the primary key was supplied) or a new instance. Then, - // append all the remaining data attributes (including the `parent_id` if - // present) and save it. Finally, tail-recurse performing the same - // operations for any child node present. Setting the `parent_id` property at - // each level will take care of the nesting work for us. - foreach($tree as $attributes) { - $node = $this->firstOrNew($this->getSearchAttributes($attributes)); - - $data = $this->getDataAttributes($attributes); - if ( !is_null($parentKey) ) - $data[$node->getParentColumnName()] = $parentKey; - - $node->fill($data); - - $result = $node->save(); - - if ( ! $result ) return false; - - $affectedKeys[] = $node->getKey(); - - if ( array_key_exists($this->getChildrenKeyName(), $attributes) ) { - $children = $attributes[$this->getChildrenKeyName()]; - - if ( count($children) > 0 ) { - $result = $this->mapTreeRecursive($children, $node->getKey(), $affectedKeys); - - if ( ! $result ) return false; - } - } - } - - return true; - } - - protected function getSearchAttributes($attributes) { - $searchable = array($this->node->getKeyName()); - - return array_only($attributes, $searchable); - } - - protected function getDataAttributes($attributes) { - $exceptions = array($this->node->getKeyName(), $this->getChildrenKeyName()); - - return array_except($attributes, $exceptions); - } - - protected function firstOrNew($attributes) { - $className = get_class($this->node); - - if ( count($attributes) === 0 ) - return new $className; - - return forward_static_call(array($className, 'firstOrNew'), $attributes); - } - - protected function pruneScope() { - if ( $this->node->exists ) - return $this->node->descendants(); - - return $this->node->newNestedSetQuery(); - } - - protected function deleteUnaffected($keys = array()) { - return $this->pruneScope()->whereNotIn($this->node->getKeyName(), $keys)->delete(); - } - - protected function wrapInTransaction(Closure $callback) { - return $this->node->getConnection()->transaction($callback); - } - -} diff --git a/src/SetValidator.php b/src/SetValidator.php deleted file mode 100644 index 10d87640..00000000 --- a/src/SetValidator.php +++ /dev/null @@ -1,224 +0,0 @@ -node = $node; - } - - /** - * Determine if the validation passes. - * - * @return boolean - */ - public function passes() { - return $this->validateBounds() && $this->validateDuplicates() && - $this->validateRoots(); - } - - /** - * Determine if validation fails. - * - * @return boolean - */ - public function fails() { - return !$this->passes(); - } - - /** - * Validates bounds of the nested tree structure. It will perform checks on - * the `lft`, `rgt` and `parent_id` columns. Mainly that they're not null, - * rights greater than lefts, and that they're within the bounds of the parent. - * - * @return boolean - */ - protected function validateBounds() { - $connection = $this->node->getConnection(); - $grammar = $connection->getQueryGrammar(); - - $tableName = $this->node->getTable(); - $primaryKeyName = $this->node->getKeyName(); - $parentColumn = $this->node->getQualifiedParentColumnName(); - - $lftCol = $grammar->wrap($this->node->getLeftColumnName()); - $rgtCol = $grammar->wrap($this->node->getRightColumnName()); - - $qualifiedLftCol = $grammar->wrap($this->node->getQualifiedLeftColumnName()); - $qualifiedRgtCol = $grammar->wrap($this->node->getQualifiedRightColumnName()); - $qualifiedParentCol = $grammar->wrap($this->node->getQualifiedParentColumnName()); - - $whereStm = "($qualifiedLftCol IS NULL OR - $qualifiedRgtCol IS NULL OR - $qualifiedLftCol >= $qualifiedRgtCol OR - ($qualifiedParentCol IS NOT NULL AND - ($qualifiedLftCol <= parent.$lftCol OR - $qualifiedRgtCol >= parent.$rgtCol)))"; - - $query = $this->node->newQuery() - ->join($connection->raw($grammar->wrapTable($tableName).' AS parent'), - $parentColumn, '=', $connection->raw('parent.'.$grammar->wrap($primaryKeyName)), - 'left outer') - ->whereRaw($whereStm); - - return ($query->count() == 0); - } - - /** - * Checks that there are no duplicates for the `lft` and `rgt` columns. - * - * @return boolean - */ - protected function validateDuplicates() { - return ( - !$this->duplicatesExistForColumn($this->node->getQualifiedLeftColumnName()) && - !$this->duplicatesExistForColumn($this->node->getQualifiedRightColumnName()) - ); - } - - /** - * For each root of the whole nested set tree structure, checks that their - * `lft` and `rgt` bounds are properly set. - * - * @return boolean - */ - protected function validateRoots() { - $roots = forward_static_call(array(get_class($this->node), 'roots'))->get(); - - // If a scope is defined in the model we should check that the roots are - // valid *for each* value in the scope columns. - if ( $this->node->isScoped() ) - return $this->validateRootsByScope($roots); - - return $this->isEachRootValid($roots); - } - - /** - * Checks if duplicate values for the column specified exist. Takes - * the Nested Set scope columns into account (if appropiate). - * - * @param string $column - * @return boolean - */ - protected function duplicatesExistForColumn($column) { - $connection = $this->node->getConnection(); - $grammar = $connection->getQueryGrammar(); - - $columns = array_merge($this->node->getQualifiedScopedColumns(), array($column)); - - $columnsForSelect = implode(', ', array_map(function($col) use ($grammar) { - return $grammar->wrap($col); }, $columns)); - - $wrappedColumn = $grammar->wrap($column); - - $query = $this->node->newQuery() - ->select($connection->raw("$columnsForSelect, COUNT($wrappedColumn)")) - ->havingRaw("COUNT($wrappedColumn) > 1"); - - foreach($columns as $col) - $query->groupBy($col); - - $result = $query->first(); - - return !is_null($result); - } - - /** - * Check that each root node in the list supplied satisfies that its bounds - * values (lft, rgt indexes) are less than the next. - * - * @param mixed $roots - * @return boolean - */ - protected function isEachRootValid($roots) { - $left = $right = 0; - - foreach($roots as $root) { - $rootLeft = $root->getLeft(); - $rootRight = $root->getRight(); - - if ( !($rootLeft > $left && $rootRight > $right) ) - return false; - - $left = $rootLeft; - $right = $rootRight; - } - - return true; - } - - /** - * Check that each root node in the list supplied satisfies that its bounds - * values (lft, rgt indexes) are less than the next *within each scope*. - * - * @param mixed $roots - * @return boolean - */ - protected function validateRootsByScope($roots) { - foreach($this->groupRootsByScope($roots) as $scope => $groupedRoots) { - $valid = $this->isEachRootValid($groupedRoots); - - if ( !$valid ) - return false; - } - - return true; - } - - /** - * Given a list of root nodes, it returns an array in which the keys are the - * array of the actual scope column values and the values are the root nodes - * inside that scope themselves - * - * @param mixed $roots - * @return array - */ - protected function groupRootsByScope($roots) { - $rootsGroupedByScope = array(); - - foreach($roots as $root) { - $key = $this->keyForScope($root); - - if ( !isset($rootsGroupedByScope[$key]) ) - $rootsGroupedByScope[$key] = array(); - - $rootsGroupedByScope[$key][] = $root; - } - - return $rootsGroupedByScope; - } - - /** - * Builds a single string for the given scope columns values. Useful for - * making array keys for grouping. - * - * @param Baum\Node $node - * @return string - */ - protected function keyForScope($node) { - return implode('-', array_map(function($column) use ($node) { - $value = $node->getAttribute($column); - - if ( is_null($value) ) - return 'NULL'; - - return $value; - }, $node->getScopedColumns())); - } - -} diff --git a/tests/config/database.php b/tests/config/database.php index 81b28bb7..c84adca6 100644 --- a/tests/config/database.php +++ b/tests/config/database.php @@ -1,32 +1,32 @@ 'sqlite', - 'database' => ':memory:', - 'prefix' => '' -); +return [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '' +]; // // Test against local Postgres -// return array( -// 'driver' => 'pgsql', -// 'host' => 'localhost', -// 'database' => 'baum_test', -// 'username' => 'postgres', -// 'password' => 'postgres', -// 'charset' => 'utf8', -// 'prefix' => '', -// 'schema' => 'public', -// ); +// return [ +// 'driver' => 'pgsql', +// 'host' => 'localhost', +// 'database' => 'baum_test', +// 'username' => 'postgres', +// 'password' => 'postgres', +// 'charset' => 'utf8', +// 'prefix' => '', +// 'schema' => 'public', +// ]; // // Test against local MySQL -// return array( -// 'driver' => 'mysql', -// 'host' => 'localhost', -// 'database' => 'baum_test', -// 'username' => 'mysql', -// 'password' => 'mysql', -// 'charset' => 'utf8', -// 'collation' => 'utf8_unicode_ci', -// 'prefix' => '', -// ); +// return [ +// 'driver' => 'mysql', +// 'host' => 'localhost', +// 'database' => 'baum_test', +// 'username' => 'mysql', +// 'password' => 'mysql', +// 'charset' => 'utf8', +// 'collation' => 'utf8_unicode_ci', +// 'prefix' => '', +// ]; diff --git a/tests/migrators/CategoryMigrator.php b/tests/migrators/CategoryMigrator.php deleted file mode 100644 index 9197cac3..00000000 --- a/tests/migrators/CategoryMigrator.php +++ /dev/null @@ -1,34 +0,0 @@ -dropIfExists('categories'); - - DB::schema()->create('categories', function($t) { - $t->increments('id'); - - $t->integer('parent_id')->nullable(); - $t->integer('lft')->nullable(); - $t->integer('rgt')->nullable(); - $t->integer('depth')->nullable(); - - $t->string('name'); - - $t->integer('company_id')->unsigned()->nullable(); - $t->string('language', 3)->nullable(); - - $t->timestamp('created_at')->nullable(); - $t->timestamp('updated_at')->nullable(); - - $t->softDeletes(); - }); - } - - public function down() { - DB::schema()->drop('categories'); - } - -} diff --git a/tests/migrators/ClusterMigrator.php b/tests/migrators/ClusterMigrator.php deleted file mode 100644 index 97536ff7..00000000 --- a/tests/migrators/ClusterMigrator.php +++ /dev/null @@ -1,34 +0,0 @@ -dropIfExists('clusters'); - - DB::schema()->create('clusters', function($t) { - $t->string('id'); - - $t->string('parent_id')->nullable(); - $t->integer('lft')->nullable(); - $t->integer('rgt')->nullable(); - $t->integer('depth')->nullable(); - - $t->string('name'); - - $t->integer('company_id')->unsigned()->nullable(); - $t->string('language', 3)->nullable(); - - $t->timestamp('created_at')->nullable(); - $t->timestamp('updated_at')->nullable(); - - $t->softDeletes(); - }); - } - - public function down() { - DB::schema()->drop('clusters'); - } - -} diff --git a/tests/models/Category.php b/tests/models/Category.php deleted file mode 100644 index 8eadd6ad..00000000 --- a/tests/models/Category.php +++ /dev/null @@ -1,48 +0,0 @@ -ensureUuid(); - }); - } - - public function ensureUuid() { - if ( is_null($this->getAttribute($this->getKeyName())) ) - $this->setAttribute($this->getKeyName(), $this->generateUuid()); - - return $this; - } - - protected function generateUuid() { - return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', - mt_rand(0, 0xffff), mt_rand(0, 0xffff), - mt_rand(0, 0xffff), - mt_rand(0, 0x0fff) | 0x4000, - mt_rand(0, 0x3fff) | 0x8000, - mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) - ); - } - -} - -class ScopedCluster extends Cluster { - - protected $scoped = array('company_id'); - -} - -class MultiScopedCluster extends Cluster { - - protected $scoped = array('company_id', 'language'); - -} - -class OrderedCluster extends Cluster { - - protected $orderColumn = 'name'; - -} - -class SoftCluster extends Cluster { - - use SoftDeletes; - - public $timestamps = true; - - protected $dates = ['deleted_at']; - -} diff --git a/tests/seeders/CategorySeeder.php b/tests/seeders/CategorySeeder.php deleted file mode 100644 index bfc8a965..00000000 --- a/tests/seeders/CategorySeeder.php +++ /dev/null @@ -1,167 +0,0 @@ -delete(); - - Category::unguard(); - - Category::create(array('id' => 1, 'name' => 'Root 1' , 'lft' => 1 , 'rgt' => 10 , 'depth' => 0)); - Category::create(array('id' => 2, 'name' => 'Child 1' , 'lft' => 2 , 'rgt' => 3 , 'depth' => 1, 'parent_id' => 1)); - Category::create(array('id' => 3, 'name' => 'Child 2' , 'lft' => 4 , 'rgt' => 7 , 'depth' => 1, 'parent_id' => 1)); - Category::create(array('id' => 4, 'name' => 'Child 2.1', 'lft' => 5 , 'rgt' => 6 , 'depth' => 2, 'parent_id' => 3)); - Category::create(array('id' => 5, 'name' => 'Child 3' , 'lft' => 8 , 'rgt' => 9 , 'depth' => 1, 'parent_id' => 1)); - Category::create(array('id' => 6, 'name' => 'Root 2' , 'lft' => 11 , 'rgt' => 12 , 'depth' => 0)); - - Category::reguard(); - - if ( DB::connection()->getDriverName() === 'pgsql' ) { - $tablePrefix = DB::connection()->getTablePrefix(); - - $sequenceName = $tablePrefix . 'categories_id_seq'; - - DB::connection()->statement('ALTER SEQUENCE ' . $sequenceName . ' RESTART WITH 7'); - } - } - - public function nestUptoAt($node, $levels=10, $attrs=array()) { - for($i=0; $i < $levels; $i++, $node=$new) { - $new = Category::create(array_merge($attrs, array('name' => "{$node->name}.1"))); - $new->makeChildOf($node); - } - } - -} - -class ScopedCategorySeeder { - - public function run() { - DB::table('categories')->delete(); - - ScopedCategory::unguard(); - - ScopedCategory::create(array('id' => 1 , 'company_id' => 1, 'name' => 'Root 1' , 'lft' => 1 , 'rgt' => 10 , 'depth' => 0)); - ScopedCategory::create(array('id' => 2 , 'company_id' => 1, 'name' => 'Child 1' , 'lft' => 2 , 'rgt' => 3 , 'depth' => 1, 'parent_id' => 1)); - ScopedCategory::create(array('id' => 3 , 'company_id' => 1, 'name' => 'Child 2' , 'lft' => 4 , 'rgt' => 7 , 'depth' => 1, 'parent_id' => 1)); - ScopedCategory::create(array('id' => 4 , 'company_id' => 1, 'name' => 'Child 2.1', 'lft' => 5 , 'rgt' => 6 , 'depth' => 2, 'parent_id' => 3)); - ScopedCategory::create(array('id' => 5 , 'company_id' => 1, 'name' => 'Child 3' , 'lft' => 8 , 'rgt' => 9 , 'depth' => 1, 'parent_id' => 1)); - ScopedCategory::create(array('id' => 6 , 'company_id' => 2, 'name' => 'Root 2' , 'lft' => 1 , 'rgt' => 10 , 'depth' => 0)); - ScopedCategory::create(array('id' => 7 , 'company_id' => 2, 'name' => 'Child 4' , 'lft' => 2 , 'rgt' => 3 , 'depth' => 1, 'parent_id' => 6)); - ScopedCategory::create(array('id' => 8 , 'company_id' => 2, 'name' => 'Child 5' , 'lft' => 4 , 'rgt' => 7 , 'depth' => 1, 'parent_id' => 6)); - ScopedCategory::create(array('id' => 9 , 'company_id' => 2, 'name' => 'Child 5.1', 'lft' => 5 , 'rgt' => 6 , 'depth' => 2, 'parent_id' => 8)); - ScopedCategory::create(array('id' => 10, 'company_id' => 2, 'name' => 'Child 6' , 'lft' => 8 , 'rgt' => 9 , 'depth' => 1, 'parent_id' => 6)); - - ScopedCategory::reguard(); - - if ( DB::connection()->getDriverName() === 'pgsql' ) { - $tablePrefix = DB::connection()->getTablePrefix(); - - $sequenceName = $tablePrefix . 'categories_id_seq'; - - DB::connection()->statement('ALTER SEQUENCE ' . $sequenceName . ' RESTART WITH 11'); - } - } - -} - -class MultiScopedCategorySeeder { - - public function run() { - DB::table('categories')->delete(); - - MultiScopedCategory::unguard(); - - MultiScopedCategory::create(array('id' => 1 , 'company_id' => 1, 'language' => 'en', 'name' => 'Root 1' , 'lft' => 1 , 'rgt' => 10 , 'depth' => 0)); - MultiScopedCategory::create(array('id' => 2 , 'company_id' => 1, 'language' => 'en', 'name' => 'Child 1' , 'lft' => 2 , 'rgt' => 3 , 'depth' => 1, 'parent_id' => 1)); - MultiScopedCategory::create(array('id' => 3 , 'company_id' => 1, 'language' => 'en', 'name' => 'Child 2' , 'lft' => 4 , 'rgt' => 7 , 'depth' => 1, 'parent_id' => 1)); - MultiScopedCategory::create(array('id' => 4 , 'company_id' => 1, 'language' => 'en', 'name' => 'Child 2.1' , 'lft' => 5 , 'rgt' => 6 , 'depth' => 2, 'parent_id' => 3)); - MultiScopedCategory::create(array('id' => 5 , 'company_id' => 1, 'language' => 'en', 'name' => 'Child 3' , 'lft' => 8 , 'rgt' => 9 , 'depth' => 1, 'parent_id' => 1)); - MultiScopedCategory::create(array('id' => 6 , 'company_id' => 2, 'language' => 'en', 'name' => 'Root 2' , 'lft' => 1 , 'rgt' => 10 , 'depth' => 0)); - MultiScopedCategory::create(array('id' => 7 , 'company_id' => 2, 'language' => 'en', 'name' => 'Child 4' , 'lft' => 2 , 'rgt' => 3 , 'depth' => 1, 'parent_id' => 6)); - MultiScopedCategory::create(array('id' => 8 , 'company_id' => 2, 'language' => 'en', 'name' => 'Child 5' , 'lft' => 4 , 'rgt' => 7 , 'depth' => 1, 'parent_id' => 6)); - MultiScopedCategory::create(array('id' => 9 , 'company_id' => 2, 'language' => 'en', 'name' => 'Child 5.1' , 'lft' => 5 , 'rgt' => 6 , 'depth' => 2, 'parent_id' => 8)); - MultiScopedCategory::create(array('id' => 10, 'company_id' => 2, 'language' => 'en', 'name' => 'Child 6' , 'lft' => 8 , 'rgt' => 9 , 'depth' => 1, 'parent_id' => 6)); - MultiScopedCategory::create(array('id' => 11, 'company_id' => 3, 'language' => 'fr', 'name' => 'Racine 1' , 'lft' => 1 , 'rgt' => 10 , 'depth' => 0)); - MultiScopedCategory::create(array('id' => 12, 'company_id' => 3, 'language' => 'fr', 'name' => 'Enfant 1' , 'lft' => 2 , 'rgt' => 3 , 'depth' => 1, 'parent_id' => 11)); - MultiScopedCategory::create(array('id' => 13, 'company_id' => 3, 'language' => 'fr', 'name' => 'Enfant 2' , 'lft' => 4 , 'rgt' => 7 , 'depth' => 1, 'parent_id' => 11)); - MultiScopedCategory::create(array('id' => 14, 'company_id' => 3, 'language' => 'fr', 'name' => 'Enfant 2.1' , 'lft' => 5 , 'rgt' => 6 , 'depth' => 2, 'parent_id' => 13)); - MultiScopedCategory::create(array('id' => 15, 'company_id' => 3, 'language' => 'fr', 'name' => 'Enfant 3' , 'lft' => 8 , 'rgt' => 9 , 'depth' => 1, 'parent_id' => 11)); - MultiScopedCategory::create(array('id' => 16, 'company_id' => 3, 'language' => 'es', 'name' => 'Raiz 1' , 'lft' => 1 , 'rgt' => 10 , 'depth' => 0)); - MultiScopedCategory::create(array('id' => 17, 'company_id' => 3, 'language' => 'es', 'name' => 'Hijo 1' , 'lft' => 2 , 'rgt' => 3 , 'depth' => 1, 'parent_id' => 16)); - MultiScopedCategory::create(array('id' => 18, 'company_id' => 3, 'language' => 'es', 'name' => 'Hijo 2' , 'lft' => 4 , 'rgt' => 7 , 'depth' => 1, 'parent_id' => 16)); - MultiScopedCategory::create(array('id' => 19, 'company_id' => 3, 'language' => 'es', 'name' => 'Hijo 2.1' , 'lft' => 5 , 'rgt' => 6 , 'depth' => 2, 'parent_id' => 18)); - MultiScopedCategory::create(array('id' => 20, 'company_id' => 3, 'language' => 'es', 'name' => 'Hijo 3' , 'lft' => 8 , 'rgt' => 9 , 'depth' => 1, 'parent_id' => 16)); - - MultiScopedCategory::reguard(); - - if ( DB::connection()->getDriverName() === 'pgsql' ) { - $tablePrefix = DB::connection()->getTablePrefix(); - - $sequenceName = $tablePrefix . 'categories_id_seq'; - - DB::connection()->statement('ALTER SEQUENCE ' . $sequenceName . ' RESTART WITH 21'); - } - } - -} - -class OrderedCategorySeeder { - - public function run() { - DB::table('categories')->delete(); - - OrderedCategory::unguard(); - - OrderedCategory::create(array('id' => 1, 'name' => 'Root Z' , 'lft' => 1 , 'rgt' => 10 , 'depth' => 0)); - OrderedCategory::create(array('id' => 2, 'name' => 'Child C' , 'lft' => 2 , 'rgt' => 3 , 'depth' => 1, 'parent_id' => 1)); - OrderedCategory::create(array('id' => 3, 'name' => 'Child G' , 'lft' => 4 , 'rgt' => 7 , 'depth' => 1, 'parent_id' => 1)); - OrderedCategory::create(array('id' => 4, 'name' => 'Child G.1', 'lft' => 5 , 'rgt' => 6 , 'depth' => 2, 'parent_id' => 3)); - OrderedCategory::create(array('id' => 5, 'name' => 'Child A' , 'lft' => 8 , 'rgt' => 9 , 'depth' => 1, 'parent_id' => 1)); - OrderedCategory::create(array('id' => 6, 'name' => 'Root A' , 'lft' => 11 , 'rgt' => 12 , 'depth' => 0)); - - OrderedCategory::reguard(); - - if ( DB::connection()->getDriverName() === 'pgsql' ) { - $tablePrefix = DB::connection()->getTablePrefix(); - - $sequenceName = $tablePrefix . 'categories_id_seq'; - - DB::connection()->statement('ALTER SEQUENCE ' . $sequenceName . ' RESTART WITH 7'); - } - } - -} - -class OrderedScopedCategorySeeder { - - public function run() { - DB::table('categories')->delete(); - - OrderedScopedCategory::unguard(); - - OrderedScopedCategory::create(array('id' => 1 , 'company_id' => 1, 'name' => 'Root 1' , 'lft' => 1 , 'rgt' => 10 , 'depth' => 0)); - OrderedScopedCategory::create(array('id' => 2 , 'company_id' => 1, 'name' => 'Child 3' , 'lft' => 8 , 'rgt' => 9 , 'depth' => 1, 'parent_id' => 1)); - OrderedScopedCategory::create(array('id' => 3 , 'company_id' => 1, 'name' => 'Child 2' , 'lft' => 4 , 'rgt' => 7 , 'depth' => 1, 'parent_id' => 1)); - OrderedScopedCategory::create(array('id' => 4 , 'company_id' => 1, 'name' => 'Child 2.1', 'lft' => 5 , 'rgt' => 6 , 'depth' => 2, 'parent_id' => 3)); - OrderedScopedCategory::create(array('id' => 5 , 'company_id' => 1, 'name' => 'Child 1' , 'lft' => 2 , 'rgt' => 3 , 'depth' => 1, 'parent_id' => 1)); - OrderedScopedCategory::create(array('id' => 6 , 'company_id' => 2, 'name' => 'Root 2' , 'lft' => 1 , 'rgt' => 10 , 'depth' => 0)); - OrderedScopedCategory::create(array('id' => 7 , 'company_id' => 2, 'name' => 'Child 4' , 'lft' => 2 , 'rgt' => 3 , 'depth' => 1, 'parent_id' => 6)); - OrderedScopedCategory::create(array('id' => 8 , 'company_id' => 2, 'name' => 'Child 5' , 'lft' => 4 , 'rgt' => 7 , 'depth' => 1, 'parent_id' => 6)); - OrderedScopedCategory::create(array('id' => 9 , 'company_id' => 2, 'name' => 'Child 5.1', 'lft' => 5 , 'rgt' => 6 , 'depth' => 2, 'parent_id' => 8)); - OrderedScopedCategory::create(array('id' => 10, 'company_id' => 2, 'name' => 'Child 6' , 'lft' => 8 , 'rgt' => 9 , 'depth' => 1, 'parent_id' => 6)); - - OrderedScopedCategory::reguard(); - - if ( DB::connection()->getDriverName() === 'pgsql' ) { - $tablePrefix = DB::connection()->getTablePrefix(); - - $sequenceName = $tablePrefix . 'categories_id_seq'; - - DB::connection()->statement('ALTER SEQUENCE ' . $sequenceName . ' RESTART WITH 11'); - } - } - -} diff --git a/tests/seeders/ClusterSeeder.php b/tests/seeders/ClusterSeeder.php deleted file mode 100644 index d708b705..00000000 --- a/tests/seeders/ClusterSeeder.php +++ /dev/null @@ -1,48 +0,0 @@ -delete(); - - Cluster::unguard(); - - Cluster::create(array('id' => '7461d8f5-2ea9-4788-99c4-9d0244f0bfb1', 'name' => 'Root 1' , 'lft' => 1 , 'rgt' => 10 , 'depth' => 0)); - Cluster::create(array('id' => '5d7ce1fd-6151-46d3-a5b3-0ebb9988dc57', 'name' => 'Child 1' , 'lft' => 2 , 'rgt' => 3 , 'depth' => 1, 'parent_id' => '7461d8f5-2ea9-4788-99c4-9d0244f0bfb1')); - Cluster::create(array('id' => '07c1fc8c-53b5-4fe7-b9c4-e09f266a455c', 'name' => 'Child 2' , 'lft' => 4 , 'rgt' => 7 , 'depth' => 1, 'parent_id' => '7461d8f5-2ea9-4788-99c4-9d0244f0bfb1')); - Cluster::create(array('id' => '3315a297-af87-4ad3-9fa5-19785407573d', 'name' => 'Child 2.1', 'lft' => 5 , 'rgt' => 6 , 'depth' => 2, 'parent_id' => '07c1fc8c-53b5-4fe7-b9c4-e09f266a455c')); - Cluster::create(array('id' => '054476d2-6830-4014-a181-4de010ef7114', 'name' => 'Child 3' , 'lft' => 8 , 'rgt' => 9 , 'depth' => 1, 'parent_id' => '7461d8f5-2ea9-4788-99c4-9d0244f0bfb1')); - Cluster::create(array('id' => '3bb62314-9e1e-49c6-a5cb-17a9ab9b1b9a', 'name' => 'Root 2' , 'lft' => 11 , 'rgt' => 12 , 'depth' => 0)); - - Cluster::reguard(); - } - - public function nestUptoAt($node, $levels=10, $attrs=array()) { - for($i=0; $i < $levels; $i++, $node=$new) { - $new = Cluster::create(array_merge($attrs, array('name' => "{$node->name}.1"))); - $new->makeChildOf($node); - } - } - -} - -class OrderedClusterSeeder { - - public function run() { - DB::table('clusters')->delete(); - - OrderedCluster::unguard(); - - OrderedCluster::create(array('id' => '7461d8f5-2ea9-4788-99c4-9d0244f0bfb1', 'name' => 'Root Z' , 'lft' => 1 , 'rgt' => 10 , 'depth' => 0)); - OrderedCluster::create(array('id' => '5d7ce1fd-6151-46d3-a5b3-0ebb9988dc57', 'name' => 'Child C' , 'lft' => 2 , 'rgt' => 3 , 'depth' => 1, 'parent_id' => '7461d8f5-2ea9-4788-99c4-9d0244f0bfb1')); - OrderedCluster::create(array('id' => '07c1fc8c-53b5-4fe7-b9c4-e09f266a455c', 'name' => 'Child G' , 'lft' => 4 , 'rgt' => 7 , 'depth' => 1, 'parent_id' => '7461d8f5-2ea9-4788-99c4-9d0244f0bfb1')); - OrderedCluster::create(array('id' => '3315a297-af87-4ad3-9fa5-19785407573d', 'name' => 'Child G.1', 'lft' => 5 , 'rgt' => 6 , 'depth' => 2, 'parent_id' => '07c1fc8c-53b5-4fe7-b9c4-e09f266a455c')); - OrderedCluster::create(array('id' => '054476d2-6830-4014-a181-4de010ef7114', 'name' => 'Child A' , 'lft' => 8 , 'rgt' => 9 , 'depth' => 1, 'parent_id' => '7461d8f5-2ea9-4788-99c4-9d0244f0bfb1')); - OrderedCluster::create(array('id' => '3bb62314-9e1e-49c6-a5cb-17a9ab9b1b9a', 'name' => 'Root A' , 'lft' => 11 , 'rgt' => 12 , 'depth' => 0)); - - OrderedCluster::reguard(); - } - -} diff --git a/tests/suite/BaumTestCase.php b/tests/suite/BaumTestCase.php deleted file mode 100644 index 68b6e445..00000000 --- a/tests/suite/BaumTestCase.php +++ /dev/null @@ -1,14 +0,0 @@ -assertEquals($ex, $ac, $message); - } - -} diff --git a/tests/suite/Category/CategoryColumnsTest.php b/tests/suite/Category/CategoryColumnsTest.php deleted file mode 100644 index 02f1bd3b..00000000 --- a/tests/suite/Category/CategoryColumnsTest.php +++ /dev/null @@ -1,131 +0,0 @@ -assertEquals(with(new Category)->getParentColumnName(), 'parent_id'); - } - - public function testGetQualifiedParentColumnName() { - $category = new Category; - - $this->assertEquals($category->getQualifiedParentColumnName(), 'categories.parent_id'); - } - - public function testGetParentId() { - $this->assertNull($this->categories('Root 1')->getParentId()); - - $this->assertEquals($this->categories('Child 1')->getParentId(), 1); - } - - public function testGetLeftColumnName() { - $category = new Category; - - $this->assertEquals($category->getLeftColumnName(), 'lft'); - } - - public function testGetQualifiedLeftColumnName() { - $category = new Category; - - $this->assertEquals($category->getQualifiedLeftColumnName(), 'categories.lft'); - } - - public function testGetLeft() { - $category = $this->categories('Root 1'); - - $this->assertEquals($category->getLeft(), 1); - } - - public function testGetRightColumnName() { - $category = new Category; - - $this->assertEquals($category->getRightColumnName(), 'rgt'); - } - - public function testGetQualifiedRightColumnName() { - $category = new Category; - - $this->assertEquals($category->getQualifiedRightColumnName(), 'categories.rgt'); - } - - public function testGetRight() { - $category = $this->categories('Root 1'); - - $this->assertEquals($category->getRight(), 10); - } - - public function testGetOrderColumName() { - $category = new Category; - - $this->assertEquals($category->getOrderColumnName(), $category->getLeftColumnName()); - } - - public function testGetQualifiedOrderColumnName() { - $category = new Category; - - $this->assertEquals($category->getQualifiedOrderColumnName(), $category->getQualifiedLeftColumnName()); - } - - public function testGetOrder() { - $category = $this->categories('Root 1'); - - $this->assertEquals($category->getOrder(), $category->getLeft()); - } - - public function testGetOrderColumnNameNonDefault() { - $category = new OrderedCategory; - - $this->assertEquals($category->getOrderColumnName(), 'name'); - } - - public function testGetQualifiedOrderColumnNameNonDefault() { - $category = new OrderedCategory; - - $this->assertEquals($category->getQualifiedOrderColumnName(), 'categories.name'); - } - - public function testGetOrderNonDefault() { - $category = $this->categories('Root 1', 'OrderedCategory'); - - $this->assertEquals($category->getOrder(), 'Root 1'); - } - - public function testGetScopedColumns() { - $category = new Category; - $this->assertEquals($category->getScopedColumns(), array()); - - $category = new ScopedCategory; - $this->assertEquals($category->getScopedColumns(), array('company_id')); - - $category = new MultiScopedCategory; - $this->assertEquals($category->getScopedColumns(), array('company_id', 'language')); - } - - public function testGetQualifiedScopedColumns() { - $category = new Category; - $this->assertEquals($category->getQualifiedScopedColumns(), array()); - - $category = new ScopedCategory; - $this->assertEquals($category->getQualifiedScopedColumns(), array('categories.company_id')); - - $category = new MultiScopedCategory; - $this->assertEquals($category->getQualifiedScopedColumns(), array('categories.company_id', 'categories.language')); - } - - public function testIsScoped() { - $category = new Category; - $this->assertFalse($category->isScoped()); - - $category = new ScopedCategory; - $this->assertTrue($category->isScoped()); - - $category = new MultiScopedCategory; - $this->assertTrue($category->isScoped()); - - $category = new OrderedCategory(); - $this->assertFalse($category->isScoped()); - } - -} diff --git a/tests/suite/Category/CategoryCustomEventsTest.php b/tests/suite/Category/CategoryCustomEventsTest.php deleted file mode 100644 index ec2ada15..00000000 --- a/tests/suite/Category/CategoryCustomEventsTest.php +++ /dev/null @@ -1,56 +0,0 @@ -categories('Child 1'); - $child3 = $this->categories('Child 3'); - - $dispatcher = Category::getEventDispatcher(); - - Category::setEventDispatcher($events = m::mock('\Illuminate\Events\Dispatcher')->makePartial()); - - $events->shouldReceive('until')->once()->with('eloquent.moving: '.get_class($child1), $child1)->andReturn(true); - - $events->shouldReceive('dispatch')->once()->with('eloquent.moved: '.get_class($child1), $child1)->andReturn(true); - - $child1->moveToRightOf($child3); - - Category::unsetEventDispatcher(); - Category::setEventDispatcher($dispatcher); - } - - public function testMovementHaltsWhenReturningFalseFromMoving() { - $unchanged = $this->categories('Child 2'); - - $dispatcher = Category::getEventDispatcher(); - - Category::setEventDispatcher($events = m::mock('Illuminate\Events\Dispatcher')->makePartial()); - $events->shouldReceive('until')->once()->with('eloquent.moving: '.get_class($unchanged), $unchanged)->andReturn(false); - - // Force "moving" to return false - Category::moving(function($node) { return false; }); - - $unchanged->makeRoot(); - - $unchanged->reload(); - - $this->assertEquals(1, $unchanged->getParentId()); - $this->assertEquals(1, $unchanged->getLevel()); - $this->assertEquals(4, $unchanged->getLeft()); - $this->assertEquals(7, $unchanged->getRight()); - - // Restore - Category::getEventDispatcher()->forget('eloquent.moving: '.get_class($unchanged)); - - Category::unsetEventDispatcher(); - Category::setEventDispatcher($dispatcher); - } - -} diff --git a/tests/suite/Category/CategoryHierarchyTest.php b/tests/suite/Category/CategoryHierarchyTest.php deleted file mode 100644 index bad299c6..00000000 --- a/tests/suite/Category/CategoryHierarchyTest.php +++ /dev/null @@ -1,705 +0,0 @@ -orderBy('lft')->get(); - - $this->assertEquals($results, $expected); - } - - public function testAllStaticWithCustomOrder() { - $results = OrderedCategory::all(); - $expected = OrderedCategory::query()->orderBy('name')->get(); - - $this->assertEquals($results, $expected); - } - - public function testRootsStatic() { - $query = Category::whereNull('parent_id')->get(); - - $roots = Category::roots()->get(); - - $this->assertEquals($query->count(), $roots->count()); - $this->assertCount(2, $roots); - - foreach($query->pluck('id')as $node) - $this->assertContains($node, $roots->pluck('id')); - } - - public function testRootsStaticWithCustomOrder() { - $category = OrderedCategory::create(array('name' => 'A new root is born')); - $category->syncOriginal(); // ¿? --> This should be done already !? - - $roots = OrderedCategory::roots()->get(); - - $this->assertCount(3, $roots); - $this->assertEquals($category->getAttributes(), $roots->first()->getAttributes()); - } - - public function testRootStatic() { - $this->assertEquals(Category::root(), $this->categories('Root 1')); - } - - public function testAllLeavesStatic() { - $allLeaves = Category::allLeaves()->get(); - - $this->assertCount(4, $allLeaves); - - $leaves = $allLeaves->pluck('name'); - - $this->assertContains('Child 1' , $leaves); - $this->assertContains('Child 2.1' , $leaves); - $this->assertContains('Child 3' , $leaves); - $this->assertContains('Root 2' , $leaves); - } - - public function testAllTrunksStatic() { - $allTrunks = Category::allTrunks()->get(); - - $this->assertCount(1, $allTrunks); - - $trunks = $allTrunks->pluck('name'); - $this->assertContains('Child 2', $trunks); - } - - public function testGetRoot() { - $this->assertEquals($this->categories('Root 1'), $this->categories('Root 1')->getRoot()); - $this->assertEquals($this->categories('Root 2'), $this->categories('Root 2')->getRoot()); - - $this->assertEquals($this->categories('Root 1'), $this->categories('Child 1')->getRoot()); - $this->assertEquals($this->categories('Root 1'), $this->categories('Child 2')->getRoot()); - $this->assertEquals($this->categories('Root 1'), $this->categories('Child 2.1')->getRoot()); - $this->assertEquals($this->categories('Root 1'), $this->categories('Child 3')->getRoot()); - } - - public function testGetRootEqualsSelfIfUnpersisted() { - $category = new Category; - - $this->assertEquals($category->getRoot(), $category); - } - - public function testGetRootEqualsValueIfSetIfUnpersisted() { - $parent = Category::roots()->first(); - - $child = new Category; - $child->setAttribute($child->getParentColumnName(), $parent->getKey()); - - $this->assertEquals($child->getRoot(), $parent); - } - - public function testIsRoot() { - $this->assertTrue($this->categories('Root 1')->isRoot()); - $this->assertTrue($this->categories('Root 2')->isRoot()); - - $this->assertFalse($this->categories('Child 1')->isRoot()); - $this->assertFalse($this->categories('Child 2')->isRoot()); - $this->assertFalse($this->categories('Child 2.1')->isRoot()); - $this->assertFalse($this->categories('Child 3')->isRoot()); - } - - public function testGetLeaves() { - $leaves = array($this->categories('Child 1'), $this->categories('Child 2.1'), $this->categories('Child 3')); - - $this->assertEquals($leaves, $this->categories('Root 1')->getLeaves()->all()); - } - - public function testGetLeavesInIteration() { - $node = $this->categories('Root 1'); - - $expectedIds = array(2, 4, 5); - - foreach($node->getLeaves() as $i => $leaf) - $this->assertEquals($expectedIds[$i], $leaf->getKey()); - } - - public function testGetTrunks() { - $trunks = array($this->categories('Child 2')); - - $this->assertEquals($trunks, $this->categories('Root 1')->getTrunks()->all()); - } - - public function testGetTrunksInIteration() { - $node = $this->categories('Root 1'); - - $expectedIds = array(3); - - foreach($node->getTrunks() as $i => $trunk) - $this->assertEquals($expectedIds[$i], $trunk->getKey()); - } - - public function testIsLeaf() { - $this->assertTrue($this->categories('Child 1')->isLeaf()); - $this->assertTrue($this->categories('Child 2.1')->isLeaf()); - $this->assertTrue($this->categories('Child 3')->isLeaf()); - $this->assertTrue($this->categories('Root 2')->isLeaf()); - - $this->assertFalse($this->categories('Root 1')->isLeaf()); - $this->assertFalse($this->categories('Child 2')->isLeaf()); - - $new = new Category; - $this->assertFalse($new->isLeaf()); - } - - public function testIsTrunk() { - $this->assertFalse($this->categories('Child 1')->isTrunk()); - $this->assertFalse($this->categories('Child 2.1')->isTrunk()); - $this->assertFalse($this->categories('Child 3')->isTrunk()); - $this->assertFalse($this->categories('Root 2')->isTrunk()); - - $this->assertFalse($this->categories('Root 1')->isTrunk()); - $this->assertTrue($this->categories('Child 2')->isTrunk()); - - $new = new Category; - $this->assertFalse($new->isTrunk()); - } - - public function testWithoutNodeScope() { - $child = $this->categories('Child 2.1'); - - $expected = array($this->categories('Root 1'), $child); - - $this->assertEquals($expected, $child->ancestorsAndSelf()->withoutNode($this->categories('Child 2'))->get()->all()); - } - - public function testWithoutSelfScope() { - $child = $this->categories('Child 2.1'); - - $expected = array($this->categories('Root 1'), $this->categories('Child 2')); - - $this->assertEquals($expected, $child->ancestorsAndSelf()->withoutSelf()->get()->all()); - } - - public function testWithoutRootScope() { - $child = $this->categories('Child 2.1'); - - $expected = array($this->categories('Child 2'), $child); - - $this->assertEquals($expected, $child->ancestorsAndSelf()->withoutRoot()->get()->all()); - } - - public function testLimitDepthScope() { - with(new CategorySeeder)->nestUptoAt($this->categories('Child 2.1'), 10); - - $node = $this->categories('Child 2'); - - $descendancy = $node->descendants()->pluck('id')->all(); - - $this->assertEmpty($node->descendants()->limitDepth(0)->pluck('id')->all()); - $this->assertEquals($node->getAttributes(), $node->descendantsAndSelf()->limitDepth(0)->first()->getAttributes()); - - $this->assertEquals(array_slice($descendancy, 0, 3), $node->descendants()->limitDepth(3)->pluck('id')->all()); - $this->assertEquals(array_slice($descendancy, 0, 5), $node->descendants()->limitDepth(5)->pluck('id')->all()); - $this->assertEquals(array_slice($descendancy, 0, 7), $node->descendants()->limitDepth(7)->pluck('id')->all()); - - $this->assertEquals($descendancy, $node->descendants()->limitDepth(1000)->pluck('id')->all()); - } - - public function testGetAncestorsAndSelf() { - $child = $this->categories('Child 2.1'); - - $expected = array($this->categories('Root 1'), $this->categories('Child 2'), $child); - - $this->assertEquals($expected, $child->getAncestorsAndSelf()->all()); - } - - public function testGetAncestorsAndSelfWithoutRoot() { - $child = $this->categories('Child 2.1'); - - $expected = array($this->categories('Child 2'), $child); - - $this->assertEquals($expected, $child->getAncestorsAndSelfWithoutRoot()->all()); - } - - public function testGetAncestors() { - $child = $this->categories('Child 2.1'); - - $expected = array($this->categories('Root 1'), $this->categories('Child 2')); - - $this->assertEquals($expected, $child->getAncestors()->all()); - } - - public function testGetAncestorsWithoutRoot() { - $child = $this->categories('Child 2.1'); - - $expected = array($this->categories('Child 2')); - - $this->assertEquals($expected, $child->getAncestorsWithoutRoot()->all()); - } - - public function testGetDescendantsAndSelf() { - $parent = $this->categories('Root 1'); - - $expected = array( - $parent, - $this->categories('Child 1'), - $this->categories('Child 2'), - $this->categories('Child 2.1'), - $this->categories('Child 3') - ); - - $this->assertCount(count($expected), $parent->getDescendantsAndSelf()); - - $this->assertEquals($expected, $parent->getDescendantsAndSelf()->all()); - } - - public function testGetDescendantsAndSelfWithLimit() { - with(new CategorySeeder)->nestUptoAt($this->categories('Child 2.1'), 3); - - $parent = $this->categories('Root 1'); - - $this->assertEquals(array($parent), $parent->getDescendantsAndSelf(0)->all()); - - $this->assertEquals(array( - $parent, - $this->categories('Child 1'), - $this->categories('Child 2'), - $this->categories('Child 3') - ), $parent->getDescendantsAndSelf(1)->all()); - - $this->assertEquals(array( - $parent, - $this->categories('Child 1'), - $this->categories('Child 2'), - $this->categories('Child 2.1'), - $this->categories('Child 3') - ), $parent->getDescendantsAndSelf(2)->all()); - - $this->assertEquals(array( - $parent, - $this->categories('Child 1'), - $this->categories('Child 2'), - $this->categories('Child 2.1'), - $this->categories('Child 2.1.1'), - $this->categories('Child 3') - ), $parent->getDescendantsAndSelf(3)->all()); - - $this->assertEquals(array( - $parent, - $this->categories('Child 1'), - $this->categories('Child 2'), - $this->categories('Child 2.1'), - $this->categories('Child 2.1.1'), - $this->categories('Child 2.1.1.1'), - $this->categories('Child 3') - ), $parent->getDescendantsAndSelf(4)->all()); - - $this->assertEquals(array( - $parent, - $this->categories('Child 1'), - $this->categories('Child 2'), - $this->categories('Child 2.1'), - $this->categories('Child 2.1.1'), - $this->categories('Child 2.1.1.1'), - $this->categories('Child 2.1.1.1.1'), - $this->categories('Child 3') - ), $parent->getDescendantsAndSelf(10)->all()); - } - - public function testGetDescendants() { - $parent = $this->categories('Root 1'); - - $expected = array( - $this->categories('Child 1'), - $this->categories('Child 2'), - $this->categories('Child 2.1'), - $this->categories('Child 3') - ); - - $this->assertCount(count($expected), $parent->getDescendants()); - - $this->assertEquals($expected, $parent->getDescendants()->all()); - } - - public function testGetDescendantsWithLimit() { - with(new CategorySeeder)->nestUptoAt($this->categories('Child 2.1'), 3); - - $parent = $this->categories('Root 1'); - - $this->assertEmpty($parent->getDescendants(0)->all()); - - $this->assertEquals(array( - $this->categories('Child 1'), - $this->categories('Child 2'), - $this->categories('Child 3') - ), $parent->getDescendants(1)->all()); - - $this->assertEquals(array( - $this->categories('Child 1'), - $this->categories('Child 2'), - $this->categories('Child 2.1'), - $this->categories('Child 3') - ), $parent->getDescendants(2)->all()); - - $this->assertEquals(array( - $this->categories('Child 1'), - $this->categories('Child 2'), - $this->categories('Child 2.1'), - $this->categories('Child 2.1.1'), - $this->categories('Child 3') - ), $parent->getDescendants(3)->all()); - - $this->assertEquals(array( - $this->categories('Child 1'), - $this->categories('Child 2'), - $this->categories('Child 2.1'), - $this->categories('Child 2.1.1'), - $this->categories('Child 2.1.1.1'), - $this->categories('Child 3') - ), $parent->getDescendants(4)->all()); - - $this->assertEquals(array( - $this->categories('Child 1'), - $this->categories('Child 2'), - $this->categories('Child 2.1'), - $this->categories('Child 2.1.1'), - $this->categories('Child 2.1.1.1'), - $this->categories('Child 2.1.1.1.1'), - $this->categories('Child 3') - ), $parent->getDescendants(5)->all()); - - $this->assertEquals(array( - $this->categories('Child 1'), - $this->categories('Child 2'), - $this->categories('Child 2.1'), - $this->categories('Child 2.1.1'), - $this->categories('Child 2.1.1.1'), - $this->categories('Child 2.1.1.1.1'), - $this->categories('Child 3') - ), $parent->getDescendants(10)->all()); - } - - public function testDescendantsRecursesChildren() { - $a = Category::create(array('name' => 'A')); - $b = Category::create(array('name' => 'B')); - $c = Category::create(array('name' => 'C')); - - // a > b > c - $b->makeChildOf($a); - $c->makeChildOf($b); - - $a->reload(); $b->reload(); $c->reload(); - - $this->assertEquals(1, $a->children()->count()); - $this->assertEquals(1, $b->children()->count()); - $this->assertEquals(2, $a->descendants()->count()); - } - - public function testGetImmediateDescendants() { - $expected = array($this->categories('Child 1'), $this->categories('Child 2'), $this->categories('Child 3')); - - $this->assertEquals($expected, $this->categories('Root 1')->getImmediateDescendants()->all()); - - $this->assertEquals(array($this->categories('Child 2.1')), $this->categories('Child 2')->getImmediateDescendants()->all()); - - $this->assertEmpty($this->categories('Root 2')->getImmediateDescendants()->all()); - } - - public function testIsSelfOrAncestorOf() { - $this->assertTrue($this->categories('Root 1')->isSelfOrAncestorOf($this->categories('Child 1'))); - $this->assertTrue($this->categories('Root 1')->isSelfOrAncestorOf($this->categories('Child 2.1'))); - $this->assertTrue($this->categories('Child 2')->isSelfOrAncestorOf($this->categories('Child 2.1'))); - $this->assertFalse($this->categories('Child 2.1')->isSelfOrAncestorOf($this->categories('Child 2'))); - $this->assertFalse($this->categories('Child 1')->isSelfOrAncestorOf($this->categories('Child 2'))); - $this->assertTrue($this->categories('Child 1')->isSelfOrAncestorOf($this->categories('Child 1'))); - } - - public function testIsAncestorOf() { - $this->assertTrue($this->categories('Root 1')->isAncestorOf($this->categories('Child 1'))); - $this->assertTrue($this->categories('Root 1')->isAncestorOf($this->categories('Child 2.1'))); - $this->assertTrue($this->categories('Child 2')->isAncestorOf($this->categories('Child 2.1'))); - $this->assertFalse($this->categories('Child 2.1')->isAncestorOf($this->categories('Child 2'))); - $this->assertFalse($this->categories('Child 1')->isAncestorOf($this->categories('Child 2'))); - $this->assertFalse($this->categories('Child 1')->isAncestorOf($this->categories('Child 1'))); - } - - public function testIsSelfOrDescendantOf() { - $this->assertTrue($this->categories('Child 1')->isSelfOrDescendantOf($this->categories('Root 1'))); - $this->assertTrue($this->categories('Child 2.1')->isSelfOrDescendantOf($this->categories('Root 1'))); - $this->assertTrue($this->categories('Child 2.1')->isSelfOrDescendantOf($this->categories('Child 2'))); - $this->assertFalse($this->categories('Child 2')->isSelfOrDescendantOf($this->categories('Child 2.1'))); - $this->assertFalse($this->categories('Child 2')->isSelfOrDescendantOf($this->categories('Child 1'))); - $this->assertTrue($this->categories('Child 1')->isSelfOrDescendantOf($this->categories('Child 1'))); - } - - public function testIsDescendantOf() { - $this->assertTrue($this->categories('Child 1')->isDescendantOf($this->categories('Root 1'))); - $this->assertTrue($this->categories('Child 2.1')->isDescendantOf($this->categories('Root 1'))); - $this->assertTrue($this->categories('Child 2.1')->isDescendantOf($this->categories('Child 2'))); - $this->assertFalse($this->categories('Child 2')->isDescendantOf($this->categories('Child 2.1'))); - $this->assertFalse($this->categories('Child 2')->isDescendantOf($this->categories('Child 1'))); - $this->assertFalse($this->categories('Child 1')->isDescendantOf($this->categories('Child 1'))); - } - - public function testGetSiblingsAndSelf() { - $child = $this->categories('Child 2'); - - $expected = array($this->categories('Child 1'), $child, $this->categories('Child 3')); - $this->assertEquals($expected, $child->getSiblingsAndSelf()->all()); - - $expected = array($this->categories('Root 1'), $this->categories('Root 2')); - $this->assertEquals($expected, $this->categories('Root 1')->getSiblingsAndSelf()->all()); - } - - public function testGetSiblings() { - $child = $this->categories('Child 2'); - - $expected = array($this->categories('Child 1'), $this->categories('Child 3')); - - $this->assertEquals($expected, $child->getSiblings()->all()); - } - - public function testGetLeftSibling() { - $this->assertEquals($this->categories('Child 1'), $this->categories('Child 2')->getLeftSibling()); - $this->assertEquals($this->categories('Child 2'), $this->categories('Child 3')->getLeftSibling()); - } - - public function testGetLeftSiblingOfFirstRootIsNull() { - $this->assertNull($this->categories('Root 1')->getLeftSibling()); - } - - public function testGetLeftSiblingWithNoneIsNull() { - $this->assertNull($this->categories('Child 2.1')->getLeftSibling()); - } - - public function testGetLeftSiblingOfLeftmostNodeIsNull() { - $this->assertNull($this->categories('Child 1')->getLeftSibling()); - } - - public function testGetRightSibling() { - $this->assertEquals($this->categories('Child 3'), $this->categories('Child 2')->getRightSibling()); - $this->assertEquals($this->categories('Child 2'), $this->categories('Child 1')->getRightSibling()); - } - - public function testGetRightSiblingOfRoots() { - $this->assertEquals($this->categories('Root 2'), $this->categories('Root 1')->getRightSibling()); - $this->assertNull($this->categories('Root 2')->getRightSibling()); - } - - public function testGetRightSiblingWithNoneIsNull() { - $this->assertNull($this->categories('Child 2.1')->getRightSibling()); - } - - public function testGetRightSiblingOfRightmostNodeIsNull() { - $this->assertNull($this->categories('Child 3')->getRightSibling()); - } - - public function testInsideSubtree() { - $this->assertFalse($this->categories('Child 1')->insideSubtree($this->categories('Root 2'))); - $this->assertFalse($this->categories('Child 2')->insideSubtree($this->categories('Root 2'))); - $this->assertFalse($this->categories('Child 3')->insideSubtree($this->categories('Root 2'))); - - $this->assertTrue($this->categories('Child 1')->insideSubtree($this->categories('Root 1'))); - $this->assertTrue($this->categories('Child 2')->insideSubtree($this->categories('Root 1'))); - $this->assertTrue($this->categories('Child 2.1')->insideSubtree($this->categories('Root 1'))); - $this->assertTrue($this->categories('Child 3')->insideSubtree($this->categories('Root 1'))); - - $this->assertTrue($this->categories('Child 2.1')->insideSubtree($this->categories('Child 2'))); - $this->assertFalse($this->categories('Child 2.1')->insideSubtree($this->categories('Root 2'))); - } - - public function testGetLevel() { - $this->assertEquals(0, $this->categories('Root 1')->getLevel()); - $this->assertEquals(1, $this->categories('Child 1')->getLevel()); - $this->assertEquals(2, $this->categories('Child 2.1')->getLevel()); - } - - public function testToHierarchyReturnsAnEloquentCollection() { - $categories = Category::all()->toHierarchy(); - - $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $categories); - } - - public function testToHierarchyReturnsHierarchicalData() { - $categories = Category::all()->toHierarchy(); - - $this->assertEquals(2, $categories->count()); - - $first = $categories->first(); - $this->assertEquals('Root 1', $first->name); - $this->assertEquals(3, $first->children->count()); - - $first_lvl2 = $first->children->first(); - $this->assertEquals('Child 1', $first_lvl2->name); - $this->assertEquals(0, $first_lvl2->children->count()); - } - - public function testToHierarchyNestsCorrectly() { - // Prune all categories - Category::query()->delete(); - - // Build a sample tree structure: - // - // - A - // |- A.1 - // |- A.2 - // - B - // |- B.1 - // |- B.2 - // |- B.2.1 - // |- B.2.2 - // |- B.2.2.1 - // |- B.2.3 - // |- B.3 - // - C - // |- C.1 - // |- C.2 - // - D - // - $a = Category::create(array('name' => 'A')); - $b = Category::create(array('name' => 'B')); - $c = Category::create(array('name' => 'C')); - $d = Category::create(array('name' => 'D')); - - $ch = Category::create(array('name' => 'A.1')); - $ch->makeChildOf($a); - - $ch = Category::create(array('name' => 'A.2')); - $ch->makeChildOf($a); - - $ch = Category::create(array('name' => 'B.1')); - $ch->makeChildOf($b); - - $ch = Category::create(array('name' => 'B.2')); - $ch->makeChildOf($b); - - $ch2 = Category::create(array('name' => 'B.2.1')); - $ch2->makeChildOf($ch); - - $ch2 = Category::create(array('name' => 'B.2.2')); - $ch2->makeChildOf($ch); - - $ch3 = Category::create(array('name' => 'B.2.2.1')); - $ch3->makeChildOf($ch2); - - $ch2 = Category::create(array('name' => 'B.2.3')); - $ch2->makeChildOf($ch); - - $ch = Category::create(array('name' => 'B.3')); - $ch->makeChildOf($b); - - $ch = Category::create(array('name' => 'C.1')); - $ch->makeChildOf($c); - - $ch = Category::create(array('name' => 'C.2')); - $ch->makeChildOf($c); - - $this->assertTrue(Category::isValidNestedSet()); - - // Build expectations (expected trees/subtrees) - $expectedWholeTree = array( - 'A' => array ( 'A.1' => null, 'A.2' => null ), - 'B' => array ( - 'B.1' => null, - 'B.2' => - array ( - 'B.2.1' => null, - 'B.2.2' => array ( 'B.2.2.1' => null ), - 'B.2.3' => null, - ), - 'B.3' => null, - ), - 'C' => array ( 'C.1' => null, 'C.2' => null ), - 'D' => null - ); - - $expectedSubtreeA = array('A' => array ( 'A.1' => null, 'A.2' => null )); - - $expectedSubtreeB = array( - 'B' => array ( - 'B.1' => null, - 'B.2' => - array ( - 'B.2.1' => null, - 'B.2.2' => array ( 'B.2.2.1' => null ), - 'B.2.3' => null - ), - 'B.3' => null - ) - ); - - $expectedSubtreeC = array( 'C.1' => null, 'C.2' => null ); - - $expectedSubtreeD = array('D' => null); - - // Perform assertions - $wholeTree = hmap(Category::all()->toHierarchy()->toArray()); - $this->assertArraysAreEqual($expectedWholeTree, $wholeTree); - - $subtreeA = hmap($this->categories('A')->getDescendantsAndSelf()->toHierarchy()->toArray()); - $this->assertArraysAreEqual($expectedSubtreeA, $subtreeA); - - $subtreeB = hmap($this->categories('B')->getDescendantsAndSelf()->toHierarchy()->toArray()); - $this->assertArraysAreEqual($expectedSubtreeB, $subtreeB); - - $subtreeC = hmap($this->categories('C')->getDescendants()->toHierarchy()->toArray()); - $this->assertArraysAreEqual($expectedSubtreeC, $subtreeC); - - $subtreeD = hmap($this->categories('D')->getDescendantsAndSelf()->toHierarchy()->toArray()); - $this->assertArraysAreEqual($expectedSubtreeD, $subtreeD); - - $this->assertTrue($this->categories('D')->getDescendants()->toHierarchy()->isEmpty()); - } - - public function testToHierarchyNestsCorrectlyNotSequential() { - $parent = $this->categories('Child 1'); - - $parent->children()->create(array('name' => 'Child 1.1')); - - $parent->children()->create(array('name' => 'Child 1.2')); - - $this->assertTrue(Category::isValidNestedSet()); - - $expected = array( - 'Child 1' => array( - 'Child 1.1' => null, - 'Child 1.2' => null - ) - ); - - $parent->reload(); - $this->assertArraysAreEqual($expected, hmap($parent->getDescendantsAndSelf()->toHierarchy()->toArray())); - } - - public function testToHierarchyNestsCorrectlyWithOrder() { - with(new OrderedCategorySeeder)->run(); - - $expectedWhole = array( - 'Root A' => null, - 'Root Z' => array( - 'Child A' => null, - 'Child C' => null, - 'Child G' => array( 'Child G.1' => null ) - ) - ); - - $this->assertArraysAreEqual($expectedWhole, hmap(OrderedCategory::all()->toHierarchy()->toArray())); - - $expectedSubtreeZ = array( - 'Root Z' => array( - 'Child A' => null, - 'Child C' => null, - 'Child G' => array( 'Child G.1' => null ) - ) - ); - $this->assertArraysAreEqual($expectedSubtreeZ, hmap($this->categories('Root Z', 'OrderedCategory')->getDescendantsAndSelf()->toHierarchy()->toArray())); - } - - public function testGetNestedList() { - $seperator = ' '; - $nestedList = Category::getNestedList('name', 'id', $seperator); - - $expected = array( - 1 => str_repeat($seperator, 0). 'Root 1', - 2 => str_repeat($seperator, 1). 'Child 1', - 3 => str_repeat($seperator, 1). 'Child 2', - 4 => str_repeat($seperator, 2). 'Child 2.1', - 5 => str_repeat($seperator, 1). 'Child 3', - 6 => str_repeat($seperator, 0). 'Root 2', - ); - - $this->assertArraysAreEqual($expected, $nestedList); - } - -} diff --git a/tests/suite/Category/CategoryMovementTest.php b/tests/suite/Category/CategoryMovementTest.php deleted file mode 100644 index ddb821fd..00000000 --- a/tests/suite/Category/CategoryMovementTest.php +++ /dev/null @@ -1,528 +0,0 @@ -categories('Child 2')->moveLeft(); - - $this->assertNull($this->categories('Child 2')->getLeftSibling()); - - $this->assertEquals($this->categories('Child 1'), $this->categories('Child 2')->getRightSibling()); - - $this->assertTrue(Category::isValidNestedSet()); - } - - /** - * @expectedException Baum\MoveNotPossibleException - */ - public function testMoveLeftRaisesAnExceptionWhenNotPossible() { - $node = $this->categories('Child 2'); - - $node->moveLeft(); - $node->moveLeft(); - } - - public function testMoveLeftDoesNotChangeDepth() { - $this->categories('Child 2')->moveLeft(); - - $this->assertEquals(1, $this->categories('Child 2')->getDepth()); - $this->assertEquals(2, $this->categories('Child 2.1')->getDepth()); - } - - public function testMoveLeftWithSubtree() { - $this->categories('Root 2')->moveLeft(); - - $this->assertNull($this->categories('Root 2')->getLeftSibling()); - $this->assertEquals($this->categories('Root 1'), $this->categories('Root 2')->getRightSibling()); - $this->assertTrue(Category::isValidNestedSet()); - - $this->assertEquals(0, $this->categories('Root 1')->getDepth()); - $this->assertEquals(0, $this->categories('Root 2')->getDepth()); - - $this->assertEquals(1, $this->categories('Child 1')->getDepth()); - $this->assertEquals(1, $this->categories('Child 2')->getDepth()); - $this->assertEquals(1, $this->categories('Child 3')->getDepth()); - - $this->assertEquals(2, $this->categories('Child 2.1')->getDepth()); - } - - public function testMoveToLeftOf() { - $this->categories('Child 3')->moveToLeftOf($this->categories('Child 1')); - - $this->assertNull($this->categories('Child 3')->getLeftSibling()); - - $this->assertEquals($this->categories('Child 1'), $this->categories('Child 3')->getRightSibling()); - - $this->assertTrue(Category::isValidNestedSet()); - } - - /** - * @expectedException Baum\MoveNotPossibleException - */ - public function testMoveToLeftOfRaisesAnExceptionWhenNotPossible() { - $this->categories('Child 1')->moveToLeftOf($this->categories('Child 1')->getLeftSibling()); - } - - public function testMoveToLeftOfDoesNotChangeDepth() { - $this->categories('Child 2')->moveToLeftOf($this->categories('Child 1')); - - $this->assertEquals(1, $this->categories('Child 2')->getDepth()); - $this->assertEquals(2, $this->categories('Child 2.1')->getDepth()); - } - - public function testMoveToLeftOfWithSubtree() { - $this->categories('Root 2')->moveToLeftOf($this->categories('Root 1')); - - $this->assertNull($this->categories('Root 2')->getLeftSibling()); - $this->assertEquals($this->categories('Root 1'), $this->categories('Root 2')->getRightSibling()); - $this->assertTrue(Category::isValidNestedSet()); - - $this->assertEquals(0, $this->categories('Root 1')->getDepth()); - $this->assertEquals(0, $this->categories('Root 2')->getDepth()); - - $this->assertEquals(1, $this->categories('Child 1')->getDepth()); - $this->assertEquals(1, $this->categories('Child 2')->getDepth()); - $this->assertEquals(1, $this->categories('Child 3')->getDepth()); - - $this->assertEquals(2, $this->categories('Child 2.1')->getDepth()); - } - - public function testMoveRight() { - $this->categories('Child 2')->moveRight(); - - $this->assertNull($this->categories('Child 2')->getRightSibling()); - - $this->assertEquals($this->categories('Child 3'), $this->categories('Child 2')->getLeftSibling()); - - $this->assertTrue(Category::isValidNestedSet()); - } - - /** - * @expectedException Baum\MoveNotPossibleException - */ - public function testMoveRightRaisesAnExceptionWhenNotPossible() { - $node = $this->categories('Child 2'); - - $node->moveRight(); - $node->moveRight(); - } - - public function testMoveRightDoesNotChangeDepth() { - $this->categories('Child 2')->moveRight(); - - $this->assertEquals(1, $this->categories('Child 2')->getDepth()); - $this->assertEquals(2, $this->categories('Child 2.1')->getDepth()); - } - - public function testMoveRightWithSubtree() { - $this->categories('Root 1')->moveRight(); - - $this->assertNull($this->categories('Root 1')->getRightSibling()); - $this->assertEquals($this->categories('Root 2'), $this->categories('Root 1')->getLeftSibling()); - $this->assertTrue(Category::isValidNestedSet()); - - $this->assertEquals(0, $this->categories('Root 1')->getDepth()); - $this->assertEquals(0, $this->categories('Root 2')->getDepth()); - - $this->assertEquals(1, $this->categories('Child 1')->getDepth()); - $this->assertEquals(1, $this->categories('Child 2')->getDepth()); - $this->assertEquals(1, $this->categories('Child 3')->getDepth()); - - $this->assertEquals(2, $this->categories('Child 2.1')->getDepth()); - } - - public function testMoveToRightOf() { - $this->categories('Child 1')->moveToRightOf($this->categories('Child 3')); - - $this->assertNull($this->categories('Child 1')->getRightSibling()); - - $this->assertEquals($this->categories('Child 3'), $this->categories('Child 1')->getLeftSibling()); - - $this->assertTrue(Category::isValidNestedSet()); - } - - /** - * @expectedException Baum\MoveNotPossibleException - */ - public function testMoveToRightOfRaisesAnExceptionWhenNotPossible() { - $this->categories('Child 3')->moveToRightOf($this->categories('Child 3')->getRightSibling()); - } - - public function testMoveToRightOfDoesNotChangeDepth() { - $this->categories('Child 2')->moveToRightOf($this->categories('Child 3')); - - $this->assertEquals(1, $this->categories('Child 2')->getDepth()); - $this->assertEquals(2, $this->categories('Child 2.1')->getDepth()); - } - - public function testMoveToRightOfWithSubtree() { - $this->categories('Root 1')->moveToRightOf($this->categories('Root 2')); - - $this->assertNull($this->categories('Root 1')->getRightSibling()); - $this->assertEquals($this->categories('Root 2'), $this->categories('Root 1')->getLeftSibling()); - $this->assertTrue(Category::isValidNestedSet()); - - $this->assertEquals(0, $this->categories('Root 1')->getDepth()); - $this->assertEquals(0, $this->categories('Root 2')->getDepth()); - - $this->assertEquals(1, $this->categories('Child 1')->getDepth()); - $this->assertEquals(1, $this->categories('Child 2')->getDepth()); - $this->assertEquals(1, $this->categories('Child 3')->getDepth()); - - $this->assertEquals(2, $this->categories('Child 2.1')->getDepth()); - } - - public function testMakeRoot() { - $this->categories('Child 2')->makeRoot(); - - $newRoot = $this->categories('Child 2'); - - $this->assertNull($newRoot->parent()->first()); - $this->assertEquals(0, $newRoot->getLevel()); - $this->assertEquals(9, $newRoot->getLeft()); - $this->assertEquals(12, $newRoot->getRight()); - - $this->assertEquals(1, $this->categories('Child 2.1')->getLevel()); - - $this->assertTrue(Category::isValidNestedSet()); - } - - public function testNullifyParentColumnMakesItRoot() { - $node = $this->categories('Child 2'); - - $node->parent_id = null; - - $node->save(); - - $this->assertNull($node->parent()->first()); - $this->assertEquals(0, $node->getLevel()); - $this->assertEquals(9, $node->getLeft()); - $this->assertEquals(12, $node->getRight()); - - $this->assertEquals(1, $this->categories('Child 2.1')->getLevel()); - - $this->assertTrue(Category::isValidNestedSet()); - } - - public function testNullifyParentColumnOnNewNodes() { - $node = new Category(['name' => 'Root 3']); - - $node->parent_id = null; - - $node->save(); - - $node->reload(); - - $this->assertNull($node->parent()->first()); - $this->assertEquals(0, $node->getLevel()); - $this->assertEquals(13, $node->getLeft()); - $this->assertEquals(14, $node->getRight()); - - $this->assertTrue(Category::isValidNestedSet()); - } - - public function testNewCategoryWithNullParent() { - $node = new Category(['name' => 'Root 3']); - $this->assertTrue($node->isRoot()); - - $node->save(); - $this->assertTrue($node->isRoot()); - - $node->makeRoot(); - $this->assertTrue($node->isRoot()); - } - - public function testMakeChildOf() { - $this->categories('Child 1')->makeChildOf($this->categories('Child 3')); - - $this->assertEquals($this->categories('Child 3'), $this->categories('Child 1')->parent()->first()); - - $this->assertTrue(Category::isValidNestedSet()); - } - - public function testMakeChildOfAppendsAtTheEnd() { - $newChild = Category::create(array('name' => 'Child 4')); - - $newChild->makeChildOf($this->categories('Root 1')); - - $lastChild = $this->categories('Root 1')->children()->get()->last(); - $this->assertEquals($newChild->getAttributes(), $lastChild->getAttributes()); - - $this->assertTrue(Category::isValidNestedSet()); - } - - public function testMakeChildOfMovesWithSubtree() { - $this->categories('Child 2')->makeChildOf($this->categories('Child 1')); - - $this->assertTrue(Category::isValidNestedSet()); - - $this->assertEquals($this->categories('Child 1')->getKey(), $this->categories('Child 2')->getParentId()); - - $this->assertEquals(3, $this->categories('Child 2')->getLeft()); - $this->assertEquals(6, $this->categories('Child 2')->getRight()); - - $this->assertEquals(2, $this->categories('Child 1')->getLeft()); - $this->assertEquals(7, $this->categories('Child 1')->getRight()); - } - - public function testMakeChildOfSwappingRoots() { - $newRoot = Category::create(array('name' => 'Root 3')); - - $this->assertEquals(13, $newRoot->getLeft()); - $this->assertEquals(14, $newRoot->getRight()); - - $this->categories('Root 2')->makeChildOf($newRoot); - - $this->assertTrue(Category::isValidNestedSet()); - - $this->assertEquals($newRoot->getKey(), $this->categories('Root 2')->getParentId()); - - $this->assertEquals(12, $this->categories('Root 2')->getLeft()); - $this->assertEquals(13, $this->categories('Root 2')->getRight()); - - $this->assertEquals(11, $newRoot->getLeft()); - $this->assertEquals(14, $newRoot->getRight()); - } - - public function testMakeChildOfSwappingRootsWithSubtrees() { - $newRoot = Category::create(array('name' => 'Root 3')); - - $this->categories('Root 1')->makeChildOf($newRoot); - - $this->assertTrue(Category::isValidNestedSet()); - - $this->assertEquals($newRoot->getKey(), $this->categories('Root 1')->getParentId()); - - $this->assertEquals(4, $this->categories('Root 1')->getLeft()); - $this->assertEquals(13, $this->categories('Root 1')->getRight()); - - $this->assertEquals(8, $this->categories('Child 2.1')->getLeft()); - $this->assertEquals(9, $this->categories('Child 2.1')->getRight()); - } - - public function testMakeFirstChildOf() { - $this->categories('Child 1')->makeFirstChildOf($this->categories('Child 3')); - - $this->assertEquals($this->categories('Child 3'), $this->categories('Child 1')->parent()->first()); - - $this->assertTrue(Category::isValidNestedSet()); - } - - public function testMakeFirstChildOfAppendsAtTheBeginning() { - $newChild = Category::create(array('name' => 'Child 4')); - - $newChild->makeFirstChildOf($this->categories('Root 1')); - - $lastChild = $this->categories('Root 1')->children()->get()->first(); - $this->assertEquals($newChild->getAttributes(), $lastChild->getAttributes()); - - $this->assertTrue(Category::isValidNestedSet()); - } - - public function testMakeFirstChildOfMovesWithSubtree() { - $this->categories('Child 2')->makeFirstChildOf($this->categories('Child 1')); - - $this->assertTrue(Category::isValidNestedSet()); - - $this->assertEquals($this->categories('Child 1')->getKey(), $this->categories('Child 2')->getParentId()); - - $this->assertEquals(3, $this->categories('Child 2')->getLeft()); - $this->assertEquals(6, $this->categories('Child 2')->getRight()); - - $this->assertEquals(2, $this->categories('Child 1')->getLeft()); - $this->assertEquals(7, $this->categories('Child 1')->getRight()); - } - - public function testMakeFirstChildOfSwappingRoots() { - $newRoot = Category::create(array('name' => 'Root 3')); - - $this->assertEquals(13, $newRoot->getLeft()); - $this->assertEquals(14, $newRoot->getRight()); - - $this->categories('Root 2')->makeFirstChildOf($newRoot); - - $this->assertTrue(Category::isValidNestedSet()); - - $this->assertEquals($newRoot->getKey(), $this->categories('Root 2')->getParentId()); - - $this->assertEquals(12, $this->categories('Root 2')->getLeft()); - $this->assertEquals(13, $this->categories('Root 2')->getRight()); - - $this->assertEquals(11, $newRoot->getLeft()); - $this->assertEquals(14, $newRoot->getRight()); - } - - public function testMakeFirstChildOfSwappingRootsWithSubtrees() { - $newRoot = Category::create(array('name' => 'Root 3')); - - $this->categories('Root 1')->makeFirstChildOf($newRoot); - - $this->assertTrue(Category::isValidNestedSet()); - - $this->assertEquals($newRoot->getKey(), $this->categories('Root 1')->getParentId()); - - $this->assertEquals(4, $this->categories('Root 1')->getLeft()); - $this->assertEquals(13, $this->categories('Root 1')->getRight()); - - $this->assertEquals(8, $this->categories('Child 2.1')->getLeft()); - $this->assertEquals(9, $this->categories('Child 2.1')->getRight()); - } - - public function testMakeLastChildOf() { - $this->categories('Child 1')->makeLastChildOf($this->categories('Child 3')); - - $this->assertEquals($this->categories('Child 3'), $this->categories('Child 1')->parent()->first()); - - $this->assertTrue(Category::isValidNestedSet()); - } - - public function testMakeLastChildOfAppendsAtTheEnd() { - $newChild = Category::create(array('name' => 'Child 4')); - - $newChild->makeLastChildOf($this->categories('Root 1')); - - $lastChild = $this->categories('Root 1')->children()->get()->last(); - $this->assertEquals($newChild->getAttributes(), $lastChild->getAttributes()); - - $this->assertTrue(Category::isValidNestedSet()); - } - - public function testMakeLastChildOfMovesWithSubtree() { - $this->categories('Child 2')->makeLastChildOf($this->categories('Child 1')); - - $this->assertTrue(Category::isValidNestedSet()); - - $this->assertEquals($this->categories('Child 1')->getKey(), $this->categories('Child 2')->getParentId()); - - $this->assertEquals(3, $this->categories('Child 2')->getLeft()); - $this->assertEquals(6, $this->categories('Child 2')->getRight()); - - $this->assertEquals(2, $this->categories('Child 1')->getLeft()); - $this->assertEquals(7, $this->categories('Child 1')->getRight()); - } - - public function testMakeLastChildOfSwappingRoots() { - $newRoot = Category::create(array('name' => 'Root 3')); - - $this->assertEquals(13, $newRoot->getLeft()); - $this->assertEquals(14, $newRoot->getRight()); - - $this->categories('Root 2')->makeLastChildOf($newRoot); - - $this->assertTrue(Category::isValidNestedSet()); - - $this->assertEquals($newRoot->getKey(), $this->categories('Root 2')->getParentId()); - - $this->assertEquals(12, $this->categories('Root 2')->getLeft()); - $this->assertEquals(13, $this->categories('Root 2')->getRight()); - - $this->assertEquals(11, $newRoot->getLeft()); - $this->assertEquals(14, $newRoot->getRight()); - } - - public function testMakeLastChildOfSwappingRootsWithSubtrees() { - $newRoot = Category::create(array('name' => 'Root 3')); - - $this->categories('Root 1')->makeLastChildOf($newRoot); - - $this->assertTrue(Category::isValidNestedSet()); - - $this->assertEquals($newRoot->getKey(), $this->categories('Root 1')->getParentId()); - - $this->assertEquals(4, $this->categories('Root 1')->getLeft()); - $this->assertEquals(13, $this->categories('Root 1')->getRight()); - - $this->assertEquals(8, $this->categories('Child 2.1')->getLeft()); - $this->assertEquals(9, $this->categories('Child 2.1')->getRight()); - } - - /** - * @expectedException Baum\MoveNotPossibleException - */ - public function testUnpersistedNodeCannotBeMoved() { - $unpersisted = new Category(array('name' => 'Unpersisted')); - - $unpersisted->moveToRightOf($this->categories('Root 1')); - } - - /** - * @expectedException Baum\MoveNotPossibleException - */ - public function testUnpersistedNodeCannotBeMadeChild() { - $unpersisted = new Category(array('name' => 'Unpersisted')); - - $unpersisted->makeChildOf($this->categories('Root 1')); - } - - /** - * @expectedException Baum\MoveNotPossibleException - */ - public function testNodesCannotBeMovedToItself() { - $node = $this->categories('Child 1'); - - $node->moveToRightOf($node); - } - - /** - * @expectedException Baum\MoveNotPossibleException - */ - public function testNodesCannotBeMadeChildOfThemselves() { - $node = $this->categories('Child 1'); - - $node->makeChildOf($node); - } - - /** - * @expectedException Baum\MoveNotPossibleException - */ - public function testNodesCannotBeMovedToDescendantsOfThemselves() { - $node = $this->categories('Root 1'); - - $node->makeChildOf($this->categories('Child 2.1')); - } - - public function testDepthIsUpdatedWhenMadeChild() { - $a = Category::create(array('name' => 'A')); - $b = Category::create(array('name' => 'B')); - $c = Category::create(array('name' => 'C')); - $d = Category::create(array('name' => 'D')); - - // a > b > c > d - $b->makeChildOf($a); - $c->makeChildOf($b); - $d->makeChildOf($c); - - $a->reload(); - $b->reload(); - $c->reload(); - $d->reload(); - - $this->assertEquals(0, $a->getDepth()); - $this->assertEquals(1, $b->getDepth()); - $this->assertEquals(2, $c->getDepth()); - $this->assertEquals(3, $d->getDepth()); - } - - public function testDepthIsUpdatedOnDescendantsWhenParentMoves() { - $a = Category::create(array('name' => 'A')); - $b = Category::create(array('name' => 'B')); - $c = Category::create(array('name' => 'C')); - $d = Category::create(array('name' => 'D')); - - // a > b > c > d - $b->makeChildOf($a); - $c->makeChildOf($b); - $d->makeChildOf($c); - - $a->reload(); $b->reload(); $c->reload(); $d->reload(); - - $b->moveToRightOf($a); - - $a->reload(); $b->reload(); $c->reload(); $d->reload(); - - $this->assertEquals(0, $b->getDepth()); - $this->assertEquals(1, $c->getDepth()); - $this->assertEquals(2, $d->getDepth()); - } - -} diff --git a/tests/suite/Category/CategoryRelationsTest.php b/tests/suite/Category/CategoryRelationsTest.php deleted file mode 100644 index 7f3e008e..00000000 --- a/tests/suite/Category/CategoryRelationsTest.php +++ /dev/null @@ -1,121 +0,0 @@ -assertInstanceOf('Illuminate\Database\Eloquent\Relations\BelongsTo', $category->parent()); - } - - public function testParentRelationIsSelfReferential() { - $category = new Category; - - $this->assertInstanceOf('Baum\Node', $category->parent()->getRelated()); - } - - public function testParentRelationRefersToCorrectField() { - $category = new Category; - - if (method_exists($category->parent(), 'getForeignKeyName')) { - // For Laravel 5.6+ - $this->assertEquals($category->getParentColumnName(), $category->parent()->getForeignKeyName()); - $this->assertEquals($category->getQualifiedParentColumnName(), $category->parent()->getQualifiedForeignKeyName()); - } else { - $this->assertEquals($category->getParentColumnName(), $category->parent()->getForeignKey()); - $this->assertEquals($category->getQualifiedParentColumnName(), $category->parent()->getQualifiedForeignKey()); - } - } - - public function testParentRelation() { - $this->assertEquals($this->categories('Child 2.1')->parent()->first(), $this->categories('Child 2')); - $this->assertEquals($this->categories('Child 2')->parent()->first(), $this->categories('Root 1')); - $this->assertNull($this->categories('Root 1')->parent()->first()); - } - - public function testChildrenRelationIsAHasMany() { - $category = new Category; - - $this->assertInstanceOf('Illuminate\Database\Eloquent\Relations\HasMany', $category->children()); - } - - public function testChildrenRelationIsSelfReferential() { - $category = new Category; - - $this->assertInstanceOf('Baum\Node', $category->children()->getRelated()); - } - - public function testChildrenRelationReferesToCorrectField() { - $category = new Category; - - $this->assertEquals($category->getParentColumnName(), $category->children()->getForeignKeyName()); - - $this->assertEquals($category->getQualifiedParentColumnName(), $category->children()->getQualifiedForeignKeyName()); - } - - public function testChildrenRelation() { - $root = $this->categories('Root 1'); - - foreach($root->children() as $child) - $this->assertEquals($root->getKey(), $child->getParentId()); - - $expected = array($this->categories('Child 1'), $this->categories('Child 2'), $this->categories('Child 3')); - - $this->assertEquals($expected, $root->children()->get()->all()); - - $this->assertEmpty($this->categories('Child 3')->children()->get()->all()); - } - - public function testChildrenRelationUsesDefaultOrdering() { - $category = new Category; - - $query = $category->children()->getQuery()->getQuery(); - - $expected = array('column' => 'lft', 'direction' => 'asc'); - $this->assertEquals($expected, $query->orders[0]); - } - - public function testChildrenRelationUsesCustomOrdering() { - $category = new OrderedCategory; - - $query = $category->children()->getQuery()->getQuery(); - - $expected = array('column' => 'name', 'direction' => 'asc'); - $this->assertEquals($expected, $query->orders[0]); - } - - public function testChildrenRelationObeysDefaultOrdering() { - $children = $this->categories('Root 1')->children()->get()->all(); - - $expected = array($this->categories('Child 1'), $this->categories('Child 2'), $this->categories('Child 3')); - $this->assertEquals($expected, $children); - - // Swap 2 nodes & re-test - Category::query()->where('id', '=', 2)->update(array('lft' => 8, 'rgt' => 9)); - Category::query()->where('id', '=', 5)->update(array('lft' => 2, 'rgt' => 3)); - - $children = $this->categories('Root 1')->children()->get()->all(); - - $expected = array($this->categories('Child 3'), $this->categories('Child 2'), $this->categories('Child 1')); - $this->assertEquals($expected, $children); - } - - public function testChildrenRelationObeysCustomOrdering() { - with(new OrderedCategorySeeder)->run(); - - $children = OrderedCategory::find(1)->children()->get()->all(); - - $expected = array(OrderedCategory::find(5), OrderedCategory::find(2), OrderedCategory::find(3)); - $this->assertEquals($expected, $children); - } - - public function testChildrenRelationAllowsNodeCreation() { - $child = new Category(array('name' => 'Child 3.1')); - - $this->categories('Child 3')->children()->save($child); - - $this->assertTrue($child->exists); - $this->assertEquals($this->categories('Child 3')->getKey(), $child->getParentId()); - } - -} diff --git a/tests/suite/Category/CategoryScopedTest.php b/tests/suite/Category/CategoryScopedTest.php deleted file mode 100644 index e723e8f3..00000000 --- a/tests/suite/Category/CategoryScopedTest.php +++ /dev/null @@ -1,320 +0,0 @@ -run(); - } - - public function testInSameScope() { - $root1 = $this->categories('Root 1', 'ScopedCategory'); - $child1 = $this->categories('Child 1', 'ScopedCategory'); - $child2 = $this->categories('Child 2', 'ScopedCategory'); - - $root2 = $this->categories('Root 2', 'ScopedCategory'); - $child4 = $this->categories('Child 4', 'ScopedCategory'); - $child5 = $this->categories('Child 5', 'ScopedCategory'); - - $this->assertTrue(MultiScopedCategory::isValidNestedSet()); - - $this->assertTrue($root1->inSameScope($child1)); - $this->assertTrue($child1->inSameScope($child2)); - $this->assertTrue($child2->inSameScope($root1)); - - $this->assertTrue($root2->inSameScope($child4)); - $this->assertTrue($child4->inSameScope($child5)); - $this->assertTrue($child5->inSameScope($root2)); - - $this->assertFalse($root1->inSameScope($root2)); - $this->assertFalse($root2->inSameScope($root1)); - - $this->assertFalse($child1->inSameScope($child4)); - $this->assertFalse($child4->inSameScope($child1)); - - $this->assertFalse($child2->inSameScope($child5)); - $this->assertFalse($child5->inSameScope($child2)); - } - - public function testInSameScopeMultiple() { - $this->assertTrue(MultiScopedCategory::isValidNestedSet()); - - $child1 = $this->categories('Child 1', 'MultiScopedCategory'); - $child2 = $this->categories('Child 2', 'MultiScopedCategory'); - - $child4 = $this->categories('Child 4', 'MultiScopedCategory'); - $child5 = $this->categories('Child 5', 'MultiScopedCategory'); - - $enfant1 = $this->categories('Enfant 1', 'MultiScopedCategory'); - $enfant2 = $this->categories('Enfant 2', 'MultiScopedCategory'); - - $hijo1 = $this->categories('Hijo 1', 'MultiScopedCategory'); - $hijo2 = $this->categorieS('Hijo 2', 'MultiScopedCategory'); - - $this->assertTrue($child1->inSameScope($child2)); - $this->assertTrue($child4->inSameScope($child5)); - $this->assertTrue($enfant1->inSameScope($enfant2)); - $this->assertTrue($hijo1->inSameScope($hijo2)); - - $this->assertFalse($child2->inSameScope($child4)); - $this->assertFalse($child5->inSameScope($enfant1)); - $this->assertFalse($enfant2->inSameScope($hijo1)); - $this->assertFalse($hijo2->inSameScope($child1)); - } - - public function testIsSelfOrAncestorOf() { - $root1 = $this->categories('Root 1', 'ScopedCategory'); - $child21 = $this->categories('Child 2.1', 'ScopedCategory'); - - $root2 = $this->categories('Root 2', 'ScopedCategory'); - $child51 = $this->categories('Child 5.1', 'ScopedCategory'); - - $this->assertTrue($root1->isSelfOrAncestorOf($child21)); - $this->assertTrue($root2->isSelfOrAncestorOf($child51)); - - $this->assertFalse($root1->isSelfOrAncestorOf($child51)); - $this->assertFalse($root2->isSelfOrAncestorOf($child21)); - } - - public function testIsSelfOrDescendantOf() { - $root1 = $this->categories('Root 1', 'ScopedCategory'); - $child21 = $this->categories('Child 2.1', 'ScopedCategory'); - - $root2 = $this->categories('Root 2', 'ScopedCategory'); - $child51 = $this->categories('Child 5.1', 'ScopedCategory'); - - $this->assertTrue($child21->isSelfOrDescendantOf($root1)); - $this->assertTrue($child51->isSelfOrDescendantOf($root2)); - - $this->assertFalse($child21->isSelfOrDescendantOf($root2)); - $this->assertFalse($child51->isSelfOrDescendantOf($root1)); - } - - public function testGetSiblingsAndSelf() { - $root2 = $this->categories('Root 2', 'ScopedCategory'); - - $child1 = $this->categories('Child 1', 'ScopedCategory'); - $child2 = $this->categories('Child 2', 'ScopedCategory'); - $child3 = $this->categories('Child 3', 'ScopedCategory'); - - $expected = array($root2); - $this->assertEquals($expected, $root2->getSiblingsAndSelf()->all()); - - $expected = array($child1, $child2, $child3); - $this->assertEquals($expected, $child2->getSiblingsAndSelf()->all()); - } - - public function testGetSiblingsAndSelfMultiple() { - $root1 = $this->categories('Racine 1', 'MultiScopedCategory'); - - $child1 = $this->categories('Hijo 1', 'MultiScopedCategory'); - $child2 = $this->categories('Hijo 2', 'MultiScopedCategory'); - $child3 = $this->categories('Hijo 3', 'MultiScopedCategory'); - - $expected = array($root1); - $this->assertEquals($expected, $root1->getSiblingsAndSelf()->all()); - - $expected = array($child1, $child2, $child3); - $this->assertEquals($expected, $child3->getSiblingsAndSelf()->all()); - } - - public function testSimpleMovements() { - with(new ScopedCategorySeeder)->run(); - - $this->assertTrue(ScopedCategory::isValidNestedSet()); - - $root3 = ScopedCategory::create(array('name' => 'Root 3', 'company_id' => 2)); - $this->assertTrue(ScopedCategory::isValidNestedSet()); - - $this->categories('Child 6', 'ScopedCategory')->makeChildOf($root3); - $this->assertTrue(ScopedCategory::isValidNestedSet()); - - $root3->reload(); - $expected = array($this->categories('Child 6', 'ScopedCategory')); - $this->assertEquals($expected, $root3->children()->get()->all()); - } - - public function testSimpleSubtreeMovements() { - with(new ScopedCategorySeeder)->run(); - - $this->assertTrue(ScopedCategory::isValidNestedSet()); - - $root3 = ScopedCategory::create(array('name' => 'Root 3', 'company_id' => 2)); - $this->assertTrue(ScopedCategory::isValidNestedSet()); - - $this->categories('Child 5', 'ScopedCategory')->makeChildOf($root3); - $this->assertTrue(ScopedCategory::isValidNestedSet()); - - $root3->reload(); - $expected = array( - $this->categories('Child 5', 'ScopedCategory'), - $this->categories('Child 5.1', 'ScopedCategory') - ); - $this->assertEquals($expected, $root3->getDescendants()->all()); - } - - public function testFullSubtreeMovements() { - with(new ScopedCategorySeeder)->run(); - - $this->assertTrue(ScopedCategory::isValidNestedSet()); - - $root3 = ScopedCategory::create(array('name' => 'Root 3', 'company_id' => 2)); - $this->assertTrue(ScopedCategory::isValidNestedSet()); - - $this->categories('Root 2', 'ScopedCategory')->makeChildOf($root3); - $this->assertTrue(ScopedCategory::isValidNestedSet()); - - $root3->reload(); - $expected = array( - $this->categories('Root 2' , 'ScopedCategory'), - $this->categories('Child 4' , 'ScopedCategory'), - $this->categories('Child 5' , 'ScopedCategory'), - $this->categories('Child 5.1' , 'ScopedCategory'), - $this->categories('Child 6' , 'ScopedCategory') - ); - $this->assertEquals($expected, $root3->getDescendants()->all()); - } - - public function testSimpleMovementsMultiple() { - $this->assertTrue(MultiScopedCategory::isValidNestedSet()); - - $root2 = MultiScopedCategory::create(array('name' => 'Raiz 2', 'company_id' => 3, 'language' => 'es')); - $this->assertTrue(MultiScopedCategory::isValidNestedSet()); - - $this->categories('Hijo 1', 'MultiScopedCategory')->makeChildOf($root2); - $this->assertTrue(MultiScopedCategory::isValidNestedSet()); - - $root2->reload(); - $expected = array($this->categories('Hijo 1', 'MultiScopedCategory')); - $this->assertEquals($expected, $root2->children()->get()->all()); - } - - public function testSimpleSubtreeMovementsMultiple() { - $this->assertTrue(MultiScopedCategory::isValidNestedSet()); - - $root2 = MultiScopedCategory::create(array('name' => 'Raiz 2', 'company_id' => 3, 'language' => 'es')); - $this->assertTrue(MultiScopedCategory::isValidNestedSet()); - - $this->categories('Hijo 2', 'MultiScopedCategory')->makeChildOf($root2); - $this->assertTrue(MultiScopedCategory::isValidNestedSet()); - - $root2->reload(); - $expected = array( - $this->categories('Hijo 2', 'MultiScopedCategory'), - $this->categories('Hijo 2.1', 'MultiScopedCategory') - ); - $this->assertEquals($expected, $root2->getDescendants()->all()); - } - - public function testFullSubtreeMovementsMultiple() { - $this->assertTrue(MultiScopedCategory::isValidNestedSet()); - - $root2 = MultiScopedCategory::create(array('name' => 'Raiz 2', 'company_id' => 3, 'language' => 'es')); - $this->assertTrue(MultiScopedCategory::isValidNestedSet()); - - $this->categories('Raiz 1', 'MultiScopedCategory')->makeChildOf($root2); - $this->assertTrue(MultiScopedCategory::isValidNestedSet()); - - $root2->reload(); - $expected = array( - $this->categories('Raiz 1', 'MultiScopedCategory'), - $this->categories('Hijo 1', 'MultiScopedCategory'), - $this->categories('Hijo 2', 'MultiScopedCategory'), - $this->categories('Hijo 2.1', 'MultiScopedCategory'), - $this->categories('Hijo 3', 'MultiScopedCategory') - ); - $this->assertEquals($expected, $root2->getDescendants()->all()); - } - - public function testToHierarchyNestsCorrectlyWithScopedOrder() { - with(new OrderedScopedCategorySeeder)->run(); - - $expectedWhole1 = array( - 'Root 1' => array( - 'Child 1' => null, - 'Child 2' => array ( - 'Child 2.1' => null - ), - 'Child 3' => null - ) - ); - - $expectedWhole2 = array( - 'Root 2' => array( - 'Child 4' => null, - 'Child 5' => array ( - 'Child 5.1' => null - ), - 'Child 6' => null - ) - ); - - $this->assertArraysAreEqual($expectedWhole1, hmap(OrderedScopedCategory::where('company_id', 1)->get()->toHierarchy()->toArray())); - $this->assertArraysAreEqual($expectedWhole2, hmap(OrderedScopedCategory::where('company_id', 2)->get()->toHierarchy()->toArray())); - } - - /** - * @expectedException Baum\MoveNotPossibleException - */ - public function testNodesCannotMoveBetweenScopes() { - $child4 = $this->categories('Child 4', 'ScopedCategory'); - $root1 = $this->categories('Root 1', 'ScopedCategory'); - - $child4->makeChildOf($root1); - } - - /** - * @expectedException Baum\MoveNotPossibleException - */ - public function testNodesCannotMoveBetweenScopesMultiple() { - $root1 = $this->categories('Root 1', 'MultiScopedCategory'); - $child4 = $this->categories('Child 4', 'MultiScopedCategory'); - - $child4->makeChildOf($root1); - } - - /** - * @expectedException Baum\MoveNotPossibleException - */ - public function testNodesCannotMoveBetweenScopesMultiple2() { - $root1 = $this->categories('Racine 1', 'MultiScopedCategory'); - $child2 = $this->categories('Hijo 2', 'MultiScopedCategory'); - - $child2->makeChildOf($root1); - } - - // TODO: Moving nodes between scopes is problematic ATM. Fix it or find a work-around. - public function testMoveNodeBetweenScopes() { - $this->markTestSkipped(); - - // $root1 = Menu::create(array('caption' => 'TL1', 'site_id' => 1, 'language' => 'en')); - // $child11 = Menu::create(array('caption' => 'C11', 'site_id' => 1, 'language' => 'en')); - // $child12 = Menu::create(array('caption' => 'C12', 'site_id' => 1, 'language' => 'en')); - - // $this->assertTrue(Menu::isValidNestedSet()); - - // $child11->makeChildOf($root1); - // $child12->makeChildOf($root1); - - // $this->assertTrue(Menu::isValidNestedSet()); - - // $root2 = Menu::create(array('caption' => 'TL2', 'site_id' => 2, 'language' => 'en')); - // $child21 = Menu::create(array('caption' => 'C21', 'site_id' => 2, 'language' => 'en')); - // $child22 = Menu::create(array('caption' => 'C22', 'site_id' => 2, 'language' => 'en')); - // $child21->makeChildOf($root2); - // $child22->makeChildOf($root2); - - // $this->assertTrue(Menu::isValidNestedSet()); - - // $child11->update(array('site_id' => 2)); - // $child11->makeChildOf($root2); - - // $this->assertTrue(Menu::isValidNestedSet()); - - // $expected = array($this->menus('C12')); - // $this->assertEquals($expected, $root1->children()->get()->all()); - - // $expected = array($this->menus('C21'), $this->menus('C22'), $this->menus('C11')); - // $this->assertEquals($expected, $root2->children()->get()->all()); - } - -} diff --git a/tests/suite/Category/CategorySoftDeletesTest.php b/tests/suite/Category/CategorySoftDeletesTest.php deleted file mode 100644 index 62ccbe55..00000000 --- a/tests/suite/Category/CategorySoftDeletesTest.php +++ /dev/null @@ -1,259 +0,0 @@ -categories('Child 3', 'SoftCategory'); - - $this->assertTrue($node->exists); - $this->assertFalse($node->trashed()); - - $node->delete(); - - $this->assertTrue($node->trashed()); - - $node->reload(); - - $this->assertTrue($node->trashed()); - $this->assertTrue($node->exists); - } - - public function testDeleteMaintainsTreeValid() { - $this->assertTrue(SoftCategory::isValidNestedSet()); - - $child3 = $this->categories('Child 3', 'SoftCategory'); - $child3->delete(); - - $this->assertTrue($child3->trashed()); - $this->assertTrue(SoftCategory::isValidNestedSet()); - } - - public function testDeleteMaintainsTreeValidWithSubtrees() { - $this->assertTrue(SoftCategory::isValidNestedSet()); - - $child2 = $this->categories('Child 2', 'SoftCategory'); - $child2->delete(); - $this->assertTrue(SoftCategory::isValidNestedSet()); - - $expected = array( - $this->categories('Child 1', 'SoftCategory'), - $this->categories('Child 3', 'SoftCategory') - ); - $this->assertEquals($expected, $this->categories('Root 1', 'SoftCategory')->getDescendants()->all()); - } - - public function testDeleteShiftsIndexes() { - $this->assertTrue(SoftCategory::isValidNestedSet()); - - $this->categories('Child 1', 'SoftCategory')->delete(); - - $this->assertTrue(SoftCategory::isValidNestedSet()); - - $expected = array( - $this->categories('Child 2' , 'SoftCategory'), - $this->categories('Child 2.1' , 'SoftCategory'), - $this->categories('Child 3' , 'SoftCategory') - ); - $this->assertEquals($expected, $this->categories('Root 1', 'SoftCategory')->getDescendants()->all()); - - $this->assertEquals(1, $this->categories('Root 1', 'SoftCategory')->getLeft()); - $this->assertEquals(8, $this->categories('Root 1', 'SoftCategory')->getRight()); - - $this->assertEquals(2, $this->categories('Child 2', 'SoftCategory')->getLeft()); - $this->assertEquals(5, $this->categories('Child 2', 'SoftCategory')->getRight()); - - $this->assertEquals(3, $this->categories('Child 2.1', 'SoftCategory')->getLeft()); - $this->assertEquals(4, $this->categories('Child 2.1', 'SoftCategory')->getRight()); - - $this->assertEquals(6, $this->categories('Child 3', 'SoftCategory')->getLeft()); - $this->assertEquals(7, $this->categories('Child 3', 'SoftCategory')->getRight()); - } - - public function testDeleteShiftsIndexesSubtree() { - $this->assertTrue(SoftCategory::isValidNestedSet()); - - $this->categories('Child 2', 'SoftCategory')->delete(); - - $this->assertTrue(SoftCategory::isValidNestedSet()); - - $expected = array( - $this->categories('Child 1', 'SoftCategory'), - $this->categories('Child 3', 'SoftCategory') - ); - $this->assertEquals($expected, $this->categories('Root 1', 'SoftCategory')->getDescendants()->all()); - - $this->assertEquals(1, $this->categories('Root 1', 'SoftCategory')->getLeft()); - $this->assertEquals(6, $this->categories('Root 1', 'SoftCategory')->getRight()); - - $this->assertEquals(2, $this->categories('Child 1', 'SoftCategory')->getLeft()); - $this->assertEquals(3, $this->categories('Child 1', 'SoftCategory')->getRight()); - - $this->assertEquals(4, $this->categories('Child 3', 'SoftCategory')->getLeft()); - $this->assertEquals(5, $this->categories('Child 3', 'SoftCategory')->getRight()); - } - - public function testDeleteShiftsIndexesFullSubtree() { - $this->assertTrue(SoftCategory::isValidNestedSet()); - - $this->categories('Root 1', 'SoftCategory')->delete(); - $this->assertTrue(SoftCategory::isValidNestedSet()); - - $this->assertEmpty($this->categories('Root 2', 'SoftCategory')->getSiblings()->all()); - $this->assertEquals(1, $this->categories('Root 2', 'SoftCategory')->getLeft()); - $this->assertEquals(2, $this->categories('Root 2', 'SoftCategory')->getRight()); - } - - public function testRestoreMaintainsTreeValid() { - $this->assertTrue(SoftCategory::isValidNestedSet()); - - $child3 = $this->categories('Child 3', 'SoftCategory'); - $child3->delete(); - - $this->assertTrue($child3->trashed()); - $this->assertTrue(SoftCategory::isValidNestedSet()); - - $child3->reload(); - $child3->restore(); - - $this->assertFalse($child3->trashed()); - $this->assertTrue(SoftCategory::isValidNestedSet()); - } - - public function testRestoreMaintainsTreeValidWithSubtrees() { - $this->assertTrue(SoftCategory::isValidNestedSet()); - - $child2 = $this->categories('Child 2', 'SoftCategory'); - $child2->delete(); - $this->assertTrue(SoftCategory::isValidNestedSet()); - - $child2->reload(); - $child2->restore(); - - $this->assertTrue(SoftCategory::isValidNestedSet()); - - $expected = array( - $this->categories('Child 1' , 'SoftCategory'), - $this->categories('Child 2' , 'SoftCategory'), - $this->categories('Child 2.1' , 'SoftCategory'), - $this->categories('Child 3' , 'SoftCategory') - ); - $this->assertEquals($expected, $this->categories('Root 1', 'SoftCategory')->getDescendants()->all()); - } - - public function testRestoreUnshiftsIndexes() { - $this->assertTrue(SoftCategory::isValidNestedSet()); - - $this->categories('Child 1', 'SoftCategory')->delete(); - - $this->assertTrue(SoftCategory::isValidNestedSet()); - - SoftCategory::withTrashed()->where('name', 'Child 1')->first()->restore(); - - $this->assertTrue(SoftCategory::isValidNestedSet()); - - $expected = array( - $this->categories('Child 1' , 'SoftCategory'), - $this->categories('Child 2' , 'SoftCategory'), - $this->categories('Child 2.1' , 'SoftCategory'), - $this->categories('Child 3' , 'SoftCategory') - ); - $this->assertEquals($expected, $this->categories('Root 1', 'SoftCategory')->getDescendants()->all()); - - $this->assertEquals(1, $this->categories('Root 1', 'SoftCategory')->getLeft()); - $this->assertEquals(10, $this->categories('Root 1', 'SoftCategory')->getRight()); - - $this->assertEquals(2, $this->categories('Child 1', 'SoftCategory')->getLeft()); - $this->assertEquals(3, $this->categories('Child 1', 'SoftCategory')->getRight()); - - $this->assertEquals(4, $this->categories('Child 2', 'SoftCategory')->getLeft()); - $this->assertEquals(7, $this->categories('Child 2', 'SoftCategory')->getRight()); - $this->assertEquals(5, $this->categories('Child 2.1', 'SoftCategory')->getLeft()); - $this->assertEquals(6, $this->categories('Child 2.1', 'SoftCategory')->getRight()); - - $this->assertEquals(8, $this->categories('Child 3', 'SoftCategory')->getLeft()); - $this->assertEquals(9, $this->categories('Child 3', 'SoftCategory')->getRight()); - } - - public function testRestoreUnshiftsIndexesSubtree() { - $this->assertTrue(SoftCategory::isValidNestedSet()); - - $this->categories('Child 2', 'SoftCategory')->delete(); - - $this->assertTrue(SoftCategory::isValidNestedSet()); - - SoftCategory::withTrashed()->where('name', 'Child 2')->first()->restore(); - - $this->assertTrue(SoftCategory::isValidNestedSet()); - - $expected = array( - $this->categories('Child 1' , 'SoftCategory'), - $this->categories('Child 2' , 'SoftCategory'), - $this->categories('Child 2.1' , 'SoftCategory'), - $this->categories('Child 3' , 'SoftCategory') - ); - $this->assertEquals($expected, $this->categories('Root 1', 'SoftCategory')->getDescendants()->all()); - - $this->assertEquals(1, $this->categories('Root 1', 'SoftCategory')->getLeft()); - $this->assertEquals(10, $this->categories('Root 1', 'SoftCategory')->getRight()); - - $this->assertEquals(2, $this->categories('Child 1', 'SoftCategory')->getLeft()); - $this->assertEquals(3, $this->categories('Child 1', 'SoftCategory')->getRight()); - - $this->assertEquals(4, $this->categories('Child 2', 'SoftCategory')->getLeft()); - $this->assertEquals(7, $this->categories('Child 2', 'SoftCategory')->getRight()); - $this->assertEquals(5, $this->categories('Child 2.1', 'SoftCategory')->getLeft()); - $this->assertEquals(6, $this->categories('Child 2.1', 'SoftCategory')->getRight()); - - $this->assertEquals(8, $this->categories('Child 3', 'SoftCategory')->getLeft()); - $this->assertEquals(9, $this->categories('Child 3', 'SoftCategory')->getRight()); - } - - public function testRestoreUnshiftsIndexesFullSubtree() { - $this->assertTrue(SoftCategory::isValidNestedSet()); - - $this->categories('Root 1', 'SoftCategory')->delete(); - - $this->assertTrue(SoftCategory::isValidNestedSet()); - - SoftCategory::withTrashed()->where('name', 'Root 1')->first()->restore(); - - $this->assertTrue(SoftCategory::isValidNestedSet()); - - $expected = array( - $this->categories('Child 1' , 'SoftCategory'), - $this->categories('Child 2' , 'SoftCategory'), - $this->categories('Child 2.1' , 'SoftCategory'), - $this->categories('Child 3' , 'SoftCategory') - ); - $this->assertEquals($expected, $this->categories('Root 1', 'SoftCategory')->getDescendants()->all()); - - $this->assertEquals(1, $this->categories('Root 1', 'SoftCategory')->getLeft()); - $this->assertEquals(10, $this->categories('Root 1', 'SoftCategory')->getRight()); - - $this->assertEquals(2, $this->categories('Child 1', 'SoftCategory')->getLeft()); - $this->assertEquals(3, $this->categories('Child 1', 'SoftCategory')->getRight()); - - $this->assertEquals(4, $this->categories('Child 2', 'SoftCategory')->getLeft()); - $this->assertEquals(7, $this->categories('Child 2', 'SoftCategory')->getRight()); - $this->assertEquals(5, $this->categories('Child 2.1', 'SoftCategory')->getLeft()); - $this->assertEquals(6, $this->categories('Child 2.1', 'SoftCategory')->getRight()); - - $this->assertEquals(8, $this->categories('Child 3', 'SoftCategory')->getLeft()); - $this->assertEquals(9, $this->categories('Child 3', 'SoftCategory')->getRight()); - } - - public function testAllStatic() { - $expected = array('Root 1', 'Child 1', 'Child 2', 'Child 2.1', 'Child 3', 'Root 2'); - - $this->assertArraysAreEqual($expected, SoftCategory::all()->pluck('name')->all()); - } - - public function testAllStaticWithSoftDeletes() { - $this->categories('Child 1', 'SoftCategory')->delete(); - $this->categories('Child 3', 'SoftCategory')->delete(); - - $expected = array('Root 1', 'Child 2', 'Child 2.1', 'Root 2'); - $this->assertArraysAreEqual($expected, SoftCategory::all()->pluck('name')->all()); - } - -} diff --git a/tests/suite/Category/CategoryTreeMapperTest.php b/tests/suite/Category/CategoryTreeMapperTest.php deleted file mode 100644 index 8edd5336..00000000 --- a/tests/suite/Category/CategoryTreeMapperTest.php +++ /dev/null @@ -1,210 +0,0 @@ -up(); - } - - public function testBuildTree() { - $tree = array( - array('id' => 1, 'name' => 'A'), - array('id' => 2, 'name' => 'B'), - array('id' => 3, 'name' => 'C', 'children' => array( - array('id' => 4, 'name' => 'C.1', 'children' => array( - array('id' => 5, 'name' => 'C.1.1'), - array('id' => 6, 'name' => 'C.1.2') - )), - array('id' => 7, 'name' => 'C.2'), - array('id' => 8, 'name' => 'C.3') - )), - array('id' => 9, 'name' => 'D') - ); - $this->assertTrue(Category::buildTree($tree)); - $this->assertTrue(Category::isValidNestedSet()); - - $hierarchy = Category::all()->toHierarchy()->toArray(); - $this->assertArraysAreEqual($tree, array_ints_keys(hmap($hierarchy, array('id', 'name')))); - } - - public function testBuildTreePrunesAndInserts() { - $tree = array( - array('id' => 1, 'name' => 'A'), - array('id' => 2, 'name' => 'B'), - array('id' => 3, 'name' => 'C', 'children' => array( - array('id' => 4, 'name' => 'C.1', 'children' => array( - array('id' => 5, 'name' => 'C.1.1'), - array('id' => 6, 'name' => 'C.1.2') - )), - array('id' => 7, 'name' => 'C.2'), - array('id' => 8, 'name' => 'C.3') - )), - array('id' => 9, 'name' => 'D') - ); - $this->assertTrue(Category::buildTree($tree)); - $this->assertTrue(Category::isValidNestedSet()); - - // Postgres fix - if ( DB::connection()->getDriverName() === 'pgsql' ) { - $tablePrefix = DB::connection()->getTablePrefix(); - - $sequenceName = $tablePrefix . 'categories_id_seq'; - - DB::connection()->statement('ALTER SEQUENCE ' . $sequenceName . ' RESTART WITH 10'); - } - - $updated = array( - array('id' => 1, 'name' => 'A'), - array('id' => 2, 'name' => 'B'), - array('id' => 3, 'name' => 'C', 'children' => array( - array('id' => 4, 'name' => 'C.1', 'children' => array( - array('id' => 5, 'name' => 'C.1.1'), - array('id' => 6, 'name' => 'C.1.2') - )), - array('id' => 7, 'name' => 'C.2', 'children' => array( - array('name' => 'C.2.1'), - array('name' => 'C.2.2') - )) - )), - array('id' => 9, 'name' => 'D') - ); - $this->assertTrue(Category::buildTree($updated)); - $this->assertTrue(Category::isValidNestedSet()); - - $expected = array( - array('id' => 1, 'name' => 'A'), - array('id' => 2, 'name' => 'B'), - array('id' => 3, 'name' => 'C', 'children' => array( - array('id' => 4, 'name' => 'C.1', 'children' => array( - array('id' => 5, 'name' => 'C.1.1'), - array('id' => 6, 'name' => 'C.1.2') - )), - array('id' => 7, 'name' => 'C.2', 'children' => array( - array('id' => 10, 'name' => 'C.2.1'), - array('id' => 11, 'name' => 'C.2.2') - )) - )), - array('id' => 9, 'name' => 'D') - ); - - $hierarchy = Category::all()->toHierarchy()->toArray(); - $this->assertArraysAreEqual($expected, array_ints_keys(hmap($hierarchy, array('id', 'name')))); - } - - public function testMakeTree() { - with(new CategorySeeder)->run(); - - $parent = Category::find(3); - - $subtree = array( - array('id' => 4, 'name' => 'Child 2.1'), - array('name' => 'Child 2.2'), - array('name' => 'Child 2.3', 'children' => array( - array('name' => 'Child 2.3.1', 'children' => array( - array('name' => 'Child 2.3.1.1'), - array('name' => 'Child 2.3.1.1') - )), - array('name' => 'Child 2.3.2'), - array('name' => 'Child 2.3.3') - )), - array('name' => 'Child 2.4') - ); - - $this->assertTrue($parent->makeTree($subtree)); - $this->assertTrue(Category::isValidNestedSet()); - - $expected = array( - array('id' => 4, 'name' => 'Child 2.1'), - array('id' => 7, 'name' => 'Child 2.2'), - array('id' => 8, 'name' => 'Child 2.3', 'children' => array( - array('id' => 9, 'name' => 'Child 2.3.1', 'children' => array( - array('id' => 10, 'name' => 'Child 2.3.1.1'), - array('id' => 11, 'name' => 'Child 2.3.1.1') - )), - array('id' => 12, 'name' => 'Child 2.3.2'), - array('id' => 13, 'name' => 'Child 2.3.3') - )), - array('id' => 14, 'name' => 'Child 2.4') - ); - - $hierarchy = $parent->reload()->getDescendants()->toHierarchy()->toArray(); - $this->assertArraysAreEqual($expected, array_ints_keys(hmap($hierarchy, array('id', 'name')))); - } - - public function testMakeTreePrunesAndInserts() { - with(new CategorySeeder)->run(); - - $parent = Category::find(3); - - $subtree = array( - array('id' => 4, 'name' => 'Child 2.1'), - array('name' => 'Child 2.2'), - array('name' => 'Child 2.3', 'children' => array( - array('name' => 'Child 2.3.1', 'children' => array( - array('name' => 'Child 2.3.1.1'), - array('name' => 'Child 2.3.1.1') - )), - array('name' => 'Child 2.3.2'), - array('name' => 'Child 2.3.3') - )), - array('name' => 'Child 2.4') - ); - - $this->assertTrue($parent->makeTree($subtree)); - $this->assertTrue(Category::isValidNestedSet()); - - $expected = array( - array('id' => 4, 'name' => 'Child 2.1'), - array('id' => 7, 'name' => 'Child 2.2'), - array('id' => 8, 'name' => 'Child 2.3', 'children' => array( - array('id' => 9, 'name' => 'Child 2.3.1', 'children' => array( - array('id' => 10, 'name' => 'Child 2.3.1.1'), - array('id' => 11, 'name' => 'Child 2.3.1.1') - )), - array('id' => 12, 'name' => 'Child 2.3.2'), - array('id' => 13, 'name' => 'Child 2.3.3') - )), - array('id' => 14, 'name' => 'Child 2.4') - ); - - $hierarchy = $parent->reload()->getDescendants()->toHierarchy()->toArray(); - $this->assertArraysAreEqual($expected, array_ints_keys(hmap($hierarchy, array('id', 'name')))); - - $modified = array( - array('id' => 7, 'name' => 'Child 2.2'), - array('id' => 8, 'name' => 'Child 2.3'), - array('id' => 14, 'name' => 'Child 2.4'), - array('name' => 'Child 2.5', 'children' => array( - array('name' => 'Child 2.5.1', 'children' => array( - array('name' => 'Child 2.5.1.1'), - array('name' => 'Child 2.5.1.1') - )), - array('name' => 'Child 2.5.2'), - array('name' => 'Child 2.5.3') - )) - ); - - $this->assertTrue($parent->makeTree($modified)); - $this->assertTrue(Category::isValidNestedSet()); - - $expected = array( - array('id' => 7 , 'name' => 'Child 2.2'), - array('id' => 8 , 'name' => 'Child 2.3'), - array('id' => 14, 'name' => 'Child 2.4'), - array('id' => 15, 'name' => 'Child 2.5', 'children' => array( - array('id' => 16, 'name' => 'Child 2.5.1', 'children' => array( - array('id' => 17, 'name' => 'Child 2.5.1.1'), - array('id' => 18, 'name' => 'Child 2.5.1.1') - )), - array('id' => 19, 'name' => 'Child 2.5.2'), - array('id' => 20, 'name' => 'Child 2.5.3') - )) - ); - - $hierarchy = $parent->reload()->getDescendants()->toHierarchy()->toArray(); - $this->assertArraysAreEqual($expected, array_ints_keys(hmap($hierarchy, array('id', 'name')))); - } - -} diff --git a/tests/suite/Category/CategoryTreeRebuildingTest.php b/tests/suite/Category/CategoryTreeRebuildingTest.php deleted file mode 100644 index 8e951d58..00000000 --- a/tests/suite/Category/CategoryTreeRebuildingTest.php +++ /dev/null @@ -1,96 +0,0 @@ -assertTrue(Category::isValidNestedSet()); - - $root = Category::root(); - Category::query()->update(array('lft' => null, 'rgt' => null)); - $this->assertFalse(Category::isValidNestedSet()); - - Category::rebuild(); - $this->assertTrue(Category::isValidNestedSet()); - - $this->assertEquals($root, Category::root()); - } - - public function testRebuildPresevesRootNodes() { - $root1 = Category::create(array('name' => 'Test Root 1')); - $root2 = Category::create(array('name' => 'Test Root 2')); - $root3 = Category::create(array('name' => 'Test Root 3')); - - $root2->makeChildOf($root1); - $root3->makeChildOf($root1); - - $lastRoot = Category::roots()->reOrderBy($root1->getLeftColumnName(), 'desc')->first(); - - Category::query()->update(array('lft' => null, 'rgt' => null)); - Category::rebuild(); - - $this->assertEquals($lastRoot, Category::roots()->reOrderBy($root1->getLeftColumnName(), 'desc')->first()); - } - - public function testRebuildRecomputesDepth() { - $this->assertTrue(Category::isValidNestedSet()); - - Category::query()->update(array('lft' => null, 'rgt' => null, 'depth' => 0)); - $this->assertFalse(Category::isValidNestedSet()); - - Category::rebuild(); - - $expected = array(0, 1, 1, 2, 1, 0); - $this->assertEquals($expected, Category::all()->map(function($n) { return $n->getDepth(); })->all()); - } - - public function testRebuildWithScope() { - MultiScopedCategory::query()->delete(); - - $root = MultiScopedCategory::create(array('name' => 'A' , 'company_id' => 721, 'language' => 'es')); - $child1 = MultiScopedCategory::create(array('name' => 'A.1' , 'company_id' => 721, 'language' => 'es')); - $child2 = MultiscopedCategory::create(array('name' => 'A.2' , 'company_id' => 721, 'language' => 'es')); - - $child1->makeChildOf($root); - $child2->makeChildOf($root); - - MultiscopedCategory::query()->update(array('lft' => null, 'rgt' => null)); - $this->assertFalse(MultiscopedCategory::isValidNestedSet()); - - MultiscopedCategory::rebuild(); - $this->assertTrue(MultiscopedCategory::isValidNestedSet()); - - $this->assertEquals($root->getAttributes(), $this->categories('A', 'MultiScopedCategory')->getAttributes()); - - $expected = array($child1->getAttributes(), $child2->getAttributes()); - $actual = array_map(function ($ch) { return $ch->getAttributes(); }, $this->categories('A', 'MultiScopedCategory')->children()->get()->all()); - - $this->assertEquals($expected, $actual); - } - - public function testRebuildWithMultipleScopes() { - MultiScopedCategory::query()->delete(); - - $root1 = MultiScopedCategory::create(array('name' => 'TL1', 'company_id' => 1, 'language' => 'en')); - $child11 = MultiScopedCategory::create(array('name' => 'C11', 'company_id' => 1, 'language' => 'en')); - $child12 = MultiScopedCategory::create(array('name' => 'C12', 'company_id' => 1, 'language' => 'en')); - $child11->makeChildOf($root1); - $child12->makeChildOf($root1); - - $root2 = MultiScopedCategory::create(array('name' => 'TL2', 'company_id' => 2, 'language' => 'en')); - $child21 = MultiScopedCategory::create(array('name' => 'C21', 'company_id' => 2, 'language' => 'en')); - $child22 = MultiScopedCategory::create(array('name' => 'C22', 'company_id' => 2, 'language' => 'en')); - $child21->makeChildOf($root2); - $child22->makeChildOf($root2); - - $this->assertTrue(MultiScopedCategory::isValidNestedSet()); - - $tree = MultiScopedCategory::query()->orderBy($root1->getKeyName())->get()->all(); - - MultiScopedCategory::query()->update(array('lft' => null, 'rgt' => null)); - MultiScopedCategory::rebuild(); - - $this->assertTrue(MultiScopedCategory::isValidNestedSet()); - $this->assertEquals($tree, MultiScopedCategory::query()->orderBy($root1->getKeyName())->get()->all()); - } - -} diff --git a/tests/suite/Category/CategoryTreeValidationTest.php b/tests/suite/Category/CategoryTreeValidationTest.php deleted file mode 100644 index c00992e9..00000000 --- a/tests/suite/Category/CategoryTreeValidationTest.php +++ /dev/null @@ -1,84 +0,0 @@ -assertTrue(Category::isValidNestedSet()); - - Category::query()->update(array('lft' => null)); - $this->assertFalse(Category::isValidNestedSet()); - } - - public function testTreeIsNotValidWithNullRights() { - $this->assertTrue(Category::isValidNestedSet()); - - Category::query()->update(array('rgt' => null)); - $this->assertFalse(Category::isValidNestedSet()); - } - - public function testTreeIsNotValidWhenRightsEqualLefts() { - $this->assertTrue(Category::isValidNestedSet()); - - $child2 = $this->categories('Child 2'); - $child2->rgt = $child2->lft; - $child2->save(); - - $this->assertFalse(Category::isValidNestedSet()); - } - - public function testTreeIsNotValidWhenLeftEqualsParent() { - $this->assertTrue(Category::isValidNestedSet()); - - $child2 = $this->categories('Child 2'); - $child2->lft = $this->categories('Root 1')->getLeft(); - $child2->save(); - - $this->assertFalse(Category::isValidNestedSet()); - } - - public function testTreeIsNotValidWhenRightEqualsParent() { - $this->assertTrue(Category::isValidNestedSet()); - - $child2 = $this->categories('Child 2'); - $child2->rgt = $this->categories('Root 1')->getRight(); - $child2->save(); - - $this->assertFalse(Category::isValidNestedSet()); - } - - public function testTreeIsValidWithMissingMiddleNode() { - $this->assertTrue(Category::isValidNestedSet()); - - Category::query()->delete($this->categories('Child 2')->getKey()); - $this->assertTrue(Category::isValidNestedSet()); - } - - public function testTreeIsNotValidWithOverlappingRoots() { - $this->assertTrue(Category::isValidNestedSet()); - - // Force Root 2 to overlap with Root 1 - $root = $this->categories('Root 2'); - $root->lft = 0; - $root->save(); - - $this->assertFalse(Category::isValidNestedSet()); - } - - public function testNodeDeletionDoesNotMakeTreeInvalid() { - $this->assertTrue(Category::isValidNestedSet()); - - $this->categories('Root 2')->delete(); - $this->assertTrue(Category::isValidNestedSet()); - - $this->categories('Child 1')->delete(); - $this->assertTrue(Category::isValidNestedSet()); - } - - public function testNodeDeletionWithSubtreeDoesNotMakeTreeInvalid() { - $this->assertTrue(Category::isValidNestedSet()); - - $this->categories('Child 2')->delete(); - $this->assertTrue(Category::isValidNestedSet()); - } - -} diff --git a/tests/suite/CategoryTestCase.php b/tests/suite/CategoryTestCase.php deleted file mode 100644 index b7998406..00000000 --- a/tests/suite/CategoryTestCase.php +++ /dev/null @@ -1,17 +0,0 @@ -up(); - } - - public function setUp() { - with(new CategorySeeder)->run(); - } - - protected function categories($name, $className = 'Category') { - return forward_static_call_array(array($className, 'where'), array('name', '=', $name))->first(); - } - -} diff --git a/tests/suite/Cluster/ClusterColumnsTest.php b/tests/suite/Cluster/ClusterColumnsTest.php deleted file mode 100644 index 72279f3e..00000000 --- a/tests/suite/Cluster/ClusterColumnsTest.php +++ /dev/null @@ -1,19 +0,0 @@ -assertTrue(is_string($root->getKey())); - $this->assertFalse(is_numeric($root->getKey())); - } - - public function testParentKeyIsNonNumeric() { - $child1 = $this->clusters('Child 1'); - - $this->assertTrue(is_string($child1->getParentId())); - $this->assertFalse(is_numeric($child1->getParentId())); - } - -} diff --git a/tests/suite/Cluster/ClusterHierarchyTest.php b/tests/suite/Cluster/ClusterHierarchyTest.php deleted file mode 100644 index c660ee73..00000000 --- a/tests/suite/Cluster/ClusterHierarchyTest.php +++ /dev/null @@ -1,708 +0,0 @@ -orderBy('lft')->get(); - - $this->assertEquals($results, $expected); - } - - public function testAllStaticWithCustomOrder() { - $results = OrderedCluster::all(); - $expected = OrderedCluster::query()->orderBy('name')->get(); - - $this->assertEquals($results, $expected); - } - - public function testRootsStatic() { - $query = Cluster::whereNull('parent_id')->get(); - - $roots = Cluster::roots()->get(); - - $this->assertEquals($query->count(), $roots->count()); - $this->assertCount(2, $roots); - - foreach ($query->pluck('id') as $node) - $this->assertContains($node, $roots->pluck('id')); - } - - public function testRootsStaticWithCustomOrder() { - $cluster = OrderedCluster::create(array('name' => 'A new root is born')); - $cluster->syncOriginal(); // ¿? --> This should be done already !? - - $roots = OrderedCluster::roots()->get(); - - $this->assertCount(3, $roots); - $this->assertEquals($cluster->getAttributes(), $roots->first()->getAttributes()); - } - - public function testRootStatic() { - $this->assertEquals(Cluster::root(), $this->clusters('Root 1')); - } - - public function testAllLeavesStatic() { - $allLeaves = Cluster::allLeaves()->get(); - - $this->assertCount(4, $allLeaves); - - $leaves = $allLeaves->pluck('name'); - - $this->assertContains('Child 1' , $leaves); - $this->assertContains('Child 2.1' , $leaves); - $this->assertContains('Child 3' , $leaves); - $this->assertContains('Root 2' , $leaves); - } - - public function testAllTrunksStatic() { - $allTrunks = Cluster::allTrunks()->get(); - - $this->assertCount(1, $allTrunks); - - $trunks = $allTrunks->pluck('name'); - $this->assertContains('Child 2', $trunks); - } - - public function testGetRoot() { - $this->assertEquals($this->clusters('Root 1'), $this->clusters('Root 1')->getRoot()); - $this->assertEquals($this->clusters('Root 2'), $this->clusters('Root 2')->getRoot()); - - $this->assertEquals($this->clusters('Root 1'), $this->clusters('Child 1')->getRoot()); - $this->assertEquals($this->clusters('Root 1'), $this->clusters('Child 2')->getRoot()); - $this->assertEquals($this->clusters('Root 1'), $this->clusters('Child 2.1')->getRoot()); - $this->assertEquals($this->clusters('Root 1'), $this->clusters('Child 3')->getRoot()); - } - - public function testGetRootEqualsSelfIfUnpersisted() { - $cluster = new Cluster; - - $this->assertEquals($cluster->getRoot(), $cluster); - } - - public function testGetRootEqualsValueIfSetIfUnpersisted() { - $parent = Cluster::roots()->first(); - - $child = new Cluster; - $child->setAttribute($child->getParentColumnName(), $parent->getKey()); - - $this->assertEquals($child->getRoot(), $parent); - } - - public function testIsRoot() { - $this->assertTrue($this->clusters('Root 1')->isRoot()); - $this->assertTrue($this->clusters('Root 2')->isRoot()); - - $this->assertFalse($this->clusters('Child 1')->isRoot()); - $this->assertFalse($this->clusters('Child 2')->isRoot()); - $this->assertFalse($this->clusters('Child 2.1')->isRoot()); - $this->assertFalse($this->clusters('Child 3')->isRoot()); - } - - public function testGetLeaves() { - $leaves = array($this->clusters('Child 1'), $this->clusters('Child 2.1'), $this->clusters('Child 3')); - - $this->assertEquals($leaves, $this->clusters('Root 1')->getLeaves()->all()); - } - - public function testGetLeavesInIteration() { - $node = $this->clusters('Root 1'); - - $expectedIds = array( - '5d7ce1fd-6151-46d3-a5b3-0ebb9988dc57', - '3315a297-af87-4ad3-9fa5-19785407573d', - '054476d2-6830-4014-a181-4de010ef7114' - ); - - foreach($node->getLeaves() as $i => $leaf) - $this->assertEquals($expectedIds[$i], $leaf->getKey()); - } - - public function testGetTrunks() { - $trunks = array($this->clusters('Child 2')); - - $this->assertEquals($trunks, $this->clusters('Root 1')->getTrunks()->all()); - } - - public function testGetTrunksInIteration() { - $node = $this->clusters('Root 1'); - - $expectedIds = array('07c1fc8c-53b5-4fe7-b9c4-e09f266a455c'); - - foreach($node->getTrunks() as $i => $trunk) - $this->assertEquals($expectedIds[$i], $trunk->getKey()); - } - - public function testIsLeaf() { - $this->assertTrue($this->clusters('Child 1')->isLeaf()); - $this->assertTrue($this->clusters('Child 2.1')->isLeaf()); - $this->assertTrue($this->clusters('Child 3')->isLeaf()); - $this->assertTrue($this->clusters('Root 2')->isLeaf()); - - $this->assertFalse($this->clusters('Root 1')->isLeaf()); - $this->assertFalse($this->clusters('Child 2')->isLeaf()); - - $new = new Cluster; - $this->assertFalse($new->isLeaf()); - } - - public function testIsTrunk() { - $this->assertFalse($this->clusters('Child 1')->isTrunk()); - $this->assertFalse($this->clusters('Child 2.1')->isTrunk()); - $this->assertFalse($this->clusters('Child 3')->isTrunk()); - $this->assertFalse($this->clusters('Root 2')->isTrunk()); - - $this->assertFalse($this->clusters('Root 1')->isTrunk()); - $this->assertTrue($this->clusters('Child 2')->isTrunk()); - - $new = new Cluster; - $this->assertFalse($new->isTrunk()); - } - - public function testWithoutNodeScope() { - $child = $this->clusters('Child 2.1'); - - $expected = array($this->clusters('Root 1'), $child); - - $this->assertEquals($expected, $child->ancestorsAndSelf()->withoutNode($this->clusters('Child 2'))->get()->all()); - } - - public function testWithoutSelfScope() { - $child = $this->clusters('Child 2.1'); - - $expected = array($this->clusters('Root 1'), $this->clusters('Child 2')); - - $this->assertEquals($expected, $child->ancestorsAndSelf()->withoutSelf()->get()->all()); - } - - public function testWithoutRootScope() { - $child = $this->clusters('Child 2.1'); - - $expected = array($this->clusters('Child 2'), $child); - - $this->assertEquals($expected, $child->ancestorsAndSelf()->withoutRoot()->get()->all()); - } - - public function testLimitDepthScope() { - with(new ClusterSeeder)->nestUptoAt($this->clusters('Child 2.1'), 10); - - $node = $this->clusters('Child 2'); - - $descendancy = $node->descendants()->pluck('id')->all(); - - $this->assertEmpty($node->descendants()->limitDepth(0)->pluck('id')->all()); - $this->assertEquals($node->getAttributes(), $node->descendantsAndSelf()->limitDepth(0)->first()->getAttributes()); - - $this->assertEquals(array_slice($descendancy, 0, 3), $node->descendants()->limitDepth(3)->pluck('id')->all()); - $this->assertEquals(array_slice($descendancy, 0, 5), $node->descendants()->limitDepth(5)->pluck('id')->all()); - $this->assertEquals(array_slice($descendancy, 0, 7), $node->descendants()->limitDepth(7)->pluck('id')->all()); - - $this->assertEquals($descendancy, $node->descendants()->limitDepth(1000)->pluck('id')->all()); - } - - public function testGetAncestorsAndSelf() { - $child = $this->clusters('Child 2.1'); - - $expected = array($this->clusters('Root 1'), $this->clusters('Child 2'), $child); - - $this->assertEquals($expected, $child->getAncestorsAndSelf()->all()); - } - - public function testGetAncestorsAndSelfWithoutRoot() { - $child = $this->clusters('Child 2.1'); - - $expected = array($this->clusters('Child 2'), $child); - - $this->assertEquals($expected, $child->getAncestorsAndSelfWithoutRoot()->all()); - } - - public function testGetAncestors() { - $child = $this->clusters('Child 2.1'); - - $expected = array($this->clusters('Root 1'), $this->clusters('Child 2')); - - $this->assertEquals($expected, $child->getAncestors()->all()); - } - - public function testGetAncestorsWithoutRoot() { - $child = $this->clusters('Child 2.1'); - - $expected = array($this->clusters('Child 2')); - - $this->assertEquals($expected, $child->getAncestorsWithoutRoot()->all()); - } - - public function testGetDescendantsAndSelf() { - $parent = $this->clusters('Root 1'); - - $expected = array( - $parent, - $this->clusters('Child 1'), - $this->clusters('Child 2'), - $this->clusters('Child 2.1'), - $this->clusters('Child 3') - ); - - $this->assertCount(count($expected), $parent->getDescendantsAndSelf()); - - $this->assertEquals($expected, $parent->getDescendantsAndSelf()->all()); - } - - public function testGetDescendantsAndSelfWithLimit() { - with(new ClusterSeeder)->nestUptoAt($this->clusters('Child 2.1'), 3); - - $parent = $this->clusters('Root 1'); - - $this->assertEquals(array($parent), $parent->getDescendantsAndSelf(0)->all()); - - $this->assertEquals(array( - $parent, - $this->clusters('Child 1'), - $this->clusters('Child 2'), - $this->clusters('Child 3') - ), $parent->getDescendantsAndSelf(1)->all()); - - $this->assertEquals(array( - $parent, - $this->clusters('Child 1'), - $this->clusters('Child 2'), - $this->clusters('Child 2.1'), - $this->clusters('Child 3') - ), $parent->getDescendantsAndSelf(2)->all()); - - $this->assertEquals(array( - $parent, - $this->clusters('Child 1'), - $this->clusters('Child 2'), - $this->clusters('Child 2.1'), - $this->clusters('Child 2.1.1'), - $this->clusters('Child 3') - ), $parent->getDescendantsAndSelf(3)->all()); - - $this->assertEquals(array( - $parent, - $this->clusters('Child 1'), - $this->clusters('Child 2'), - $this->clusters('Child 2.1'), - $this->clusters('Child 2.1.1'), - $this->clusters('Child 2.1.1.1'), - $this->clusters('Child 3') - ), $parent->getDescendantsAndSelf(4)->all()); - - $this->assertEquals(array( - $parent, - $this->clusters('Child 1'), - $this->clusters('Child 2'), - $this->clusters('Child 2.1'), - $this->clusters('Child 2.1.1'), - $this->clusters('Child 2.1.1.1'), - $this->clusters('Child 2.1.1.1.1'), - $this->clusters('Child 3') - ), $parent->getDescendantsAndSelf(10)->all()); - } - - public function testGetDescendants() { - $parent = $this->clusters('Root 1'); - - $expected = array( - $this->clusters('Child 1'), - $this->clusters('Child 2'), - $this->clusters('Child 2.1'), - $this->clusters('Child 3') - ); - - $this->assertCount(count($expected), $parent->getDescendants()); - - $this->assertEquals($expected, $parent->getDescendants()->all()); - } - - public function testGetDescendantsWithLimit() { - with(new ClusterSeeder)->nestUptoAt($this->clusters('Child 2.1'), 3); - - $parent = $this->clusters('Root 1'); - - $this->assertEmpty($parent->getDescendants(0)->all()); - - $this->assertEquals(array( - $this->clusters('Child 1'), - $this->clusters('Child 2'), - $this->clusters('Child 3') - ), $parent->getDescendants(1)->all()); - - $this->assertEquals(array( - $this->clusters('Child 1'), - $this->clusters('Child 2'), - $this->clusters('Child 2.1'), - $this->clusters('Child 3') - ), $parent->getDescendants(2)->all()); - - $this->assertEquals(array( - $this->clusters('Child 1'), - $this->clusters('Child 2'), - $this->clusters('Child 2.1'), - $this->clusters('Child 2.1.1'), - $this->clusters('Child 3') - ), $parent->getDescendants(3)->all()); - - $this->assertEquals(array( - $this->clusters('Child 1'), - $this->clusters('Child 2'), - $this->clusters('Child 2.1'), - $this->clusters('Child 2.1.1'), - $this->clusters('Child 2.1.1.1'), - $this->clusters('Child 3') - ), $parent->getDescendants(4)->all()); - - $this->assertEquals(array( - $this->clusters('Child 1'), - $this->clusters('Child 2'), - $this->clusters('Child 2.1'), - $this->clusters('Child 2.1.1'), - $this->clusters('Child 2.1.1.1'), - $this->clusters('Child 2.1.1.1.1'), - $this->clusters('Child 3') - ), $parent->getDescendants(5)->all()); - - $this->assertEquals(array( - $this->clusters('Child 1'), - $this->clusters('Child 2'), - $this->clusters('Child 2.1'), - $this->clusters('Child 2.1.1'), - $this->clusters('Child 2.1.1.1'), - $this->clusters('Child 2.1.1.1.1'), - $this->clusters('Child 3') - ), $parent->getDescendants(10)->all()); - } - - public function testDescendantsRecursesChildren() { - $a = Cluster::create(array('name' => 'A')); - $b = Cluster::create(array('name' => 'B')); - $c = Cluster::create(array('name' => 'C')); - - // a > b > c - $b->makeChildOf($a); - $c->makeChildOf($b); - - $a->reload(); $b->reload(); $c->reload(); - - $this->assertEquals(1, $a->children()->count()); - $this->assertEquals(1, $b->children()->count()); - $this->assertEquals(2, $a->descendants()->count()); - } - - public function testGetImmediateDescendants() { - $expected = array($this->clusters('Child 1'), $this->clusters('Child 2'), $this->clusters('Child 3')); - - $this->assertEquals($expected, $this->clusters('Root 1')->getImmediateDescendants()->all()); - - $this->assertEquals(array($this->clusters('Child 2.1')), $this->clusters('Child 2')->getImmediateDescendants()->all()); - - $this->assertEmpty($this->clusters('Root 2')->getImmediateDescendants()->all()); - } - - public function testIsSelfOrAncestorOf() { - $this->assertTrue($this->clusters('Root 1')->isSelfOrAncestorOf($this->clusters('Child 1'))); - $this->assertTrue($this->clusters('Root 1')->isSelfOrAncestorOf($this->clusters('Child 2.1'))); - $this->assertTrue($this->clusters('Child 2')->isSelfOrAncestorOf($this->clusters('Child 2.1'))); - $this->assertFalse($this->clusters('Child 2.1')->isSelfOrAncestorOf($this->clusters('Child 2'))); - $this->assertFalse($this->clusters('Child 1')->isSelfOrAncestorOf($this->clusters('Child 2'))); - $this->assertTrue($this->clusters('Child 1')->isSelfOrAncestorOf($this->clusters('Child 1'))); - } - - public function testIsAncestorOf() { - $this->assertTrue($this->clusters('Root 1')->isAncestorOf($this->clusters('Child 1'))); - $this->assertTrue($this->clusters('Root 1')->isAncestorOf($this->clusters('Child 2.1'))); - $this->assertTrue($this->clusters('Child 2')->isAncestorOf($this->clusters('Child 2.1'))); - $this->assertFalse($this->clusters('Child 2.1')->isAncestorOf($this->clusters('Child 2'))); - $this->assertFalse($this->clusters('Child 1')->isAncestorOf($this->clusters('Child 2'))); - $this->assertFalse($this->clusters('Child 1')->isAncestorOf($this->clusters('Child 1'))); - } - - public function testIsSelfOrDescendantOf() { - $this->assertTrue($this->clusters('Child 1')->isSelfOrDescendantOf($this->clusters('Root 1'))); - $this->assertTrue($this->clusters('Child 2.1')->isSelfOrDescendantOf($this->clusters('Root 1'))); - $this->assertTrue($this->clusters('Child 2.1')->isSelfOrDescendantOf($this->clusters('Child 2'))); - $this->assertFalse($this->clusters('Child 2')->isSelfOrDescendantOf($this->clusters('Child 2.1'))); - $this->assertFalse($this->clusters('Child 2')->isSelfOrDescendantOf($this->clusters('Child 1'))); - $this->assertTrue($this->clusters('Child 1')->isSelfOrDescendantOf($this->clusters('Child 1'))); - } - - public function testIsDescendantOf() { - $this->assertTrue($this->clusters('Child 1')->isDescendantOf($this->clusters('Root 1'))); - $this->assertTrue($this->clusters('Child 2.1')->isDescendantOf($this->clusters('Root 1'))); - $this->assertTrue($this->clusters('Child 2.1')->isDescendantOf($this->clusters('Child 2'))); - $this->assertFalse($this->clusters('Child 2')->isDescendantOf($this->clusters('Child 2.1'))); - $this->assertFalse($this->clusters('Child 2')->isDescendantOf($this->clusters('Child 1'))); - $this->assertFalse($this->clusters('Child 1')->isDescendantOf($this->clusters('Child 1'))); - } - - public function testGetSiblingsAndSelf() { - $child = $this->clusters('Child 2'); - - $expected = array($this->clusters('Child 1'), $child, $this->clusters('Child 3')); - $this->assertEquals($expected, $child->getSiblingsAndSelf()->all()); - - $expected = array($this->clusters('Root 1'), $this->clusters('Root 2')); - $this->assertEquals($expected, $this->clusters('Root 1')->getSiblingsAndSelf()->all()); - } - - public function testGetSiblings() { - $child = $this->clusters('Child 2'); - - $expected = array($this->clusters('Child 1'), $this->clusters('Child 3')); - - $this->assertEquals($expected, $child->getSiblings()->all()); - } - - public function testGetLeftSibling() { - $this->assertEquals($this->clusters('Child 1'), $this->clusters('Child 2')->getLeftSibling()); - $this->assertEquals($this->clusters('Child 2'), $this->clusters('Child 3')->getLeftSibling()); - } - - public function testGetLeftSiblingOfFirstRootIsNull() { - $this->assertNull($this->clusters('Root 1')->getLeftSibling()); - } - - public function testGetLeftSiblingWithNoneIsNull() { - $this->assertNull($this->clusters('Child 2.1')->getLeftSibling()); - } - - public function testGetLeftSiblingOfLeftmostNodeIsNull() { - $this->assertNull($this->clusters('Child 1')->getLeftSibling()); - } - - public function testGetRightSibling() { - $this->assertEquals($this->clusters('Child 3'), $this->clusters('Child 2')->getRightSibling()); - $this->assertEquals($this->clusters('Child 2'), $this->clusters('Child 1')->getRightSibling()); - } - - public function testGetRightSiblingOfRoots() { - $this->assertEquals($this->clusters('Root 2'), $this->clusters('Root 1')->getRightSibling()); - $this->assertNull($this->clusters('Root 2')->getRightSibling()); - } - - public function testGetRightSiblingWithNoneIsNull() { - $this->assertNull($this->clusters('Child 2.1')->getRightSibling()); - } - - public function testGetRightSiblingOfRightmostNodeIsNull() { - $this->assertNull($this->clusters('Child 3')->getRightSibling()); - } - - public function testInsideSubtree() { - $this->assertFalse($this->clusters('Child 1')->insideSubtree($this->clusters('Root 2'))); - $this->assertFalse($this->clusters('Child 2')->insideSubtree($this->clusters('Root 2'))); - $this->assertFalse($this->clusters('Child 3')->insideSubtree($this->clusters('Root 2'))); - - $this->assertTrue($this->clusters('Child 1')->insideSubtree($this->clusters('Root 1'))); - $this->assertTrue($this->clusters('Child 2')->insideSubtree($this->clusters('Root 1'))); - $this->assertTrue($this->clusters('Child 2.1')->insideSubtree($this->clusters('Root 1'))); - $this->assertTrue($this->clusters('Child 3')->insideSubtree($this->clusters('Root 1'))); - - $this->assertTrue($this->clusters('Child 2.1')->insideSubtree($this->clusters('Child 2'))); - $this->assertFalse($this->clusters('Child 2.1')->insideSubtree($this->clusters('Root 2'))); - } - - public function testGetLevel() { - $this->assertEquals(0, $this->clusters('Root 1')->getLevel()); - $this->assertEquals(1, $this->clusters('Child 1')->getLevel()); - $this->assertEquals(2, $this->clusters('Child 2.1')->getLevel()); - } - - public function testToHierarchyReturnsAnEloquentCollection() { - $categories = Cluster::all()->toHierarchy(); - - $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $categories); - } - - public function testToHierarchyReturnsHierarchicalData() { - $categories = Cluster::all()->toHierarchy(); - - $this->assertEquals(2, $categories->count()); - - $first = $categories->first(); - $this->assertEquals('Root 1', $first->name); - $this->assertEquals(3, $first->children->count()); - - $first_lvl2 = $first->children->first(); - $this->assertEquals('Child 1', $first_lvl2->name); - $this->assertEquals(0, $first_lvl2->children->count()); - } - - public function testToHierarchyNestsCorrectly() { - // Prune all categories - Cluster::query()->delete(); - - // Build a sample tree structure: - // - // - A - // |- A.1 - // |- A.2 - // - B - // |- B.1 - // |- B.2 - // |- B.2.1 - // |- B.2.2 - // |- B.2.2.1 - // |- B.2.3 - // |- B.3 - // - C - // |- C.1 - // |- C.2 - // - D - // - $a = Cluster::create(array('name' => 'A')); - $b = Cluster::create(array('name' => 'B')); - $c = Cluster::create(array('name' => 'C')); - $d = Cluster::create(array('name' => 'D')); - - $ch = Cluster::create(array('name' => 'A.1')); - $ch->makeChildOf($a); - - $ch = Cluster::create(array('name' => 'A.2')); - $ch->makeChildOf($a); - - $ch = Cluster::create(array('name' => 'B.1')); - $ch->makeChildOf($b); - - $ch = Cluster::create(array('name' => 'B.2')); - $ch->makeChildOf($b); - - $ch2 = Cluster::create(array('name' => 'B.2.1')); - $ch2->makeChildOf($ch); - - $ch2 = Cluster::create(array('name' => 'B.2.2')); - $ch2->makeChildOf($ch); - - $ch3 = Cluster::create(array('name' => 'B.2.2.1')); - $ch3->makeChildOf($ch2); - - $ch2 = Cluster::create(array('name' => 'B.2.3')); - $ch2->makeChildOf($ch); - - $ch = Cluster::create(array('name' => 'B.3')); - $ch->makeChildOf($b); - - $ch = Cluster::create(array('name' => 'C.1')); - $ch->makeChildOf($c); - - $ch = Cluster::create(array('name' => 'C.2')); - $ch->makeChildOf($c); - - $this->assertTrue(Cluster::isValidNestedSet()); - - // Build expectations (expected trees/subtrees) - $expectedWholeTree = array( - 'A' => array ( 'A.1' => null, 'A.2' => null ), - 'B' => array ( - 'B.1' => null, - 'B.2' => - array ( - 'B.2.1' => null, - 'B.2.2' => array ( 'B.2.2.1' => null ), - 'B.2.3' => null, - ), - 'B.3' => null, - ), - 'C' => array ( 'C.1' => null, 'C.2' => null ), - 'D' => null - ); - - $expectedSubtreeA = array('A' => array ( 'A.1' => null, 'A.2' => null )); - - $expectedSubtreeB = array( - 'B' => array ( - 'B.1' => null, - 'B.2' => - array ( - 'B.2.1' => null, - 'B.2.2' => array ( 'B.2.2.1' => null ), - 'B.2.3' => null - ), - 'B.3' => null - ) - ); - - $expectedSubtreeC = array( 'C.1' => null, 'C.2' => null ); - - $expectedSubtreeD = array('D' => null); - - // Perform assertions - $wholeTree = hmap(Cluster::all()->toHierarchy()->toArray()); - $this->assertArraysAreEqual($expectedWholeTree, $wholeTree); - - $subtreeA = hmap($this->clusters('A')->getDescendantsAndSelf()->toHierarchy()->toArray()); - $this->assertArraysAreEqual($expectedSubtreeA, $subtreeA); - - $subtreeB = hmap($this->clusters('B')->getDescendantsAndSelf()->toHierarchy()->toArray()); - $this->assertArraysAreEqual($expectedSubtreeB, $subtreeB); - - $subtreeC = hmap($this->clusters('C')->getDescendants()->toHierarchy()->toArray()); - $this->assertArraysAreEqual($expectedSubtreeC, $subtreeC); - - $subtreeD = hmap($this->clusters('D')->getDescendantsAndSelf()->toHierarchy()->toArray()); - $this->assertArraysAreEqual($expectedSubtreeD, $subtreeD); - - $this->assertTrue($this->clusters('D')->getDescendants()->toHierarchy()->isEmpty()); - } - - public function testToHierarchyNestsCorrectlyNotSequential() { - $parent = $this->clusters('Child 1'); - - $parent->children()->create(array('name' => 'Child 1.1')); - - $parent->children()->create(array('name' => 'Child 1.2')); - - $this->assertTrue(Cluster::isValidNestedSet()); - - $expected = array( - 'Child 1' => array( - 'Child 1.1' => null, - 'Child 1.2' => null - ) - ); - - $parent->reload(); - $this->assertArraysAreEqual($expected, hmap($parent->getDescendantsAndSelf()->toHierarchy()->toArray())); - } - - public function testToHierarchyNestsCorrectlyWithOrder() { - with(new OrderedClusterSeeder)->run(); - - $expectedWhole = array( - 'Root A' => null, - 'Root Z' => array( - 'Child A' => null, - 'Child C' => null, - 'Child G' => array( 'Child G.1' => null ) - ) - ); - $this->assertArraysAreEqual($expectedWhole, hmap(OrderedCluster::all()->toHierarchy()->toArray())); - - $expectedSubtreeZ = array( - 'Root Z' => array( - 'Child A' => null, - 'Child C' => null, - 'Child G' => array( 'Child G.1' => null ) - ) - ); - $this->assertArraysAreEqual($expectedSubtreeZ, hmap($this->clusters('Root Z', 'OrderedCluster')->getDescendantsAndSelf()->toHierarchy()->toArray())); - } - - public function testGetNestedList() { - $seperator = ' '; - $nestedList = Cluster::getNestedList('name', 'id', $seperator); - - $expected = array( - '7461d8f5-2ea9-4788-99c4-9d0244f0bfb1' => str_repeat($seperator, 0). 'Root 1', - '5d7ce1fd-6151-46d3-a5b3-0ebb9988dc57' => str_repeat($seperator, 1). 'Child 1', - '07c1fc8c-53b5-4fe7-b9c4-e09f266a455c' => str_repeat($seperator, 1). 'Child 2', - '3315a297-af87-4ad3-9fa5-19785407573d' => str_repeat($seperator, 2). 'Child 2.1', - '054476d2-6830-4014-a181-4de010ef7114' => str_repeat($seperator, 1). 'Child 3', - '3bb62314-9e1e-49c6-a5cb-17a9ab9b1b9a' => str_repeat($seperator, 0). 'Root 2', - ); - - $this->assertArraysAreEqual($expected, $nestedList); - } - -} diff --git a/tests/suite/Cluster/ClusterMovementTest.php b/tests/suite/Cluster/ClusterMovementTest.php deleted file mode 100644 index 967a68ea..00000000 --- a/tests/suite/Cluster/ClusterMovementTest.php +++ /dev/null @@ -1,528 +0,0 @@ -clusters('Child 2')->moveLeft(); - - $this->assertNull($this->clusters('Child 2')->getLeftSibling()); - - $this->assertEquals($this->clusters('Child 1'), $this->clusters('Child 2')->getRightSibling()); - - $this->assertTrue(Cluster::isValidNestedSet()); - } - - /** - * @expectedException Baum\MoveNotPossibleException - */ - public function testMoveLeftRaisesAnExceptionWhenNotPossible() { - $node = $this->clusters('Child 2'); - - $node->moveLeft(); - $node->moveLeft(); - } - - public function testMoveLeftDoesNotChangeDepth() { - $this->clusters('Child 2')->moveLeft(); - - $this->assertEquals(1, $this->clusters('Child 2')->getDepth()); - $this->assertEquals(2, $this->clusters('Child 2.1')->getDepth()); - } - - public function testMoveLeftWithSubtree() { - $this->clusters('Root 2')->moveLeft(); - - $this->assertNull($this->clusters('Root 2')->getLeftSibling()); - $this->assertEquals($this->clusters('Root 1'), $this->clusters('Root 2')->getRightSibling()); - $this->assertTrue(Cluster::isValidNestedSet()); - - $this->assertEquals(0, $this->clusters('Root 1')->getDepth()); - $this->assertEquals(0, $this->clusters('Root 2')->getDepth()); - - $this->assertEquals(1, $this->clusters('Child 1')->getDepth()); - $this->assertEquals(1, $this->clusters('Child 2')->getDepth()); - $this->assertEquals(1, $this->clusters('Child 3')->getDepth()); - - $this->assertEquals(2, $this->clusters('Child 2.1')->getDepth()); - } - - public function testMoveToLeftOf() { - $this->clusters('Child 3')->moveToLeftOf($this->clusters('Child 1')); - - $this->assertNull($this->clusters('Child 3')->getLeftSibling()); - - $this->assertEquals($this->clusters('Child 1'), $this->clusters('Child 3')->getRightSibling()); - - $this->assertTrue(Cluster::isValidNestedSet()); - } - - /** - * @expectedException Baum\MoveNotPossibleException - */ - public function testMoveToLeftOfRaisesAnExceptionWhenNotPossible() { - $this->clusters('Child 1')->moveToLeftOf($this->clusters('Child 1')->getLeftSibling()); - } - - public function testMoveToLeftOfDoesNotChangeDepth() { - $this->clusters('Child 2')->moveToLeftOf($this->clusters('Child 1')); - - $this->assertEquals(1, $this->clusters('Child 2')->getDepth()); - $this->assertEquals(2, $this->clusters('Child 2.1')->getDepth()); - } - - public function testMoveToLeftOfWithSubtree() { - $this->clusters('Root 2')->moveToLeftOf($this->clusters('Root 1')); - - $this->assertNull($this->clusters('Root 2')->getLeftSibling()); - $this->assertEquals($this->clusters('Root 1'), $this->clusters('Root 2')->getRightSibling()); - $this->assertTrue(Cluster::isValidNestedSet()); - - $this->assertEquals(0, $this->clusters('Root 1')->getDepth()); - $this->assertEquals(0, $this->clusters('Root 2')->getDepth()); - - $this->assertEquals(1, $this->clusters('Child 1')->getDepth()); - $this->assertEquals(1, $this->clusters('Child 2')->getDepth()); - $this->assertEquals(1, $this->clusters('Child 3')->getDepth()); - - $this->assertEquals(2, $this->clusters('Child 2.1')->getDepth()); - } - - public function testMoveRight() { - $this->clusters('Child 2')->moveRight(); - - $this->assertNull($this->clusters('Child 2')->getRightSibling()); - - $this->assertEquals($this->clusters('Child 3'), $this->clusters('Child 2')->getLeftSibling()); - - $this->assertTrue(Cluster::isValidNestedSet()); - } - - /** - * @expectedException Baum\MoveNotPossibleException - */ - public function testMoveRightRaisesAnExceptionWhenNotPossible() { - $node = $this->clusters('Child 2'); - - $node->moveRight(); - $node->moveRight(); - } - - public function testMoveRightDoesNotChangeDepth() { - $this->clusters('Child 2')->moveRight(); - - $this->assertEquals(1, $this->clusters('Child 2')->getDepth()); - $this->assertEquals(2, $this->clusters('Child 2.1')->getDepth()); - } - - public function testMoveRightWithSubtree() { - $this->clusters('Root 1')->moveRight(); - - $this->assertNull($this->clusters('Root 1')->getRightSibling()); - $this->assertEquals($this->clusters('Root 2'), $this->clusters('Root 1')->getLeftSibling()); - $this->assertTrue(Cluster::isValidNestedSet()); - - $this->assertEquals(0, $this->clusters('Root 1')->getDepth()); - $this->assertEquals(0, $this->clusters('Root 2')->getDepth()); - - $this->assertEquals(1, $this->clusters('Child 1')->getDepth()); - $this->assertEquals(1, $this->clusters('Child 2')->getDepth()); - $this->assertEquals(1, $this->clusters('Child 3')->getDepth()); - - $this->assertEquals(2, $this->clusters('Child 2.1')->getDepth()); - } - - public function testMoveToRightOf() { - $this->clusters('Child 1')->moveToRightOf($this->clusters('Child 3')); - - $this->assertNull($this->clusters('Child 1')->getRightSibling()); - - $this->assertEquals($this->clusters('Child 3'), $this->clusters('Child 1')->getLeftSibling()); - - $this->assertTrue(Cluster::isValidNestedSet()); - } - - /** - * @expectedException Baum\MoveNotPossibleException - */ - public function testMoveToRightOfRaisesAnExceptionWhenNotPossible() { - $this->clusters('Child 3')->moveToRightOf($this->clusters('Child 3')->getRightSibling()); - } - - public function testMoveToRightOfDoesNotChangeDepth() { - $this->clusters('Child 2')->moveToRightOf($this->clusters('Child 3')); - - $this->assertEquals(1, $this->clusters('Child 2')->getDepth()); - $this->assertEquals(2, $this->clusters('Child 2.1')->getDepth()); - } - - public function testMoveToRightOfWithSubtree() { - $this->clusters('Root 1')->moveToRightOf($this->clusters('Root 2')); - - $this->assertNull($this->clusters('Root 1')->getRightSibling()); - $this->assertEquals($this->clusters('Root 2'), $this->clusters('Root 1')->getLeftSibling()); - $this->assertTrue(Cluster::isValidNestedSet()); - - $this->assertEquals(0, $this->clusters('Root 1')->getDepth()); - $this->assertEquals(0, $this->clusters('Root 2')->getDepth()); - - $this->assertEquals(1, $this->clusters('Child 1')->getDepth()); - $this->assertEquals(1, $this->clusters('Child 2')->getDepth()); - $this->assertEquals(1, $this->clusters('Child 3')->getDepth()); - - $this->assertEquals(2, $this->clusters('Child 2.1')->getDepth()); - } - - public function testMakeRoot() { - $this->clusters('Child 2')->makeRoot(); - - $newRoot = $this->clusters('Child 2'); - - $this->assertNull($newRoot->parent()->first()); - $this->assertEquals(0, $newRoot->getLevel()); - $this->assertEquals(9, $newRoot->getLeft()); - $this->assertEquals(12, $newRoot->getRight()); - - $this->assertEquals(1, $this->clusters('Child 2.1')->getLevel()); - - $this->assertTrue(Cluster::isValidNestedSet()); - } - - public function testNullifyParentColumnMakesItRoot() { - $node = $this->clusters('Child 2'); - - $node->parent_id = null; - - $node->save(); - - $this->assertNull($node->parent()->first()); - $this->assertEquals(0, $node->getLevel()); - $this->assertEquals(9, $node->getLeft()); - $this->assertEquals(12, $node->getRight()); - - $this->assertEquals(1, $this->clusters('Child 2.1')->getLevel()); - - $this->assertTrue(Cluster::isValidNestedSet()); - } - - public function testNullifyParentColumnOnNewNodes() { - $node = new Cluster(['name' => 'Root 3']); - - $node->parent_id = null; - - $node->save(); - - $node->reload(); - - $this->assertNull($node->parent()->first()); - $this->assertEquals(0, $node->getLevel()); - $this->assertEquals(13, $node->getLeft()); - $this->assertEquals(14, $node->getRight()); - - $this->assertTrue(Cluster::isValidNestedSet()); - } - - public function testNewClusterWithNullParent() { - $node = new Cluster(['name' => 'Root 3']); - $this->assertTrue($node->isRoot()); - - $node->save(); - $this->assertTrue($node->isRoot()); - - $node->makeRoot(); - $this->assertTrue($node->isRoot()); - } - - public function testMakeChildOf() { - $this->clusters('Child 1')->makeChildOf($this->clusters('Child 3')); - - $this->assertEquals($this->clusters('Child 3'), $this->clusters('Child 1')->parent()->first()); - - $this->assertTrue(Cluster::isValidNestedSet()); - } - - public function testMakeChildOfAppendsAtTheEnd() { - $newChild = Cluster::create(array('name' => 'Child 4')); - - $newChild->makeChildOf($this->clusters('Root 1')); - - $lastChild = $this->clusters('Root 1')->children()->get()->last(); - $this->assertEquals($newChild->getAttributes(), $lastChild->getAttributes()); - - $this->assertTrue(Cluster::isValidNestedSet()); - } - - public function testMakeChildOfMovesWithSubtree() { - $this->clusters('Child 2')->makeChildOf($this->clusters('Child 1')); - - $this->assertTrue(Cluster::isValidNestedSet()); - - $this->assertEquals($this->clusters('Child 1')->getKey(), $this->clusters('Child 2')->getParentId()); - - $this->assertEquals(3, $this->clusters('Child 2')->getLeft()); - $this->assertEquals(6, $this->clusters('Child 2')->getRight()); - - $this->assertEquals(2, $this->clusters('Child 1')->getLeft()); - $this->assertEquals(7, $this->clusters('Child 1')->getRight()); - } - - public function testMakeChildOfSwappingRoots() { - $newRoot = Cluster::create(array('name' => 'Root 3')); - - $this->assertEquals(13, $newRoot->getLeft()); - $this->assertEquals(14, $newRoot->getRight()); - - $this->clusters('Root 2')->makeChildOf($newRoot); - - $this->assertTrue(Cluster::isValidNestedSet()); - - $this->assertEquals($newRoot->getKey(), $this->clusters('Root 2')->getParentId()); - - $this->assertEquals(12, $this->clusters('Root 2')->getLeft()); - $this->assertEquals(13, $this->clusters('Root 2')->getRight()); - - $this->assertEquals(11, $newRoot->getLeft()); - $this->assertEquals(14, $newRoot->getRight()); - } - - public function testMakeChildOfSwappingRootsWithSubtrees() { - $newRoot = Cluster::create(array('name' => 'Root 3')); - - $this->clusters('Root 1')->makeChildOf($newRoot); - - $this->assertTrue(Cluster::isValidNestedSet()); - - $this->assertEquals($newRoot->getKey(), $this->clusters('Root 1')->getParentId()); - - $this->assertEquals(4, $this->clusters('Root 1')->getLeft()); - $this->assertEquals(13, $this->clusters('Root 1')->getRight()); - - $this->assertEquals(8, $this->clusters('Child 2.1')->getLeft()); - $this->assertEquals(9, $this->clusters('Child 2.1')->getRight()); - } - - public function testMakeFirstChildOf() { - $this->clusters('Child 1')->makeFirstChildOf($this->clusters('Child 3')); - - $this->assertEquals($this->clusters('Child 3'), $this->clusters('Child 1')->parent()->first()); - - $this->assertTrue(Cluster::isValidNestedSet()); - } - - public function testMakeFirstChildOfAppendsAtTheBeginning() { - $newChild = Cluster::create(array('name' => 'Child 4')); - - $newChild->makeFirstChildOf($this->clusters('Root 1')); - - $lastChild = $this->clusters('Root 1')->children()->get()->first(); - $this->assertEquals($newChild->getAttributes(), $lastChild->getAttributes()); - - $this->assertTrue(Cluster::isValidNestedSet()); - } - - public function testMakeFirstChildOfMovesWithSubtree() { - $this->clusters('Child 2')->makeFirstChildOf($this->clusters('Child 1')); - - $this->assertTrue(Cluster::isValidNestedSet()); - - $this->assertEquals($this->clusters('Child 1')->getKey(), $this->clusters('Child 2')->getParentId()); - - $this->assertEquals(3, $this->clusters('Child 2')->getLeft()); - $this->assertEquals(6, $this->clusters('Child 2')->getRight()); - - $this->assertEquals(2, $this->clusters('Child 1')->getLeft()); - $this->assertEquals(7, $this->clusters('Child 1')->getRight()); - } - - public function testMakeFirstChildOfSwappingRoots() { - $newRoot = Cluster::create(array('name' => 'Root 3')); - - $this->assertEquals(13, $newRoot->getLeft()); - $this->assertEquals(14, $newRoot->getRight()); - - $this->clusters('Root 2')->makeFirstChildOf($newRoot); - - $this->assertTrue(Cluster::isValidNestedSet()); - - $this->assertEquals($newRoot->getKey(), $this->clusters('Root 2')->getParentId()); - - $this->assertEquals(12, $this->clusters('Root 2')->getLeft()); - $this->assertEquals(13, $this->clusters('Root 2')->getRight()); - - $this->assertEquals(11, $newRoot->getLeft()); - $this->assertEquals(14, $newRoot->getRight()); - } - - public function testMakeFirstChildOfSwappingRootsWithSubtrees() { - $newRoot = Cluster::create(array('name' => 'Root 3')); - - $this->clusters('Root 1')->makeFirstChildOf($newRoot); - - $this->assertTrue(Cluster::isValidNestedSet()); - - $this->assertEquals($newRoot->getKey(), $this->clusters('Root 1')->getParentId()); - - $this->assertEquals(4, $this->clusters('Root 1')->getLeft()); - $this->assertEquals(13, $this->clusters('Root 1')->getRight()); - - $this->assertEquals(8, $this->clusters('Child 2.1')->getLeft()); - $this->assertEquals(9, $this->clusters('Child 2.1')->getRight()); - } - - public function testMakeLastChildOf() { - $this->clusters('Child 1')->makeLastChildOf($this->clusters('Child 3')); - - $this->assertEquals($this->clusters('Child 3'), $this->clusters('Child 1')->parent()->first()); - - $this->assertTrue(Cluster::isValidNestedSet()); - } - - public function testMakeLastChildOfAppendsAtTheEnd() { - $newChild = Cluster::create(array('name' => 'Child 4')); - - $newChild->makeLastChildOf($this->clusters('Root 1')); - - $lastChild = $this->clusters('Root 1')->children()->get()->last(); - $this->assertEquals($newChild->getAttributes(), $lastChild->getAttributes()); - - $this->assertTrue(Cluster::isValidNestedSet()); - } - - public function testMakeLastChildOfMovesWithSubtree() { - $this->clusters('Child 2')->makeLastChildOf($this->clusters('Child 1')); - - $this->assertTrue(Cluster::isValidNestedSet()); - - $this->assertEquals($this->clusters('Child 1')->getKey(), $this->clusters('Child 2')->getParentId()); - - $this->assertEquals(3, $this->clusters('Child 2')->getLeft()); - $this->assertEquals(6, $this->clusters('Child 2')->getRight()); - - $this->assertEquals(2, $this->clusters('Child 1')->getLeft()); - $this->assertEquals(7, $this->clusters('Child 1')->getRight()); - } - - public function testMakeLastChildOfSwappingRoots() { - $newRoot = Cluster::create(array('name' => 'Root 3')); - - $this->assertEquals(13, $newRoot->getLeft()); - $this->assertEquals(14, $newRoot->getRight()); - - $this->clusters('Root 2')->makeLastChildOf($newRoot); - - $this->assertTrue(Cluster::isValidNestedSet()); - - $this->assertEquals($newRoot->getKey(), $this->clusters('Root 2')->getParentId()); - - $this->assertEquals(12, $this->clusters('Root 2')->getLeft()); - $this->assertEquals(13, $this->clusters('Root 2')->getRight()); - - $this->assertEquals(11, $newRoot->getLeft()); - $this->assertEquals(14, $newRoot->getRight()); - } - - public function testMakeLastChildOfSwappingRootsWithSubtrees() { - $newRoot = Cluster::create(array('name' => 'Root 3')); - - $this->clusters('Root 1')->makeLastChildOf($newRoot); - - $this->assertTrue(Cluster::isValidNestedSet()); - - $this->assertEquals($newRoot->getKey(), $this->clusters('Root 1')->getParentId()); - - $this->assertEquals(4, $this->clusters('Root 1')->getLeft()); - $this->assertEquals(13, $this->clusters('Root 1')->getRight()); - - $this->assertEquals(8, $this->clusters('Child 2.1')->getLeft()); - $this->assertEquals(9, $this->clusters('Child 2.1')->getRight()); - } - - /** - * @expectedException Baum\MoveNotPossibleException - */ - public function testUnpersistedNodeCannotBeMoved() { - $unpersisted = new Cluster(array('name' => 'Unpersisted')); - - $unpersisted->moveToRightOf($this->clusters('Root 1')); - } - - /** - * @expectedException Baum\MoveNotPossibleException - */ - public function testUnpersistedNodeCannotBeMadeChild() { - $unpersisted = new Cluster(array('name' => 'Unpersisted')); - - $unpersisted->makeChildOf($this->clusters('Root 1')); - } - - /** - * @expectedException Baum\MoveNotPossibleException - */ - public function testNodesCannotBeMovedToItself() { - $node = $this->clusters('Child 1'); - - $node->moveToRightOf($node); - } - - /** - * @expectedException Baum\MoveNotPossibleException - */ - public function testNodesCannotBeMadeChildOfThemselves() { - $node = $this->clusters('Child 1'); - - $node->makeChildOf($node); - } - - /** - * @expectedException Baum\MoveNotPossibleException - */ - public function testNodesCannotBeMovedToDescendantsOfThemselves() { - $node = $this->clusters('Root 1'); - - $node->makeChildOf($this->clusters('Child 2.1')); - } - - public function testDepthIsUpdatedWhenMadeChild() { - $a = Cluster::create(array('name' => 'A')); - $b = Cluster::create(array('name' => 'B')); - $c = Cluster::create(array('name' => 'C')); - $d = Cluster::create(array('name' => 'D')); - - // a > b > c > d - $b->makeChildOf($a); - $c->makeChildOf($b); - $d->makeChildOf($c); - - $a->reload(); - $b->reload(); - $c->reload(); - $d->reload(); - - $this->assertEquals(0, $a->getDepth()); - $this->assertEquals(1, $b->getDepth()); - $this->assertEquals(2, $c->getDepth()); - $this->assertEquals(3, $d->getDepth()); - } - - public function testDepthIsUpdatedOnDescendantsWhenParentMoves() { - $a = Cluster::create(array('name' => 'A')); - $b = Cluster::create(array('name' => 'B')); - $c = Cluster::create(array('name' => 'C')); - $d = Cluster::create(array('name' => 'D')); - - // a > b > c > d - $b->makeChildOf($a); - $c->makeChildOf($b); - $d->makeChildOf($c); - - $a->reload(); $b->reload(); $c->reload(); $d->reload(); - - $b->moveToRightOf($a); - - $a->reload(); $b->reload(); $c->reload(); $d->reload(); - - $this->assertEquals(0, $b->getDepth()); - $this->assertEquals(1, $c->getDepth()); - $this->assertEquals(2, $d->getDepth()); - } - -} diff --git a/tests/suite/ClusterTestCase.php b/tests/suite/ClusterTestCase.php deleted file mode 100644 index 81038ad0..00000000 --- a/tests/suite/ClusterTestCase.php +++ /dev/null @@ -1,17 +0,0 @@ -up(); - } - - public function setUp() { - with(new ClusterSeeder)->run(); - } - - protected function clusters($name, $className = 'Cluster') { - return forward_static_call_array(array($className, 'where'), array('name', '=', $name))->first(); - } - -} diff --git a/tests/suite/ColumnsTest.php b/tests/suite/ColumnsTest.php new file mode 100644 index 00000000..3fe0492f --- /dev/null +++ b/tests/suite/ColumnsTest.php @@ -0,0 +1,172 @@ +assertEquals($category->getParentColumnName(), 'parent_id'); + } + + public function testGetQualifiedParentColumnName() + { + $category = new Category; + + $this->assertEquals($category->getQualifiedParentColumnName(), 'categories.parent_id'); + } + + public function testGetParentKey() + { + $this->assertNull($this->categories('Root 1')->getParentKey()); + + $this->assertEquals($this->categories('Child 1')->getParentKey(), 1); + } + + public function testGetLeftColumnName() + { + $category = new Category; + + $this->assertEquals($category->getLeftColumnName(), 'left'); + } + + public function testGetQualifiedLeftColumnName() + { + $category = new Category; + + $this->assertEquals($category->getQualifiedLeftColumnName(), 'categories.left'); + } + + public function testGetLeft() + { + $category = $this->categories('Root 1'); + + $this->assertEquals($category->getLeft(), 1); + } + + public function testGetRightColumnName() + { + $category = new Category; + + $this->assertEquals($category->getRightColumnName(), 'right'); + } + + public function testGetQualifiedRightColumnName() + { + $category = new Category; + + $this->assertEquals($category->getQualifiedRightColumnName(), 'categories.right'); + } + + public function testGetRight() + { + $category = $this->categories('Root 1'); + + $this->assertEquals($category->getRight(), 10); + } + + public function testGetOrderColumName() + { + $category = new Category; + + $this->assertEquals($category->getOrderColumnName(), $category->getLeftColumnName()); + } + + public function testGetQualifiedOrderColumnName() + { + $category = new Category; + + $this->assertEquals($category->getQualifiedOrderColumnName(), $category->getQualifiedLeftColumnName()); + } + + public function testGetOrder() + { + $category = $this->categories('Root 1'); + + $this->assertEquals($category->getOrder(), $category->getLeft()); + } + + public function testGetOrderColumnNameNonDefault() + { + $category = new OrderedCategory; + + $this->assertEquals($category->getOrderColumnName(), 'name'); + } + + public function testGetQualifiedOrderColumnNameNonDefault() + { + $category = new OrderedCategory; + + $this->assertEquals($category->getQualifiedOrderColumnName(), 'categories.name'); + } + + public function testGetOrderNonDefault() + { + $category = $this->categories('Root 1', OrderedCategory::class); + + $this->assertEquals($category->getOrder(), 'Root 1'); + } + + public function testGetScopedColumnNames() + { + $category = new Category; + $this->assertEquals($category->getScopedColumnNames(), []); + + $category = new ScopedCategory; + $this->assertEquals($category->getScopedColumnNames(), ['company_id']); + + $category = new MultiScopedCategory; + $this->assertEquals($category->getScopedColumnNames(), ['company_id', 'language']); + } + + public function testGetQualifiedScopedColumnNames() + { + $category = new Category; + $this->assertEquals($category->getQualifiedScopedColumnNames(), []); + + $category = new ScopedCategory; + $this->assertEquals($category->getQualifiedScopedColumnNames(), ['categories.company_id']); + + $category = new MultiScopedCategory; + $this->assertEquals($category->getQualifiedScopedColumnNames(), ['categories.company_id', 'categories.language']); + } + + public function testIsScoped() + { + $category = new Category; + $this->assertFalse($category->isScoped()); + + $category = new ScopedCategory; + $this->assertTrue($category->isScoped()); + + $category = new MultiScopedCategory; + $this->assertTrue($category->isScoped()); + + $category = new OrderedCategory(); + $this->assertFalse($category->isScoped()); + } + + public function testNonNumericKey() + { + $root = Cluster::root(); + + $this->assertTrue(is_string($root->getKey())); + $this->assertFalse(is_numeric($root->getKey())); + } + + public function testNonNumericParentKey() + { + $child1 = $this->clusters('Child 1'); + + $this->assertTrue(is_string($child1->getParentKey())); + $this->assertFalse(is_numeric($child1->getParentKey())); + } +} diff --git a/tests/suite/Concerns/MigratesDatabase.php b/tests/suite/Concerns/MigratesDatabase.php new file mode 100644 index 00000000..95073963 --- /dev/null +++ b/tests/suite/Concerns/MigratesDatabase.php @@ -0,0 +1,15 @@ +up(); + } +} diff --git a/tests/suite/Concerns/SeedsDatabase.php b/tests/suite/Concerns/SeedsDatabase.php new file mode 100644 index 00000000..fc637b43 --- /dev/null +++ b/tests/suite/Concerns/SeedsDatabase.php @@ -0,0 +1,15 @@ +run(); + } +} diff --git a/tests/suite/CustomEventsTest.php b/tests/suite/CustomEventsTest.php new file mode 100644 index 00000000..a3f5f762 --- /dev/null +++ b/tests/suite/CustomEventsTest.php @@ -0,0 +1,109 @@ +categories('Child 1'); + $child3 = $this->categories('Child 3'); + + $dispatcher = Category::getEventDispatcher(); + + Category::setEventDispatcher($events = m::mock('Illuminate\Events\Dispatcher')->makePartial()); + + $events->shouldReceive('until')->once()->with('eloquent.moving: '.get_class($child1), $child1)->andReturn(true); + + $events->shouldReceive('dispatch')->once()->with('eloquent.moved: '.get_class($child1), $child1)->andReturn(true); + + $child1->moveToRightOf($child3); + + Category::unsetEventDispatcher(); + Category::setEventDispatcher($dispatcher); + } + + public function testMovementHaltsWhenReturningFalseFromMoving() + { + $unchanged = $this->categories('Child 2'); + + $dispatcher = Category::getEventDispatcher(); + + Category::setEventDispatcher($events = m::mock('Illuminate\Events\Dispatcher')->makePartial()); + $events->shouldReceive('until')->once()->with('eloquent.moving: '.get_class($unchanged), $unchanged)->andReturn(false); + + // Force "moving" to return false + Category::moving(function ($node) { + return false; + }); + + $unchanged->makeRoot(); + + $unchanged->refresh(); + + $this->assertEquals(1, $unchanged->getParentKey()); + $this->assertEquals(1, $unchanged->getLevel()); + $this->assertEquals(4, $unchanged->getLeft()); + $this->assertEquals(7, $unchanged->getRight()); + + // Restore + Category::getEventDispatcher()->forget('eloquent.moving: '.get_class($unchanged)); + + Category::unsetEventDispatcher(); + Category::setEventDispatcher($dispatcher); + } + + + public function testMoving() + { + $dispatcher = Category::getEventDispatcher(); + + Category::setEventDispatcher($events = m::mock('Illuminate\Contracts\Events\Dispatcher')); + + $closure = function () { + }; + + $events->shouldReceive('listen')->once()->with('eloquent.moving: ' . Category::class, $closure); + + Category::moving($closure); + + Category::unsetEventDispatcher(); + + Category::setEventDispatcher($dispatcher); + } + + public function testMoved() + { + $dispatcher = Category::getEventDispatcher(); + + Category::setEventDispatcher($events = m::mock('Illuminate\Contracts\Events\Dispatcher')); + + $closure = function () { + }; + + $events->shouldReceive('listen')->once()->with('eloquent.moved: ' . Category::class, $closure); + + Category::moved($closure); + + Category::unsetEventDispatcher(); + + Category::setEventDispatcher($dispatcher); + } + + public function testGetObservableEventsIncludesMovingEvents() + { + $events = with(new Category)->getObservableEvents(); + + $this->assertContains('moving', $events); + $this->assertContains('moved', $events); + } +} diff --git a/tests/suite/HierarchyTest.php b/tests/suite/HierarchyTest.php new file mode 100644 index 00000000..290d6856 --- /dev/null +++ b/tests/suite/HierarchyTest.php @@ -0,0 +1,1532 @@ +orderBy('left')->get(); + + $this->assertEquals($results, $expected); + } + + public function testAllStaticWithCustomOrder() + { + $results = OrderedCategory::all(); + $expected = OrderedCategory::query()->orderBy('name')->get(); + + $this->assertEquals($results, $expected); + } + + public function testRootsStatic() + { + $query = Category::whereNull('parent_id')->get(); + + $roots = Category::roots()->get(); + + $this->assertEquals($query->count(), $roots->count()); + $this->assertCount(2, $roots); + + foreach ($query->pluck('id') as $node) { + $this->assertContains($node, $roots->pluck('id')); + } + } + + public function testRootsStaticWithCustomOrder() + { + $category = OrderedCategory::create(['name' => 'A new root is born']); + // $category->syncOriginal(); // ¿? --> This should be done already !? + + $roots = OrderedCategory::roots()->get(); + + $this->assertCount(3, $roots); + $this->assertEquals($category->getAttributes(), $roots->first()->getAttributes()); + } + + public function testRootStatic() + { + $this->assertEquals(Category::root(), $this->categories('Root 1')); + } + + public function testAllLeavesStatic() + { + $allLeaves = Category::allLeaves()->get(); + + $this->assertCount(4, $allLeaves); + + $leaves = $allLeaves->pluck('name'); + + $this->assertContains('Child 1', $leaves); + $this->assertContains('Child 2.1', $leaves); + $this->assertContains('Child 3', $leaves); + $this->assertContains('Root 2', $leaves); + } + + public function testAllTrunksStatic() + { + $allTrunks = Category::allTrunks()->get(); + + $this->assertCount(1, $allTrunks); + + $trunks = $allTrunks->pluck('name'); + $this->assertContains('Child 2', $trunks); + } + + public function testGetRoot() + { + $this->assertEquals($this->categories('Root 1'), $this->categories('Root 1')->getRoot()); + $this->assertEquals($this->categories('Root 2'), $this->categories('Root 2')->getRoot()); + + $this->assertEquals($this->categories('Root 1'), $this->categories('Child 1')->getRoot()); + $this->assertEquals($this->categories('Root 1'), $this->categories('Child 2')->getRoot()); + $this->assertEquals($this->categories('Root 1'), $this->categories('Child 2.1')->getRoot()); + $this->assertEquals($this->categories('Root 1'), $this->categories('Child 3')->getRoot()); + } + + public function testGetRootEqualsSelfIfUnpersisted() + { + $category = new Category; + + $this->assertEquals($category->getRoot(), $category); + } + + public function testGetRootEqualsValueIfSetIfUnpersisted() + { + $parent = Category::roots()->first(); + + $child = new Category; + $child->setAttribute($child->getParentColumnName(), $parent->getKey()); + + $this->assertEquals($child->getRoot(), $parent); + } + + public function testIsRoot() + { + $this->assertTrue($this->categories('Root 1')->isRoot()); + $this->assertTrue($this->categories('Root 2')->isRoot()); + + $this->assertFalse($this->categories('Child 1')->isRoot()); + $this->assertFalse($this->categories('Child 2')->isRoot()); + $this->assertFalse($this->categories('Child 2.1')->isRoot()); + $this->assertFalse($this->categories('Child 3')->isRoot()); + } + + public function testGetLeaves() + { + $leaves = [$this->categories('Child 1'), $this->categories('Child 2.1'), $this->categories('Child 3')]; + + $this->assertEquals($leaves, $this->categories('Root 1')->getLeaves()->all()); + } + + public function testGetLeavesInIteration() + { + $node = $this->categories('Root 1'); + + $expectedIds = [2, 4, 5]; + + foreach ($node->getLeaves() as $i => $leaf) { + $this->assertEquals($expectedIds[$i], $leaf->getKey()); + } + } + + public function testGetTrunks() + { + $trunks = [$this->categories('Child 2')]; + + $this->assertEquals($trunks, $this->categories('Root 1')->getTrunks()->all()); + } + + public function testGetTrunksInIteration() + { + $node = $this->categories('Root 1'); + + $expectedIds = [3]; + + foreach ($node->getTrunks() as $i => $trunk) { + $this->assertEquals($expectedIds[$i], $trunk->getKey()); + } + } + + public function testIsLeaf() + { + $this->assertTrue($this->categories('Child 1')->isLeaf()); + $this->assertTrue($this->categories('Child 2.1')->isLeaf()); + $this->assertTrue($this->categories('Child 3')->isLeaf()); + $this->assertTrue($this->categories('Root 2')->isLeaf()); + + $this->assertFalse($this->categories('Root 1')->isLeaf()); + $this->assertFalse($this->categories('Child 2')->isLeaf()); + + $new = new Category; + $this->assertFalse($new->isLeaf()); + } + + public function testIsTrunk() + { + $this->assertFalse($this->categories('Child 1')->isTrunk()); + $this->assertFalse($this->categories('Child 2.1')->isTrunk()); + $this->assertFalse($this->categories('Child 3')->isTrunk()); + $this->assertFalse($this->categories('Root 2')->isTrunk()); + + $this->assertFalse($this->categories('Root 1')->isTrunk()); + $this->assertTrue($this->categories('Child 2')->isTrunk()); + + $new = new Category; + $this->assertFalse($new->isTrunk()); + } + + public function testWithoutNodeScope() + { + $child = $this->categories('Child 2.1'); + + $expected = [$this->categories('Root 1'), $child]; + + $this->assertEquals($expected, $child->ancestorsAndSelf()->withoutNode($this->categories('Child 2'))->get()->all()); + } + + public function testWithoutSelfScope() + { + $child = $this->categories('Child 2.1'); + + $expected = [$this->categories('Root 1'), $this->categories('Child 2')]; + + $this->assertEquals($expected, $child->ancestorsAndSelf()->withoutSelf()->get()->all()); + } + + public function testWithoutRootScope() + { + $child = $this->categories('Child 2.1'); + + $expected = [$this->categories('Child 2'), $child]; + + $this->assertEquals($expected, $child->ancestorsAndSelf()->withoutRoot()->get()->all()); + } + + public function testLimitDepthScope() + { + with(new CategorySeeder)->nestUptoAt($this->categories('Child 2.1'), 10); + + $node = $this->categories('Child 2'); + + $descendancy = $node->descendants()->pluck('id')->all(); + + $this->assertEmpty($node->descendants()->limitDepth(0)->pluck('id')->all()); + $this->assertEquals($node->getAttributes(), $node->descendantsAndSelf()->limitDepth(0)->first()->getAttributes()); + + $this->assertEquals(array_slice($descendancy, 0, 3), $node->descendants()->limitDepth(3)->pluck('id')->all()); + $this->assertEquals(array_slice($descendancy, 0, 5), $node->descendants()->limitDepth(5)->pluck('id')->all()); + $this->assertEquals(array_slice($descendancy, 0, 7), $node->descendants()->limitDepth(7)->pluck('id')->all()); + + $this->assertEquals($descendancy, $node->descendants()->limitDepth(1000)->pluck('id')->all()); + } + + public function testGetAncestorsAndSelf() + { + $child = $this->categories('Child 2.1'); + + $expected = [$this->categories('Root 1'), $this->categories('Child 2'), $child]; + + $this->assertEquals($expected, $child->getAncestorsAndSelf()->all()); + } + + public function testGetAncestorsAndSelfWithoutRoot() + { + $child = $this->categories('Child 2.1'); + + $expected = [$this->categories('Child 2'), $child]; + + $this->assertEquals($expected, $child->getAncestorsAndSelfWithoutRoot()->all()); + } + + public function testGetAncestors() + { + $child = $this->categories('Child 2.1'); + + $expected = [$this->categories('Root 1'), $this->categories('Child 2')]; + + $this->assertEquals($expected, $child->getAncestors()->all()); + } + + public function testGetAncestorsWithoutRoot() + { + $child = $this->categories('Child 2.1'); + + $expected = [$this->categories('Child 2')]; + + $this->assertEquals($expected, $child->getAncestorsWithoutRoot()->all()); + } + + public function testGetDescendantsAndSelf() + { + $parent = $this->categories('Root 1'); + + $expected = [ + $parent, + $this->categories('Child 1'), + $this->categories('Child 2'), + $this->categories('Child 2.1'), + $this->categories('Child 3') + ]; + + $this->assertCount(count($expected), $parent->getDescendantsAndSelf()); + + $this->assertEquals($expected, $parent->getDescendantsAndSelf()->all()); + } + + public function testGetDescendantsAndSelfWithLimit() + { + with(new CategorySeeder)->nestUptoAt($this->categories('Child 2.1'), 3); + + $parent = $this->categories('Root 1'); + + $this->assertEquals([$parent], $parent->getDescendantsAndSelf(0)->all()); + + $this->assertEquals([ + $parent, + $this->categories('Child 1'), + $this->categories('Child 2'), + $this->categories('Child 3') + ], $parent->getDescendantsAndSelf(1)->all()); + + $this->assertEquals([ + $parent, + $this->categories('Child 1'), + $this->categories('Child 2'), + $this->categories('Child 2.1'), + $this->categories('Child 3') + ], $parent->getDescendantsAndSelf(2)->all()); + + $this->assertEquals([ + $parent, + $this->categories('Child 1'), + $this->categories('Child 2'), + $this->categories('Child 2.1'), + $this->categories('Child 2.1.1'), + $this->categories('Child 3') + ], $parent->getDescendantsAndSelf(3)->all()); + + $this->assertEquals([ + $parent, + $this->categories('Child 1'), + $this->categories('Child 2'), + $this->categories('Child 2.1'), + $this->categories('Child 2.1.1'), + $this->categories('Child 2.1.1.1'), + $this->categories('Child 3') + ], $parent->getDescendantsAndSelf(4)->all()); + + $this->assertEquals([ + $parent, + $this->categories('Child 1'), + $this->categories('Child 2'), + $this->categories('Child 2.1'), + $this->categories('Child 2.1.1'), + $this->categories('Child 2.1.1.1'), + $this->categories('Child 2.1.1.1.1'), + $this->categories('Child 3') + ], $parent->getDescendantsAndSelf(10)->all()); + } + + public function testGetDescendants() + { + $parent = $this->categories('Root 1'); + + $expected = [ + $this->categories('Child 1'), + $this->categories('Child 2'), + $this->categories('Child 2.1'), + $this->categories('Child 3') + ]; + + $this->assertCount(count($expected), $parent->getDescendants()); + + $this->assertEquals($expected, $parent->getDescendants()->all()); + } + + public function testGetDescendantsWithLimit() + { + with(new CategorySeeder)->nestUptoAt($this->categories('Child 2.1'), 3); + + $parent = $this->categories('Root 1'); + + $this->assertEmpty($parent->getDescendants(0)->all()); + + $this->assertEquals([ + $this->categories('Child 1'), + $this->categories('Child 2'), + $this->categories('Child 3') + ], $parent->getDescendants(1)->all()); + + $this->assertEquals([ + $this->categories('Child 1'), + $this->categories('Child 2'), + $this->categories('Child 2.1'), + $this->categories('Child 3') + ], $parent->getDescendants(2)->all()); + + $this->assertEquals([ + $this->categories('Child 1'), + $this->categories('Child 2'), + $this->categories('Child 2.1'), + $this->categories('Child 2.1.1'), + $this->categories('Child 3') + ], $parent->getDescendants(3)->all()); + + $this->assertEquals([ + $this->categories('Child 1'), + $this->categories('Child 2'), + $this->categories('Child 2.1'), + $this->categories('Child 2.1.1'), + $this->categories('Child 2.1.1.1'), + $this->categories('Child 3') + ], $parent->getDescendants(4)->all()); + + $this->assertEquals([ + $this->categories('Child 1'), + $this->categories('Child 2'), + $this->categories('Child 2.1'), + $this->categories('Child 2.1.1'), + $this->categories('Child 2.1.1.1'), + $this->categories('Child 2.1.1.1.1'), + $this->categories('Child 3') + ], $parent->getDescendants(5)->all()); + + $this->assertEquals([ + $this->categories('Child 1'), + $this->categories('Child 2'), + $this->categories('Child 2.1'), + $this->categories('Child 2.1.1'), + $this->categories('Child 2.1.1.1'), + $this->categories('Child 2.1.1.1.1'), + $this->categories('Child 3') + ], $parent->getDescendants(10)->all()); + } + + public function testDescendantsRecursesChildren() + { + $a = Category::create(['name' => 'A']); + $b = Category::create(['name' => 'B']); + $c = Category::create(['name' => 'C']); + + // a > b > c + $b->makeChildOf($a); + $c->makeChildOf($b); + + $a->refresh(); + $b->refresh(); + $c->refresh(); + + $this->assertEquals(1, $a->children()->count()); + $this->assertEquals(1, $b->children()->count()); + $this->assertEquals(2, $a->descendants()->count()); + } + + public function testGetImmediateDescendants() + { + $expected = [$this->categories('Child 1'), $this->categories('Child 2'), $this->categories('Child 3')]; + + $this->assertEquals($expected, $this->categories('Root 1')->getImmediateDescendants()->all()); + + $this->assertEquals([$this->categories('Child 2.1')], $this->categories('Child 2')->getImmediateDescendants()->all()); + + $this->assertEmpty($this->categories('Root 2')->getImmediateDescendants()->all()); + } + + public function testIsSelfOrAncestorOf() + { + $this->assertTrue($this->categories('Root 1')->isSelfOrAncestorOf($this->categories('Child 1'))); + $this->assertTrue($this->categories('Root 1')->isSelfOrAncestorOf($this->categories('Child 2.1'))); + $this->assertTrue($this->categories('Child 2')->isSelfOrAncestorOf($this->categories('Child 2.1'))); + $this->assertFalse($this->categories('Child 2.1')->isSelfOrAncestorOf($this->categories('Child 2'))); + $this->assertFalse($this->categories('Child 1')->isSelfOrAncestorOf($this->categories('Child 2'))); + $this->assertTrue($this->categories('Child 1')->isSelfOrAncestorOf($this->categories('Child 1'))); + } + + public function testIsAncestorOf() + { + $this->assertTrue($this->categories('Root 1')->isAncestorOf($this->categories('Child 1'))); + $this->assertTrue($this->categories('Root 1')->isAncestorOf($this->categories('Child 2.1'))); + $this->assertTrue($this->categories('Child 2')->isAncestorOf($this->categories('Child 2.1'))); + $this->assertFalse($this->categories('Child 2.1')->isAncestorOf($this->categories('Child 2'))); + $this->assertFalse($this->categories('Child 1')->isAncestorOf($this->categories('Child 2'))); + $this->assertFalse($this->categories('Child 1')->isAncestorOf($this->categories('Child 1'))); + } + + public function testIsSelfOrDescendantOf() + { + $this->assertTrue($this->categories('Child 1')->isSelfOrDescendantOf($this->categories('Root 1'))); + $this->assertTrue($this->categories('Child 2.1')->isSelfOrDescendantOf($this->categories('Root 1'))); + $this->assertTrue($this->categories('Child 2.1')->isSelfOrDescendantOf($this->categories('Child 2'))); + $this->assertFalse($this->categories('Child 2')->isSelfOrDescendantOf($this->categories('Child 2.1'))); + $this->assertFalse($this->categories('Child 2')->isSelfOrDescendantOf($this->categories('Child 1'))); + $this->assertTrue($this->categories('Child 1')->isSelfOrDescendantOf($this->categories('Child 1'))); + } + + public function testIsDescendantOf() + { + $this->assertTrue($this->categories('Child 1')->isDescendantOf($this->categories('Root 1'))); + $this->assertTrue($this->categories('Child 2.1')->isDescendantOf($this->categories('Root 1'))); + $this->assertTrue($this->categories('Child 2.1')->isDescendantOf($this->categories('Child 2'))); + $this->assertFalse($this->categories('Child 2')->isDescendantOf($this->categories('Child 2.1'))); + $this->assertFalse($this->categories('Child 2')->isDescendantOf($this->categories('Child 1'))); + $this->assertFalse($this->categories('Child 1')->isDescendantOf($this->categories('Child 1'))); + } + + public function testGetSiblingsAndSelf() + { + $child = $this->categories('Child 2'); + + $expected = [$this->categories('Child 1'), $child, $this->categories('Child 3')]; + $this->assertEquals($expected, $child->getSiblingsAndSelf()->all()); + + $expected = [$this->categories('Root 1'), $this->categories('Root 2')]; + $this->assertEquals($expected, $this->categories('Root 1')->getSiblingsAndSelf()->all()); + } + + public function testGetSiblings() + { + $child = $this->categories('Child 2'); + + $expected = [$this->categories('Child 1'), $this->categories('Child 3')]; + + $this->assertEquals($expected, $child->getSiblings()->all()); + } + + public function testGetLeftSibling() + { + $this->assertEquals($this->categories('Child 1'), $this->categories('Child 2')->getLeftSibling()); + $this->assertEquals($this->categories('Child 2'), $this->categories('Child 3')->getLeftSibling()); + } + + public function testGetLeftSiblingOfFirstRootIsNull() + { + $this->assertNull($this->categories('Root 1')->getLeftSibling()); + } + + public function testGetLeftSiblingWithNoneIsNull() + { + $this->assertNull($this->categories('Child 2.1')->getLeftSibling()); + } + + public function testGetLeftSiblingOfLeftmostNodeIsNull() + { + $this->assertNull($this->categories('Child 1')->getLeftSibling()); + } + + public function testGetRightSibling() + { + $this->assertEquals($this->categories('Child 3'), $this->categories('Child 2')->getRightSibling()); + $this->assertEquals($this->categories('Child 2'), $this->categories('Child 1')->getRightSibling()); + } + + public function testGetRightSiblingOfRoots() + { + $this->assertEquals($this->categories('Root 2'), $this->categories('Root 1')->getRightSibling()); + $this->assertNull($this->categories('Root 2')->getRightSibling()); + } + + public function testGetRightSiblingWithNoneIsNull() + { + $this->assertNull($this->categories('Child 2.1')->getRightSibling()); + } + + public function testGetRightSiblingOfRightmostNodeIsNull() + { + $this->assertNull($this->categories('Child 3')->getRightSibling()); + } + + public function testInsideSubtree() + { + $this->assertFalse($this->categories('Child 1')->insideSubtree($this->categories('Root 2'))); + $this->assertFalse($this->categories('Child 2')->insideSubtree($this->categories('Root 2'))); + $this->assertFalse($this->categories('Child 3')->insideSubtree($this->categories('Root 2'))); + + $this->assertTrue($this->categories('Child 1')->insideSubtree($this->categories('Root 1'))); + $this->assertTrue($this->categories('Child 2')->insideSubtree($this->categories('Root 1'))); + $this->assertTrue($this->categories('Child 2.1')->insideSubtree($this->categories('Root 1'))); + $this->assertTrue($this->categories('Child 3')->insideSubtree($this->categories('Root 1'))); + + $this->assertTrue($this->categories('Child 2.1')->insideSubtree($this->categories('Child 2'))); + $this->assertFalse($this->categories('Child 2.1')->insideSubtree($this->categories('Root 2'))); + } + + public function testGetLevel() + { + $this->assertEquals(0, $this->categories('Root 1')->getLevel()); + $this->assertEquals(1, $this->categories('Child 1')->getLevel()); + $this->assertEquals(2, $this->categories('Child 2.1')->getLevel()); + } + + public function testToHierarchyReturnsAnEloquentCollection() + { + $categories = Category::all()->toHierarchy(); + + $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $categories); + } + + public function testToHierarchyReturnsHierarchicalData() + { + $categories = Category::all()->toHierarchy(); + + $this->assertEquals(2, $categories->count()); + + $first = $categories->first(); + $this->assertEquals('Root 1', $first->name); + $this->assertEquals(3, $first->children->count()); + + $first_lvl2 = $first->children->first(); + $this->assertEquals('Child 1', $first_lvl2->name); + $this->assertEquals(0, $first_lvl2->children->count()); + } + + public function testToHierarchyNestsCorrectly() + { + // Prune all categories + Category::query()->delete(); + + // Build a sample tree structure: + // + // - A + // |- A.1 + // |- A.2 + // - B + // |- B.1 + // |- B.2 + // |- B.2.1 + // |- B.2.2 + // |- B.2.2.1 + // |- B.2.3 + // |- B.3 + // - C + // |- C.1 + // |- C.2 + // - D + // + $a = Category::create(['name' => 'A']); + $b = Category::create(['name' => 'B']); + $c = Category::create(['name' => 'C']); + $d = Category::create(['name' => 'D']); + + $ch = Category::create(['name' => 'A.1']); + $ch->makeChildOf($a); + + $ch = Category::create(['name' => 'A.2']); + $ch->makeChildOf($a); + + $ch = Category::create(['name' => 'B.1']); + $ch->makeChildOf($b); + + $ch = Category::create(['name' => 'B.2']); + $ch->makeChildOf($b); + + $ch2 = Category::create(['name' => 'B.2.1']); + $ch2->makeChildOf($ch); + + $ch2 = Category::create(['name' => 'B.2.2']); + $ch2->makeChildOf($ch); + + $ch3 = Category::create(['name' => 'B.2.2.1']); + $ch3->makeChildOf($ch2); + + $ch2 = Category::create(['name' => 'B.2.3']); + $ch2->makeChildOf($ch); + + $ch = Category::create(['name' => 'B.3']); + $ch->makeChildOf($b); + + $ch = Category::create(['name' => 'C.1']); + $ch->makeChildOf($c); + + $ch = Category::create(['name' => 'C.2']); + $ch->makeChildOf($c); + + $this->assertTrue(Category::isValidNestedSet()); + + // Build expectations (expected trees/subtrees) + $expectedWholeTree = [ + 'A' => [ 'A.1' => null, 'A.2' => null ], + 'B' => [ + 'B.1' => null, + 'B.2' => [ + 'B.2.1' => null, + 'B.2.2' => [ 'B.2.2.1' => null ], + 'B.2.3' => null, + ], + 'B.3' => null, + ], + 'C' => [ 'C.1' => null, 'C.2' => null ], + 'D' => null + ]; + + $expectedSubtreeA = ['A' => [ 'A.1' => null, 'A.2' => null ]]; + + $expectedSubtreeB = [ + 'B' => [ + 'B.1' => null, + 'B.2' => [ + 'B.2.1' => null, + 'B.2.2' => [ 'B.2.2.1' => null ], + 'B.2.3' => null + ], + 'B.3' => null + ] + ]; + + $expectedSubtreeC = [ 'C.1' => null, 'C.2' => null ]; + + $expectedSubtreeD = ['D' => null]; + + // Perform assertions + $wholeTree = hmap(Category::all()->toHierarchy()->toArray()); + $this->assertEquals($expectedWholeTree, $wholeTree); + + $subtreeA = hmap($this->categories('A')->getDescendantsAndSelf()->toHierarchy()->toArray()); + $this->assertEquals($expectedSubtreeA, $subtreeA); + + $subtreeB = hmap($this->categories('B')->getDescendantsAndSelf()->toHierarchy()->toArray()); + $this->assertEquals($expectedSubtreeB, $subtreeB); + + $subtreeC = hmap($this->categories('C')->getDescendants()->toHierarchy()->toArray()); + $this->assertEquals($expectedSubtreeC, $subtreeC); + + $subtreeD = hmap($this->categories('D')->getDescendantsAndSelf()->toHierarchy()->toArray()); + $this->assertEquals($expectedSubtreeD, $subtreeD); + + $this->assertTrue($this->categories('D')->getDescendants()->toHierarchy()->isEmpty()); + } + + public function testToHierarchyNestsCorrectlyNotSequential() + { + $parent = $this->categories('Child 1'); + + $parent->children()->create(['name' => 'Child 1.1']); + + $parent->children()->create(['name' => 'Child 1.2']); + + $this->assertTrue(Category::isValidNestedSet()); + + $expected = [ + 'Child 1' => [ + 'Child 1.1' => null, + 'Child 1.2' => null + ] + ]; + + $parent->refresh(); + $this->assertEquals($expected, hmap($parent->getDescendantsAndSelf()->toHierarchy()->toArray())); + } + + public function testToHierarchyNestsCorrectlyWithOrder() + { + with(new OrderedCategorySeeder)->run(); + + $expectedWhole = [ + 'Root A' => null, + 'Root Z' => [ + 'Child A' => null, + 'Child C' => null, + 'Child G' => [ 'Child G.1' => null ] + ] + ]; + + $this->assertEquals($expectedWhole, hmap(OrderedCategory::all()->toHierarchy()->toArray())); + + $expectedSubtreeZ = [ + 'Root Z' => [ + 'Child A' => null, + 'Child C' => null, + 'Child G' => [ 'Child G.1' => null ] + ] + ]; + $this->assertEquals($expectedSubtreeZ, hmap($this->categories('Root Z', OrderedCategory::class)->getDescendantsAndSelf()->toHierarchy()->toArray())); + } + + public function testGetNestedList() + { + $seperator = ' '; + $nestedList = Category::getNestedList('name', 'id', $seperator); + + $expected = [ + 1 => str_repeat($seperator, 0). 'Root 1', + 2 => str_repeat($seperator, 1). 'Child 1', + 3 => str_repeat($seperator, 1). 'Child 2', + 4 => str_repeat($seperator, 2). 'Child 2.1', + 5 => str_repeat($seperator, 1). 'Child 3', + 6 => str_repeat($seperator, 0). 'Root 2', + ]; + + $this->assertEquals($expected, $nestedList); + } + + public function testNonNumericAllStatic() + { + $results = Cluster::all(); + $expected = Cluster::query()->orderBy('left')->get(); + + $this->assertEquals($results, $expected); + } + + public function testNonNumericAllStaticWithCustomOrder() + { + $results = OrderedCluster::all(); + $expected = OrderedCluster::query()->orderBy('name')->get(); + + $this->assertEquals($results, $expected); + } + + public function testNonNumericRootsStatic() + { + $query = Cluster::whereNull('parent_id')->get(); + + $roots = Cluster::roots()->get(); + + $this->assertEquals($query->count(), $roots->count()); + $this->assertCount(2, $roots); + + foreach ($query->pluck('id') as $node) { + $this->assertContains($node, $roots->pluck('id')); + } + } + + public function testNonNumericRootsStaticWithCustomOrder() + { + $cluster = OrderedCluster::create(['name' => 'A new root is born']); + $cluster->syncOriginal(); // ¿? --> This should be done already !? + + $roots = OrderedCluster::roots()->get(); + + $this->assertCount(3, $roots); + $this->assertEquals($cluster->getAttributes(), $roots->first()->getAttributes()); + } + + public function testNonNumericRootStatic() + { + $this->assertEquals(Cluster::root(), $this->clusters('Root 1')); + } + + public function testNonNumericAllLeavesStatic() + { + $allLeaves = Cluster::allLeaves()->get(); + + $this->assertCount(4, $allLeaves); + + $leaves = $allLeaves->pluck('name'); + + $this->assertContains('Child 1', $leaves); + $this->assertContains('Child 2.1', $leaves); + $this->assertContains('Child 3', $leaves); + $this->assertContains('Root 2', $leaves); + } + + public function testNonNumericAllTrunksStatic() + { + $allTrunks = Cluster::allTrunks()->get(); + + $this->assertCount(1, $allTrunks); + + $trunks = $allTrunks->pluck('name'); + $this->assertContains('Child 2', $trunks); + } + + public function testNonNumericGetRoot() + { + $this->assertEquals($this->clusters('Root 1'), $this->clusters('Root 1')->getRoot()); + $this->assertEquals($this->clusters('Root 2'), $this->clusters('Root 2')->getRoot()); + + $this->assertEquals($this->clusters('Root 1'), $this->clusters('Child 1')->getRoot()); + $this->assertEquals($this->clusters('Root 1'), $this->clusters('Child 2')->getRoot()); + $this->assertEquals($this->clusters('Root 1'), $this->clusters('Child 2.1')->getRoot()); + $this->assertEquals($this->clusters('Root 1'), $this->clusters('Child 3')->getRoot()); + } + + public function testNonNumericGetRootEqualsSelfIfUnpersisted() + { + $cluster = new Cluster; + + $this->assertEquals($cluster->getRoot(), $cluster); + } + + public function testNonNumericGetRootEqualsValueIfSetIfUnpersisted() + { + $parent = Cluster::roots()->first(); + + $child = new Cluster; + $child->setAttribute($child->getParentColumnName(), $parent->getKey()); + + $this->assertEquals($child->getRoot(), $parent); + } + + public function testNonNumericIsRoot() + { + $this->assertTrue($this->clusters('Root 1')->isRoot()); + $this->assertTrue($this->clusters('Root 2')->isRoot()); + + $this->assertFalse($this->clusters('Child 1')->isRoot()); + $this->assertFalse($this->clusters('Child 2')->isRoot()); + $this->assertFalse($this->clusters('Child 2.1')->isRoot()); + $this->assertFalse($this->clusters('Child 3')->isRoot()); + } + + public function testNonNumericGetLeaves() + { + $leaves = [$this->clusters('Child 1'), $this->clusters('Child 2.1'), $this->clusters('Child 3')]; + + $this->assertEquals($leaves, $this->clusters('Root 1')->getLeaves()->all()); + } + + public function testNonNumericGetLeavesInIteration() + { + $node = $this->clusters('Root 1'); + + $expectedIds = [ + '5d7ce1fd-6151-46d3-a5b3-0ebb9988dc57', + '3315a297-af87-4ad3-9fa5-19785407573d', + '054476d2-6830-4014-a181-4de010ef7114' + ]; + + foreach ($node->getLeaves() as $i => $leaf) { + $this->assertEquals($expectedIds[$i], $leaf->getKey()); + } + } + + public function testNonNumericGetTrunks() + { + $trunks = [$this->clusters('Child 2')]; + + $this->assertEquals($trunks, $this->clusters('Root 1')->getTrunks()->all()); + } + + public function testNonNumericGetTrunksInIteration() + { + $node = $this->clusters('Root 1'); + + $expectedIds = ['07c1fc8c-53b5-4fe7-b9c4-e09f266a455c']; + + foreach ($node->getTrunks() as $i => $trunk) { + $this->assertEquals($expectedIds[$i], $trunk->getKey()); + } + } + + public function testNonNumericIsLeaf() + { + $this->assertTrue($this->clusters('Child 1')->isLeaf()); + $this->assertTrue($this->clusters('Child 2.1')->isLeaf()); + $this->assertTrue($this->clusters('Child 3')->isLeaf()); + $this->assertTrue($this->clusters('Root 2')->isLeaf()); + + $this->assertFalse($this->clusters('Root 1')->isLeaf()); + $this->assertFalse($this->clusters('Child 2')->isLeaf()); + + $new = new Cluster; + $this->assertFalse($new->isLeaf()); + } + + public function testNonNumericIsTrunk() + { + $this->assertFalse($this->clusters('Child 1')->isTrunk()); + $this->assertFalse($this->clusters('Child 2.1')->isTrunk()); + $this->assertFalse($this->clusters('Child 3')->isTrunk()); + $this->assertFalse($this->clusters('Root 2')->isTrunk()); + + $this->assertFalse($this->clusters('Root 1')->isTrunk()); + $this->assertTrue($this->clusters('Child 2')->isTrunk()); + + $new = new Cluster; + $this->assertFalse($new->isTrunk()); + } + + public function testNonNumericWithoutNodeScope() + { + $child = $this->clusters('Child 2.1'); + + $expected = [$this->clusters('Root 1'), $child]; + + $this->assertEquals($expected, $child->ancestorsAndSelf()->withoutNode($this->clusters('Child 2'))->get()->all()); + } + + public function testNonNumericWithoutSelfScope() + { + $child = $this->clusters('Child 2.1'); + + $expected = [$this->clusters('Root 1'), $this->clusters('Child 2')]; + + $this->assertEquals($expected, $child->ancestorsAndSelf()->withoutSelf()->get()->all()); + } + + public function testNonNumericWithoutRootScope() + { + $child = $this->clusters('Child 2.1'); + + $expected = [$this->clusters('Child 2'), $child]; + + $this->assertEquals($expected, $child->ancestorsAndSelf()->withoutRoot()->get()->all()); + } + + public function testNonNumericLimitDepthScope() + { + with(new ClusterSeeder)->nestUptoAt($this->clusters('Child 2.1'), 10); + + $node = $this->clusters('Child 2'); + + $descendancy = $node->descendants()->pluck('id')->all(); + + $this->assertEmpty($node->descendants()->limitDepth(0)->pluck('id')->all()); + $this->assertEquals($node->getAttributes(), $node->descendantsAndSelf()->limitDepth(0)->first()->getAttributes()); + + $this->assertEquals(array_slice($descendancy, 0, 3), $node->descendants()->limitDepth(3)->pluck('id')->all()); + $this->assertEquals(array_slice($descendancy, 0, 5), $node->descendants()->limitDepth(5)->pluck('id')->all()); + $this->assertEquals(array_slice($descendancy, 0, 7), $node->descendants()->limitDepth(7)->pluck('id')->all()); + + $this->assertEquals($descendancy, $node->descendants()->limitDepth(1000)->pluck('id')->all()); + } + + public function testNonNumericGetAncestorsAndSelf() + { + $child = $this->clusters('Child 2.1'); + + $expected = [$this->clusters('Root 1'), $this->clusters('Child 2'), $child]; + + $this->assertEquals($expected, $child->getAncestorsAndSelf()->all()); + } + + public function testNonNumericGetAncestorsAndSelfWithoutRoot() + { + $child = $this->clusters('Child 2.1'); + + $expected = [$this->clusters('Child 2'), $child]; + + $this->assertEquals($expected, $child->getAncestorsAndSelfWithoutRoot()->all()); + } + + public function testNonNumericGetAncestors() + { + $child = $this->clusters('Child 2.1'); + + $expected = [$this->clusters('Root 1'), $this->clusters('Child 2')]; + + $this->assertEquals($expected, $child->getAncestors()->all()); + } + + public function testNonNumericGetAncestorsWithoutRoot() + { + $child = $this->clusters('Child 2.1'); + + $expected = [$this->clusters('Child 2')]; + + $this->assertEquals($expected, $child->getAncestorsWithoutRoot()->all()); + } + + public function testNonNumericGetDescendantsAndSelf() + { + $parent = $this->clusters('Root 1'); + + $expected = [ + $parent, + $this->clusters('Child 1'), + $this->clusters('Child 2'), + $this->clusters('Child 2.1'), + $this->clusters('Child 3') + ]; + + $this->assertCount(count($expected), $parent->getDescendantsAndSelf()); + + $this->assertEquals($expected, $parent->getDescendantsAndSelf()->all()); + } + + public function testNonNumericGetDescendantsAndSelfWithLimit() + { + with(new ClusterSeeder)->nestUptoAt($this->clusters('Child 2.1'), 3); + + $parent = $this->clusters('Root 1'); + + $this->assertEquals([$parent], $parent->getDescendantsAndSelf(0)->all()); + + $this->assertEquals([ + $parent, + $this->clusters('Child 1'), + $this->clusters('Child 2'), + $this->clusters('Child 3') + ], $parent->getDescendantsAndSelf(1)->all()); + + $this->assertEquals([ + $parent, + $this->clusters('Child 1'), + $this->clusters('Child 2'), + $this->clusters('Child 2.1'), + $this->clusters('Child 3') + ], $parent->getDescendantsAndSelf(2)->all()); + + $this->assertEquals([ + $parent, + $this->clusters('Child 1'), + $this->clusters('Child 2'), + $this->clusters('Child 2.1'), + $this->clusters('Child 2.1.1'), + $this->clusters('Child 3') + ], $parent->getDescendantsAndSelf(3)->all()); + + $this->assertEquals([ + $parent, + $this->clusters('Child 1'), + $this->clusters('Child 2'), + $this->clusters('Child 2.1'), + $this->clusters('Child 2.1.1'), + $this->clusters('Child 2.1.1.1'), + $this->clusters('Child 3') + ], $parent->getDescendantsAndSelf(4)->all()); + + $this->assertEquals([ + $parent, + $this->clusters('Child 1'), + $this->clusters('Child 2'), + $this->clusters('Child 2.1'), + $this->clusters('Child 2.1.1'), + $this->clusters('Child 2.1.1.1'), + $this->clusters('Child 2.1.1.1.1'), + $this->clusters('Child 3') + ], $parent->getDescendantsAndSelf(10)->all()); + } + + public function testNonNumericGetDescendants() + { + $parent = $this->clusters('Root 1'); + + $expected = [ + $this->clusters('Child 1'), + $this->clusters('Child 2'), + $this->clusters('Child 2.1'), + $this->clusters('Child 3') + ]; + + $this->assertCount(count($expected), $parent->getDescendants()); + + $this->assertEquals($expected, $parent->getDescendants()->all()); + } + + public function testNonNumericGetDescendantsWithLimit() + { + with(new ClusterSeeder)->nestUptoAt($this->clusters('Child 2.1'), 3); + + $parent = $this->clusters('Root 1'); + + $this->assertEmpty($parent->getDescendants(0)->all()); + + $this->assertEquals([ + $this->clusters('Child 1'), + $this->clusters('Child 2'), + $this->clusters('Child 3') + ], $parent->getDescendants(1)->all()); + + $this->assertEquals([ + $this->clusters('Child 1'), + $this->clusters('Child 2'), + $this->clusters('Child 2.1'), + $this->clusters('Child 3') + ], $parent->getDescendants(2)->all()); + + $this->assertEquals([ + $this->clusters('Child 1'), + $this->clusters('Child 2'), + $this->clusters('Child 2.1'), + $this->clusters('Child 2.1.1'), + $this->clusters('Child 3') + ], $parent->getDescendants(3)->all()); + + $this->assertEquals([ + $this->clusters('Child 1'), + $this->clusters('Child 2'), + $this->clusters('Child 2.1'), + $this->clusters('Child 2.1.1'), + $this->clusters('Child 2.1.1.1'), + $this->clusters('Child 3') + ], $parent->getDescendants(4)->all()); + + $this->assertEquals([ + $this->clusters('Child 1'), + $this->clusters('Child 2'), + $this->clusters('Child 2.1'), + $this->clusters('Child 2.1.1'), + $this->clusters('Child 2.1.1.1'), + $this->clusters('Child 2.1.1.1.1'), + $this->clusters('Child 3') + ], $parent->getDescendants(5)->all()); + + $this->assertEquals([ + $this->clusters('Child 1'), + $this->clusters('Child 2'), + $this->clusters('Child 2.1'), + $this->clusters('Child 2.1.1'), + $this->clusters('Child 2.1.1.1'), + $this->clusters('Child 2.1.1.1.1'), + $this->clusters('Child 3') + ], $parent->getDescendants(10)->all()); + } + + public function testNonNumericDescendantsRecursesChildren() + { + $a = Cluster::create(['name' => 'A']); + $b = Cluster::create(['name' => 'B']); + $c = Cluster::create(['name' => 'C']); + + // a > b > c + $b->makeChildOf($a); + $c->makeChildOf($b); + + $a->refresh(); + $b->refresh(); + $c->refresh(); + + $this->assertEquals(1, $a->children()->count()); + $this->assertEquals(1, $b->children()->count()); + $this->assertEquals(2, $a->descendants()->count()); + } + + public function testNonNumericGetImmediateDescendants() + { + $expected = [$this->clusters('Child 1'), $this->clusters('Child 2'), $this->clusters('Child 3')]; + + $this->assertEquals($expected, $this->clusters('Root 1')->getImmediateDescendants()->all()); + + $this->assertEquals([$this->clusters('Child 2.1')], $this->clusters('Child 2')->getImmediateDescendants()->all()); + + $this->assertEmpty($this->clusters('Root 2')->getImmediateDescendants()->all()); + } + + public function testNonNumericIsSelfOrAncestorOf() + { + $this->assertTrue($this->clusters('Root 1')->isSelfOrAncestorOf($this->clusters('Child 1'))); + $this->assertTrue($this->clusters('Root 1')->isSelfOrAncestorOf($this->clusters('Child 2.1'))); + $this->assertTrue($this->clusters('Child 2')->isSelfOrAncestorOf($this->clusters('Child 2.1'))); + $this->assertFalse($this->clusters('Child 2.1')->isSelfOrAncestorOf($this->clusters('Child 2'))); + $this->assertFalse($this->clusters('Child 1')->isSelfOrAncestorOf($this->clusters('Child 2'))); + $this->assertTrue($this->clusters('Child 1')->isSelfOrAncestorOf($this->clusters('Child 1'))); + } + + public function testNonNumericIsAncestorOf() + { + $this->assertTrue($this->clusters('Root 1')->isAncestorOf($this->clusters('Child 1'))); + $this->assertTrue($this->clusters('Root 1')->isAncestorOf($this->clusters('Child 2.1'))); + $this->assertTrue($this->clusters('Child 2')->isAncestorOf($this->clusters('Child 2.1'))); + $this->assertFalse($this->clusters('Child 2.1')->isAncestorOf($this->clusters('Child 2'))); + $this->assertFalse($this->clusters('Child 1')->isAncestorOf($this->clusters('Child 2'))); + $this->assertFalse($this->clusters('Child 1')->isAncestorOf($this->clusters('Child 1'))); + } + + public function testNonNumericIsSelfOrDescendantOf() + { + $this->assertTrue($this->clusters('Child 1')->isSelfOrDescendantOf($this->clusters('Root 1'))); + $this->assertTrue($this->clusters('Child 2.1')->isSelfOrDescendantOf($this->clusters('Root 1'))); + $this->assertTrue($this->clusters('Child 2.1')->isSelfOrDescendantOf($this->clusters('Child 2'))); + $this->assertFalse($this->clusters('Child 2')->isSelfOrDescendantOf($this->clusters('Child 2.1'))); + $this->assertFalse($this->clusters('Child 2')->isSelfOrDescendantOf($this->clusters('Child 1'))); + $this->assertTrue($this->clusters('Child 1')->isSelfOrDescendantOf($this->clusters('Child 1'))); + } + + public function testNonNumericIsDescendantOf() + { + $this->assertTrue($this->clusters('Child 1')->isDescendantOf($this->clusters('Root 1'))); + $this->assertTrue($this->clusters('Child 2.1')->isDescendantOf($this->clusters('Root 1'))); + $this->assertTrue($this->clusters('Child 2.1')->isDescendantOf($this->clusters('Child 2'))); + $this->assertFalse($this->clusters('Child 2')->isDescendantOf($this->clusters('Child 2.1'))); + $this->assertFalse($this->clusters('Child 2')->isDescendantOf($this->clusters('Child 1'))); + $this->assertFalse($this->clusters('Child 1')->isDescendantOf($this->clusters('Child 1'))); + } + + public function testNonNumericGetSiblingsAndSelf() + { + $child = $this->clusters('Child 2'); + + $expected = [$this->clusters('Child 1'), $child, $this->clusters('Child 3')]; + $this->assertEquals($expected, $child->getSiblingsAndSelf()->all()); + + $expected = [$this->clusters('Root 1'), $this->clusters('Root 2')]; + $this->assertEquals($expected, $this->clusters('Root 1')->getSiblingsAndSelf()->all()); + } + + public function testNonNumericGetSiblings() + { + $child = $this->clusters('Child 2'); + + $expected = [$this->clusters('Child 1'), $this->clusters('Child 3')]; + + $this->assertEquals($expected, $child->getSiblings()->all()); + } + + public function testNonNumericGetLeftSibling() + { + $this->assertEquals($this->clusters('Child 1'), $this->clusters('Child 2')->getLeftSibling()); + $this->assertEquals($this->clusters('Child 2'), $this->clusters('Child 3')->getLeftSibling()); + } + + public function testNonNumericGetLeftSiblingOfFirstRootIsNull() + { + $this->assertNull($this->clusters('Root 1')->getLeftSibling()); + } + + public function testNonNumericGetLeftSiblingWithNoneIsNull() + { + $this->assertNull($this->clusters('Child 2.1')->getLeftSibling()); + } + + public function testNonNumericGetLeftSiblingOfLeftmostNodeIsNull() + { + $this->assertNull($this->clusters('Child 1')->getLeftSibling()); + } + + public function testNonNumericGetRightSibling() + { + $this->assertEquals($this->clusters('Child 3'), $this->clusters('Child 2')->getRightSibling()); + $this->assertEquals($this->clusters('Child 2'), $this->clusters('Child 1')->getRightSibling()); + } + + public function testNonNumericGetRightSiblingOfRoots() + { + $this->assertEquals($this->clusters('Root 2'), $this->clusters('Root 1')->getRightSibling()); + $this->assertNull($this->clusters('Root 2')->getRightSibling()); + } + + public function testNonNumericGetRightSiblingWithNoneIsNull() + { + $this->assertNull($this->clusters('Child 2.1')->getRightSibling()); + } + + public function testNonNumericGetRightSiblingOfRightmostNodeIsNull() + { + $this->assertNull($this->clusters('Child 3')->getRightSibling()); + } + + public function testNonNumericInsideSubtree() + { + $this->assertFalse($this->clusters('Child 1')->insideSubtree($this->clusters('Root 2'))); + $this->assertFalse($this->clusters('Child 2')->insideSubtree($this->clusters('Root 2'))); + $this->assertFalse($this->clusters('Child 3')->insideSubtree($this->clusters('Root 2'))); + + $this->assertTrue($this->clusters('Child 1')->insideSubtree($this->clusters('Root 1'))); + $this->assertTrue($this->clusters('Child 2')->insideSubtree($this->clusters('Root 1'))); + $this->assertTrue($this->clusters('Child 2.1')->insideSubtree($this->clusters('Root 1'))); + $this->assertTrue($this->clusters('Child 3')->insideSubtree($this->clusters('Root 1'))); + + $this->assertTrue($this->clusters('Child 2.1')->insideSubtree($this->clusters('Child 2'))); + $this->assertFalse($this->clusters('Child 2.1')->insideSubtree($this->clusters('Root 2'))); + } + + public function testNonNumericGetLevel() + { + $this->assertEquals(0, $this->clusters('Root 1')->getLevel()); + $this->assertEquals(1, $this->clusters('Child 1')->getLevel()); + $this->assertEquals(2, $this->clusters('Child 2.1')->getLevel()); + } + + public function testNonNumericToHierarchyReturnsAnEloquentCollection() + { + $categories = Cluster::all()->toHierarchy(); + + $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $categories); + } + + public function testNonNumericToHierarchyReturnsHierarchicalData() + { + $categories = Cluster::all()->toHierarchy(); + + $this->assertEquals(2, $categories->count()); + + $first = $categories->first(); + $this->assertEquals('Root 1', $first->name); + $this->assertEquals(3, $first->children->count()); + + $first_lvl2 = $first->children->first(); + $this->assertEquals('Child 1', $first_lvl2->name); + $this->assertEquals(0, $first_lvl2->children->count()); + } + + public function testNonNumericToHierarchyNestsCorrectly() + { + // Prune all categories + Cluster::query()->delete(); + + // Build a sample tree structure: + // + // - A + // |- A.1 + // |- A.2 + // - B + // |- B.1 + // |- B.2 + // |- B.2.1 + // |- B.2.2 + // |- B.2.2.1 + // |- B.2.3 + // |- B.3 + // - C + // |- C.1 + // |- C.2 + // - D + // + $a = Cluster::create(['name' => 'A']); + $b = Cluster::create(['name' => 'B']); + $c = Cluster::create(['name' => 'C']); + $d = Cluster::create(['name' => 'D']); + + $ch = Cluster::create(['name' => 'A.1']); + $ch->makeChildOf($a); + + $ch = Cluster::create(['name' => 'A.2']); + $ch->makeChildOf($a); + + $ch = Cluster::create(['name' => 'B.1']); + $ch->makeChildOf($b); + + $ch = Cluster::create(['name' => 'B.2']); + $ch->makeChildOf($b); + + $ch2 = Cluster::create(['name' => 'B.2.1']); + $ch2->makeChildOf($ch); + + $ch2 = Cluster::create(['name' => 'B.2.2']); + $ch2->makeChildOf($ch); + + $ch3 = Cluster::create(['name' => 'B.2.2.1']); + $ch3->makeChildOf($ch2); + + $ch2 = Cluster::create(['name' => 'B.2.3']); + $ch2->makeChildOf($ch); + + $ch = Cluster::create(['name' => 'B.3']); + $ch->makeChildOf($b); + + $ch = Cluster::create(['name' => 'C.1']); + $ch->makeChildOf($c); + + $ch = Cluster::create(['name' => 'C.2']); + $ch->makeChildOf($c); + + $this->assertTrue(Cluster::isValidNestedSet()); + + // Build expectations (expected trees/subtrees) + $expectedWholeTree = [ + 'A' => [ 'A.1' => null, 'A.2' => null ], + 'B' => [ + 'B.1' => null, + 'B.2' => + [ + 'B.2.1' => null, + 'B.2.2' => [ 'B.2.2.1' => null ], + 'B.2.3' => null, + ], + 'B.3' => null, + ], + 'C' => [ 'C.1' => null, 'C.2' => null ], + 'D' => null + ]; + + $expectedSubtreeA = ['A' => [ 'A.1' => null, 'A.2' => null ]]; + + $expectedSubtreeB = [ + 'B' => [ + 'B.1' => null, + 'B.2' => + [ + 'B.2.1' => null, + 'B.2.2' => [ 'B.2.2.1' => null ], + 'B.2.3' => null + ], + 'B.3' => null + ] + ]; + + $expectedSubtreeC = [ 'C.1' => null, 'C.2' => null ]; + + $expectedSubtreeD = ['D' => null]; + + // Perform assertions + $wholeTree = hmap(Cluster::all()->toHierarchy()->toArray()); + $this->assertEquals($expectedWholeTree, $wholeTree); + + $subtreeA = hmap($this->clusters('A')->getDescendantsAndSelf()->toHierarchy()->toArray()); + $this->assertEquals($expectedSubtreeA, $subtreeA); + + $subtreeB = hmap($this->clusters('B')->getDescendantsAndSelf()->toHierarchy()->toArray()); + $this->assertEquals($expectedSubtreeB, $subtreeB); + + $subtreeC = hmap($this->clusters('C')->getDescendants()->toHierarchy()->toArray()); + $this->assertEquals($expectedSubtreeC, $subtreeC); + + $subtreeD = hmap($this->clusters('D')->getDescendantsAndSelf()->toHierarchy()->toArray()); + $this->assertEquals($expectedSubtreeD, $subtreeD); + + $this->assertTrue($this->clusters('D')->getDescendants()->toHierarchy()->isEmpty()); + } + + public function testNonNumericToHierarchyNestsCorrectlyNotSequential() + { + $parent = $this->clusters('Child 1'); + + $parent->children()->create(['name' => 'Child 1.1']); + + $parent->children()->create(['name' => 'Child 1.2']); + + $this->assertTrue(Cluster::isValidNestedSet()); + + $expected = [ + 'Child 1' => [ + 'Child 1.1' => null, + 'Child 1.2' => null + ] + ]; + + $parent->refresh(); + $this->assertEquals($expected, hmap($parent->getDescendantsAndSelf()->toHierarchy()->toArray())); + } + + public function testNonNumericToHierarchyNestsCorrectlyWithOrder() + { + with(new OrderedClusterSeeder)->run(); + + $expectedWhole = [ + 'Root A' => null, + 'Root Z' => [ + 'Child A' => null, + 'Child C' => null, + 'Child G' => [ 'Child G.1' => null ] + ] + ]; + $this->assertEquals($expectedWhole, hmap(OrderedCluster::all()->toHierarchy()->toArray())); + + $expectedSubtreeZ = [ + 'Root Z' => [ + 'Child A' => null, + 'Child C' => null, + 'Child G' => [ 'Child G.1' => null ] + ] + ]; + $this->assertEquals($expectedSubtreeZ, hmap($this->clusters('Root Z', OrderedCluster::class)->getDescendantsAndSelf()->toHierarchy()->toArray())); + } + + public function testNonNumericGetNestedList() + { + $seperator = ' '; + $nestedList = Cluster::getNestedList('name', 'id', $seperator); + + $expected = [ + '7461d8f5-2ea9-4788-99c4-9d0244f0bfb1' => str_repeat($seperator, 0). 'Root 1', + '5d7ce1fd-6151-46d3-a5b3-0ebb9988dc57' => str_repeat($seperator, 1). 'Child 1', + '07c1fc8c-53b5-4fe7-b9c4-e09f266a455c' => str_repeat($seperator, 1). 'Child 2', + '3315a297-af87-4ad3-9fa5-19785407573d' => str_repeat($seperator, 2). 'Child 2.1', + '054476d2-6830-4014-a181-4de010ef7114' => str_repeat($seperator, 1). 'Child 3', + '3bb62314-9e1e-49c6-a5cb-17a9ab9b1b9a' => str_repeat($seperator, 0). 'Root 2', + ]; + + $this->assertEquals($expected, $nestedList); + } +} diff --git a/tests/suite/MovementTest.php b/tests/suite/MovementTest.php new file mode 100644 index 00000000..6c9306a9 --- /dev/null +++ b/tests/suite/MovementTest.php @@ -0,0 +1,1151 @@ +categories('Child 2')->moveLeft(); + + $this->assertNull($this->categories('Child 2')->getLeftSibling()); + + $this->assertEquals($this->categories('Child 1'), $this->categories('Child 2')->getRightSibling()); + + $this->assertTrue(Category::isValidNestedSet()); + } + + /** + * @expectedException Baum\NestedSet\MoveNotPossibleException + */ + public function testMoveLeftRaisesAnExceptionWhenNotPossible() + { + $node = $this->categories('Child 2'); + + $node->moveLeft(); + $node->moveLeft(); + } + + public function testMoveLeftDoesNotChangeDepth() + { + $this->categories('Child 2')->moveLeft(); + + $this->assertEquals(1, $this->categories('Child 2')->getDepth()); + $this->assertEquals(2, $this->categories('Child 2.1')->getDepth()); + } + + public function testMoveLeftWithSubtree() + { + $this->categories('Root 2')->moveLeft(); + + $this->assertNull($this->categories('Root 2')->getLeftSibling()); + $this->assertEquals($this->categories('Root 1'), $this->categories('Root 2')->getRightSibling()); + $this->assertTrue(Category::isValidNestedSet()); + + $this->assertEquals(0, $this->categories('Root 1')->getDepth()); + $this->assertEquals(0, $this->categories('Root 2')->getDepth()); + + $this->assertEquals(1, $this->categories('Child 1')->getDepth()); + $this->assertEquals(1, $this->categories('Child 2')->getDepth()); + $this->assertEquals(1, $this->categories('Child 3')->getDepth()); + + $this->assertEquals(2, $this->categories('Child 2.1')->getDepth()); + } + + public function testMoveToLeftOf() + { + $this->categories('Child 3')->moveToLeftOf($this->categories('Child 1')); + + $this->assertNull($this->categories('Child 3')->getLeftSibling()); + + $this->assertEquals($this->categories('Child 1'), $this->categories('Child 3')->getRightSibling()); + + $this->assertTrue(Category::isValidNestedSet()); + } + + /** + * @expectedException Baum\NestedSet\MoveNotPossibleException + */ + public function testMoveToLeftOfRaisesAnExceptionWhenNotPossible() + { + $this->categories('Child 1')->moveToLeftOf($this->categories('Child 1')->getLeftSibling()); + } + + public function testMoveToLeftOfDoesNotChangeDepth() + { + $this->categories('Child 2')->moveToLeftOf($this->categories('Child 1')); + + $this->assertEquals(1, $this->categories('Child 2')->getDepth()); + $this->assertEquals(2, $this->categories('Child 2.1')->getDepth()); + } + + public function testMoveToLeftOfWithSubtree() + { + $this->categories('Root 2')->moveToLeftOf($this->categories('Root 1')); + + $this->assertNull($this->categories('Root 2')->getLeftSibling()); + $this->assertEquals($this->categories('Root 1'), $this->categories('Root 2')->getRightSibling()); + $this->assertTrue(Category::isValidNestedSet()); + + $this->assertEquals(0, $this->categories('Root 1')->getDepth()); + $this->assertEquals(0, $this->categories('Root 2')->getDepth()); + + $this->assertEquals(1, $this->categories('Child 1')->getDepth()); + $this->assertEquals(1, $this->categories('Child 2')->getDepth()); + $this->assertEquals(1, $this->categories('Child 3')->getDepth()); + + $this->assertEquals(2, $this->categories('Child 2.1')->getDepth()); + } + + public function testMoveRight() + { + $this->categories('Child 2')->moveRight(); + + $this->assertNull($this->categories('Child 2')->getRightSibling()); + + $this->assertEquals($this->categories('Child 3'), $this->categories('Child 2')->getLeftSibling()); + + $this->assertTrue(Category::isValidNestedSet()); + } + + /** + * @expectedException Baum\NestedSet\MoveNotPossibleException + */ + public function testMoveRightRaisesAnExceptionWhenNotPossible() + { + $node = $this->categories('Child 2'); + + $node->moveRight(); + $node->moveRight(); + } + + public function testMoveRightDoesNotChangeDepth() + { + $this->categories('Child 2')->moveRight(); + + $this->assertEquals(1, $this->categories('Child 2')->getDepth()); + $this->assertEquals(2, $this->categories('Child 2.1')->getDepth()); + } + + public function testMoveRightWithSubtree() + { + $this->categories('Root 1')->moveRight(); + + $this->assertNull($this->categories('Root 1')->getRightSibling()); + $this->assertEquals($this->categories('Root 2'), $this->categories('Root 1')->getLeftSibling()); + $this->assertTrue(Category::isValidNestedSet()); + + $this->assertEquals(0, $this->categories('Root 1')->getDepth()); + $this->assertEquals(0, $this->categories('Root 2')->getDepth()); + + $this->assertEquals(1, $this->categories('Child 1')->getDepth()); + $this->assertEquals(1, $this->categories('Child 2')->getDepth()); + $this->assertEquals(1, $this->categories('Child 3')->getDepth()); + + $this->assertEquals(2, $this->categories('Child 2.1')->getDepth()); + } + + public function testMoveToRightOf() + { + $this->categories('Child 1')->moveToRightOf($this->categories('Child 3')); + + $this->assertNull($this->categories('Child 1')->getRightSibling()); + + $this->assertEquals($this->categories('Child 3'), $this->categories('Child 1')->getLeftSibling()); + + $this->assertTrue(Category::isValidNestedSet()); + } + + /** + * @expectedException Baum\NestedSet\MoveNotPossibleException + */ + public function testMoveToRightOfRaisesAnExceptionWhenNotPossible() + { + $this->categories('Child 3')->moveToRightOf($this->categories('Child 3')->getRightSibling()); + } + + public function testMoveToRightOfDoesNotChangeDepth() + { + $this->categories('Child 2')->moveToRightOf($this->categories('Child 3')); + + $this->assertEquals(1, $this->categories('Child 2')->getDepth()); + $this->assertEquals(2, $this->categories('Child 2.1')->getDepth()); + } + + public function testMoveToRightOfWithSubtree() + { + $this->categories('Root 1')->moveToRightOf($this->categories('Root 2')); + + $this->assertNull($this->categories('Root 1')->getRightSibling()); + $this->assertEquals($this->categories('Root 2'), $this->categories('Root 1')->getLeftSibling()); + $this->assertTrue(Category::isValidNestedSet()); + + $this->assertEquals(0, $this->categories('Root 1')->getDepth()); + $this->assertEquals(0, $this->categories('Root 2')->getDepth()); + + $this->assertEquals(1, $this->categories('Child 1')->getDepth()); + $this->assertEquals(1, $this->categories('Child 2')->getDepth()); + $this->assertEquals(1, $this->categories('Child 3')->getDepth()); + + $this->assertEquals(2, $this->categories('Child 2.1')->getDepth()); + } + + public function testMakeRoot() + { + $this->categories('Child 2')->makeRoot(); + + $newRoot = $this->categories('Child 2'); + + $this->assertNull($newRoot->parent()->first()); + $this->assertEquals(0, $newRoot->getLevel()); + $this->assertEquals(9, $newRoot->getLeft()); + $this->assertEquals(12, $newRoot->getRight()); + + $this->assertEquals(1, $this->categories('Child 2.1')->getLevel()); + + $this->assertTrue(Category::isValidNestedSet()); + } + + public function testNullifyParentColumnMakesItRoot() + { + $node = $this->categories('Child 2'); + + $node->parent_id = null; + + $node->save(); + + $this->assertNull($node->parent()->first()); + $this->assertEquals(0, $node->getLevel()); + $this->assertEquals(9, $node->getLeft()); + $this->assertEquals(12, $node->getRight()); + + $this->assertEquals(1, $this->categories('Child 2.1')->getLevel()); + + $this->assertTrue(Category::isValidNestedSet()); + } + + public function testNullifyParentColumnOnNewNodes() + { + $node = new Category(['name' => 'Root 3']); + + $node->parent_id = null; + + $node->save(); + + $node->refresh(); + + $this->assertNull($node->parent()->first()); + $this->assertEquals(0, $node->getLevel()); + $this->assertEquals(13, $node->getLeft()); + $this->assertEquals(14, $node->getRight()); + + $this->assertTrue(Category::isValidNestedSet()); + } + + public function testNewCategoryWithNullParent() + { + $node = new Category(['name' => 'Root 3']); + $this->assertTrue($node->isRoot()); + + $node->save(); + $this->assertTrue($node->isRoot()); + + $node->makeRoot(); + $this->assertTrue($node->isRoot()); + } + + public function testMakeChildOf() + { + $this->categories('Child 1')->makeChildOf($this->categories('Child 3')); + + $this->assertEquals($this->categories('Child 3'), $this->categories('Child 1')->parent()->first()); + + $this->assertTrue(Category::isValidNestedSet()); + } + + public function testMakeChildOfAppendsAtTheEnd() + { + $newChild = Category::create(['name' => 'Child 4']); + + $newChild->makeChildOf($this->categories('Root 1')); + + $lastChild = $this->categories('Root 1')->children()->get()->last(); + $this->assertEquals($newChild->getAttributes(), $lastChild->getAttributes()); + + $this->assertTrue(Category::isValidNestedSet()); + } + + public function testMakeChildOfMovesWithSubtree() + { + $this->categories('Child 2')->makeChildOf($this->categories('Child 1')); + + $this->assertTrue(Category::isValidNestedSet()); + + $this->assertEquals($this->categories('Child 1')->getKey(), $this->categories('Child 2')->getParentKey()); + + $this->assertEquals(3, $this->categories('Child 2')->getLeft()); + $this->assertEquals(6, $this->categories('Child 2')->getRight()); + + $this->assertEquals(2, $this->categories('Child 1')->getLeft()); + $this->assertEquals(7, $this->categories('Child 1')->getRight()); + } + + public function testMakeChildOfSwappingRoots() + { + $newRoot = Category::create(['name' => 'Root 3']); + + $this->assertEquals(13, $newRoot->getLeft()); + $this->assertEquals(14, $newRoot->getRight()); + + $this->categories('Root 2')->makeChildOf($newRoot); + + $this->assertTrue(Category::isValidNestedSet()); + + $this->assertEquals($newRoot->getKey(), $this->categories('Root 2')->getParentKey()); + + $this->assertEquals(12, $this->categories('Root 2')->getLeft()); + $this->assertEquals(13, $this->categories('Root 2')->getRight()); + + $this->assertEquals(11, $newRoot->getLeft()); + $this->assertEquals(14, $newRoot->getRight()); + } + + public function testMakeChildOfSwappingRootsWithSubtrees() + { + $newRoot = Category::create(['name' => 'Root 3']); + + $this->categories('Root 1')->makeChildOf($newRoot); + + $this->assertTrue(Category::isValidNestedSet()); + + $this->assertEquals($newRoot->getKey(), $this->categories('Root 1')->getParentKey()); + + $this->assertEquals(4, $this->categories('Root 1')->getLeft()); + $this->assertEquals(13, $this->categories('Root 1')->getRight()); + + $this->assertEquals(8, $this->categories('Child 2.1')->getLeft()); + $this->assertEquals(9, $this->categories('Child 2.1')->getRight()); + } + + public function testMakeFirstChildOf() + { + $this->categories('Child 1')->makeFirstChildOf($this->categories('Child 3')); + + $this->assertEquals($this->categories('Child 3'), $this->categories('Child 1')->parent()->first()); + + $this->assertTrue(Category::isValidNestedSet()); + } + + public function testMakeFirstChildOfAppendsAtTheBeginning() + { + $newChild = Category::create(['name' => 'Child 4']); + + $newChild->makeFirstChildOf($this->categories('Root 1')); + + $lastChild = $this->categories('Root 1')->children()->get()->first(); + $this->assertEquals($newChild->getAttributes(), $lastChild->getAttributes()); + + $this->assertTrue(Category::isValidNestedSet()); + } + + public function testMakeFirstChildOfMovesWithSubtree() + { + $this->categories('Child 2')->makeFirstChildOf($this->categories('Child 1')); + + $this->assertTrue(Category::isValidNestedSet()); + + $this->assertEquals($this->categories('Child 1')->getKey(), $this->categories('Child 2')->getParentKey()); + + $this->assertEquals(3, $this->categories('Child 2')->getLeft()); + $this->assertEquals(6, $this->categories('Child 2')->getRight()); + + $this->assertEquals(2, $this->categories('Child 1')->getLeft()); + $this->assertEquals(7, $this->categories('Child 1')->getRight()); + } + + public function testMakeFirstChildOfSwappingRoots() + { + $newRoot = Category::create(['name' => 'Root 3']); + + $this->assertEquals(13, $newRoot->getLeft()); + $this->assertEquals(14, $newRoot->getRight()); + + $this->categories('Root 2')->makeFirstChildOf($newRoot); + + $this->assertTrue(Category::isValidNestedSet()); + + $this->assertEquals($newRoot->getKey(), $this->categories('Root 2')->getParentKey()); + + $this->assertEquals(12, $this->categories('Root 2')->getLeft()); + $this->assertEquals(13, $this->categories('Root 2')->getRight()); + + $this->assertEquals(11, $newRoot->getLeft()); + $this->assertEquals(14, $newRoot->getRight()); + } + + public function testMakeFirstChildOfSwappingRootsWithSubtrees() + { + $newRoot = Category::create(['name' => 'Root 3']); + + $this->categories('Root 1')->makeFirstChildOf($newRoot); + + $this->assertTrue(Category::isValidNestedSet()); + + $this->assertEquals($newRoot->getKey(), $this->categories('Root 1')->getParentKey()); + + $this->assertEquals(4, $this->categories('Root 1')->getLeft()); + $this->assertEquals(13, $this->categories('Root 1')->getRight()); + + $this->assertEquals(8, $this->categories('Child 2.1')->getLeft()); + $this->assertEquals(9, $this->categories('Child 2.1')->getRight()); + } + + public function testMakeLastChildOf() + { + $this->categories('Child 1')->makeLastChildOf($this->categories('Child 3')); + + $this->assertEquals($this->categories('Child 3'), $this->categories('Child 1')->parent()->first()); + + $this->assertTrue(Category::isValidNestedSet()); + } + + public function testMakeLastChildOfAppendsAtTheEnd() + { + $newChild = Category::create(['name' => 'Child 4']); + + $newChild->makeLastChildOf($this->categories('Root 1')); + + $lastChild = $this->categories('Root 1')->children()->get()->last(); + $this->assertEquals($newChild->getAttributes(), $lastChild->getAttributes()); + + $this->assertTrue(Category::isValidNestedSet()); + } + + public function testMakeLastChildOfMovesWithSubtree() + { + $this->categories('Child 2')->makeLastChildOf($this->categories('Child 1')); + + $this->assertTrue(Category::isValidNestedSet()); + + $this->assertEquals($this->categories('Child 1')->getKey(), $this->categories('Child 2')->getParentKey()); + + $this->assertEquals(3, $this->categories('Child 2')->getLeft()); + $this->assertEquals(6, $this->categories('Child 2')->getRight()); + + $this->assertEquals(2, $this->categories('Child 1')->getLeft()); + $this->assertEquals(7, $this->categories('Child 1')->getRight()); + } + + public function testMakeLastChildOfSwappingRoots() + { + $newRoot = Category::create(['name' => 'Root 3']); + + $this->assertEquals(13, $newRoot->getLeft()); + $this->assertEquals(14, $newRoot->getRight()); + + $this->categories('Root 2')->makeLastChildOf($newRoot); + + $this->assertTrue(Category::isValidNestedSet()); + + $this->assertEquals($newRoot->getKey(), $this->categories('Root 2')->getParentKey()); + + $this->assertEquals(12, $this->categories('Root 2')->getLeft()); + $this->assertEquals(13, $this->categories('Root 2')->getRight()); + + $this->assertEquals(11, $newRoot->getLeft()); + $this->assertEquals(14, $newRoot->getRight()); + } + + public function testMakeLastChildOfSwappingRootsWithSubtrees() + { + $newRoot = Category::create(['name' => 'Root 3']); + + $this->categories('Root 1')->makeLastChildOf($newRoot); + + $this->assertTrue(Category::isValidNestedSet()); + + $this->assertEquals($newRoot->getKey(), $this->categories('Root 1')->getParentKey()); + + $this->assertEquals(4, $this->categories('Root 1')->getLeft()); + $this->assertEquals(13, $this->categories('Root 1')->getRight()); + + $this->assertEquals(8, $this->categories('Child 2.1')->getLeft()); + $this->assertEquals(9, $this->categories('Child 2.1')->getRight()); + } + + /** + * @expectedException Baum\NestedSet\MoveNotPossibleException + */ + public function testUnpersistedNodeCannotBeMoved() + { + $unpersisted = new Category(['name' => 'Unpersisted']); + + $unpersisted->moveToRightOf($this->categories('Root 1')); + } + + /** + * @expectedException Baum\NestedSet\MoveNotPossibleException + */ + public function testUnpersistedNodeCannotBeMadeChild() + { + $unpersisted = new Category(['name' => 'Unpersisted']); + + $unpersisted->makeChildOf($this->categories('Root 1')); + } + + /** + * @expectedException Baum\NestedSet\MoveNotPossibleException + */ + public function testNodesCannotBeMovedToItself() + { + $node = $this->categories('Child 1'); + + $node->moveToRightOf($node); + } + + /** + * @expectedException Baum\NestedSet\MoveNotPossibleException + */ + public function testNodesCannotBeMadeChildOfThemselves() + { + $node = $this->categories('Child 1'); + + $node->makeChildOf($node); + } + + /** + * @expectedException Baum\NestedSet\MoveNotPossibleException + */ + public function testNodesCannotBeMovedToDescendantsOfThemselves() + { + $node = $this->categories('Root 1'); + + $node->makeChildOf($this->categories('Child 2.1')); + } + + public function testDepthIsUpdatedWhenMadeChild() + { + $a = Category::create(['name' => 'A']); + $b = Category::create(['name' => 'B']); + $c = Category::create(['name' => 'C']); + $d = Category::create(['name' => 'D']); + + // a > b > c > d + $b->makeChildOf($a); + $c->makeChildOf($b); + $d->makeChildOf($c); + + $a->refresh(); + $b->refresh(); + $c->refresh(); + $d->refresh(); + + $this->assertEquals(0, $a->getDepth()); + $this->assertEquals(1, $b->getDepth()); + $this->assertEquals(2, $c->getDepth()); + $this->assertEquals(3, $d->getDepth()); + } + + public function testDepthIsUpdatedOnDescendantsWhenParentMoves() + { + $a = Category::create(['name' => 'A']); + $b = Category::create(['name' => 'B']); + $c = Category::create(['name' => 'C']); + $d = Category::create(['name' => 'D']); + + // a > b > c > d + $b->makeChildOf($a); + $c->makeChildOf($b); + $d->makeChildOf($c); + + $a->refresh(); + $b->refresh(); + $c->refresh(); + $d->refresh(); + + $b->moveToRightOf($a); + + $a->refresh(); + $b->refresh(); + $c->refresh(); + $d->refresh(); + + $this->assertEquals(0, $b->getDepth()); + $this->assertEquals(1, $c->getDepth()); + $this->assertEquals(2, $d->getDepth()); + } + + public function testNonNumericMoveLeft() + { + $this->clusters('Child 2')->moveLeft(); + + $this->assertNull($this->clusters('Child 2')->getLeftSibling()); + + $this->assertEquals($this->clusters('Child 1'), $this->clusters('Child 2')->getRightSibling()); + + $this->assertTrue(Cluster::isValidNestedSet()); + } + + /** + * @expectedException Baum\NestedSet\MoveNotPossibleException + */ + public function testNonNumericMoveLeftRaisesAnExceptionWhenNotPossible() + { + $node = $this->clusters('Child 2'); + + $node->moveLeft(); + $node->moveLeft(); + } + + public function testNonNumericMoveLeftDoesNotChangeDepth() + { + $this->clusters('Child 2')->moveLeft(); + + $this->assertEquals(1, $this->clusters('Child 2')->getDepth()); + $this->assertEquals(2, $this->clusters('Child 2.1')->getDepth()); + } + + public function testNonNumericMoveLeftWithSubtree() + { + $this->clusters('Root 2')->moveLeft(); + + $this->assertNull($this->clusters('Root 2')->getLeftSibling()); + $this->assertEquals($this->clusters('Root 1'), $this->clusters('Root 2')->getRightSibling()); + $this->assertTrue(Cluster::isValidNestedSet()); + + $this->assertEquals(0, $this->clusters('Root 1')->getDepth()); + $this->assertEquals(0, $this->clusters('Root 2')->getDepth()); + + $this->assertEquals(1, $this->clusters('Child 1')->getDepth()); + $this->assertEquals(1, $this->clusters('Child 2')->getDepth()); + $this->assertEquals(1, $this->clusters('Child 3')->getDepth()); + + $this->assertEquals(2, $this->clusters('Child 2.1')->getDepth()); + } + + public function testNonNumericMoveToLeftOf() + { + $this->clusters('Child 3')->moveToLeftOf($this->clusters('Child 1')); + + $this->assertNull($this->clusters('Child 3')->getLeftSibling()); + + $this->assertEquals($this->clusters('Child 1'), $this->clusters('Child 3')->getRightSibling()); + + $this->assertTrue(Cluster::isValidNestedSet()); + } + + /** + * @expectedException Baum\NestedSet\MoveNotPossibleException + */ + public function testNonNumericMoveToLeftOfRaisesAnExceptionWhenNotPossible() + { + $this->clusters('Child 1')->moveToLeftOf($this->clusters('Child 1')->getLeftSibling()); + } + + public function testNonNumericMoveToLeftOfDoesNotChangeDepth() + { + $this->clusters('Child 2')->moveToLeftOf($this->clusters('Child 1')); + + $this->assertEquals(1, $this->clusters('Child 2')->getDepth()); + $this->assertEquals(2, $this->clusters('Child 2.1')->getDepth()); + } + + public function testNonNumericMoveToLeftOfWithSubtree() + { + $this->clusters('Root 2')->moveToLeftOf($this->clusters('Root 1')); + + $this->assertNull($this->clusters('Root 2')->getLeftSibling()); + $this->assertEquals($this->clusters('Root 1'), $this->clusters('Root 2')->getRightSibling()); + $this->assertTrue(Cluster::isValidNestedSet()); + + $this->assertEquals(0, $this->clusters('Root 1')->getDepth()); + $this->assertEquals(0, $this->clusters('Root 2')->getDepth()); + + $this->assertEquals(1, $this->clusters('Child 1')->getDepth()); + $this->assertEquals(1, $this->clusters('Child 2')->getDepth()); + $this->assertEquals(1, $this->clusters('Child 3')->getDepth()); + + $this->assertEquals(2, $this->clusters('Child 2.1')->getDepth()); + } + + public function testNonNumericMoveRight() + { + $this->clusters('Child 2')->moveRight(); + + $this->assertNull($this->clusters('Child 2')->getRightSibling()); + + $this->assertEquals($this->clusters('Child 3'), $this->clusters('Child 2')->getLeftSibling()); + + $this->assertTrue(Cluster::isValidNestedSet()); + } + + /** + * @expectedException Baum\NestedSet\MoveNotPossibleException + */ + public function testNonNumericMoveRightRaisesAnExceptionWhenNotPossible() + { + $node = $this->clusters('Child 2'); + + $node->moveRight(); + $node->moveRight(); + } + + public function testNonNumericMoveRightDoesNotChangeDepth() + { + $this->clusters('Child 2')->moveRight(); + + $this->assertEquals(1, $this->clusters('Child 2')->getDepth()); + $this->assertEquals(2, $this->clusters('Child 2.1')->getDepth()); + } + + public function testNonNumericMoveRightWithSubtree() + { + $this->clusters('Root 1')->moveRight(); + + $this->assertNull($this->clusters('Root 1')->getRightSibling()); + $this->assertEquals($this->clusters('Root 2'), $this->clusters('Root 1')->getLeftSibling()); + $this->assertTrue(Cluster::isValidNestedSet()); + + $this->assertEquals(0, $this->clusters('Root 1')->getDepth()); + $this->assertEquals(0, $this->clusters('Root 2')->getDepth()); + + $this->assertEquals(1, $this->clusters('Child 1')->getDepth()); + $this->assertEquals(1, $this->clusters('Child 2')->getDepth()); + $this->assertEquals(1, $this->clusters('Child 3')->getDepth()); + + $this->assertEquals(2, $this->clusters('Child 2.1')->getDepth()); + } + + public function testNonNumericMoveToRightOf() + { + $this->clusters('Child 1')->moveToRightOf($this->clusters('Child 3')); + + $this->assertNull($this->clusters('Child 1')->getRightSibling()); + + $this->assertEquals($this->clusters('Child 3'), $this->clusters('Child 1')->getLeftSibling()); + + $this->assertTrue(Cluster::isValidNestedSet()); + } + + /** + * @expectedException Baum\NestedSet\MoveNotPossibleException + */ + public function testNonNumericMoveToRightOfRaisesAnExceptionWhenNotPossible() + { + $this->clusters('Child 3')->moveToRightOf($this->clusters('Child 3')->getRightSibling()); + } + + public function testNonNumericMoveToRightOfDoesNotChangeDepth() + { + $this->clusters('Child 2')->moveToRightOf($this->clusters('Child 3')); + + $this->assertEquals(1, $this->clusters('Child 2')->getDepth()); + $this->assertEquals(2, $this->clusters('Child 2.1')->getDepth()); + } + + public function testNonNumericMoveToRightOfWithSubtree() + { + $this->clusters('Root 1')->moveToRightOf($this->clusters('Root 2')); + + $this->assertNull($this->clusters('Root 1')->getRightSibling()); + $this->assertEquals($this->clusters('Root 2'), $this->clusters('Root 1')->getLeftSibling()); + $this->assertTrue(Cluster::isValidNestedSet()); + + $this->assertEquals(0, $this->clusters('Root 1')->getDepth()); + $this->assertEquals(0, $this->clusters('Root 2')->getDepth()); + + $this->assertEquals(1, $this->clusters('Child 1')->getDepth()); + $this->assertEquals(1, $this->clusters('Child 2')->getDepth()); + $this->assertEquals(1, $this->clusters('Child 3')->getDepth()); + + $this->assertEquals(2, $this->clusters('Child 2.1')->getDepth()); + } + + public function testNonNumericMakeRoot() + { + $this->clusters('Child 2')->makeRoot(); + + $newRoot = $this->clusters('Child 2'); + + $this->assertNull($newRoot->parent()->first()); + $this->assertEquals(0, $newRoot->getLevel()); + $this->assertEquals(9, $newRoot->getLeft()); + $this->assertEquals(12, $newRoot->getRight()); + + $this->assertEquals(1, $this->clusters('Child 2.1')->getLevel()); + + $this->assertTrue(Cluster::isValidNestedSet()); + } + + public function testNonNumericNullifyParentColumnMakesItRoot() + { + $node = $this->clusters('Child 2'); + + $node->parent_id = null; + + $node->save(); + + $this->assertNull($node->parent()->first()); + $this->assertEquals(0, $node->getLevel()); + $this->assertEquals(9, $node->getLeft()); + $this->assertEquals(12, $node->getRight()); + + $this->assertEquals(1, $this->clusters('Child 2.1')->getLevel()); + + $this->assertTrue(Cluster::isValidNestedSet()); + } + + public function testNonNumericNullifyParentColumnOnNewNodes() + { + $node = new Cluster(['name' => 'Root 3']); + + $node->parent_id = null; + + $node->save(); + + $node->refresh(); + + $this->assertNull($node->parent()->first()); + $this->assertEquals(0, $node->getLevel()); + $this->assertEquals(13, $node->getLeft()); + $this->assertEquals(14, $node->getRight()); + + $this->assertTrue(Cluster::isValidNestedSet()); + } + + public function testNonNumericNewClusterWithNullParent() + { + $node = new Cluster(['name' => 'Root 3']); + $this->assertTrue($node->isRoot()); + + $node->save(); + $this->assertTrue($node->isRoot()); + + $node->makeRoot(); + $this->assertTrue($node->isRoot()); + } + + public function testNonNumericMakeChildOf() + { + $this->clusters('Child 1')->makeChildOf($this->clusters('Child 3')); + + $this->assertEquals($this->clusters('Child 3'), $this->clusters('Child 1')->parent()->first()); + + $this->assertTrue(Cluster::isValidNestedSet()); + } + + public function testNonNumericMakeChildOfAppendsAtTheEnd() + { + $newChild = Cluster::create(['name' => 'Child 4']); + + $newChild->makeChildOf($this->clusters('Root 1')); + + $lastChild = $this->clusters('Root 1')->children()->get()->last(); + $this->assertEquals($newChild->getAttributes(), $lastChild->getAttributes()); + + $this->assertTrue(Cluster::isValidNestedSet()); + } + + public function testNonNumericMakeChildOfMovesWithSubtree() + { + $this->clusters('Child 2')->makeChildOf($this->clusters('Child 1')); + + $this->assertTrue(Cluster::isValidNestedSet()); + + $this->assertEquals($this->clusters('Child 1')->getKey(), $this->clusters('Child 2')->getParentKey()); + + $this->assertEquals(3, $this->clusters('Child 2')->getLeft()); + $this->assertEquals(6, $this->clusters('Child 2')->getRight()); + + $this->assertEquals(2, $this->clusters('Child 1')->getLeft()); + $this->assertEquals(7, $this->clusters('Child 1')->getRight()); + } + + public function testNonNumericMakeChildOfSwappingRoots() + { + $newRoot = Cluster::create(['name' => 'Root 3']); + + $this->assertEquals(13, $newRoot->getLeft()); + $this->assertEquals(14, $newRoot->getRight()); + + $this->clusters('Root 2')->makeChildOf($newRoot); + + $this->assertTrue(Cluster::isValidNestedSet()); + + $this->assertEquals($newRoot->getKey(), $this->clusters('Root 2')->getParentKey()); + + $this->assertEquals(12, $this->clusters('Root 2')->getLeft()); + $this->assertEquals(13, $this->clusters('Root 2')->getRight()); + + $this->assertEquals(11, $newRoot->getLeft()); + $this->assertEquals(14, $newRoot->getRight()); + } + + public function testNonNumericMakeChildOfSwappingRootsWithSubtrees() + { + $newRoot = Cluster::create(['name' => 'Root 3']); + + $this->clusters('Root 1')->makeChildOf($newRoot); + + $this->assertTrue(Cluster::isValidNestedSet()); + + $this->assertEquals($newRoot->getKey(), $this->clusters('Root 1')->getParentKey()); + + $this->assertEquals(4, $this->clusters('Root 1')->getLeft()); + $this->assertEquals(13, $this->clusters('Root 1')->getRight()); + + $this->assertEquals(8, $this->clusters('Child 2.1')->getLeft()); + $this->assertEquals(9, $this->clusters('Child 2.1')->getRight()); + } + + public function testNonNumericMakeFirstChildOf() + { + $this->clusters('Child 1')->makeFirstChildOf($this->clusters('Child 3')); + + $this->assertEquals($this->clusters('Child 3'), $this->clusters('Child 1')->parent()->first()); + + $this->assertTrue(Cluster::isValidNestedSet()); + } + + public function testNonNumericMakeFirstChildOfAppendsAtTheBeginning() + { + $newChild = Cluster::create(['name' => 'Child 4']); + + $newChild->makeFirstChildOf($this->clusters('Root 1')); + + $lastChild = $this->clusters('Root 1')->children()->get()->first(); + $this->assertEquals($newChild->getAttributes(), $lastChild->getAttributes()); + + $this->assertTrue(Cluster::isValidNestedSet()); + } + + public function testNonNumericMakeFirstChildOfMovesWithSubtree() + { + $this->clusters('Child 2')->makeFirstChildOf($this->clusters('Child 1')); + + $this->assertTrue(Cluster::isValidNestedSet()); + + $this->assertEquals($this->clusters('Child 1')->getKey(), $this->clusters('Child 2')->getParentKey()); + + $this->assertEquals(3, $this->clusters('Child 2')->getLeft()); + $this->assertEquals(6, $this->clusters('Child 2')->getRight()); + + $this->assertEquals(2, $this->clusters('Child 1')->getLeft()); + $this->assertEquals(7, $this->clusters('Child 1')->getRight()); + } + + public function testNonNumericMakeFirstChildOfSwappingRoots() + { + $newRoot = Cluster::create(['name' => 'Root 3']); + + $this->assertEquals(13, $newRoot->getLeft()); + $this->assertEquals(14, $newRoot->getRight()); + + $this->clusters('Root 2')->makeFirstChildOf($newRoot); + + $this->assertTrue(Cluster::isValidNestedSet()); + + $this->assertEquals($newRoot->getKey(), $this->clusters('Root 2')->getParentKey()); + + $this->assertEquals(12, $this->clusters('Root 2')->getLeft()); + $this->assertEquals(13, $this->clusters('Root 2')->getRight()); + + $this->assertEquals(11, $newRoot->getLeft()); + $this->assertEquals(14, $newRoot->getRight()); + } + + public function testNonNumericMakeFirstChildOfSwappingRootsWithSubtrees() + { + $newRoot = Cluster::create(['name' => 'Root 3']); + + $this->clusters('Root 1')->makeFirstChildOf($newRoot); + + $this->assertTrue(Cluster::isValidNestedSet()); + + $this->assertEquals($newRoot->getKey(), $this->clusters('Root 1')->getParentKey()); + + $this->assertEquals(4, $this->clusters('Root 1')->getLeft()); + $this->assertEquals(13, $this->clusters('Root 1')->getRight()); + + $this->assertEquals(8, $this->clusters('Child 2.1')->getLeft()); + $this->assertEquals(9, $this->clusters('Child 2.1')->getRight()); + } + + public function testNonNumericMakeLastChildOf() + { + $this->clusters('Child 1')->makeLastChildOf($this->clusters('Child 3')); + + $this->assertEquals($this->clusters('Child 3'), $this->clusters('Child 1')->parent()->first()); + + $this->assertTrue(Cluster::isValidNestedSet()); + } + + public function testNonNumericMakeLastChildOfAppendsAtTheEnd() + { + $newChild = Cluster::create(['name' => 'Child 4']); + + $newChild->makeLastChildOf($this->clusters('Root 1')); + + $lastChild = $this->clusters('Root 1')->children()->get()->last(); + $this->assertEquals($newChild->getAttributes(), $lastChild->getAttributes()); + + $this->assertTrue(Cluster::isValidNestedSet()); + } + + public function testNonNumericMakeLastChildOfMovesWithSubtree() + { + $this->clusters('Child 2')->makeLastChildOf($this->clusters('Child 1')); + + $this->assertTrue(Cluster::isValidNestedSet()); + + $this->assertEquals($this->clusters('Child 1')->getKey(), $this->clusters('Child 2')->getParentKey()); + + $this->assertEquals(3, $this->clusters('Child 2')->getLeft()); + $this->assertEquals(6, $this->clusters('Child 2')->getRight()); + + $this->assertEquals(2, $this->clusters('Child 1')->getLeft()); + $this->assertEquals(7, $this->clusters('Child 1')->getRight()); + } + + public function testNonNumericMakeLastChildOfSwappingRoots() + { + $newRoot = Cluster::create(['name' => 'Root 3']); + + $this->assertEquals(13, $newRoot->getLeft()); + $this->assertEquals(14, $newRoot->getRight()); + + $this->clusters('Root 2')->makeLastChildOf($newRoot); + + $this->assertTrue(Cluster::isValidNestedSet()); + + $this->assertEquals($newRoot->getKey(), $this->clusters('Root 2')->getParentKey()); + + $this->assertEquals(12, $this->clusters('Root 2')->getLeft()); + $this->assertEquals(13, $this->clusters('Root 2')->getRight()); + + $this->assertEquals(11, $newRoot->getLeft()); + $this->assertEquals(14, $newRoot->getRight()); + } + + public function testNonNumericMakeLastChildOfSwappingRootsWithSubtrees() + { + $newRoot = Cluster::create(['name' => 'Root 3']); + + $this->clusters('Root 1')->makeLastChildOf($newRoot); + + $this->assertTrue(Cluster::isValidNestedSet()); + + $this->assertEquals($newRoot->getKey(), $this->clusters('Root 1')->getParentKey()); + + $this->assertEquals(4, $this->clusters('Root 1')->getLeft()); + $this->assertEquals(13, $this->clusters('Root 1')->getRight()); + + $this->assertEquals(8, $this->clusters('Child 2.1')->getLeft()); + $this->assertEquals(9, $this->clusters('Child 2.1')->getRight()); + } + + /** + * @expectedException Baum\NestedSet\MoveNotPossibleException + */ + public function testNonNumericUnpersistedNodeCannotBeMoved() + { + $unpersisted = new Cluster(['name' => 'Unpersisted']); + + $unpersisted->moveToRightOf($this->clusters('Root 1')); + } + + /** + * @expectedException Baum\NestedSet\MoveNotPossibleException + */ + public function testNonNumericUnpersistedNodeCannotBeMadeChild() + { + $unpersisted = new Cluster(['name' => 'Unpersisted']); + + $unpersisted->makeChildOf($this->clusters('Root 1')); + } + + /** + * @expectedException Baum\NestedSet\MoveNotPossibleException + */ + public function testNonNumericNodesCannotBeMovedToItself() + { + $node = $this->clusters('Child 1'); + + $node->moveToRightOf($node); + } + + /** + * @expectedException Baum\NestedSet\MoveNotPossibleException + */ + public function testNonNumericNodesCannotBeMadeChildOfThemselves() + { + $node = $this->clusters('Child 1'); + + $node->makeChildOf($node); + } + + /** + * @expectedException Baum\NestedSet\MoveNotPossibleException + */ + public function testNonNumericNodesCannotBeMovedToDescendantsOfThemselves() + { + $node = $this->clusters('Root 1'); + + $node->makeChildOf($this->clusters('Child 2.1')); + } + + public function testNonNumericDepthIsUpdatedWhenMadeChild() + { + $a = Cluster::create(['name' => 'A']); + $b = Cluster::create(['name' => 'B']); + $c = Cluster::create(['name' => 'C']); + $d = Cluster::create(['name' => 'D']); + + // a > b > c > d + $b->makeChildOf($a); + $c->makeChildOf($b); + $d->makeChildOf($c); + + $a->refresh(); + $b->refresh(); + $c->refresh(); + $d->refresh(); + + $this->assertEquals(0, $a->getDepth()); + $this->assertEquals(1, $b->getDepth()); + $this->assertEquals(2, $c->getDepth()); + $this->assertEquals(3, $d->getDepth()); + } + + public function testNonNumericDepthIsUpdatedOnDescendantsWhenParentMoves() + { + $a = Cluster::create(['name' => 'A']); + $b = Cluster::create(['name' => 'B']); + $c = Cluster::create(['name' => 'C']); + $d = Cluster::create(['name' => 'D']); + + // a > b > c > d + $b->makeChildOf($a); + $c->makeChildOf($b); + $d->makeChildOf($c); + + $a->refresh(); + $b->refresh(); + $c->refresh(); + $d->refresh(); + + $b->moveToRightOf($a); + + $a->refresh(); + $b->refresh(); + $c->refresh(); + $d->refresh(); + + $this->assertEquals(0, $b->getDepth()); + $this->assertEquals(1, $c->getDepth()); + $this->assertEquals(2, $d->getDepth()); + } +} diff --git a/tests/suite/NodeModelExtensionsTest.php b/tests/suite/NodeModelExtensionsTest.php deleted file mode 100644 index 0714d888..00000000 --- a/tests/suite/NodeModelExtensionsTest.php +++ /dev/null @@ -1,182 +0,0 @@ -up(); - } - - public function setUp() { - DB::table('categories')->delete(); - } - - protected function categories($name, $className = 'Category') { - return forward_static_call_array(array($className, 'where'), array('name', '=', $name))->first(); - } - - public function tearDown() { - m::close(); - } - - public function testNewQueryReturnsEloquentBuilderWithExtendedQueryBuilder() { - $query = with(new Category)->newQuery()->getQuery(); - - $this->assertInstanceOf('Baum\Extensions\Query\Builder', $query); - } - - public function testNewCollectionReturnsCustomOne() { - $this->assertInstanceOf('\Baum\Extensions\Eloquent\Collection', with(new Category)->newCollection()); - } - - public function testGetObservableEventsIncludesMovingEvents() { - $events = with(new Category)->getObservableEvents(); - - $this->assertContains('moving', $events); - $this->assertContains('moved', $events); - } - - public function testAreSoftDeletesEnabled() { - $this->assertFalse(with(new Category)->areSoftDeletesEnabled()); - $this->assertTrue(with(new SoftCategory)->areSoftDeletesEnabled()); - } - - public function testSoftDeletesEnabledStatic() { - $this->assertFalse(Category::softDeletesEnabled()); - $this->assertTrue(SoftCategory::softDeletesEnabled()); - } - - public function testMoving() { - $dispatcher = Category::getEventDispatcher(); - - Category::setEventDispatcher($events = m::mock('Illuminate\Contracts\Events\Dispatcher')); - - $closure = function() {}; - - $events->shouldReceive('listen')->once()->with('eloquent.moving: ' . Category::class, $closure); - - Category::moving($closure); - - Category::unsetEventDispatcher(); - - Category::setEventDispatcher($dispatcher); - } - - public function testMoved() { - $dispatcher = Category::getEventDispatcher(); - - Category::setEventDispatcher($events = m::mock('Illuminate\Contracts\Events\Dispatcher')); - - $closure = function() {}; - - $events->shouldReceive('listen')->once()->with('eloquent.moved: ' . Category::class, $closure); - - Category::moved($closure); - - Category::unsetEventDispatcher(); - - Category::setEventDispatcher($dispatcher); - } - - public function testReloadResetsChangesOnFreshNodes() { - $new = new Category; - - $new->name = 'Some new category'; - $new->reload(); - - $this->assertNull($new->name); - } - - public function testReloadResetsChangesOnPersistedNodes() { - $node = Category::create(['name' => 'Some node']); - - $node->name = 'A better node'; - $node->lft = 10; - $node->reload(); - - $this->assertEquals($this->categories('Some node')->getAttributes(), $node->getAttributes()); - } - - public function testReloadResetsChangesOnDeletedNodes() { - $node = Category::create(['name' => 'Some node']); - $this->assertNotNull($node->getKey()); - - $node->delete(); - $this->assertNull($this->categories('Some node')); - - $node->name = 'A better node'; - $node->reload(); - - $this->assertEquals('Some node', $node->name); - } - - /** - * @expectedException Illuminate\Database\Eloquent\ModelNotFoundException - */ - public function testReloadThrowsExceptionIfNodeCannotBeLocated() { - $node = Category::create(['name' => 'Some node']); - $this->assertNotNull($node->getKey()); - - $node->delete(); - $this->assertNull($this->categories('Some node')); - $this->assertFalse($node->exists); - - // Fake persisted state, reload & expect failure - $node->exists = true; - $node->reload(); - } - - public function testNewNestedSetQueryUsesInternalBuilder() { - $category = new Category; - $builder = $category->newNestedSetQuery(); - $query = $builder->getQuery(); - - $this->assertInstanceOf('Baum\Extensions\Query\Builder', $query); - } - - public function testNewNestedSetQueryIsOrderedByDefault() { - $category = new Category; - $builder = $category->newNestedSetQuery(); - $query = $builder->getQuery(); - - $this->assertEmpty($query->wheres); - $this->assertNotEmpty($query->orders); - $this->assertEquals($category->getLeftColumnName(), $category->getOrderColumnName()); - $this->assertEquals($category->getQualifiedLeftColumnName(), $category->getQualifiedOrderColumnName()); - $this->assertEquals($category->getQualifiedOrderColumnName(), $query->orders[0]['column']); - } - - public function testNewNestedSetQueryIsOrderedByCustom() { - $category = new OrderedCategory; - $builder = $category->newNestedSetQuery(); - $query = $builder->getQuery(); - - $this->assertEmpty($query->wheres); - $this->assertNotEmpty($query->orders); - $this->assertEquals('name', $category->getOrderColumnName()); - $this->assertEquals('categories.name', $category->getQualifiedOrderColumnName()); - $this->assertEquals($category->getQualifiedOrderColumnName(), $query->orders[0]['column']); - } - - public function testNewNestedSetQueryIncludesScopedColumns() { - $category = new Category; - $simpleQuery = $category->newNestedSetQuery()->getQuery(); - $this->assertEmpty($simpleQuery->wheres); - - $scopedCategory = new ScopedCategory; - $scopedQuery = $scopedCategory->newNestedSetQuery()->getQuery(); - $this->assertCount(1, $scopedQuery->wheres); - $this->assertEquals($scopedCategory->getScopedColumns(), array_map(function($elem) { - return $elem['column']; }, $scopedQuery->wheres)); - - $multiScopedCategory = new MultiScopedCategory; - $multiScopedQuery = $multiScopedCategory->newNestedSetQuery()->getQuery(); - $this->assertCount(2, $multiScopedQuery->wheres); - $this->assertEquals($multiScopedCategory->getScopedColumns(), array_map(function($elem) { - return $elem['column']; }, $multiScopedQuery->wheres)); - } - -} diff --git a/tests/suite/QueryBuilderExtensionTest.php b/tests/suite/QueryBuilderExtensionTest.php deleted file mode 100644 index 06aa2b89..00000000 --- a/tests/suite/QueryBuilderExtensionTest.php +++ /dev/null @@ -1,64 +0,0 @@ -getBuilder(); - - $builder->select('*')->from('users')->orderBy('email')->orderBy('age', 'desc')->reOrderBy('full_name', 'asc'); - - $this->assertEquals('select * from "users" order by "full_name" asc', $builder->toSql()); - } - - public function testAggregatesRemoveOrderBy() { - $builder = $this->getBuilder(); - $builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as aggregate from "users"', [], true)->andReturn([['aggregate' => 1]]); - $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function($builder, $results) { return $results; }); - $results = $builder->from('users')->orderBy('age', 'desc')->count(); - $this->assertEquals(1, $results); - - $builder = $this->getBuilder(); - - $builder->getConnection()->shouldReceive('select')->once()->with('select exists(select * from "users") as "exists"', [], true)->andReturn([['exists' => 1]]); - $results = $builder->from('users')->orderBy('age', 'desc')->exists(); - $this->assertTrue($results); - - $builder = $this->getBuilder(); - $builder->getConnection()->shouldReceive('select')->once()->with('select max("id") as aggregate from "users"', [], true)->andReturn([['aggregate' => 1]]); - $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function($builder, $results) { return $results; }); - $results = $builder->from('users')->orderBy('age', 'desc')->max('id'); - $this->assertEquals(1, $results); - - $builder = $this->getBuilder(); - $builder->getConnection()->shouldReceive('select')->once()->with('select min("id") as aggregate from "users"', [], true)->andReturn([['aggregate' => 1]]); - $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function($builder, $results) { return $results; }); - $results = $builder->from('users')->orderBy('age', 'desc')->min('id'); - $this->assertEquals(1, $results); - - $builder = $this->getBuilder(); - $builder->getConnection()->shouldReceive('select')->once()->with('select sum("id") as aggregate from "users"', [], true)->andReturn([['aggregate' => 1]]); - $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function($builder, $results) { return $results; }); - $results = $builder->from('users')->orderBy('age', 'desc')->sum('id'); - $this->assertEquals(1, $results); - } - -} diff --git a/tests/suite/RelationsTest.php b/tests/suite/RelationsTest.php new file mode 100644 index 00000000..00f1ffc4 --- /dev/null +++ b/tests/suite/RelationsTest.php @@ -0,0 +1,150 @@ +assertInstanceOf('Illuminate\Database\Eloquent\Relations\BelongsTo', $category->parent()); + } + + public function testParentRelationIsSelfReferential() + { + $category = new Category; + + $klass = get_class($category); + + $this->assertInstanceOf($klass, $category); + $this->assertInstanceOf($klass, $category->parent()->getRelated()); + $this->assertEquals($klass, get_class($category->parent()->getRelated())); + } + + public function testParentRelationRefersToCorrectField() + { + $category = new Category; + + if (method_exists($category->parent(), 'getForeignKeyName')) { + // For Laravel 5.6+ + $this->assertEquals($category->getParentColumnName(), $category->parent()->getForeignKeyName()); + $this->assertEquals($category->getQualifiedParentColumnName(), $category->parent()->getQualifiedForeignKeyName()); + } else { + $this->assertEquals($category->getParentColumnName(), $category->parent()->getForeignKey()); + $this->assertEquals($category->getQualifiedParentColumnName(), $category->parent()->getQualifiedForeignKey()); + } + } + + public function testParentRelation() + { + $this->assertEquals($this->categories('Child 2.1')->parent()->first(), $this->categories('Child 2')); + $this->assertEquals($this->categories('Child 2')->parent()->first(), $this->categories('Root 1')); + $this->assertNull($this->categories('Root 1')->parent()->first()); + } + + public function testChildrenRelationIsAHasMany() + { + $category = new Category; + + $this->assertInstanceOf('Illuminate\Database\Eloquent\Relations\HasMany', $category->children()); + } + + public function testChildrenRelationIsSelfReferential() + { + $category = new Category; + + $klass = get_class($category); + + $this->assertInstanceOf($klass, $category); + $this->assertInstanceOf($klass, $category->children()->getRelated()); + $this->assertEquals($klass, get_class($category->parent()->getRelated())); + } + + public function testChildrenRelationRefersToCorrectField() + { + $category = new Category; + + $this->assertEquals($category->getParentColumnName(), $category->children()->getForeignKeyName()); + + $this->assertEquals($category->getQualifiedParentColumnName(), $category->children()->getQualifiedForeignKeyName()); + } + + public function testChildrenRelation() + { + $root = $this->categories('Root 1'); + + foreach ($root->children() as $child) { + $this->assertEquals($root->getKey(), $child->getParentKey()); + } + + $expected = [$this->categories('Child 1'), $this->categories('Child 2'), $this->categories('Child 3')]; + + $this->assertEquals($expected, $root->children()->get()->all()); + + $this->assertEmpty($this->categories('Child 3')->children()->get()->all()); + } + + public function testChildrenRelationUsesDefaultOrdering() + { + $category = new Category; + + $query = $category->children()->getQuery()->toBase(); + + $expected = ['column' => $category->qualifyColumn('left'), 'direction' => 'asc']; + + $this->assertEquals($expected, $query->orders[0]); + } + + public function testChildrenRelationUsesCustomOrdering() + { + $category = new OrderedCategory; + + $query = $category->children()->getQuery()->toBase(); + + $expected = ['column' => $category->qualifyColumn('name'), 'direction' => 'asc']; + + $this->assertEquals($expected, $query->orders[0]); + } + + public function testChildrenRelationObeysDefaultOrdering() + { + $children = $this->categories('Root 1')->children()->get()->all(); + + $expected = [$this->categories('Child 1'), $this->categories('Child 2'), $this->categories('Child 3')]; + $this->assertEquals($expected, $children); + + // Swap 2 nodes & re-test + Category::query()->where('id', '=', 2)->update(['left' => 8, 'right' => 9]); + Category::query()->where('id', '=', 5)->update(['left' => 2, 'right' => 3]); + + $children = $this->categories('Root 1')->children()->get()->all(); + + $expected = [$this->categories('Child 3'), $this->categories('Child 2'), $this->categories('Child 1')]; + $this->assertEquals($expected, $children); + } + + public function testChildrenRelationObeysCustomOrdering() + { + with(new OrderedCategorySeeder)->run(); + + $children = OrderedCategory::find(1)->children()->get()->all(); + + $expected = [OrderedCategory::find(5), OrderedCategory::find(2), OrderedCategory::find(3)]; + $this->assertEquals($expected, $children); + } + + public function testChildrenRelationAllowsNodeCreation() + { + $child = new Category(['name' => 'Child 3.1']); + + $this->categories('Child 3')->children()->save($child); + + $this->assertTrue($child->exists); + $this->assertEquals($this->categories('Child 3')->getKey(), $child->getParentKey()); + } +} diff --git a/tests/suite/ScopingTest.php b/tests/suite/ScopingTest.php new file mode 100644 index 00000000..1b4e425d --- /dev/null +++ b/tests/suite/ScopingTest.php @@ -0,0 +1,346 @@ +run(); + } + + public function testInSameScope() + { + $root1 = $this->categories('Root 1', ScopedCategory::class); + $child1 = $this->categories('Child 1', ScopedCategory::class); + $child2 = $this->categories('Child 2', ScopedCategory::class); + + $root2 = $this->categories('Root 2', ScopedCategory::class); + $child4 = $this->categories('Child 4', ScopedCategory::class); + $child5 = $this->categories('Child 5', ScopedCategory::class); + + $this->assertTrue(MultiScopedCategory::isValidNestedSet()); + + $this->assertTrue($root1->inSameScope($child1)); + $this->assertTrue($child1->inSameScope($child2)); + $this->assertTrue($child2->inSameScope($root1)); + + $this->assertTrue($root2->inSameScope($child4)); + $this->assertTrue($child4->inSameScope($child5)); + $this->assertTrue($child5->inSameScope($root2)); + + $this->assertFalse($root1->inSameScope($root2)); + $this->assertFalse($root2->inSameScope($root1)); + + $this->assertFalse($child1->inSameScope($child4)); + $this->assertFalse($child4->inSameScope($child1)); + + $this->assertFalse($child2->inSameScope($child5)); + $this->assertFalse($child5->inSameScope($child2)); + } + + public function testInSameScopeMultiple() + { + $this->assertTrue(MultiScopedCategory::isValidNestedSet()); + + $child1 = $this->categories('Child 1', MultiScopedCategory::class); + $child2 = $this->categories('Child 2', MultiScopedCategory::class); + + $child4 = $this->categories('Child 4', MultiScopedCategory::class); + $child5 = $this->categories('Child 5', MultiScopedCategory::class); + + $enfant1 = $this->categories('Enfant 1', MultiScopedCategory::class); + $enfant2 = $this->categories('Enfant 2', MultiScopedCategory::class); + + $hijo1 = $this->categories('Hijo 1', MultiScopedCategory::class); + $hijo2 = $this->categorieS('Hijo 2', MultiScopedCategory::class); + + $this->assertTrue($child1->inSameScope($child2)); + $this->assertTrue($child4->inSameScope($child5)); + $this->assertTrue($enfant1->inSameScope($enfant2)); + $this->assertTrue($hijo1->inSameScope($hijo2)); + + $this->assertFalse($child2->inSameScope($child4)); + $this->assertFalse($child5->inSameScope($enfant1)); + $this->assertFalse($enfant2->inSameScope($hijo1)); + $this->assertFalse($hijo2->inSameScope($child1)); + } + + public function testIsSelfOrAncestorOf() + { + $root1 = $this->categories('Root 1', ScopedCategory::class); + $child21 = $this->categories('Child 2.1', ScopedCategory::class); + + $root2 = $this->categories('Root 2', ScopedCategory::class); + $child51 = $this->categories('Child 5.1', ScopedCategory::class); + + $this->assertTrue($root1->isSelfOrAncestorOf($child21)); + $this->assertTrue($root2->isSelfOrAncestorOf($child51)); + + $this->assertFalse($root1->isSelfOrAncestorOf($child51)); + $this->assertFalse($root2->isSelfOrAncestorOf($child21)); + } + + public function testIsSelfOrDescendantOf() + { + $root1 = $this->categories('Root 1', ScopedCategory::class); + $child21 = $this->categories('Child 2.1', ScopedCategory::class); + + $root2 = $this->categories('Root 2', ScopedCategory::class); + $child51 = $this->categories('Child 5.1', ScopedCategory::class); + + $this->assertTrue($child21->isSelfOrDescendantOf($root1)); + $this->assertTrue($child51->isSelfOrDescendantOf($root2)); + + $this->assertFalse($child21->isSelfOrDescendantOf($root2)); + $this->assertFalse($child51->isSelfOrDescendantOf($root1)); + } + + public function testGetSiblingsAndSelf() + { + $root2 = $this->categories('Root 2', ScopedCategory::class); + + $child1 = $this->categories('Child 1', ScopedCategory::class); + $child2 = $this->categories('Child 2', ScopedCategory::class); + $child3 = $this->categories('Child 3', ScopedCategory::class); + + $expected = [$root2]; + $this->assertEquals($expected, $root2->getSiblingsAndSelf()->all()); + + $expected = [$child1, $child2, $child3]; + $this->assertEquals($expected, $child2->getSiblingsAndSelf()->all()); + } + + public function testGetSiblingsAndSelfMultiple() + { + $root1 = $this->categories('Racine 1', MultiScopedCategory::class); + + $child1 = $this->categories('Hijo 1', MultiScopedCategory::class); + $child2 = $this->categories('Hijo 2', MultiScopedCategory::class); + $child3 = $this->categories('Hijo 3', MultiScopedCategory::class); + + $expected = [$root1]; + $this->assertEquals($expected, $root1->getSiblingsAndSelf()->all()); + + $expected = [$child1, $child2, $child3]; + $this->assertEquals($expected, $child3->getSiblingsAndSelf()->all()); + } + + public function testSimpleMovements() + { + with(new ScopedCategorySeeder)->run(); + + $this->assertTrue(ScopedCategory::isValidNestedSet()); + + $root3 = ScopedCategory::create(['name' => 'Root 3', 'company_id' => 2]); + $this->assertTrue(ScopedCategory::isValidNestedSet()); + + $this->categories('Child 6', ScopedCategory::class)->makeChildOf($root3); + $this->assertTrue(ScopedCategory::isValidNestedSet()); + + $root3->refresh(); + $expected = [$this->categories('Child 6', ScopedCategory::class)]; + $this->assertEquals($expected, $root3->children()->get()->all()); + } + + public function testSimpleSubtreeMovements() + { + with(new ScopedCategorySeeder)->run(); + + $this->assertTrue(ScopedCategory::isValidNestedSet()); + + $root3 = ScopedCategory::create(['name' => 'Root 3', 'company_id' => 2]); + $this->assertTrue(ScopedCategory::isValidNestedSet()); + + $this->categories('Child 5', ScopedCategory::class)->makeChildOf($root3); + $this->assertTrue(ScopedCategory::isValidNestedSet()); + + $root3->refresh(); + $expected = [ + $this->categories('Child 5', ScopedCategory::class), + $this->categories('Child 5.1', ScopedCategory::class) + ]; + $this->assertEquals($expected, $root3->getDescendants()->all()); + } + + public function testFullSubtreeMovements() + { + with(new ScopedCategorySeeder)->run(); + + $this->assertTrue(ScopedCategory::isValidNestedSet()); + + $root3 = ScopedCategory::create(['name' => 'Root 3', 'company_id' => 2]); + $this->assertTrue(ScopedCategory::isValidNestedSet()); + + $this->categories('Root 2', ScopedCategory::class)->makeChildOf($root3); + $this->assertTrue(ScopedCategory::isValidNestedSet()); + + $root3->refresh(); + $expected = [ + $this->categories('Root 2', ScopedCategory::class), + $this->categories('Child 4', ScopedCategory::class), + $this->categories('Child 5', ScopedCategory::class), + $this->categories('Child 5.1', ScopedCategory::class), + $this->categories('Child 6', ScopedCategory::class) + ]; + $this->assertEquals($expected, $root3->getDescendants()->all()); + } + + public function testSimpleMovementsMultiple() + { + $this->assertTrue(MultiScopedCategory::isValidNestedSet()); + + $root2 = MultiScopedCategory::create(['name' => 'Raiz 2', 'company_id' => 3, 'language' => 'es']); + $this->assertTrue(MultiScopedCategory::isValidNestedSet()); + + $this->categories('Hijo 1', MultiScopedCategory::class)->makeChildOf($root2); + $this->assertTrue(MultiScopedCategory::isValidNestedSet()); + + $root2->refresh(); + $expected = [$this->categories('Hijo 1', MultiScopedCategory::class)]; + $this->assertEquals($expected, $root2->children()->get()->all()); + } + + public function testSimpleSubtreeMovementsMultiple() + { + $this->assertTrue(MultiScopedCategory::isValidNestedSet()); + + $root2 = MultiScopedCategory::create(['name' => 'Raiz 2', 'company_id' => 3, 'language' => 'es']); + $this->assertTrue(MultiScopedCategory::isValidNestedSet()); + + $this->categories('Hijo 2', MultiScopedCategory::class)->makeChildOf($root2); + $this->assertTrue(MultiScopedCategory::isValidNestedSet()); + + $root2->refresh(); + $expected = [ + $this->categories('Hijo 2', MultiScopedCategory::class), + $this->categories('Hijo 2.1', MultiScopedCategory::class) + ]; + $this->assertEquals($expected, $root2->getDescendants()->all()); + } + + public function testFullSubtreeMovementsMultiple() + { + $this->assertTrue(MultiScopedCategory::isValidNestedSet()); + + $root2 = MultiScopedCategory::create(['name' => 'Raiz 2', 'company_id' => 3, 'language' => 'es']); + $this->assertTrue(MultiScopedCategory::isValidNestedSet()); + + $this->categories('Raiz 1', MultiScopedCategory::class)->makeChildOf($root2); + $this->assertTrue(MultiScopedCategory::isValidNestedSet()); + + $root2->refresh(); + $expected = [ + $this->categories('Raiz 1', MultiScopedCategory::class), + $this->categories('Hijo 1', MultiScopedCategory::class), + $this->categories('Hijo 2', MultiScopedCategory::class), + $this->categories('Hijo 2.1', MultiScopedCategory::class), + $this->categories('Hijo 3', MultiScopedCategory::class) + ]; + $this->assertEquals($expected, $root2->getDescendants()->all()); + } + + public function testToHierarchyNestsCorrectlyWithScopedOrder() + { + with(new OrderedScopedCategorySeeder)->run(); + + $expectedWhole1 = [ + 'Root 1' => [ + 'Child 1' => null, + 'Child 2' => [ + 'Child 2.1' => null + ], + 'Child 3' => null + ] + ]; + + $expectedWhole2 = [ + 'Root 2' => [ + 'Child 4' => null, + 'Child 5' => [ + 'Child 5.1' => null + ], + 'Child 6' => null + ] + ]; + + $this->assertEquals($expectedWhole1, hmap(OrderedScopedCategory::where('company_id', 1)->get()->toHierarchy()->toArray())); + $this->assertEquals($expectedWhole2, hmap(OrderedScopedCategory::where('company_id', 2)->get()->toHierarchy()->toArray())); + } + + /** + * @expectedException Baum\NestedSet\MoveNotPossibleException + */ + public function testNodesCannotMoveBetweenScopes() + { + $child4 = $this->categories('Child 4', ScopedCategory::class); + $root1 = $this->categories('Root 1', ScopedCategory::class); + + $child4->makeChildOf($root1); + } + + /** + * @expectedException Baum\NestedSet\MoveNotPossibleException + */ + public function testNodesCannotMoveBetweenScopesMultiple() + { + $root1 = $this->categories('Root 1', MultiScopedCategory::class); + $child4 = $this->categories('Child 4', MultiScopedCategory::class); + + $child4->makeChildOf($root1); + } + + /** + * @expectedException Baum\NestedSet\MoveNotPossibleException + */ + public function testNodesCannotMoveBetweenScopesMultiple2() + { + $root1 = $this->categories('Racine 1', MultiScopedCategory::class); + $child2 = $this->categories('Hijo 2', MultiScopedCategory::class); + + $child2->makeChildOf($root1); + } + + // TODO: Moving nodes between scopes is problematic ATM. Fix it or find a work-around. + public function testMoveNodeBetweenScopes() + { + $this->markTestSkipped(); + + // $root1 = Menu::create(array('caption' => 'TL1', 'site_id' => 1, 'language' => 'en')); + // $child11 = Menu::create(array('caption' => 'C11', 'site_id' => 1, 'language' => 'en')); + // $child12 = Menu::create(array('caption' => 'C12', 'site_id' => 1, 'language' => 'en')); + + // $this->assertTrue(Menu::isValidNestedSet()); + + // $child11->makeChildOf($root1); + // $child12->makeChildOf($root1); + + // $this->assertTrue(Menu::isValidNestedSet()); + + // $root2 = Menu::create(array('caption' => 'TL2', 'site_id' => 2, 'language' => 'en')); + // $child21 = Menu::create(array('caption' => 'C21', 'site_id' => 2, 'language' => 'en')); + // $child22 = Menu::create(array('caption' => 'C22', 'site_id' => 2, 'language' => 'en')); + // $child21->makeChildOf($root2); + // $child22->makeChildOf($root2); + + // $this->assertTrue(Menu::isValidNestedSet()); + + // $child11->update(array('site_id' => 2)); + // $child11->makeChildOf($root2); + + // $this->assertTrue(Menu::isValidNestedSet()); + + // $expected = array($this->menus('C12')); + // $this->assertEquals($expected, $root1->children()->get()->all()); + + // $expected = array($this->menus('C21'), $this->menus('C22'), $this->menus('C11')); + // $this->assertEquals($expected, $root2->children()->get()->all()); + } +} diff --git a/tests/suite/SoftDeletesTest.php b/tests/suite/SoftDeletesTest.php new file mode 100644 index 00000000..d7b79ca9 --- /dev/null +++ b/tests/suite/SoftDeletesTest.php @@ -0,0 +1,282 @@ +assertFalse(with(new Category)->hasSoftDeletes()); + $this->assertTrue(with(new SoftCategory())->hasSoftDeletes()); + } + + public function testReload() + { + $node = $this->categories('Child 3', SoftCategory::class); + + $this->assertTrue($node->exists); + $this->assertFalse($node->trashed()); + + $node->delete(); + + $this->assertTrue($node->trashed()); + + $node->refresh(); + + $this->assertTrue($node->trashed()); + $this->assertTrue($node->exists); + } + + public function testDeleteMaintainsTreeValid() + { + $this->assertTrue(SoftCategory::isValidNestedSet()); + + $child3 = $this->categories('Child 3', SoftCategory::class); + $child3->delete(); + + $this->assertTrue($child3->trashed()); + $this->assertTrue(SoftCategory::isValidNestedSet()); + } + + public function testDeleteMaintainsTreeValidWithSubtrees() + { + $this->assertTrue(SoftCategory::isValidNestedSet()); + + $child2 = $this->categories('Child 2', SoftCategory::class); + $child2->delete(); + $this->assertTrue(SoftCategory::isValidNestedSet()); + + $expected = [ + $this->categories('Child 1', SoftCategory::class), + $this->categories('Child 3', SoftCategory::class) + ]; + $this->assertEquals($expected, $this->categories('Root 1', SoftCategory::class)->getDescendants()->all()); + } + + public function testDeleteShiftsIndexes() + { + $this->assertTrue(SoftCategory::isValidNestedSet()); + + $this->categories('Child 1', SoftCategory::class)->delete(); + + $this->assertTrue(SoftCategory::isValidNestedSet()); + + $expected = [ + $this->categories('Child 2', SoftCategory::class), + $this->categories('Child 2.1', SoftCategory::class), + $this->categories('Child 3', SoftCategory::class) + ]; + $this->assertEquals($expected, $this->categories('Root 1', SoftCategory::class)->getDescendants()->all()); + + $this->assertEquals(1, $this->categories('Root 1', SoftCategory::class)->getLeft()); + $this->assertEquals(8, $this->categories('Root 1', SoftCategory::class)->getRight()); + + $this->assertEquals(2, $this->categories('Child 2', SoftCategory::class)->getLeft()); + $this->assertEquals(5, $this->categories('Child 2', SoftCategory::class)->getRight()); + + $this->assertEquals(3, $this->categories('Child 2.1', SoftCategory::class)->getLeft()); + $this->assertEquals(4, $this->categories('Child 2.1', SoftCategory::class)->getRight()); + + $this->assertEquals(6, $this->categories('Child 3', SoftCategory::class)->getLeft()); + $this->assertEquals(7, $this->categories('Child 3', SoftCategory::class)->getRight()); + } + + public function testDeleteShiftsIndexesSubtree() + { + $this->assertTrue(SoftCategory::isValidNestedSet()); + + $this->categories('Child 2', SoftCategory::class)->delete(); + + $this->assertTrue(SoftCategory::isValidNestedSet()); + + $expected = [ + $this->categories('Child 1', SoftCategory::class), + $this->categories('Child 3', SoftCategory::class) + ]; + $this->assertEquals($expected, $this->categories('Root 1', SoftCategory::class)->getDescendants()->all()); + + $this->assertEquals(1, $this->categories('Root 1', SoftCategory::class)->getLeft()); + $this->assertEquals(6, $this->categories('Root 1', SoftCategory::class)->getRight()); + + $this->assertEquals(2, $this->categories('Child 1', SoftCategory::class)->getLeft()); + $this->assertEquals(3, $this->categories('Child 1', SoftCategory::class)->getRight()); + + $this->assertEquals(4, $this->categories('Child 3', SoftCategory::class)->getLeft()); + $this->assertEquals(5, $this->categories('Child 3', SoftCategory::class)->getRight()); + } + + public function testDeleteShiftsIndexesFullSubtree() + { + $this->assertTrue(SoftCategory::isValidNestedSet()); + + $this->categories('Root 1', SoftCategory::class)->delete(); + $this->assertTrue(SoftCategory::isValidNestedSet()); + + $this->assertEmpty($this->categories('Root 2', SoftCategory::class)->getSiblings()->all()); + $this->assertEquals(1, $this->categories('Root 2', SoftCategory::class)->getLeft()); + $this->assertEquals(2, $this->categories('Root 2', SoftCategory::class)->getRight()); + } + + public function testRestoreMaintainsTreeValid() + { + $this->assertTrue(SoftCategory::isValidNestedSet()); + + $child3 = $this->categories('Child 3', SoftCategory::class); + $child3->delete(); + + $this->assertTrue($child3->trashed()); + $this->assertTrue(SoftCategory::isValidNestedSet()); + + $child3->refresh(); + $child3->restore(); + + $this->assertFalse($child3->trashed()); + $this->assertTrue(SoftCategory::isValidNestedSet()); + } + + public function testRestoreMaintainsTreeValidWithSubtrees() + { + $this->assertTrue(SoftCategory::isValidNestedSet()); + + $child2 = $this->categories('Child 2', SoftCategory::class); + $child2->delete(); + $this->assertTrue(SoftCategory::isValidNestedSet()); + + $child2->refresh(); + $child2->restore(); + + $this->assertTrue(SoftCategory::isValidNestedSet()); + + $expected = [ + $this->categories('Child 1', SoftCategory::class), + $this->categories('Child 2', SoftCategory::class), + $this->categories('Child 2.1', SoftCategory::class), + $this->categories('Child 3', SoftCategory::class) + ]; + $this->assertEquals($expected, $this->categories('Root 1', SoftCategory::class)->getDescendants()->all()); + } + + public function testRestoreUnshiftsIndexes() + { + $this->assertTrue(SoftCategory::isValidNestedSet()); + + $this->categories('Child 1', SoftCategory::class)->delete(); + + $this->assertTrue(SoftCategory::isValidNestedSet()); + + SoftCategory::withTrashed()->where('name', 'Child 1')->first()->restore(); + + $this->assertTrue(SoftCategory::isValidNestedSet()); + + $expected = [ + $this->categories('Child 1', SoftCategory::class), + $this->categories('Child 2', SoftCategory::class), + $this->categories('Child 2.1', SoftCategory::class), + $this->categories('Child 3', SoftCategory::class) + ]; + $this->assertEquals($expected, $this->categories('Root 1', SoftCategory::class)->getDescendants()->all()); + + $this->assertEquals(1, $this->categories('Root 1', SoftCategory::class)->getLeft()); + $this->assertEquals(10, $this->categories('Root 1', SoftCategory::class)->getRight()); + + $this->assertEquals(2, $this->categories('Child 1', SoftCategory::class)->getLeft()); + $this->assertEquals(3, $this->categories('Child 1', SoftCategory::class)->getRight()); + + $this->assertEquals(4, $this->categories('Child 2', SoftCategory::class)->getLeft()); + $this->assertEquals(7, $this->categories('Child 2', SoftCategory::class)->getRight()); + $this->assertEquals(5, $this->categories('Child 2.1', SoftCategory::class)->getLeft()); + $this->assertEquals(6, $this->categories('Child 2.1', SoftCategory::class)->getRight()); + + $this->assertEquals(8, $this->categories('Child 3', SoftCategory::class)->getLeft()); + $this->assertEquals(9, $this->categories('Child 3', SoftCategory::class)->getRight()); + } + + public function testRestoreUnshiftsIndexesSubtree() + { + $this->assertTrue(SoftCategory::isValidNestedSet()); + + $this->categories('Child 2', SoftCategory::class)->delete(); + + $this->assertTrue(SoftCategory::isValidNestedSet()); + + SoftCategory::withTrashed()->where('name', 'Child 2')->first()->restore(); + + $this->assertTrue(SoftCategory::isValidNestedSet()); + + $expected = [ + $this->categories('Child 1', SoftCategory::class), + $this->categories('Child 2', SoftCategory::class), + $this->categories('Child 2.1', SoftCategory::class), + $this->categories('Child 3', SoftCategory::class) + ]; + $this->assertEquals($expected, $this->categories('Root 1', SoftCategory::class)->getDescendants()->all()); + + $this->assertEquals(1, $this->categories('Root 1', SoftCategory::class)->getLeft()); + $this->assertEquals(10, $this->categories('Root 1', SoftCategory::class)->getRight()); + + $this->assertEquals(2, $this->categories('Child 1', SoftCategory::class)->getLeft()); + $this->assertEquals(3, $this->categories('Child 1', SoftCategory::class)->getRight()); + + $this->assertEquals(4, $this->categories('Child 2', SoftCategory::class)->getLeft()); + $this->assertEquals(7, $this->categories('Child 2', SoftCategory::class)->getRight()); + $this->assertEquals(5, $this->categories('Child 2.1', SoftCategory::class)->getLeft()); + $this->assertEquals(6, $this->categories('Child 2.1', SoftCategory::class)->getRight()); + + $this->assertEquals(8, $this->categories('Child 3', SoftCategory::class)->getLeft()); + $this->assertEquals(9, $this->categories('Child 3', SoftCategory::class)->getRight()); + } + + public function testRestoreUnshiftsIndexesFullSubtree() + { + $this->assertTrue(SoftCategory::isValidNestedSet()); + + $this->categories('Root 1', SoftCategory::class)->delete(); + + $this->assertTrue(SoftCategory::isValidNestedSet()); + + SoftCategory::withTrashed()->where('name', 'Root 1')->first()->restore(); + + $this->assertTrue(SoftCategory::isValidNestedSet()); + + $expected = [ + $this->categories('Child 1', SoftCategory::class), + $this->categories('Child 2', SoftCategory::class), + $this->categories('Child 2.1', SoftCategory::class), + $this->categories('Child 3', SoftCategory::class) + ]; + $this->assertEquals($expected, $this->categories('Root 1', SoftCategory::class)->getDescendants()->all()); + + $this->assertEquals(1, $this->categories('Root 1', SoftCategory::class)->getLeft()); + $this->assertEquals(10, $this->categories('Root 1', SoftCategory::class)->getRight()); + + $this->assertEquals(2, $this->categories('Child 1', SoftCategory::class)->getLeft()); + $this->assertEquals(3, $this->categories('Child 1', SoftCategory::class)->getRight()); + + $this->assertEquals(4, $this->categories('Child 2', SoftCategory::class)->getLeft()); + $this->assertEquals(7, $this->categories('Child 2', SoftCategory::class)->getRight()); + $this->assertEquals(5, $this->categories('Child 2.1', SoftCategory::class)->getLeft()); + $this->assertEquals(6, $this->categories('Child 2.1', SoftCategory::class)->getRight()); + + $this->assertEquals(8, $this->categories('Child 3', SoftCategory::class)->getLeft()); + $this->assertEquals(9, $this->categories('Child 3', SoftCategory::class)->getRight()); + } + + public function testAllStatic() + { + $expected = ['Root 1', 'Child 1', 'Child 2', 'Child 2.1', 'Child 3', 'Root 2']; + + $this->assertEquals($expected, SoftCategory::all()->pluck('name')->all()); + } + + public function testAllStaticWithSoftDeletes() + { + $this->categories('Child 1', SoftCategory::class)->delete(); + $this->categories('Child 3', SoftCategory::class)->delete(); + + $expected = ['Root 1', 'Child 2', 'Child 2.1', 'Root 2']; + $this->assertEquals($expected, SoftCategory::all()->pluck('name')->all()); + } +} diff --git a/tests/suite/Support/Migrators/CategoryMigrator.php b/tests/suite/Support/Migrators/CategoryMigrator.php new file mode 100644 index 00000000..3bc281bb --- /dev/null +++ b/tests/suite/Support/Migrators/CategoryMigrator.php @@ -0,0 +1,37 @@ +transaction(function () { + DB::schema()->dropIfExists('categories'); + + DB::schema()->create('categories', function ($t) { + $t->increments('id'); + + $t->nestedSet(); + + // $t->integer('parent_id')->unsigned()->nullable()->index(); + // $t->foreign('parent_id')->references('id')->on('categories'); + // $t->integer('left')->unsigned()->nullable()->index(); + // $t->integer('right')->unsigned()->nullable()->index(); + // $t->integer('depth')->unsigned()->nullable()->index(); + + $t->string('name')->index(); + + $t->integer('company_id')->unsigned()->nullable()->index(); + $t->string('language', 3)->nullable()->index(); + + $t->timestamp('created_at')->nullable(); + $t->timestamp('updated_at')->nullable(); + + $t->softDeletes(); + }); + }); + } +} diff --git a/tests/suite/Support/Migrators/ClusterMigrator.php b/tests/suite/Support/Migrators/ClusterMigrator.php new file mode 100644 index 00000000..89dde8a6 --- /dev/null +++ b/tests/suite/Support/Migrators/ClusterMigrator.php @@ -0,0 +1,38 @@ +transaction(function () { + DB::schema()->dropIfExists('clusters'); + + DB::schema()->create('clusters', function ($t) { + $t->string('id')->primary(); + + $t->string('parent_id')->nullable()->index(); + $t->integer('left')->unsigned()->nullable()->index(); + $t->integer('right')->unsigned()->nullable()->index(); + $t->integer('depth')->unsigned()->nullable()->index(); + + $t->string('name')->index(); + + $t->integer('company_id')->unsigned()->nullable()->index(); + $t->string('language', 3)->nullable()->index(); + + $t->timestamp('created_at')->nullable(); + $t->timestamp('updated_at')->nullable(); + + $t->softDeletes(); + }); + + DB::schema()->table('clusters', function ($t) { + $t->foreign('parent_id')->references('id')->on('clusters'); + }); + }); + } +} diff --git a/tests/suite/Support/Models/Category.php b/tests/suite/Support/Models/Category.php new file mode 100644 index 00000000..34f64bce --- /dev/null +++ b/tests/suite/Support/Models/Category.php @@ -0,0 +1,14 @@ +ensureUuid(); + }); + } + + public function ensureUuid() + { + if (is_null($this->getAttribute($this->getKeyName()))) { + $this->setAttribute($this->getKeyName(), $this->generateUuid()); + } + + return $this; + } + + protected function generateUuid() + { + return sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, + mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0xffff) + ); + } +} diff --git a/tests/suite/Support/Models/MultiScopedCategory.php b/tests/suite/Support/Models/MultiScopedCategory.php new file mode 100644 index 00000000..3deb2cb5 --- /dev/null +++ b/tests/suite/Support/Models/MultiScopedCategory.php @@ -0,0 +1,10 @@ +transaction(function () { + DB::table('categories')->delete(); + + DB::table('categories')->insert([ + ['id' => 1, 'name' => 'Root 1', 'left' => 1, 'right' => 10 , 'depth' => 0, 'parent_id' => null], + ['id' => 2, 'name' => 'Child 1', 'left' => 2, 'right' => 3, 'depth' => 1, 'parent_id' => 1], + ['id' => 3, 'name' => 'Child 2', 'left' => 4, 'right' => 7, 'depth' => 1, 'parent_id' => 1], + ['id' => 4, 'name' => 'Child 2.1', 'left' => 5, 'right' => 6, 'depth' => 2, 'parent_id' => 3], + ['id' => 5, 'name' => 'Child 3', 'left' => 8, 'right' => 9, 'depth' => 1, 'parent_id' => 1], + ['id' => 6, 'name' => 'Root 2', 'left' => 11, 'right' => 12, 'depth' => 0, 'parent_id' => null], + ]); + + if (DB::connection()->getDriverName() === 'pgsql') { + $tablePrefix = DB::connection()->getTablePrefix(); + + $sequenceName = $tablePrefix . 'categories_id_seq'; + + DB::connection()->statement('ALTER SEQUENCE ' . $sequenceName . ' RESTART WITH 7'); + } + }); + } + + public function nestUptoAt($node, $levels = 10, $attrs = []) + { + for ($i=$levels; $i > 0; $i--) { + $nested = Category::create(array_merge($attrs, ['name' => "{$node->name}.1"])); + + $nested->makeChildOf($node); + + $node = $nested->fresh(); + } + } +} diff --git a/tests/suite/Support/Seeders/ClusterSeeder.php b/tests/suite/Support/Seeders/ClusterSeeder.php new file mode 100644 index 00000000..dea088ca --- /dev/null +++ b/tests/suite/Support/Seeders/ClusterSeeder.php @@ -0,0 +1,36 @@ +transaction(function () { + DB::table('clusters')->delete(); + + DB::table('clusters')->insert([ + ['id' => '7461d8f5-2ea9-4788-99c4-9d0244f0bfb1', 'name' => 'Root 1' , 'left' => 1 , 'right' => 10 , 'depth' => 0, 'parent_id' => null], + ['id' => '5d7ce1fd-6151-46d3-a5b3-0ebb9988dc57', 'name' => 'Child 1' , 'left' => 2 , 'right' => 3 , 'depth' => 1, 'parent_id' => '7461d8f5-2ea9-4788-99c4-9d0244f0bfb1'], + ['id' => '07c1fc8c-53b5-4fe7-b9c4-e09f266a455c', 'name' => 'Child 2' , 'left' => 4 , 'right' => 7 , 'depth' => 1, 'parent_id' => '7461d8f5-2ea9-4788-99c4-9d0244f0bfb1'], + ['id' => '3315a297-af87-4ad3-9fa5-19785407573d', 'name' => 'Child 2.1', 'left' => 5 , 'right' => 6 , 'depth' => 2, 'parent_id' => '07c1fc8c-53b5-4fe7-b9c4-e09f266a455c'], + ['id' => '054476d2-6830-4014-a181-4de010ef7114', 'name' => 'Child 3' , 'left' => 8 , 'right' => 9 , 'depth' => 1, 'parent_id' => '7461d8f5-2ea9-4788-99c4-9d0244f0bfb1'], + ['id' => '3bb62314-9e1e-49c6-a5cb-17a9ab9b1b9a', 'name' => 'Root 2' , 'left' => 11 , 'right' => 12 , 'depth' => 0, 'parent_id' => null] + ]); + }); + } + + public function nestUptoAt($node, $levels = 10, $attrs = []) + { + for ($i=$levels; $i > 0; $i--) { + $nested = Cluster::create(array_merge($attrs, ['name' => "{$node->name}.1"])); + + $nested->makeChildOf($node); + + $node = $nested->fresh(); + } + } +} diff --git a/tests/suite/Support/Seeders/MultiScopedCategorySeeder.php b/tests/suite/Support/Seeders/MultiScopedCategorySeeder.php new file mode 100644 index 00000000..0c2f7b6c --- /dev/null +++ b/tests/suite/Support/Seeders/MultiScopedCategorySeeder.php @@ -0,0 +1,46 @@ +transaction(function () { + DB::table('categories')->delete(); + + DB::table('categories')->insert([ + ['id' => 1 , 'company_id' => 1, 'language' => 'en', 'name' => 'Root 1' , 'left' => 1 , 'right' => 10 , 'depth' => 0, 'parent_id' => null], + ['id' => 2 , 'company_id' => 1, 'language' => 'en', 'name' => 'Child 1' , 'left' => 2 , 'right' => 3 , 'depth' => 1, 'parent_id' => 1], + ['id' => 3 , 'company_id' => 1, 'language' => 'en', 'name' => 'Child 2' , 'left' => 4 , 'right' => 7 , 'depth' => 1, 'parent_id' => 1], + ['id' => 4 , 'company_id' => 1, 'language' => 'en', 'name' => 'Child 2.1' , 'left' => 5 , 'right' => 6 , 'depth' => 2, 'parent_id' => 3], + ['id' => 5 , 'company_id' => 1, 'language' => 'en', 'name' => 'Child 3' , 'left' => 8 , 'right' => 9 , 'depth' => 1, 'parent_id' => 1], + ['id' => 6 , 'company_id' => 2, 'language' => 'en', 'name' => 'Root 2' , 'left' => 1 , 'right' => 10 , 'depth' => 0, 'parent_id' => null], + ['id' => 7 , 'company_id' => 2, 'language' => 'en', 'name' => 'Child 4' , 'left' => 2 , 'right' => 3 , 'depth' => 1, 'parent_id' => 6], + ['id' => 8 , 'company_id' => 2, 'language' => 'en', 'name' => 'Child 5' , 'left' => 4 , 'right' => 7 , 'depth' => 1, 'parent_id' => 6], + ['id' => 9 , 'company_id' => 2, 'language' => 'en', 'name' => 'Child 5.1' , 'left' => 5 , 'right' => 6 , 'depth' => 2, 'parent_id' => 8], + ['id' => 10, 'company_id' => 2, 'language' => 'en', 'name' => 'Child 6' , 'left' => 8 , 'right' => 9 , 'depth' => 1, 'parent_id' => 6], + ['id' => 11, 'company_id' => 3, 'language' => 'fr', 'name' => 'Racine 1' , 'left' => 1 , 'right' => 10 , 'depth' => 0, 'parent_id' => null], + ['id' => 12, 'company_id' => 3, 'language' => 'fr', 'name' => 'Enfant 1' , 'left' => 2 , 'right' => 3 , 'depth' => 1, 'parent_id' => 11], + ['id' => 13, 'company_id' => 3, 'language' => 'fr', 'name' => 'Enfant 2' , 'left' => 4 , 'right' => 7 , 'depth' => 1, 'parent_id' => 11], + ['id' => 14, 'company_id' => 3, 'language' => 'fr', 'name' => 'Enfant 2.1' , 'left' => 5 , 'right' => 6 , 'depth' => 2, 'parent_id' => 13], + ['id' => 15, 'company_id' => 3, 'language' => 'fr', 'name' => 'Enfant 3' , 'left' => 8 , 'right' => 9 , 'depth' => 1, 'parent_id' => 11], + ['id' => 16, 'company_id' => 3, 'language' => 'es', 'name' => 'Raiz 1' , 'left' => 1 , 'right' => 10, 'depth' => 0, 'parent_id' => null], + ['id' => 17, 'company_id' => 3, 'language' => 'es', 'name' => 'Hijo 1' , 'left' => 2 , 'right' => 3 , 'depth' => 1, 'parent_id' => 16], + ['id' => 18, 'company_id' => 3, 'language' => 'es', 'name' => 'Hijo 2' , 'left' => 4 , 'right' => 7 , 'depth' => 1, 'parent_id' => 16], + ['id' => 19, 'company_id' => 3, 'language' => 'es', 'name' => 'Hijo 2.1' , 'left' => 5 , 'right' => 6 , 'depth' => 2, 'parent_id' => 18], + ['id' => 20, 'company_id' => 3, 'language' => 'es', 'name' => 'Hijo 3' , 'left' => 8 , 'right' => 9 , 'depth' => 1, 'parent_id' => 16], + ]); + + if (DB::connection()->getDriverName() === 'pgsql') { + $tablePrefix = DB::connection()->getTablePrefix(); + + $sequenceName = $tablePrefix . 'categories_id_seq'; + + DB::connection()->statement('ALTER SEQUENCE ' . $sequenceName . ' RESTART WITH 21'); + } + }); + } +} diff --git a/tests/suite/Support/Seeders/OrderedCategorySeeder.php b/tests/suite/Support/Seeders/OrderedCategorySeeder.php new file mode 100644 index 00000000..864428fd --- /dev/null +++ b/tests/suite/Support/Seeders/OrderedCategorySeeder.php @@ -0,0 +1,32 @@ +transaction(function () { + DB::table('categories')->delete(); + + DB::table('categories')->insert([ + ['id' => 1, 'name' => 'Root Z' , 'left' => 1 , 'right' => 10 , 'depth' => 0, 'parent_id' => null], + ['id' => 2, 'name' => 'Child C' , 'left' => 2 , 'right' => 3 , 'depth' => 1, 'parent_id' => 1], + ['id' => 3, 'name' => 'Child G' , 'left' => 4 , 'right' => 7 , 'depth' => 1, 'parent_id' => 1], + ['id' => 4, 'name' => 'Child G.1', 'left' => 5 , 'right' => 6 , 'depth' => 2, 'parent_id' => 3], + ['id' => 5, 'name' => 'Child A' , 'left' => 8 , 'right' => 9 , 'depth' => 1, 'parent_id' => 1], + ['id' => 6, 'name' => 'Root A' , 'left' => 11 , 'right' => 12 , 'depth' => 0, 'parent_id' => null], + ]); + + if (DB::connection()->getDriverName() === 'pgsql') { + $tablePrefix = DB::connection()->getTablePrefix(); + + $sequenceName = $tablePrefix . 'categories_id_seq'; + + DB::connection()->statement('ALTER SEQUENCE ' . $sequenceName . ' RESTART WITH 7'); + } + }); + } +} diff --git a/tests/suite/Support/Seeders/OrderedClusterSeeder.php b/tests/suite/Support/Seeders/OrderedClusterSeeder.php new file mode 100644 index 00000000..7a04a1f2 --- /dev/null +++ b/tests/suite/Support/Seeders/OrderedClusterSeeder.php @@ -0,0 +1,25 @@ +transaction(function () { + DB::table('clusters')->delete(); + + DB::table('clusters')->insert([ + ['id' => '7461d8f5-2ea9-4788-99c4-9d0244f0bfb1', 'name' => 'Root Z' , 'left' => 1 , 'right' => 10 , 'depth' => 0, 'parent_id' => null], + ['id' => '5d7ce1fd-6151-46d3-a5b3-0ebb9988dc57', 'name' => 'Child C' , 'left' => 2 , 'right' => 3 , 'depth' => 1, 'parent_id' => '7461d8f5-2ea9-4788-99c4-9d0244f0bfb1'], + ['id' => '07c1fc8c-53b5-4fe7-b9c4-e09f266a455c', 'name' => 'Child G' , 'left' => 4 , 'right' => 7 , 'depth' => 1, 'parent_id' => '7461d8f5-2ea9-4788-99c4-9d0244f0bfb1'], + ['id' => '3315a297-af87-4ad3-9fa5-19785407573d', 'name' => 'Child G.1', 'left' => 5 , 'right' => 6 , 'depth' => 2, 'parent_id' => '07c1fc8c-53b5-4fe7-b9c4-e09f266a455c'], + ['id' => '054476d2-6830-4014-a181-4de010ef7114', 'name' => 'Child A' , 'left' => 8 , 'right' => 9 , 'depth' => 1, 'parent_id' => '7461d8f5-2ea9-4788-99c4-9d0244f0bfb1'], + ['id' => '3bb62314-9e1e-49c6-a5cb-17a9ab9b1b9a', 'name' => 'Root A' , 'left' => 11 , 'right' => 12 , 'depth' => 0, 'parent_id' => null], + ]); + }); + } +} diff --git a/tests/suite/Support/Seeders/OrderedScopedCategorySeeder.php b/tests/suite/Support/Seeders/OrderedScopedCategorySeeder.php new file mode 100644 index 00000000..1905d5e1 --- /dev/null +++ b/tests/suite/Support/Seeders/OrderedScopedCategorySeeder.php @@ -0,0 +1,36 @@ +transaction(function () { + DB::table('categories')->delete(); + + DB::table('categories')->insert([ + ['id' => 1 , 'company_id' => 1, 'name' => 'Root 1' , 'left' => 1 , 'right' => 10 , 'depth' => 0, 'parent_id' => null], + ['id' => 2 , 'company_id' => 1, 'name' => 'Child 3' , 'left' => 8 , 'right' => 9 , 'depth' => 1, 'parent_id' => 1], + ['id' => 3 , 'company_id' => 1, 'name' => 'Child 2' , 'left' => 4 , 'right' => 7 , 'depth' => 1, 'parent_id' => 1], + ['id' => 4 , 'company_id' => 1, 'name' => 'Child 2.1', 'left' => 5 , 'right' => 6 , 'depth' => 2, 'parent_id' => 3], + ['id' => 5 , 'company_id' => 1, 'name' => 'Child 1' , 'left' => 2 , 'right' => 3 , 'depth' => 1, 'parent_id' => 1], + ['id' => 6 , 'company_id' => 2, 'name' => 'Root 2' , 'left' => 1 , 'right' => 10 , 'depth' => 0, 'parent_id' => null], + ['id' => 7 , 'company_id' => 2, 'name' => 'Child 4' , 'left' => 2 , 'right' => 3 , 'depth' => 1, 'parent_id' => 6], + ['id' => 8 , 'company_id' => 2, 'name' => 'Child 5' , 'left' => 4 , 'right' => 7 , 'depth' => 1, 'parent_id' => 6], + ['id' => 9 , 'company_id' => 2, 'name' => 'Child 5.1', 'left' => 5 , 'right' => 6 , 'depth' => 2, 'parent_id' => 8], + ['id' => 10, 'company_id' => 2, 'name' => 'Child 6' , 'left' => 8 , 'right' => 9 , 'depth' => 1, 'parent_id' => 6] + ]); + + if (DB::connection()->getDriverName() === 'pgsql') { + $tablePrefix = DB::connection()->getTablePrefix(); + + $sequenceName = $tablePrefix . 'categories_id_seq'; + + DB::connection()->statement('ALTER SEQUENCE ' . $sequenceName . ' RESTART WITH 11'); + } + }); + } +} diff --git a/tests/suite/Support/Seeders/ScopedCategorySeeder.php b/tests/suite/Support/Seeders/ScopedCategorySeeder.php new file mode 100644 index 00000000..837b2b23 --- /dev/null +++ b/tests/suite/Support/Seeders/ScopedCategorySeeder.php @@ -0,0 +1,37 @@ +transaction(function () { + DB::table('categories')->delete(); + + DB::table('categories')->insert([ + ['id' => 1 , 'company_id' => 1, 'name' => 'Root 1' , 'left' => 1 , 'right' => 10 , 'depth' => 0, 'parent_id' => null], + ['id' => 2 , 'company_id' => 1, 'name' => 'Child 1' , 'left' => 2 , 'right' => 3 , 'depth' => 1, 'parent_id' => 1], + ['id' => 3 , 'company_id' => 1, 'name' => 'Child 2' , 'left' => 4 , 'right' => 7 , 'depth' => 1, 'parent_id' => 1], + ['id' => 4 , 'company_id' => 1, 'name' => 'Child 2.1', 'left' => 5 , 'right' => 6 , 'depth' => 2, 'parent_id' => 3], + ['id' => 5 , 'company_id' => 1, 'name' => 'Child 3' , 'left' => 8 , 'right' => 9 , 'depth' => 1, 'parent_id' => 1], + ['id' => 6 , 'company_id' => 2, 'name' => 'Root 2' , 'left' => 1 , 'right' => 10 , 'depth' => 0, 'parent_id' => null], + ['id' => 7 , 'company_id' => 2, 'name' => 'Child 4' , 'left' => 2 , 'right' => 3 , 'depth' => 1, 'parent_id' => 6], + ['id' => 8 , 'company_id' => 2, 'name' => 'Child 5' , 'left' => 4 , 'right' => 7 , 'depth' => 1, 'parent_id' => 6], + ['id' => 9 , 'company_id' => 2, 'name' => 'Child 5.1', 'left' => 5 , 'right' => 6 , 'depth' => 2, 'parent_id' => 8], + ['id' => 10, 'company_id' => 2, 'name' => 'Child 6' , 'left' => 8 , 'right' => 9 , 'depth' => 1, 'parent_id' => 6] + ]); + + if (DB::connection()->getDriverName() === 'pgsql') { + $tablePrefix = DB::connection()->getTablePrefix(); + + $sequenceName = $tablePrefix . 'categories_id_seq'; + + DB::connection()->statement('ALTER SEQUENCE ' . $sequenceName . ' RESTART WITH 11'); + } + }); + } +} diff --git a/tests/suite/Support/helpers.php b/tests/suite/Support/helpers.php new file mode 100644 index 00000000..4383e13a --- /dev/null +++ b/tests/suite/Support/helpers.php @@ -0,0 +1,59 @@ + 0) { + $current['children'] = hmap($children, $preserve); + } + } + + $output[] = $current; + } + } + + return $output; + } +} + +if (!function_exists('array_ints_keys')) { + /** + * Cast provided keys's values into ints. This is to wrestle with PDO driver + * inconsistencies. + * + * @param array $input + * @param mixed $keys + * @return array + */ + function array_ints_keys(array $input, $keys = 'id') + { + $keys = is_string($keys) ? [$keys] : $keys; + + array_walk_recursive($input, function (&$value, $key) use ($keys) { + if (array_search($key, $keys) !== false) { + $value = (int) $value; + } + }); + + return $input; + } +} diff --git a/tests/suite/TestCase.php b/tests/suite/TestCase.php new file mode 100644 index 00000000..9d0f3ea8 --- /dev/null +++ b/tests/suite/TestCase.php @@ -0,0 +1,49 @@ +migrate(CategoryMigrator::class); + $instance->migrate(ClusterMigrator::class); + } + + public function setUp() + { + $this->seed(CategorySeeder::class); + $this->seed(ClusterSeeder::class); + } + + protected function findByName(string $name, $klass = Category::class) + { + $instance = new $klass; + + return $instance->newQueryWithoutScopes()->where('name', '=', $name)->first(); + } + + protected function categories($name, $klass = Category::class) + { + return $this->findByName($name, $klass); + } + + protected function clusters($name, $klass = Cluster::class) + { + return $this->findByName($name, $klass); + } +} diff --git a/tests/suite/TreeMapperTest.php b/tests/suite/TreeMapperTest.php new file mode 100644 index 00000000..3d5e88e2 --- /dev/null +++ b/tests/suite/TreeMapperTest.php @@ -0,0 +1,266 @@ +up(); + } + + public function testBuildTree() + { + $tree = [ + ['id' => 1, 'name' => 'A'], + ['id' => 2, 'name' => 'B'], + [ + 'id' => 3, 'name' => 'C', 'children' => [ + [ + 'id' => 4, 'name' => 'C.1', 'children' => [ + ['id' => 5, 'name' => 'C.1.1'], + ['id' => 6, 'name' => 'C.1.2'] + ] + ], + ['id' => 7, 'name' => 'C.2'], + ['id' => 8, 'name' => 'C.3'] + ] + ], + ['id' => 9, 'name' => 'D'] + ]; + + $this->assertTrue(Category::buildTree($tree)); + $this->assertTrue(Category::isValidNestedSet()); + + $hierarchy = Category::all()->toHierarchy()->toArray(); + $this->assertEquals($tree, array_ints_keys(hmap($hierarchy, ['id', 'name']))); + } + + public function testBuildTreePrunesAndInserts() + { + $tree = [ + ['id' => 1, 'name' => 'A'], + ['id' => 2, 'name' => 'B'], + [ + 'id' => 3, 'name' => 'C', 'children' => [ + [ + 'id' => 4, 'name' => 'C.1', 'children' => [ + ['id' => 5, 'name' => 'C.1.1'], + ['id' => 6, 'name' => 'C.1.2'] + ] + ], + ['id' => 7, 'name' => 'C.2'], + ['id' => 8, 'name' => 'C.3'] + ] + ], + ['id' => 9, 'name' => 'D'] + ]; + + $this->assertTrue(Category::buildTree($tree)); + $this->assertTrue(Category::isValidNestedSet()); + + // Postgres fix + if (DB::connection()->getDriverName() === 'pgsql') { + $tablePrefix = DB::connection()->getTablePrefix(); + + $sequenceName = $tablePrefix . 'categories_id_seq'; + + DB::connection()->statement('ALTER SEQUENCE ' . $sequenceName . ' RESTART WITH 10'); + } + + $updated = [ + ['id' => 1, 'name' => 'A'], + ['id' => 2, 'name' => 'B'], + [ + 'id' => 3, 'name' => 'C', 'children' => [ + [ + 'id' => 4, 'name' => 'C.1', 'children' => [ + ['id' => 5, 'name' => 'C.1.1'], + ['id' => 6, 'name' => 'C.1.2'] + ] + ], + [ + 'id' => 7, 'name' => 'C.2', 'children' => [ + ['name' => 'C.2.1'], + ['name' => 'C.2.2'] + ] + ] + ] + ], + ['id' => 9, 'name' => 'D'] + ]; + $this->assertTrue(Category::buildTree($updated)); + $this->assertTrue(Category::isValidNestedSet()); + + $expected = [ + ['id' => 1, 'name' => 'A'], + ['id' => 2, 'name' => 'B'], + [ + 'id' => 3, 'name' => 'C', 'children' => [ + [ + 'id' => 4, 'name' => 'C.1', 'children' => [ + ['id' => 5, 'name' => 'C.1.1'], + ['id' => 6, 'name' => 'C.1.2'] + ] + ], + [ + 'id' => 7, 'name' => 'C.2', 'children' => [ + ['id' => 10, 'name' => 'C.2.1'], + ['id' => 11, 'name' => 'C.2.2'] + ] + ] + ] + ], + ['id' => 9, 'name' => 'D'] + ]; + + $hierarchy = Category::all()->toHierarchy()->toArray(); + $this->assertEquals($expected, array_ints_keys(hmap($hierarchy, ['id', 'name']))); + } + + public function testMakeTree() + { + with(new CategorySeeder)->run(); + + $parent = Category::find(3); + + $subtree = [ + ['id' => 4, 'name' => 'Child 2.1'], + ['name' => 'Child 2.2'], + [ + 'name' => 'Child 2.3', 'children' => [ + [ + 'name' => 'Child 2.3.1', 'children' => [ + ['name' => 'Child 2.3.1.1'], + ['name' => 'Child 2.3.1.1'] + ] + ], + ['name' => 'Child 2.3.2'], + ['name' => 'Child 2.3.3'] + ] + ], + ['name' => 'Child 2.4'] + ]; + + $this->assertTrue($parent->makeTree($subtree)); + $this->assertTrue(Category::isValidNestedSet()); + + $expected = [ + ['id' => 4, 'name' => 'Child 2.1'], + ['id' => 7, 'name' => 'Child 2.2'], + [ + 'id' => 8, 'name' => 'Child 2.3', 'children' => [ + [ + 'id' => 9, 'name' => 'Child 2.3.1', 'children' => [ + ['id' => 10, 'name' => 'Child 2.3.1.1'], + ['id' => 11, 'name' => 'Child 2.3.1.1'] + ] + ], + ['id' => 12, 'name' => 'Child 2.3.2'], + ['id' => 13, 'name' => 'Child 2.3.3'] + ] + ], + ['id' => 14, 'name' => 'Child 2.4'] + ]; + + $hierarchy = $parent->refresh()->getDescendants()->toHierarchy()->toArray(); + $this->assertEquals($expected, array_ints_keys(hmap($hierarchy, ['id', 'name']))); + } + + public function testMakeTreePrunesAndInserts() + { + with(new CategorySeeder)->run(); + + $parent = Category::find(3); + + $subtree = [ + ['id' => 4, 'name' => 'Child 2.1'], + ['name' => 'Child 2.2'], + [ + 'name' => 'Child 2.3', 'children' => [ + [ + 'name' => 'Child 2.3.1', 'children' => [ + ['name' => 'Child 2.3.1.1'], + ['name' => 'Child 2.3.1.1'] + ] + ], + ['name' => 'Child 2.3.2'], + ['name' => 'Child 2.3.3'] + ] + ], + ['name' => 'Child 2.4'] + ]; + + $this->assertTrue($parent->makeTree($subtree)); + $this->assertTrue(Category::isValidNestedSet()); + + $expected = [ + ['id' => 4, 'name' => 'Child 2.1'], + ['id' => 7, 'name' => 'Child 2.2'], + [ + 'id' => 8, 'name' => 'Child 2.3', 'children' => [ + [ + 'id' => 9, 'name' => 'Child 2.3.1', 'children' => [ + ['id' => 10, 'name' => 'Child 2.3.1.1'], + ['id' => 11, 'name' => 'Child 2.3.1.1'] + ] + ], + ['id' => 12, 'name' => 'Child 2.3.2'], + ['id' => 13, 'name' => 'Child 2.3.3'] + ] + ], + ['id' => 14, 'name' => 'Child 2.4'] + ]; + + $hierarchy = $parent->refresh()->getDescendants()->toHierarchy()->toArray(); + $this->assertEquals($expected, array_ints_keys(hmap($hierarchy, ['id', 'name']))); + + $modified = [ + ['id' => 7, 'name' => 'Child 2.2'], + ['id' => 8, 'name' => 'Child 2.3'], + ['id' => 14, 'name' => 'Child 2.4'], + [ + 'name' => 'Child 2.5', 'children' => [ + [ + 'name' => 'Child 2.5.1', 'children' => [ + ['name' => 'Child 2.5.1.1'], + ['name' => 'Child 2.5.1.1'] + ] + ], + ['name' => 'Child 2.5.2'], + ['name' => 'Child 2.5.3'] + ] + ] + ]; + + $this->assertTrue($parent->makeTree($modified)); + $this->assertTrue(Category::isValidNestedSet()); + + $expected = [ + ['id' => 7 , 'name' => 'Child 2.2'], + ['id' => 8 , 'name' => 'Child 2.3'], + ['id' => 14, 'name' => 'Child 2.4'], + [ + 'id' => 15, 'name' => 'Child 2.5', 'children' => [ + [ + 'id' => 16, 'name' => 'Child 2.5.1', 'children' => [ + ['id' => 17, 'name' => 'Child 2.5.1.1'], + ['id' => 18, 'name' => 'Child 2.5.1.1'] + ] + ], + ['id' => 19, 'name' => 'Child 2.5.2'], + ['id' => 20, 'name' => 'Child 2.5.3'] + ] + ] + ]; + + $hierarchy = $parent->refresh()->getDescendants()->toHierarchy()->toArray(); + $this->assertEquals($expected, array_ints_keys(hmap($hierarchy, ['id', 'name']))); + } +} diff --git a/tests/suite/TreeRebuildingTest.php b/tests/suite/TreeRebuildingTest.php new file mode 100644 index 00000000..4cac8e1a --- /dev/null +++ b/tests/suite/TreeRebuildingTest.php @@ -0,0 +1,109 @@ +assertTrue(Category::isValidNestedSet()); + + $root = Category::root(); + Category::query()->update(['left' => null, 'right' => null]); + $this->assertFalse(Category::isValidNestedSet()); + + Category::rebuild(); + $this->assertTrue(Category::isValidNestedSet()); + + $this->assertEquals($root, Category::root()); + } + + public function testRebuildPresevesRootNodes() + { + $root1 = Category::create(['name' => 'Test Root 1']); + $root2 = Category::create(['name' => 'Test Root 2']); + $root3 = Category::create(['name' => 'Test Root 3']); + + $root2->makeChildOf($root1); + $root3->makeChildOf($root1); + + $lastRoot = Category::roots()->reOrderBy($root1->getLeftColumnName(), 'desc')->first(); + + Category::query()->update(['left' => null, 'right' => null]); + Category::rebuild(); + + $this->assertEquals($lastRoot, Category::roots()->reOrderBy($root1->getLeftColumnName(), 'desc')->first()); + } + + public function testRebuildRecomputesDepth() + { + $this->assertTrue(Category::isValidNestedSet()); + + Category::query()->update(['left' => null, 'right' => null, 'depth' => 0]); + $this->assertFalse(Category::isValidNestedSet()); + + Category::rebuild(); + + $expected = [0, 1, 1, 2, 1, 0]; + $this->assertEquals($expected, Category::all()->map(function ($n) { + return $n->getDepth(); + })->all()); + } + + public function testRebuildWithScope() + { + MultiScopedCategory::query()->delete(); + + $root = MultiScopedCategory::create(['name' => 'A' , 'company_id' => 721, 'language' => 'es']); + $child1 = MultiScopedCategory::create(['name' => 'A.1' , 'company_id' => 721, 'language' => 'es']); + $child2 = MultiscopedCategory::create(['name' => 'A.2' , 'company_id' => 721, 'language' => 'es']); + + $child1->makeChildOf($root); + $child2->makeChildOf($root); + + MultiscopedCategory::query()->update(['left' => null, 'right' => null]); + $this->assertFalse(MultiscopedCategory::isValidNestedSet()); + + MultiscopedCategory::rebuild(); + $this->assertTrue(MultiscopedCategory::isValidNestedSet()); + + $this->assertEquals($root->getAttributes(), $this->categories('A', MultiScopedCategory::class)->getAttributes()); + + $expected = [$child1->getAttributes(), $child2->getAttributes()]; + $actual = array_map(function ($ch) { + return $ch->getAttributes(); + }, $this->categories('A', MultiScopedCategory::class)->children()->get()->all()); + + $this->assertEquals($expected, $actual); + } + + public function testRebuildWithMultipleScopes() + { + MultiScopedCategory::query()->delete(); + + $root1 = MultiScopedCategory::create(['name' => 'TL1', 'company_id' => 1, 'language' => 'en']); + $child11 = MultiScopedCategory::create(['name' => 'C11', 'company_id' => 1, 'language' => 'en']); + $child12 = MultiScopedCategory::create(['name' => 'C12', 'company_id' => 1, 'language' => 'en']); + $child11->makeChildOf($root1); + $child12->makeChildOf($root1); + + $root2 = MultiScopedCategory::create(['name' => 'TL2', 'company_id' => 2, 'language' => 'en']); + $child21 = MultiScopedCategory::create(['name' => 'C21', 'company_id' => 2, 'language' => 'en']); + $child22 = MultiScopedCategory::create(['name' => 'C22', 'company_id' => 2, 'language' => 'en']); + $child21->makeChildOf($root2); + $child22->makeChildOf($root2); + + $this->assertTrue(MultiScopedCategory::isValidNestedSet()); + + $tree = MultiScopedCategory::query()->orderBy($root1->getKeyName())->get()->all(); + + MultiScopedCategory::query()->update(['left' => null, 'right' => null]); + MultiScopedCategory::rebuild(); + + $this->assertTrue(MultiScopedCategory::isValidNestedSet()); + $this->assertEquals($tree, MultiScopedCategory::query()->orderBy($root1->getKeyName())->get()->all()); + } +} diff --git a/tests/suite/TreeValidationTest.php b/tests/suite/TreeValidationTest.php new file mode 100644 index 00000000..2994fafd --- /dev/null +++ b/tests/suite/TreeValidationTest.php @@ -0,0 +1,96 @@ +assertTrue(Category::isValidNestedSet()); + + Category::query()->update(['left' => null]); + $this->assertFalse(Category::isValidNestedSet()); + } + + public function testTreeIsNotValidWithNullRights() + { + $this->assertTrue(Category::isValidNestedSet()); + + Category::query()->update(['right' => null]); + $this->assertFalse(Category::isValidNestedSet()); + } + + public function testTreeIsNotValidWhenRightsEqualLefts() + { + $this->assertTrue(Category::isValidNestedSet()); + + $child2 = $this->categories('Child 2'); + $child2->right = $child2->left; + $child2->save(); + + $this->assertFalse(Category::isValidNestedSet()); + } + + public function testTreeIsNotValidWhenLeftEqualsParent() + { + $this->assertTrue(Category::isValidNestedSet()); + + $child2 = $this->categories('Child 2'); + $child2->left = $this->categories('Root 1')->getLeft(); + $child2->save(); + + $this->assertFalse(Category::isValidNestedSet()); + } + + public function testTreeIsNotValidWhenRightEqualsParent() + { + $this->assertTrue(Category::isValidNestedSet()); + + $child2 = $this->categories('Child 2'); + $child2->right = $this->categories('Root 1')->getRight(); + $child2->save(); + + $this->assertFalse(Category::isValidNestedSet()); + } + + public function testTreeIsValidWithMissingMiddleNode() + { + $this->assertTrue(Category::isValidNestedSet()); + + Category::query()->delete($this->categories('Child 2')->getKey()); + $this->assertTrue(Category::isValidNestedSet()); + } + + public function testTreeIsNotValidWithOverlappingRoots() + { + $this->assertTrue(Category::isValidNestedSet()); + + // Force Root 2 to overlap with Root 1 + $root = $this->categories('Root 2'); + $root->left = 0; + $root->save(); + + $this->assertFalse(Category::isValidNestedSet()); + } + + public function testNodeDeletionDoesNotMakeTreeInvalid() + { + $this->assertTrue(Category::isValidNestedSet()); + + $this->categories('Root 2')->delete(); + $this->assertTrue(Category::isValidNestedSet()); + + $this->categories('Child 1')->delete(); + $this->assertTrue(Category::isValidNestedSet()); + } + + public function testNodeDeletionWithSubtreeDoesNotMakeTreeInvalid() + { + $this->assertTrue(Category::isValidNestedSet()); + + $this->categories('Child 2')->delete(); + $this->assertTrue(Category::isValidNestedSet()); + } +} diff --git a/tests/suite/support.php b/tests/suite/support.php deleted file mode 100644 index 54c7777f..00000000 --- a/tests/suite/support.php +++ /dev/null @@ -1,59 +0,0 @@ - 0 ) - $current['children'] = hmap($children, $preserve); - } - - $output[] = $current; - } - } - - return $output; - } - -} - -if ( !function_exists('array_ints_keys') ) { - - /** - * Cast provided keys's values into ints. This is to wrestle with PDO driver - * inconsistencies. - * - * @param array $input - * @param mixed $keys - * @return array - */ - function array_ints_keys(array $input, $keys='id') { - $keys = is_string($keys) ? array($keys) : $keys; - - array_walk_recursive($input, function(&$value, $key) use ($keys) { - if ( array_search($key, $keys) !== false ) - $value = (int) $value; - }); - - return $input; - } - -} From 89310497124809aae23f3915cb375174235df571 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Wed, 8 May 2019 16:52:42 +0200 Subject: [PATCH 12/62] Use class based array helpers. --- src/NestedSet/Concerns/HasEvents.php | 4 ++-- src/NestedSet/Mapper.php | 6 +++--- tests/suite/Support/helpers.php | 4 +++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/NestedSet/Concerns/HasEvents.php b/src/NestedSet/Concerns/HasEvents.php index e9bc11ec..23b228dc 100644 --- a/src/NestedSet/Concerns/HasEvents.php +++ b/src/NestedSet/Concerns/HasEvents.php @@ -7,7 +7,7 @@ trait HasEvents { /** - * Boot the soft deleting trait for a model. + * Boot the HasEvents trait for a model. * * @return void */ @@ -17,7 +17,7 @@ public static function bootHasEvents() } /** - * Initialize the soft deleting trait for an instance. + * Initialize the HasEvents trait for an instance. * * @return void */ diff --git a/src/NestedSet/Mapper.php b/src/NestedSet/Mapper.php index 086917f2..618ba095 100644 --- a/src/NestedSet/Mapper.php +++ b/src/NestedSet/Mapper.php @@ -4,7 +4,7 @@ use Closure; use Illuminate\Support\Contracts\ArrayableInterface; -use Baum\Node; +use Illuminate\Support\Arr; class Mapper { @@ -141,14 +141,14 @@ protected function getSearchAttributes($attributes) { $searchable = [$this->node->getKeyName()]; - return array_only($attributes, $searchable); + return Arr::only($attributes, $searchable); } protected function getDataAttributes($attributes) { $exceptions = [$this->node->getKeyName(), $this->getChildrenKeyName()]; - return array_except($attributes, $exceptions); + return Arr::except($attributes, $exceptions); } protected function firstOrNew($attributes) diff --git a/tests/suite/Support/helpers.php b/tests/suite/Support/helpers.php index 4383e13a..d6920cd1 100644 --- a/tests/suite/Support/helpers.php +++ b/tests/suite/Support/helpers.php @@ -1,5 +1,7 @@ Date: Wed, 8 May 2019 16:53:41 +0200 Subject: [PATCH 13/62] Change test models to use trait interface. --- tests/suite/Support/Models/Category.php | 7 +++++-- tests/suite/Support/Models/Cluster.php | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/suite/Support/Models/Category.php b/tests/suite/Support/Models/Category.php index 34f64bce..440ff9f8 100644 --- a/tests/suite/Support/Models/Category.php +++ b/tests/suite/Support/Models/Category.php @@ -2,10 +2,13 @@ namespace Baum\Tests\Support\Models; -use Baum\Node; +use Illuminate\Database\Eloquent\Model; +use Baum\NestedSet\Node; -class Category extends Node +class Category extends Model { + use Node; + protected $table = 'categories'; protected $fillable = ['name']; diff --git a/tests/suite/Support/Models/Cluster.php b/tests/suite/Support/Models/Cluster.php index f9d260a6..eb0ebf7a 100644 --- a/tests/suite/Support/Models/Cluster.php +++ b/tests/suite/Support/Models/Cluster.php @@ -2,10 +2,13 @@ namespace Baum\Tests\Support\Models; -use Baum\Node; +use Illuminate\Database\Eloquent\Model; +use Baum\NestedSet\Node; -class Cluster extends Node +class Cluster extends Model { + use Node; + protected $table = 'clusters'; protected $fillable = ['name']; From bc7e84199118ac623da61fb4eb897ffef9ba9985 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Wed, 8 May 2019 16:55:10 +0200 Subject: [PATCH 14/62] Qualify column names on delete descendants query. --- src/NestedSet/Node.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/NestedSet/Node.php b/src/NestedSet/Node.php index a2203a2e..5baaa86b 100644 --- a/src/NestedSet/Node.php +++ b/src/NestedSet/Node.php @@ -2,8 +2,6 @@ namespace Baum\NestedSet; -use Illuminate\Database\Eloquent\SoftDeletingScope; - /** * Node * @@ -227,22 +225,24 @@ public function destroyDescendants() $this->getConnection()->transaction(function () { $this->refresh(); - $lftCol = $this->getLeftColumnName(); - $rgtCol = $this->getRightColumnName(); - $lft = $this->getLeft(); - $rgt = $this->getRight(); + $lftCol = $this->getLeftColumnName(); + $rgtCol = $this->getRightColumnName(); + $lft = $this->getLeft(); + $rgt = $this->getRight(); + $qualifiedLftCol = $this->getQualifiedLeftColumnName(); + $qualifiedRgtCol = $this->getQualifiedRightColumnName(); // Apply a lock to the rows which fall past the deletion point - $this->newQuery()->where($lftCol, '>=', $lft)->select($this->getKeyName())->lockForUpdate()->get(); + $this->newQuery()->where($qualifiedLftCol, '>=', $lft)->select($this->getQualifiedKeyName())->lockForUpdate()->get(); // Prune children - $this->newQuery()->where($lftCol, '>', $lft)->where($rgtCol, '<', $rgt)->delete(); + $this->newQuery()->where($qualifiedLftCol, '>', $lft)->where($qualifiedRgtCol, '<', $rgt)->delete(); // Update left and right indexes for the remaining nodes $diff = $rgt - $lft + 1; - $this->newQuery()->where($lftCol, '>', $rgt)->decrement($lftCol, $diff); - $this->newQuery()->where($rgtCol, '>', $rgt)->decrement($rgtCol, $diff); + $this->newQuery()->where($qualifiedLftCol, '>', $rgt)->decrement($lftCol, $diff); + $this->newQuery()->where($qualifiedRgtCol, '>', $rgt)->decrement($rgtCol, $diff); }); } From d33e8d67c43f70d296aa491ded5d3a1e87162967 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Wed, 8 May 2019 16:57:15 +0200 Subject: [PATCH 15/62] Improve target node resolving logic on move operation. --- src/NestedSet/Move.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/NestedSet/Move.php b/src/NestedSet/Move.php index 0069a35f..5bd5f0da 100644 --- a/src/NestedSet/Move.php +++ b/src/NestedSet/Move.php @@ -2,7 +2,7 @@ namespace Baum\NestedSet; -use \Illuminate\Events\Dispatcher; +use Illuminate\Events\Dispatcher; /** * Move @@ -189,11 +189,21 @@ public function updateStructure() */ protected function resolveNode($node) { - if ($node instanceof \Baum\Node) { + if (method_exists($node, 'refresh') && is_callable([$node, 'refresh'])) { return $node->refresh(); } - return $this->node->newQuery()->find($node); + if ($node instanceof \Illuminate\Database\Eloquent\Model) { + return $node->newQuery()->find($node->getKey()); + } + + $key = (int) $node; + + if ($key !== 0) { + return $this->node->newQuery()->find($node); + } + + throw new MoveNotPossibleException('Could not resolve target node.'); } /** From f61672aacddb9083f7a3f24e1329e47928bb349f Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Wed, 8 May 2019 16:57:45 +0200 Subject: [PATCH 16/62] Update dependencies. --- composer.json | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index f1b3e8b2..fc15fcaf 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "baum/baum", "type": "library", "description": "Baum is an implementation of the Nested Set pattern for Eloquent models.", - "keywords": ["nested set", "laravel", "laravel 4", "laravel 5", "eloquent", "database"], + "keywords": ["nested set", "laravel", "laravel 5", "eloquent", "database"], "license": "MIT", "authors": [ { @@ -12,15 +12,14 @@ } ], "require": { - "php": ">=7.0.0", - "illuminate/database": "~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0|~5.9.0", - "illuminate/events": "~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0|~5.9.0", - "illuminate/support": "~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0|~5.9.0" + "php": "^7.1.3", + "illuminate/database": "~5.7.0|~5.8.0|~5.9.0", + "illuminate/events": "~5.7.0|~5.8.0|~5.9.0", + "illuminate/support": "~5.7.0|~5.8.0|~5.9.0" }, "require-dev": { - "doctrine/dbal": "~2.9.2", "mockery/mockery": "^1.0", - "phpunit/phpunit": "~6.5.14|^7.0", + "phpunit/phpunit": "^7.5", "psy/psysh": "@stable", "squizlabs/php_codesniffer": "^3.4" }, From ad9bb89dc3cf45df1d1b013ae652b0f5cf4f7b6e Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Wed, 8 May 2019 16:57:54 +0200 Subject: [PATCH 17/62] Update travis config. --- .travis.yml | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/.travis.yml b/.travis.yml index 341f0200..d668fd94 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,35 +1,27 @@ language: php +sudo: false + php: -- '7.0' -- '7.1' -- '7.2' -- '7.3' + - '7.1' + - '7.2' + - '7.3' env: -- LARAVEL=5.4.* -- LARAVEL=5.5.* -- LARAVEL=5.6.* -- LARAVEL=5.7.* -- LARAVEL=5.8.* -- LARAVEL=5.9.* + matrix: + - LARAVEL=5.7.* + - LARAVEL=5.8.* + - LARAVEL=5.9.* matrix: - exclude: - - php: '7.0' - env: LARAVEL=5.6.* - - php: '7.0' - env: LARAVEL=5.7.* - - php: '7.0' - env: LARAVEL=5.8.* - - php: '7.0' - env: LARAVEL=5.9.* fast_finish: true -sudo: false +before_install: + - travis_retry composer self-update install: - - composer require "laravel/framework=${LARAVEL}" --dev --prefer-dist --no-interaction --no-suggest + - travis_retry composer require "illuminate/framework=${LARAVEL}" --dev --prefer-dist --no-interaction --no-suggest - travis_retry composer update --prefer-dist --no-interaction --prefer-stable --no-suggest -script: vendor/bin/phpunit --verbose +script: + - vendor/bin/phpunit --verbose From 749d635d27f639b98f75af12569ee29391b39189 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Wed, 8 May 2019 17:08:55 +0200 Subject: [PATCH 18/62] More travis changes. --- .travis.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index d668fd94..578072c6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +dist: xenial language: php sudo: false @@ -11,17 +12,16 @@ env: matrix: - LARAVEL=5.7.* - LARAVEL=5.8.* - - LARAVEL=5.9.* + - LARAVEL=dev-master matrix: fast_finish: true - -before_install: - - travis_retry composer self-update + exclude: + - php: '7.1' + env: LARAVEL=dev-master install: - - travis_retry composer require "illuminate/framework=${LARAVEL}" --dev --prefer-dist --no-interaction --no-suggest - - travis_retry composer update --prefer-dist --no-interaction --prefer-stable --no-suggest + - travis_retry composer require "illuminate/database=${LARAVEL}" --dev --prefer-dist --no-interaction --no-suggest script: - vendor/bin/phpunit --verbose From bf558418d99991dc71b47af4c238071507a23ad1 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Wed, 8 May 2019 17:52:45 +0200 Subject: [PATCH 19/62] Migrate to phpunit 8. --- .gitignore | 1 + composer.json | 2 +- tests/suite/CustomEventsTest.php | 2 +- tests/suite/MovementTest.php | 92 +++++++++++++------------------- tests/suite/ScopingTest.php | 18 +++---- tests/suite/TestCase.php | 4 +- tests/suite/TreeMapperTest.php | 2 +- 7 files changed, 52 insertions(+), 69 deletions(-) diff --git a/.gitignore b/.gitignore index a439bd57..826d2ac0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /vendor composer.lock .vscode +.phpunit.result.cache diff --git a/composer.json b/composer.json index fc15fcaf..779a3a68 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ }, "require-dev": { "mockery/mockery": "^1.0", - "phpunit/phpunit": "^7.5", + "phpunit/phpunit": "^7.5|^8.0", "psy/psysh": "@stable", "squizlabs/php_codesniffer": "^3.4" }, diff --git a/tests/suite/CustomEventsTest.php b/tests/suite/CustomEventsTest.php index a3f5f762..07969473 100644 --- a/tests/suite/CustomEventsTest.php +++ b/tests/suite/CustomEventsTest.php @@ -8,7 +8,7 @@ class CustomEventsTest extends TestCase { - public function tearDown() + public function tearDown(): void { m::close(); } diff --git a/tests/suite/MovementTest.php b/tests/suite/MovementTest.php index 6c9306a9..70fb5290 100644 --- a/tests/suite/MovementTest.php +++ b/tests/suite/MovementTest.php @@ -2,6 +2,8 @@ namespace Baum\Tests; +use Baum\NestedSet\MoveNotPossibleException; + use Baum\Tests\Support\Models\Category; use Baum\Tests\Support\Models\Cluster; @@ -18,11 +20,10 @@ public function testMoveLeft() $this->assertTrue(Category::isValidNestedSet()); } - /** - * @expectedException Baum\NestedSet\MoveNotPossibleException - */ public function testMoveLeftRaisesAnExceptionWhenNotPossible() { + $this->expectException(MoveNotPossibleException::class); + $node = $this->categories('Child 2'); $node->moveLeft(); @@ -66,11 +67,10 @@ public function testMoveToLeftOf() $this->assertTrue(Category::isValidNestedSet()); } - /** - * @expectedException Baum\NestedSet\MoveNotPossibleException - */ public function testMoveToLeftOfRaisesAnExceptionWhenNotPossible() { + $this->expectException(MoveNotPossibleException::class); + $this->categories('Child 1')->moveToLeftOf($this->categories('Child 1')->getLeftSibling()); } @@ -111,11 +111,10 @@ public function testMoveRight() $this->assertTrue(Category::isValidNestedSet()); } - /** - * @expectedException Baum\NestedSet\MoveNotPossibleException - */ public function testMoveRightRaisesAnExceptionWhenNotPossible() { + $this->expectException(MoveNotPossibleException::class); + $node = $this->categories('Child 2'); $node->moveRight(); @@ -159,11 +158,10 @@ public function testMoveToRightOf() $this->assertTrue(Category::isValidNestedSet()); } - /** - * @expectedException Baum\NestedSet\MoveNotPossibleException - */ public function testMoveToRightOfRaisesAnExceptionWhenNotPossible() { + $this->expectException(MoveNotPossibleException::class); + $this->categories('Child 3')->moveToRightOf($this->categories('Child 3')->getRightSibling()); } @@ -476,51 +474,46 @@ public function testMakeLastChildOfSwappingRootsWithSubtrees() $this->assertEquals(9, $this->categories('Child 2.1')->getRight()); } - /** - * @expectedException Baum\NestedSet\MoveNotPossibleException - */ public function testUnpersistedNodeCannotBeMoved() { + $this->expectException(MoveNotPossibleException::class); + $unpersisted = new Category(['name' => 'Unpersisted']); $unpersisted->moveToRightOf($this->categories('Root 1')); } - /** - * @expectedException Baum\NestedSet\MoveNotPossibleException - */ public function testUnpersistedNodeCannotBeMadeChild() { + $this->expectException(MoveNotPossibleException::class); + $unpersisted = new Category(['name' => 'Unpersisted']); $unpersisted->makeChildOf($this->categories('Root 1')); } - /** - * @expectedException Baum\NestedSet\MoveNotPossibleException - */ public function testNodesCannotBeMovedToItself() { + $this->expectException(MoveNotPossibleException::class); + $node = $this->categories('Child 1'); $node->moveToRightOf($node); } - /** - * @expectedException Baum\NestedSet\MoveNotPossibleException - */ public function testNodesCannotBeMadeChildOfThemselves() { + $this->expectException(MoveNotPossibleException::class); + $node = $this->categories('Child 1'); $node->makeChildOf($node); } - /** - * @expectedException Baum\NestedSet\MoveNotPossibleException - */ public function testNodesCannotBeMovedToDescendantsOfThemselves() { + $this->expectException(MoveNotPossibleException::class); + $node = $this->categories('Root 1'); $node->makeChildOf($this->categories('Child 2.1')); @@ -589,11 +582,10 @@ public function testNonNumericMoveLeft() $this->assertTrue(Cluster::isValidNestedSet()); } - /** - * @expectedException Baum\NestedSet\MoveNotPossibleException - */ public function testNonNumericMoveLeftRaisesAnExceptionWhenNotPossible() { + $this->expectException(MoveNotPossibleException::class); + $node = $this->clusters('Child 2'); $node->moveLeft(); @@ -637,11 +629,10 @@ public function testNonNumericMoveToLeftOf() $this->assertTrue(Cluster::isValidNestedSet()); } - /** - * @expectedException Baum\NestedSet\MoveNotPossibleException - */ public function testNonNumericMoveToLeftOfRaisesAnExceptionWhenNotPossible() { + $this->expectException(MoveNotPossibleException::class); + $this->clusters('Child 1')->moveToLeftOf($this->clusters('Child 1')->getLeftSibling()); } @@ -682,11 +673,10 @@ public function testNonNumericMoveRight() $this->assertTrue(Cluster::isValidNestedSet()); } - /** - * @expectedException Baum\NestedSet\MoveNotPossibleException - */ public function testNonNumericMoveRightRaisesAnExceptionWhenNotPossible() { + $this->expectException(MoveNotPossibleException::class); + $node = $this->clusters('Child 2'); $node->moveRight(); @@ -730,11 +720,10 @@ public function testNonNumericMoveToRightOf() $this->assertTrue(Cluster::isValidNestedSet()); } - /** - * @expectedException Baum\NestedSet\MoveNotPossibleException - */ public function testNonNumericMoveToRightOfRaisesAnExceptionWhenNotPossible() { + $this->expectException(MoveNotPossibleException::class); + $this->clusters('Child 3')->moveToRightOf($this->clusters('Child 3')->getRightSibling()); } @@ -1047,51 +1036,46 @@ public function testNonNumericMakeLastChildOfSwappingRootsWithSubtrees() $this->assertEquals(9, $this->clusters('Child 2.1')->getRight()); } - /** - * @expectedException Baum\NestedSet\MoveNotPossibleException - */ public function testNonNumericUnpersistedNodeCannotBeMoved() { + $this->expectException(MoveNotPossibleException::class); + $unpersisted = new Cluster(['name' => 'Unpersisted']); $unpersisted->moveToRightOf($this->clusters('Root 1')); } - /** - * @expectedException Baum\NestedSet\MoveNotPossibleException - */ public function testNonNumericUnpersistedNodeCannotBeMadeChild() { + $this->expectException(MoveNotPossibleException::class); + $unpersisted = new Cluster(['name' => 'Unpersisted']); $unpersisted->makeChildOf($this->clusters('Root 1')); } - /** - * @expectedException Baum\NestedSet\MoveNotPossibleException - */ public function testNonNumericNodesCannotBeMovedToItself() { + $this->expectException(MoveNotPossibleException::class); + $node = $this->clusters('Child 1'); $node->moveToRightOf($node); } - /** - * @expectedException Baum\NestedSet\MoveNotPossibleException - */ public function testNonNumericNodesCannotBeMadeChildOfThemselves() { + $this->expectException(MoveNotPossibleException::class); + $node = $this->clusters('Child 1'); $node->makeChildOf($node); } - /** - * @expectedException Baum\NestedSet\MoveNotPossibleException - */ public function testNonNumericNodesCannotBeMovedToDescendantsOfThemselves() { + $this->expectException(MoveNotPossibleException::class); + $node = $this->clusters('Root 1'); $node->makeChildOf($this->clusters('Child 2.1')); diff --git a/tests/suite/ScopingTest.php b/tests/suite/ScopingTest.php index 1b4e425d..6faa7756 100644 --- a/tests/suite/ScopingTest.php +++ b/tests/suite/ScopingTest.php @@ -2,6 +2,7 @@ namespace Baum\Tests; +use Baum\NestedSet\MoveNotPossibleException; use Baum\Tests\Support\Models\MultiScopedCategory; use Baum\Tests\Support\Models\OrderedScopedCategory; use Baum\Tests\Support\Models\ScopedCategory; @@ -11,7 +12,7 @@ class ScopingTest extends TestCase { - public function setUp() + public function setUp(): void { with(new MultiScopedCategorySeeder)->run(); } @@ -275,33 +276,30 @@ public function testToHierarchyNestsCorrectlyWithScopedOrder() $this->assertEquals($expectedWhole2, hmap(OrderedScopedCategory::where('company_id', 2)->get()->toHierarchy()->toArray())); } - /** - * @expectedException Baum\NestedSet\MoveNotPossibleException - */ public function testNodesCannotMoveBetweenScopes() { + $this->expectException(MoveNotPossibleException::class); + $child4 = $this->categories('Child 4', ScopedCategory::class); $root1 = $this->categories('Root 1', ScopedCategory::class); $child4->makeChildOf($root1); } - /** - * @expectedException Baum\NestedSet\MoveNotPossibleException - */ public function testNodesCannotMoveBetweenScopesMultiple() { + $this->expectException(MoveNotPossibleException::class); + $root1 = $this->categories('Root 1', MultiScopedCategory::class); $child4 = $this->categories('Child 4', MultiScopedCategory::class); $child4->makeChildOf($root1); } - /** - * @expectedException Baum\NestedSet\MoveNotPossibleException - */ public function testNodesCannotMoveBetweenScopesMultiple2() { + $this->expectException(MoveNotPossibleException::class); + $root1 = $this->categories('Racine 1', MultiScopedCategory::class); $child2 = $this->categories('Hijo 2', MultiScopedCategory::class); diff --git a/tests/suite/TestCase.php b/tests/suite/TestCase.php index 9d0f3ea8..e32c88fd 100644 --- a/tests/suite/TestCase.php +++ b/tests/suite/TestCase.php @@ -16,7 +16,7 @@ class TestCase extends BaseTestCase use Concerns\MigratesDatabase; use Concerns\SeedsDatabase; - public static function setUpBeforeClass() + public static function setUpBeforeClass(): void { $instance = new static; @@ -24,7 +24,7 @@ public static function setUpBeforeClass() $instance->migrate(ClusterMigrator::class); } - public function setUp() + public function setUp(): void { $this->seed(CategorySeeder::class); $this->seed(ClusterSeeder::class); diff --git a/tests/suite/TreeMapperTest.php b/tests/suite/TreeMapperTest.php index 3d5e88e2..a2b0c058 100644 --- a/tests/suite/TreeMapperTest.php +++ b/tests/suite/TreeMapperTest.php @@ -10,7 +10,7 @@ class TreeMapperTest extends TestCase { - public function setUp() + public function setUp(): void { with(new CategoryMigrator)->up(); } From 4993ee6e41d0d0bd82ed908fc1a6ff6a70cc9b85 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Wed, 8 May 2019 17:55:39 +0200 Subject: [PATCH 20/62] Change indent size for yaml files in editorconfig. --- .editorconfig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.editorconfig b/.editorconfig index 56f3b6bf..6f313c6a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,3 +10,6 @@ trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false + +[*.yml] +indent_size = 2 From fadc32b00738be4b991939d0a7c9a091cf6bf19f Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Thu, 9 May 2019 19:46:13 +0200 Subject: [PATCH 21/62] Cast nested set attributes to int. --- src/NestedSet/Concerns/HasAttributes.php | 6 +++--- src/NestedSet/Concerns/Relatable.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NestedSet/Concerns/HasAttributes.php b/src/NestedSet/Concerns/HasAttributes.php index 54a45a79..006df059 100644 --- a/src/NestedSet/Concerns/HasAttributes.php +++ b/src/NestedSet/Concerns/HasAttributes.php @@ -21,7 +21,7 @@ public function getParentKey() */ public function getLeft() { - return $this->getAttribute($this->getLeftColumnName()); + return (int) $this->getAttribute($this->getLeftColumnName()); } /** @@ -31,7 +31,7 @@ public function getLeft() */ public function getRight() { - return $this->getAttribute($this->getRightColumnName()); + return (int) $this->getAttribute($this->getRightColumnName()); } /** @@ -41,7 +41,7 @@ public function getRight() */ public function getDepth() { - return $this->getAttribute($this->getDepthColumnName()); + return (int) $this->getAttribute($this->getDepthColumnName()); } /** diff --git a/src/NestedSet/Concerns/Relatable.php b/src/NestedSet/Concerns/Relatable.php index 85b501d0..38b980f9 100644 --- a/src/NestedSet/Concerns/Relatable.php +++ b/src/NestedSet/Concerns/Relatable.php @@ -74,7 +74,7 @@ public function isRoot() */ public function isLeaf() { - return $this->exists && ($this->getRight() - $this->getLeft() == 1); + return ($this->getRight() - $this->getLeft() === 1); } /** From 7f5d2cf3aa1bbfb65f2761c53f6ae06d227cf32a Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Fri, 10 May 2019 17:39:05 +0200 Subject: [PATCH 22/62] Remove xdebug from travis. --- .travis.yml | 5 ++++- composer.json | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 578072c6..a963f1db 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,8 +20,11 @@ matrix: - php: '7.1' env: LARAVEL=dev-master +before_install: + - phpenv config-rm xdebug.ini || true + install: - - travis_retry composer require "illuminate/database=${LARAVEL}" --dev --prefer-dist --no-interaction --no-suggest + - travis_retry composer require "illuminate/contracts=${LARAVEL}" --dev --prefer-dist --no-interaction --no-suggest script: - vendor/bin/phpunit --verbose diff --git a/composer.json b/composer.json index 779a3a68..4c2c9f8c 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ ], "require": { "php": "^7.1.3", + "illuminate/contracts": "~5.7.0|~5.8.0|~5.9.0", "illuminate/database": "~5.7.0|~5.8.0|~5.9.0", "illuminate/events": "~5.7.0|~5.8.0|~5.9.0", "illuminate/support": "~5.7.0|~5.8.0|~5.9.0" From e09c0f6928d39bd51f574dd26ace00221bcd9a35 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Thu, 16 May 2019 19:15:09 +0200 Subject: [PATCH 23/62] Test config changes. --- .travis.yml | 2 +- bootstrap.php | 38 ++++++++++++++++++++++---------------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/.travis.yml b/.travis.yml index a963f1db..c22f19b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ sudo: false php: - '7.1' - - '7.2' + - '7.2.18' - '7.3' env: diff --git a/bootstrap.php b/bootstrap.php index 0283d8c1..e37a1182 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -14,19 +14,31 @@ /* |-------------------------------------------------------------------------- -| Initialize the Collection extensions +| Initialize the Eloquent database manager |-------------------------------------------------------------------------- | -| This would normally be under a service provider on a laravel application but -| for testing we just initialize the mixin here. +| Eloquent can run as a stand-alone library (outside of Laravel applications) +| with all its functionality intact. We just need to bootstrap it correctly. | */ -\Illuminate\Database\Eloquent\Collection::mixin(new \Baum\Mixins\Collection); +$container = new \Illuminate\Container\Container; + +$dispatcher = new \Illuminate\Events\Dispatcher($container); + +$capsule = new \Illuminate\Database\Capsule\Manager($container); + +$capsule->setEventDispatcher($dispatcher); + +$capsule->addConnection(require(__DIR__.'/tests/config/database.php')); + +$capsule->bootEloquent(); + +$capsule->setAsGlobal(); /* |-------------------------------------------------------------------------- -| Initialize the schema blueprint extensions +| Initialize the Collection extensions |-------------------------------------------------------------------------- | | This would normally be under a service provider on a laravel application but @@ -34,22 +46,16 @@ | */ -\Illuminate\Database\Schema\Blueprint::mixin(new \Baum\Mixins\Blueprint); +\Illuminate\Database\Eloquent\Collection::mixin(new \Baum\Mixins\Collection); /* |-------------------------------------------------------------------------- -| Initialize the Eloquent database manager +| Initialize the schema blueprint extensions |-------------------------------------------------------------------------- | -| Eloquent can run as a stand-alone library (outside of Laravel applications) -| with all its functionality intact. We just need to bootstrap it correctly. +| This would normally be under a service provider on a laravel application but +| for testing we just initialize the mixin here. | */ -$capsule = new \Illuminate\Database\Capsule\Manager; - -$capsule->addConnection(require(__DIR__.'/tests/config/database.php')); -$capsule->setEventDispatcher(new \Illuminate\Events\Dispatcher(new \Illuminate\Container\Container)); -$capsule->bootEloquent(); - -$capsule->setAsGlobal(); +\Illuminate\Database\Schema\Blueprint::mixin(new \Baum\Mixins\Blueprint); From c60fd80cc0832e33d40577fc936869640d73f859 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Fri, 17 May 2019 12:02:48 +0200 Subject: [PATCH 24/62] Update depedencies. --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 4c2c9f8c..779a3a68 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,6 @@ ], "require": { "php": "^7.1.3", - "illuminate/contracts": "~5.7.0|~5.8.0|~5.9.0", "illuminate/database": "~5.7.0|~5.8.0|~5.9.0", "illuminate/events": "~5.7.0|~5.8.0|~5.9.0", "illuminate/support": "~5.7.0|~5.8.0|~5.9.0" From 3a65927706c35b02ac71676f2cd78f695ca33508 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Fri, 17 May 2019 12:03:02 +0200 Subject: [PATCH 25/62] Change travis config. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c22f19b8..a1ac65bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ sudo: false php: - '7.1' - - '7.2.18' + - '7.2' - '7.3' env: @@ -24,7 +24,7 @@ before_install: - phpenv config-rm xdebug.ini || true install: - - travis_retry composer require "illuminate/contracts=${LARAVEL}" --dev --prefer-dist --no-interaction --no-suggest + - travis_retry composer require "illuminate/database=${LARAVEL}" --dev --prefer-dist --no-interaction --no-suggest script: - vendor/bin/phpunit --verbose From cad1e497146e18ccee6b237fcd6ae9696854c817 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Tue, 21 May 2019 10:55:46 +0200 Subject: [PATCH 26/62] Change test bootstrap init process. --- bootstrap.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bootstrap.php b/bootstrap.php index e37a1182..31ae7958 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -22,20 +22,20 @@ | */ -$container = new \Illuminate\Container\Container; +use Illuminate\Container\Container; +use Illuminate\Database\Capsule\Manager as Capsule; +use Illuminate\Events\Dispatcher; -$dispatcher = new \Illuminate\Events\Dispatcher($container); - -$capsule = new \Illuminate\Database\Capsule\Manager($container); - -$capsule->setEventDispatcher($dispatcher); +$capsule = new Capsule; $capsule->addConnection(require(__DIR__.'/tests/config/database.php')); -$capsule->bootEloquent(); +$capsule->setEventDispatcher(new Dispatcher(new Container)); $capsule->setAsGlobal(); +$capsule->bootEloquent(); + /* |-------------------------------------------------------------------------- | Initialize the Collection extensions From 85ad40fb8e192f2b192fa739fddeed7625311168 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Tue, 21 May 2019 11:14:49 +0200 Subject: [PATCH 27/62] Modify column scope implementation. --- src/NestedSet/Scopes/ScopedByScope.php | 40 +++++++++++++++++--------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/NestedSet/Scopes/ScopedByScope.php b/src/NestedSet/Scopes/ScopedByScope.php index 52df942a..567df588 100644 --- a/src/NestedSet/Scopes/ScopedByScope.php +++ b/src/NestedSet/Scopes/ScopedByScope.php @@ -17,11 +17,7 @@ class ScopedByScope implements Scope */ public function apply(Builder $builder, Model $model) { - if ($model->isScoped() && $model->exists) { - foreach ($model->getScopedColumnNames() as $fld) { - $builder->where($model->qualifyColumn($fld), '=', $model->getAttribute($fld)); - } - } + $this->restrictByScope($builder, $model); } /** @@ -59,16 +55,32 @@ public function addUnscoped(Builder $builder) public function addScopedBy(Builder $builder) { $builder->macro('scopedBy', function (Builder $builder, $scopedBy = []) { - $model = $builder->getModel(); + return $this->restrictByScope($builder, $builder->getModel(), $scopedBy); + }); + } - $scopeColumns = array_merge($scopedBy, $model->isScoped() ? $model->getScopedColumnNames() : []); + /** + * Restricts current query builder object by its associated model scope + * columns. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @param \Illuminate\Database\Eloquent\Model $model + * @param array $extra + * @return void + */ + protected function restrictByScope(Builder $builder, Model $model = null, array $extra = []) + { + $model = $model ?: $builder->getModel(); - return array_reduce($scopeColumns, function ($builder, $column) use ($model) { - return $builder->where( - $model->qualifyColumn($column), - $model->getAttribute($column) - ); - }, $builder->unscoped()); - }); + $attributes = $model->getAttributes(); + + $scopeColumns = array_merge($extra, $model->isScoped() ? $model->getScopedColumnNames() : []); + + return array_reduce($scopeColumns, function ($builder, $column) use ($model, $attributes) { + return array_key_exists($column, $attributes) ? $builder->where( + $model->qualifyColumn($column), + $model->getAttribute($column) + ) : $builder; + }, $builder->unscoped()); } } From d15e8809534272940f5a7d28cbc324d7b9f3c6f3 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Tue, 21 May 2019 18:09:17 +0200 Subject: [PATCH 28/62] Simplify max right finding query. --- src/NestedSet/Node.php | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/NestedSet/Node.php b/src/NestedSet/Node.php index 5baaa86b..67e5c95c 100644 --- a/src/NestedSet/Node.php +++ b/src/NestedSet/Node.php @@ -144,17 +144,7 @@ protected function determineDepth($node, $nesting = 0) */ public function setDefaultLeftAndRight() { - $withHighestRight = $this->newQuery() - ->scopedBy() - ->reOrderBy($this->getRightColumnName(), 'desc') - ->take(1) - ->sharedLock() - ->first(); - - $maxRgt = 0; - if (!is_null($withHighestRight)) { - $maxRgt = $withHighestRight->getRight(); - } + $maxRgt = (int) $this->newQuery()->max($this->getQualifiedRightColumnName()); $this->setAttribute($this->getLeftColumnName(), $maxRgt + 1); $this->setAttribute($this->getRightColumnName(), $maxRgt + 2); From 8770e4f53072a68c28589bd14cd3cb0c259f6d5c Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Tue, 21 May 2019 18:09:41 +0200 Subject: [PATCH 29/62] Simplify 'destroyDescendants' query. --- src/NestedSet/Node.php | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/NestedSet/Node.php b/src/NestedSet/Node.php index 67e5c95c..3f615099 100644 --- a/src/NestedSet/Node.php +++ b/src/NestedSet/Node.php @@ -215,24 +215,22 @@ public function destroyDescendants() $this->getConnection()->transaction(function () { $this->refresh(); - $lftCol = $this->getLeftColumnName(); - $rgtCol = $this->getRightColumnName(); - $lft = $this->getLeft(); - $rgt = $this->getRight(); - $qualifiedLftCol = $this->getQualifiedLeftColumnName(); - $qualifiedRgtCol = $this->getQualifiedRightColumnName(); + $lftCol = $this->getQualifiedLeftColumnName(); + $rgtCol = $this->getQualifiedRightColumnName(); + $lft = $this->getLeft(); + $rgt = $this->getRight(); // Apply a lock to the rows which fall past the deletion point - $this->newQuery()->where($qualifiedLftCol, '>=', $lft)->select($this->getQualifiedKeyName())->lockForUpdate()->get(); + $this->newQuery()->where($lftCol, '>=', $lft)->select($this->getQualifiedKeyName())->lockForUpdate()->get(); // Prune children - $this->newQuery()->where($qualifiedLftCol, '>', $lft)->where($qualifiedRgtCol, '<', $rgt)->delete(); + $this->newQuery()->where($lftCol, '>', $lft)->where($rgtCol, '<', $rgt)->delete(); // Update left and right indexes for the remaining nodes $diff = $rgt - $lft + 1; - $this->newQuery()->where($qualifiedLftCol, '>', $rgt)->decrement($lftCol, $diff); - $this->newQuery()->where($qualifiedRgtCol, '>', $rgt)->decrement($rgtCol, $diff); + $this->newQuery()->where($lftCol, '>', $rgt)->decrement($lftCol, $diff); + $this->newQuery()->where($rgtCol, '>', $rgt)->decrement($rgtCol, $diff); }); } From 31e09f173a2472d194e714c9d248c7f101a58a63 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Tue, 21 May 2019 18:36:29 +0200 Subject: [PATCH 30/62] Simplify query. --- src/NestedSet/Concerns/HasAttributes.php | 2 +- src/NestedSet/Node.php | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/NestedSet/Concerns/HasAttributes.php b/src/NestedSet/Concerns/HasAttributes.php index 006df059..4b396098 100644 --- a/src/NestedSet/Concerns/HasAttributes.php +++ b/src/NestedSet/Concerns/HasAttributes.php @@ -7,7 +7,7 @@ trait HasAttributes /** * Get the value of the model's "parent_id" field. * - * @return int + * @return mixed */ public function getParentKey() { diff --git a/src/NestedSet/Node.php b/src/NestedSet/Node.php index 3f615099..818d1572 100644 --- a/src/NestedSet/Node.php +++ b/src/NestedSet/Node.php @@ -595,9 +595,7 @@ public function getLeftSibling() return $this->siblings() ->where($this->getLeftColumnName(), '<', $this->getLeft()) ->orderBy($this->getOrderColumnName(), 'desc') - ->get() ->first(); - // ->last(); } /** From e1ec622f10b811c8068a8c161d148fcbb8d3ffce Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Thu, 6 Jun 2019 19:39:49 +0200 Subject: [PATCH 31/62] Remove class methods dependencies on tests. --- tests/suite/TestCase.php | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/suite/TestCase.php b/tests/suite/TestCase.php index e32c88fd..0f8cccae 100644 --- a/tests/suite/TestCase.php +++ b/tests/suite/TestCase.php @@ -16,17 +16,14 @@ class TestCase extends BaseTestCase use Concerns\MigratesDatabase; use Concerns\SeedsDatabase; - public static function setUpBeforeClass(): void + public function setUp(): void { - $instance = new static; + $this->migrate(CategoryMigrator::class); - $instance->migrate(CategoryMigrator::class); - $instance->migrate(ClusterMigrator::class); - } + $this->migrate(ClusterMigrator::class); - public function setUp(): void - { $this->seed(CategorySeeder::class); + $this->seed(ClusterSeeder::class); } From dbdf2d5816e9079d4758f550ff4057a84d95592b Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Thu, 6 Jun 2019 19:41:08 +0200 Subject: [PATCH 32/62] More 'qualify', simplify and improve logic a little. --- src/NestedSet/Move.php | 136 ++++++++++++++++++++--------------------- 1 file changed, 65 insertions(+), 71 deletions(-) diff --git a/src/NestedSet/Move.php b/src/NestedSet/Move.php index 5bd5f0da..94f14b00 100644 --- a/src/NestedSet/Move.php +++ b/src/NestedSet/Move.php @@ -2,6 +2,7 @@ namespace Baum\NestedSet; +use Illuminate\Database\Eloquent\Model; use Illuminate\Events\Dispatcher; /** @@ -10,47 +11,47 @@ class Move { - /** - * Node on which the move operation will be performed - * - * @var \Baum\Node - */ - protected $node = null; + /** + * Node on which the move operation will be performed + * + * @var \Baum\Node + */ + protected $node; /** * Destination node * * @var \Baum\Node | int */ - protected $target = null; + protected $target; /** - * Move target position, one of: child, left, right, root + * Move target position, one of: child, left, right or root * * @var string */ - protected $position = null; + protected $position; /** * Memoized 1st boundary. * * @var int */ - protected $_bound1 = null; + protected $_bound1; /** * Memoized 2nd boundary. * * @var int */ - protected $_bound2 = null; + protected $_bound2; /** * Memoized boundaries array. * * @var array */ - protected $_boundaries = null; + protected $_boundaries; /** * The event dispatcher instance. @@ -73,7 +74,7 @@ public function __construct($node, $target, $position, $dispatcher = null) $this->target = $this->resolveNode($target); $this->position = $position; - $this->setEventDispatcher($dispatcher ?? $node->getEventDispatcher()); + $this->setEventDispatcher($dispatcher ?? $this->node->getEventDispatcher()); } /** @@ -139,13 +140,13 @@ public function updateStructure() $currentId = $this->quoteIdentifier($this->node->getKey()); $parentId = $this->quoteIdentifier($this->parentId()); - $leftColumn = $this->node->getLeftColumnName(); - $rightColumn = $this->node->getRightColumnName(); - $parentColumn = $this->node->getParentColumnName(); + $leftColumn = $this->node->getQualifiedLeftColumnName(); + $rightColumn = $this->node->getQualifiedRightColumnName(); + $parentColumn = $this->node->getQualifiedParentColumnName(); $wrappedLeft = $grammar->wrap($leftColumn); $wrappedRight = $grammar->wrap($rightColumn); $wrappedParent = $grammar->wrap($parentColumn); - $wrappedId = $grammar->wrap($this->node->getKeyName()); + $wrappedId = $grammar->wrap($this->node->getQualifiedKeyName()); $lftSql = "CASE WHEN $wrappedLeft BETWEEN $a AND $b THEN $wrappedLeft + $d - $b @@ -172,11 +173,11 @@ public function updateStructure() } return $this->node - ->newQuery() - ->where(function ($query) use ($leftColumn, $rightColumn, $a, $d) { - $query->whereBetween($leftColumn, [$a, $d])->orWhereBetween($rightColumn, [$a, $d]); - }) - ->update($updateConditions); + ->newQuery() + ->where(function ($query) use ($leftColumn, $rightColumn, $a, $d) { + $query->whereBetween($leftColumn, [$a, $d])->orWhereBetween($rightColumn, [$a, $d]); + }) + ->update($updateConditions); } /** @@ -189,18 +190,16 @@ public function updateStructure() */ protected function resolveNode($node) { - if (method_exists($node, 'refresh') && is_callable([$node, 'refresh'])) { - return $node->refresh(); - } + if ($node instanceof Model) { + if (method_exists($node, 'refresh') && is_callable([$node, 'refresh'])) { + return $node->refresh(); + } - if ($node instanceof \Illuminate\Database\Eloquent\Model) { - return $node->newQuery()->find($node->getKey()); + return $this->node->newQuery()->find($node->getKey()); } - $key = (int) $node; - - if ($key !== 0) { - return $this->node->newQuery()->find($node); + if ($resolved = $this->node->newQuery()->find($node)) { + return $resolved; } throw new MoveNotPossibleException('Could not resolve target node.'); @@ -251,30 +250,28 @@ protected function guardAgainstImpossibleMove() */ protected function bound1() { - if (!is_null($this->_bound1)) { - return $this->_bound1; - } - - switch ($this->position) { - case 'child': - $this->_bound1 = $this->target->getRight(); - break; - - case 'left': - $this->_bound1 = $this->target->getLeft(); - break; - - case 'right': - $this->_bound1 = $this->target->getRight() + 1; - break; + if (is_null($this->_bound1)) { + switch ($this->position) { + case 'child': + $this->_bound1 = $this->target->getRight(); + break; + + case 'left': + $this->_bound1 = $this->target->getLeft(); + break; + + case 'right': + $this->_bound1 = $this->target->getRight() + 1; + break; + + case 'root': + $this->_bound1 = $this->node->newQuery()->max($this->node->getRightColumnName()) + 1; + break; + } - case 'root': - $this->_bound1 = $this->node->newQuery()->max($this->node->getRightColumnName()) + 1; - break; + $this->_bound1 = (($this->_bound1 > $this->node->getRight()) ? $this->_bound1 - 1 : $this->_bound1); } - $this->_bound1 = (($this->_bound1 > $this->node->getRight()) ? $this->_bound1 - 1 : $this->_bound1); - return $this->_bound1; } @@ -286,11 +283,10 @@ protected function bound1() */ protected function bound2() { - if (!is_null($this->_bound2)) { - return $this->_bound2; + if (is_null($this->_bound2)) { + $this->_bound2 = (($this->bound1() > $this->node->getRight()) ? $this->node->getRight() + 1 : $this->node->getLeft() - 1); } - $this->_bound2 = (($this->bound1() > $this->node->getRight()) ? $this->node->getRight() + 1 : $this->node->getLeft() - 1); return $this->_bound2; } @@ -301,20 +297,18 @@ protected function bound2() */ protected function boundaries() { - if (!is_null($this->_boundaries)) { - return $this->_boundaries; + if (is_null($this->_boundaries)) { + // we have defined the boundaries of two non-overlapping intervals, + // so sorting puts both the intervals and their boundaries in order + $this->_boundaries = [ + $this->node->getLeft(), + $this->node->getRight(), + $this->bound1(), + $this->bound2() + ]; + sort($this->_boundaries); } - // we have defined the boundaries of two non-overlapping intervals, - // so sorting puts both the intervals and their boundaries in order - $this->_boundaries = [ - $this->node->getLeft(), - $this->node->getRight(), - $this->bound1(), - $this->bound2() - ]; - sort($this->_boundaries); - return $this->_boundaries; } @@ -354,7 +348,7 @@ protected function hasChange() */ protected function promotingToRoot() { - return ($this->position == 'root'); + return $this->position === 'root'; } /** @@ -429,9 +423,9 @@ protected function quoteIdentifier($value) protected function applyLockBetween($lft, $rgt) { $this->node->newQuery() - ->where($this->node->getLeftColumnName(), '>=', $lft) - ->where($this->node->getRightColumnName(), '<=', $rgt) - ->select($this->node->getKeyName()) + ->where($this->node->getQualifiedLeftColumnName(), '>=', $lft) + ->where($this->node->getQualifiedRightColumnName(), '<=', $rgt) + ->select($this->node->getQualifiedKeyName()) ->lockForUpdate() ->get(); } From 206d58c36919d6da373b38f8740cf3a1786269b6 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Thu, 6 Jun 2019 19:44:13 +0200 Subject: [PATCH 33/62] Control test db connection with ENV. --- tests/config/database.php | 71 ++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/tests/config/database.php b/tests/config/database.php index c84adca6..4e121b9e 100644 --- a/tests/config/database.php +++ b/tests/config/database.php @@ -1,32 +1,47 @@ 'sqlite', - 'database' => ':memory:', - 'prefix' => '' -]; +$connections = [ + // sqlite connection + 'sqlite' => [ + 'driver' => 'sqlite', + 'database' => getenv('DB_NAME') ?: ':memory:', + 'prefix' => '', + 'foreign_key_constraints' => getenv('DB_FOREIGN_KEYS') ?: true + ], + + // postgres connection + 'postgres' => [ + 'driver' => 'pgsql', + 'host' => getenv('DB_HOST') ?: 'localhost', + 'port' => getenv('DB_PORT') ?: '5432', + 'database' => getenv('DB_NAME') ?: 'baum_test', + 'username' => getenv('DB_USERNAME') ?: 'postgres', + 'password' => getenv('DB_PASSWORD') ?: '', + 'charset' => 'utf8', + 'prefix' => '', + 'schema' => 'public', + 'sslmode' => 'prefer' + ], -// // Test against local Postgres -// return [ -// 'driver' => 'pgsql', -// 'host' => 'localhost', -// 'database' => 'baum_test', -// 'username' => 'postgres', -// 'password' => 'postgres', -// 'charset' => 'utf8', -// 'prefix' => '', -// 'schema' => 'public', -// ]; + // mysql connection + 'mysql' => [ + 'driver' => 'mysql', + 'host' => getenv('DB_HOST') ?: '127.0.0.1', + 'port' => getenv('DB_PORT') ?: '3306', + 'database' => getenv('DB_NAME') ?: 'baum_test', + 'username' => getenv('DB_USERNAME') ?: 'mysql', + 'password' => getenv('DB_PASSWORD') ?: '', + 'unix_socket' => getenv('DB_SOCKET') ?: '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], +]; -// // Test against local MySQL -// return [ -// 'driver' => 'mysql', -// 'host' => 'localhost', -// 'database' => 'baum_test', -// 'username' => 'mysql', -// 'password' => 'mysql', -// 'charset' => 'utf8', -// 'collation' => 'utf8_unicode_ci', -// 'prefix' => '', -// ]; +return $connections[getenv('DB_CONNECTION') ?: 'sqlite']; From 013dcd398801ad0a86ebd7d7920efb45465d3839 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Mon, 16 Sep 2019 16:43:24 +0200 Subject: [PATCH 34/62] Fix bad getenv call. --- tests/config/database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/config/database.php b/tests/config/database.php index 4e121b9e..c278783e 100644 --- a/tests/config/database.php +++ b/tests/config/database.php @@ -39,7 +39,7 @@ 'strict' => true, 'engine' => null, 'options' => extension_loaded('pdo_mysql') ? array_filter([ - PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + PDO::MYSQL_ATTR_SSL_CA => getenv('MYSQL_ATTR_SSL_CA'), ]) : [], ], ]; From 1fb4b6ae74477e9133a17758ed64ce25b5a6ad76 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Mon, 16 Sep 2019 16:43:50 +0200 Subject: [PATCH 35/62] Add Laravel v6.x support. --- .travis.yml | 4 ++-- composer.json | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index a1ac65bf..f671cf18 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,13 +12,13 @@ env: matrix: - LARAVEL=5.7.* - LARAVEL=5.8.* - - LARAVEL=dev-master + - LARAVEL=6.0.* matrix: fast_finish: true exclude: - php: '7.1' - env: LARAVEL=dev-master + env: LARAVEL=6.0.* before_install: - phpenv config-rm xdebug.ini || true diff --git a/composer.json b/composer.json index 779a3a68..f25a89e7 100644 --- a/composer.json +++ b/composer.json @@ -13,9 +13,9 @@ ], "require": { "php": "^7.1.3", - "illuminate/database": "~5.7.0|~5.8.0|~5.9.0", - "illuminate/events": "~5.7.0|~5.8.0|~5.9.0", - "illuminate/support": "~5.7.0|~5.8.0|~5.9.0" + "illuminate/database": "~5.7.0|~5.8.0|~6.0.0", + "illuminate/events": "~5.7.0|~5.8.0|~6.0.0", + "illuminate/support": "~5.7.0|~5.8.0|~6.0.0" }, "require-dev": { "mockery/mockery": "^1.0", From e660750757c00437d6790dc71dcb256b9cc38b04 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Mon, 4 Nov 2019 11:54:00 +0100 Subject: [PATCH 36/62] Update composer.json and travis configs. --- .travis.yml | 6 +++--- composer.json | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index f671cf18..b17fb456 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,19 +12,19 @@ env: matrix: - LARAVEL=5.7.* - LARAVEL=5.8.* - - LARAVEL=6.0.* + - LARAVEL=^6.0 matrix: fast_finish: true exclude: - php: '7.1' - env: LARAVEL=6.0.* + env: LARAVEL=^6.0 before_install: - phpenv config-rm xdebug.ini || true install: - - travis_retry composer require "illuminate/database=${LARAVEL}" --dev --prefer-dist --no-interaction --no-suggest + - travis_retry composer require "illuminate/contracts=${LARAVEL}" --dev --prefer-dist --no-interaction --no-suggest script: - vendor/bin/phpunit --verbose diff --git a/composer.json b/composer.json index f25a89e7..9ec6dd46 100644 --- a/composer.json +++ b/composer.json @@ -13,9 +13,10 @@ ], "require": { "php": "^7.1.3", - "illuminate/database": "~5.7.0|~5.8.0|~6.0.0", - "illuminate/events": "~5.7.0|~5.8.0|~6.0.0", - "illuminate/support": "~5.7.0|~5.8.0|~6.0.0" + "illuminate/contracts": "~5.7.0|~5.8.0|^6.0", + "illuminate/database": "~5.7.0|~5.8.0|^6.0", + "illuminate/events": "~5.7.0|~5.8.0|^6.0", + "illuminate/support": "~5.7.0|~5.8.0|^6.0" }, "require-dev": { "mockery/mockery": "^1.0", From 02fd160e90bbbbf9fcd343c215f3b0645086870e Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Mon, 4 Nov 2019 12:53:43 +0100 Subject: [PATCH 37/62] Change travis to bionic image. --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b17fb456..29998053 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ -dist: xenial +dist: bionic + language: php sudo: false From 64a6ac9fdca7ca98f9a72fa9181ec83b969783d9 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Mon, 4 Nov 2019 19:20:27 +0100 Subject: [PATCH 38/62] Extract testing UUID related functionality. --- tests/suite/Support/Models/Cluster.php | 37 +-------- tests/suite/Support/Models/HasUuids.php | 80 +++++++++++++++++++ .../Support/Observers/ClusterObserver.php | 19 +++++ 3 files changed, 100 insertions(+), 36 deletions(-) create mode 100644 tests/suite/Support/Models/HasUuids.php create mode 100644 tests/suite/Support/Observers/ClusterObserver.php diff --git a/tests/suite/Support/Models/Cluster.php b/tests/suite/Support/Models/Cluster.php index eb0ebf7a..298d2aac 100644 --- a/tests/suite/Support/Models/Cluster.php +++ b/tests/suite/Support/Models/Cluster.php @@ -7,46 +7,11 @@ class Cluster extends Model { - use Node; + use HasUuids, Node; protected $table = 'clusters'; protected $fillable = ['name']; - public $incrementing = false; - public $timestamps = false; - - protected static function boot() - { - parent::boot(); - - static::creating(function ($cluster) { - $cluster->ensureUuid(); - }); - } - - public function ensureUuid() - { - if (is_null($this->getAttribute($this->getKeyName()))) { - $this->setAttribute($this->getKeyName(), $this->generateUuid()); - } - - return $this; - } - - protected function generateUuid() - { - return sprintf( - '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', - mt_rand(0, 0xffff), - mt_rand(0, 0xffff), - mt_rand(0, 0xffff), - mt_rand(0, 0x0fff) | 0x4000, - mt_rand(0, 0x3fff) | 0x8000, - mt_rand(0, 0xffff), - mt_rand(0, 0xffff), - mt_rand(0, 0xffff) - ); - } } diff --git a/tests/suite/Support/Models/HasUuids.php b/tests/suite/Support/Models/HasUuids.php new file mode 100644 index 00000000..2d69f434 --- /dev/null +++ b/tests/suite/Support/Models/HasUuids.php @@ -0,0 +1,80 @@ +incrementing = false; + } + + /** + * Ensures the uuid value is present on the model. + * + * @return void + */ + public function ensureUuid() + { + if (is_null($this->{$this->getUuidColumn()})) { + $this->{$this->getUuidColumn()} = $this->newUuid(); + } + } + + /** + * Get the name of the uuid column. + * + * @return string + */ + public function getUuidColumn() + { + return $this->getKeyName(); + } + + /** + * Get the 'qualified' name of the uuid column. + * + * @return string + */ + public function getQualifiedUuidColumn() + { + return $this->getQualifiedKeyName(); + } + + /** + * Return a new UUIDv4 value + * + * @return string + */ + public function newUuid() + { + return sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, + mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0xffff) + ); + } +} diff --git a/tests/suite/Support/Observers/ClusterObserver.php b/tests/suite/Support/Observers/ClusterObserver.php new file mode 100644 index 00000000..4520483a --- /dev/null +++ b/tests/suite/Support/Observers/ClusterObserver.php @@ -0,0 +1,19 @@ +ensureUuid(); + } +} From f209f67515641dc431ac2d35dcdcfe3205d53c3d Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Fri, 8 Nov 2019 13:32:28 +0100 Subject: [PATCH 39/62] Add fullpath option to migration maker command. --- src/Console/MakeMigrationCommand.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Console/MakeMigrationCommand.php b/src/Console/MakeMigrationCommand.php index 2db811a4..0ddd766e 100644 --- a/src/Console/MakeMigrationCommand.php +++ b/src/Console/MakeMigrationCommand.php @@ -16,7 +16,8 @@ class MakeMigrationCommand extends MigrateMakeCommand {--create= : The table to be created} {--table= : The table to migrate} {--path= : The location where the migration file should be created} - {--realpath : Indicate any provided migration file paths are pre-resolved absolute paths}'; + {--realpath : Indicate any provided migration file paths are pre-resolved absolute paths} + {--fullpath : Output the full path of the migration}'; /** * The console command description. From e4ac7e7eb62f5fe31cbb83ab21cd60020580418a Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Thu, 9 Apr 2020 13:33:13 +0200 Subject: [PATCH 40/62] Add laravel 7.x support. Needs testing. --- .travis.yml | 3 +++ composer.json | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 29998053..c7a03855 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,12 +14,15 @@ env: - LARAVEL=5.7.* - LARAVEL=5.8.* - LARAVEL=^6.0 + - LARAVEL=^7.0 matrix: fast_finish: true exclude: - php: '7.1' env: LARAVEL=^6.0 + - php: '7.1' + env: LARAVEL=^7.0 before_install: - phpenv config-rm xdebug.ini || true diff --git a/composer.json b/composer.json index 9ec6dd46..4c5e73e8 100644 --- a/composer.json +++ b/composer.json @@ -13,10 +13,10 @@ ], "require": { "php": "^7.1.3", - "illuminate/contracts": "~5.7.0|~5.8.0|^6.0", - "illuminate/database": "~5.7.0|~5.8.0|^6.0", - "illuminate/events": "~5.7.0|~5.8.0|^6.0", - "illuminate/support": "~5.7.0|~5.8.0|^6.0" + "illuminate/contracts": "~5.7.0|~5.8.0|^6.0|^7.0", + "illuminate/database": "~5.7.0|~5.8.0|^6.0|^7.0", + "illuminate/events": "~5.7.0|~5.8.0|^6.0|^7.0", + "illuminate/support": "~5.7.0|~5.8.0|^6.0|^7.0" }, "require-dev": { "mockery/mockery": "^1.0", From b56eb6fe8f642b2663c6841b09e8e660cd112e34 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Thu, 9 Apr 2020 18:10:35 +0200 Subject: [PATCH 41/62] Add php 7.4 to travis test matrix. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index c7a03855..3cd6e0ad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ php: - '7.1' - '7.2' - '7.3' + - '7.4' env: matrix: From 5a2fc0d93f0fc099a16d4e6cca94b810771064f1 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Tue, 1 Sep 2020 16:46:31 +0200 Subject: [PATCH 42/62] Remove phpcs cache file and add it to .gitignore file. --- .gitignore | 1 + .php_cs.cache | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 .php_cs.cache diff --git a/.gitignore b/.gitignore index 826d2ac0..04c3641c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ composer.lock .vscode .phpunit.result.cache +.php_cs.cache diff --git a/.php_cs.cache b/.php_cs.cache deleted file mode 100644 index 93af1cc0..00000000 --- a/.php_cs.cache +++ /dev/null @@ -1 +0,0 @@ -{"php":"7.2.17-1+ubuntu18.04.1+deb.sury.org+3","version":"2.14.2","rules":{"array_syntax":{"syntax":"short"}},"hashes":{"tests\/suite\/QueryBuilderExtensionTest.php":2401011848,"tests\/suite\/CategoryTestCase.php":1459149732,"tests\/suite\/Support\/helpers.php":2887944969,"tests\/suite\/Support\/Seeders\/ClusterSeeder.php":1677258117,"tests\/suite\/Support\/Seeders\/OrderedCategorySeeder.php":3399630089,"tests\/suite\/Support\/Seeders\/OrderedClusterSeeder.php":3534917921,"tests\/suite\/Support\/Seeders\/MultiScopedCategorySeeder.php":2379713817,"tests\/suite\/Support\/Seeders\/OrderedScopedCategorySeeder.php":1122912534,"tests\/suite\/Support\/Seeders\/ScopedCategorySeeder.php":1867462824,"tests\/suite\/Support\/Seeders\/CategorySeeder.php":4060707526,"tests\/suite\/Support\/Models\/OrderedCategory.php":1127747670,"tests\/suite\/Support\/Models\/OrderedScopedCategory.php":3079717558,"tests\/suite\/Support\/Models\/ScopedCluster.php":3122942005,"tests\/suite\/Support\/Models\/Cluster.php":2187538680,"tests\/suite\/Support\/Models\/OrderedCluster.php":200827106,"tests\/suite\/Support\/Models\/MultiScopedCategory.php":2608465521,"tests\/suite\/Support\/Models\/Category.php":891317858,"tests\/suite\/Support\/Models\/MultiScopedCluster.php":1919760292,"tests\/suite\/Support\/Models\/SoftCluster.php":3352690176,"tests\/suite\/Support\/Models\/SoftCategory.php":3796899808,"tests\/suite\/Support\/Models\/ScopedCategory.php":1579628681,"tests\/suite\/Support\/Migrators\/ClusterMigrator.php":1940264335,"tests\/suite\/Support\/Migrators\/CategoryMigrator.php":314973804,"tests\/suite\/Category\/CategorySoftDeletesTest.php":2524683052,"tests\/suite\/Category\/CategoryRelationsTest.php":702111616,"tests\/suite\/Category\/CategoryScopedTest.php":1926983763,"tests\/suite\/Category\/CategoryTreeMapperTest.php":2492303857,"tests\/suite\/Category\/CategoryHierarchyTest.php":990981592,"tests\/suite\/Category\/CategoryTreeRebuildingTest.php":3068289855,"tests\/suite\/Category\/CategoryTreeValidationTest.php":3527788738,"tests\/suite\/Category\/CategoryMovementTest.php":2090494941,"tests\/suite\/Category\/CategoryColumnsTest.php":2084740264,"tests\/suite\/Category\/CategoryCustomEventsTest.php":715441256,"tests\/suite\/ClusterTestCase.php":1139337425,"tests\/suite\/BaumTestCase.php":3604825002,"tests\/suite\/Cluster\/ClusterHierarchyTest.php":2678987013,"tests\/suite\/Cluster\/ClusterColumnsTest.php":4212176167,"tests\/suite\/Cluster\/ClusterMovementTest.php":681419415,"tests\/suite\/NodeModelExtensionsTest.php":4270767412}} \ No newline at end of file From 75b02fd4e6912499677c94349e2351a33e870c1d Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Tue, 1 Sep 2020 16:55:16 +0200 Subject: [PATCH 43/62] Move code around. --- src/NestedSet/Concerns/Relatable.php | 277 ++++++++++++++++++++++++++ src/NestedSet/Node.php | 283 +-------------------------- 2 files changed, 282 insertions(+), 278 deletions(-) diff --git a/src/NestedSet/Concerns/Relatable.php b/src/NestedSet/Concerns/Relatable.php index 38b980f9..f3befa51 100644 --- a/src/NestedSet/Concerns/Relatable.php +++ b/src/NestedSet/Concerns/Relatable.php @@ -56,6 +56,16 @@ public function getImmediateDescendants($columns = ['*']) return $this->getRelationValue('children'); } + /** + * Equality test. + * + * @param \Baum\NestedSet\Node + * @return boolean + */ + public function equals($other) + { + return ($this == $other); + } /** * Returns true if this is a root node. @@ -77,6 +87,16 @@ public function isLeaf() return ($this->getRight() - $this->getLeft() === 1); } + /** + * Returns true if this is a trunk node (not root or leaf). + * + * @return boolean + */ + public function isTrunk() + { + return !$this->isRoot() && !$this->isLeaf(); + } + /** * Returns true if this is a child node. * @@ -164,4 +184,261 @@ public function insideSubtree($node) $this->inSameScope($node) ); } + + /** + * Instance scope which targes all the ancestor chain nodes including + * the current one. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function ancestorsAndSelf() + { + return $this->newQuery() + ->where($this->getLeftColumnName(), '<=', $this->getLeft()) + ->where($this->getRightColumnName(), '>=', $this->getRight()); + } + + /** + * Get all the ancestor chain from the database including the current node. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getAncestorsAndSelf($columns = ['*']) + { + return $this->ancestorsAndSelf()->get($columns); + } + + /** + * Get all the ancestor chain from the database including the current node + * but without the root node. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getAncestorsAndSelfWithoutRoot($columns = ['*']) + { + return $this->ancestorsAndSelf()->withoutRoot()->get($columns); + } + + /** + * Instance scope which targets all the ancestor chain nodes excluding + * the current one. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function ancestors() + { + return $this->ancestorsAndSelf()->withoutSelf(); + } + + /** + * Get all the ancestor chain from the database excluding the current node. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getAncestors($columns = ['*']) + { + return $this->ancestors()->get($columns); + } + + /** + * Get all the ancestor chain from the database excluding the current node + * and the root node (from the current node's perspective). + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getAncestorsWithoutRoot($columns = ['*']) + { + return $this->ancestors()->withoutRoot()->get($columns); + } + + /** + * Instance scope which targets all children of the parent, including self. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function siblingsAndSelf() + { + return $this->newQuery() + ->where($this->getParentColumnName(), $this->getParentKey()); + } + + /** + * Get all children of the parent, including self. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getSiblingsAndSelf($columns = ['*']) + { + return $this->siblingsAndSelf()->get($columns); + } + + /** + * Instance scope targeting all children of the parent, except self. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function siblings() + { + return $this->siblingsAndSelf()->withoutSelf(); + } + + /** + * Return all children of the parent, except self. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getSiblings($columns = ['*']) + { + return $this->siblings()->get($columns); + } + + /** + * Instance scope targeting all of its nested children which do not have + * children. + * + * @return \Illuminate\Database\Query\Builder + */ + public function leaves() + { + $grammar = $this->getConnection()->getQueryGrammar(); + + $rgtCol = $grammar->wrap($this->getQualifiedRightColumnName()); + $lftCol = $grammar->wrap($this->getQualifiedLeftColumnName()); + + return $this->descendants() + ->whereRaw($rgtCol . ' - ' . $lftCol . ' = 1'); + } + + /** + * Return all of its nested children which do not have children. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getLeaves($columns = ['*']) + { + return $this->leaves()->get($columns); + } + + /** + * Instance scope targeting all of its nested children which are between the + * root and the leaf nodes (middle branch). + * + * @return \Illuminate\Database\Query\Builder + */ + public function trunks() + { + $grammar = $this->getConnection()->getQueryGrammar(); + + $rgtCol = $grammar->wrap($this->getQualifiedRightColumnName()); + $lftCol = $grammar->wrap($this->getQualifiedLeftColumnName()); + + return $this->descendants() + ->whereNotNull($this->getQualifiedParentColumnName()) + ->whereRaw($rgtCol . ' - ' . $lftCol . ' != 1'); + } + + /** + * Return all of its nested children which are trunks. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getTrunks($columns = ['*']) + { + return $this->trunks()->get($columns); + } + + /** + * Scope targeting itself and all of its nested children. + * + * @return \Illuminate\Database\Query\Builder + */ + public function descendantsAndSelf() + { + return $this->newQuery() + ->where($this->getLeftColumnName(), '>=', $this->getLeft()) + ->where($this->getLeftColumnName(), '<', $this->getRight()); + } + + /** + * Retrieve all nested children an self. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getDescendantsAndSelf($columns = ['*']) + { + if (is_array($columns)) { + return $this->descendantsAndSelf()->get($columns); + } + + $arguments = func_get_args(); + + $limit = intval(array_shift($arguments)); + $columns = array_shift($arguments) ?: ['*']; + + return $this->descendantsAndSelf()->limitDepth($limit)->get($columns); + } + + /** + * Set of all children & nested children. + * + * @return \Illuminate\Database\Query\Builder + */ + public function descendants() + { + return $this->descendantsAndSelf()->withoutSelf(); + } + + /** + * Retrieve all of its children & nested children. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getDescendants($columns = ['*']) + { + if (is_array($columns)) { + return $this->descendants()->get($columns); + } + + $arguments = func_get_args(); + + $limit = intval(array_shift($arguments)); + $columns = array_shift($arguments) ?: ['*']; + + return $this->descendants()->limitDepth($limit)->get($columns); + } + + /** + * Returns the first sibling to the left. + * + * @return NestedSet + */ + public function getLeftSibling() + { + return $this->siblings() + ->where($this->getLeftColumnName(), '<', $this->getLeft()) + ->orderBy($this->getOrderColumnName(), 'desc') + ->first(); + } + + /** + * Returns the first sibling to the right. + * + * @return NestedSet + */ + public function getRightSibling() + { + return $this->siblings() + ->where($this->getLeftColumnName(), '>', $this->getLeft()) + ->first(); + } } diff --git a/src/NestedSet/Node.php b/src/NestedSet/Node.php index 818d1572..4e9934b4 100644 --- a/src/NestedSet/Node.php +++ b/src/NestedSet/Node.php @@ -38,6 +38,11 @@ public static function bootNode() static::addGlobalScope(new Scopes\ScopedByScope); } + /** + * Return a new query object *without* the nested set global scopes applied. + * + * @return \Illuminate\Database\Eloquent\Builder + */ public function newQueryWithoutNestedSetScopes() { return $this->newQuery() @@ -52,17 +57,6 @@ public function newQueryWithoutNestedSetScopes() */ protected static $moveToNewParentId = null; - /** - * Equals? - * - * @param \Baum\NestedSet\Node - * @return boolean - */ - public function equals($node) - { - return ($this == $node); - } - /** * Returns the first root node. * @@ -323,16 +317,6 @@ public function scopeLimitDepth($query, $limit) return $query->whereBetween($this->getDepthColumnName(), [min($scopes), max($scopes)]); } - /** - * Returns true if this is a trunk node (not root or leaf). - * - * @return boolean - */ - public function isTrunk() - { - return !$this->isRoot() && !$this->isLeaf(); - } - /** * Returns the root node starting at the current node. * @@ -353,263 +337,6 @@ public function getRoot() } } - /** - * Instance scope which targes all the ancestor chain nodes including - * the current one. - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function ancestorsAndSelf() - { - return $this->newQuery() - ->where($this->getLeftColumnName(), '<=', $this->getLeft()) - ->where($this->getRightColumnName(), '>=', $this->getRight()); - } - - /** - * Get all the ancestor chain from the database including the current node. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getAncestorsAndSelf($columns = ['*']) - { - return $this->ancestorsAndSelf()->get($columns); - } - - /** - * Get all the ancestor chain from the database including the current node - * but without the root node. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getAncestorsAndSelfWithoutRoot($columns = ['*']) - { - return $this->ancestorsAndSelf()->withoutRoot()->get($columns); - } - - /** - * Instance scope which targets all the ancestor chain nodes excluding - * the current one. - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function ancestors() - { - return $this->ancestorsAndSelf()->withoutSelf(); - } - - /** - * Get all the ancestor chain from the database excluding the current node. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getAncestors($columns = ['*']) - { - return $this->ancestors()->get($columns); - } - - /** - * Get all the ancestor chain from the database excluding the current node - * and the root node (from the current node's perspective). - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getAncestorsWithoutRoot($columns = ['*']) - { - return $this->ancestors()->withoutRoot()->get($columns); - } - - /** - * Instance scope which targets all children of the parent, including self. - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function siblingsAndSelf() - { - return $this->newQuery() - ->where($this->getParentColumnName(), $this->getParentKey()); - } - - /** - * Get all children of the parent, including self. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getSiblingsAndSelf($columns = ['*']) - { - return $this->siblingsAndSelf()->get($columns); - } - - /** - * Instance scope targeting all children of the parent, except self. - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function siblings() - { - return $this->siblingsAndSelf()->withoutSelf(); - } - - /** - * Return all children of the parent, except self. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getSiblings($columns = ['*']) - { - return $this->siblings()->get($columns); - } - - /** - * Instance scope targeting all of its nested children which do not have - * children. - * - * @return \Illuminate\Database\Query\Builder - */ - public function leaves() - { - $grammar = $this->getConnection()->getQueryGrammar(); - - $rgtCol = $grammar->wrap($this->getQualifiedRightColumnName()); - $lftCol = $grammar->wrap($this->getQualifiedLeftColumnName()); - - return $this->descendants() - ->whereRaw($rgtCol . ' - ' . $lftCol . ' = 1'); - } - - /** - * Return all of its nested children which do not have children. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getLeaves($columns = ['*']) - { - return $this->leaves()->get($columns); - } - - /** - * Instance scope targeting all of its nested children which are between the - * root and the leaf nodes (middle branch). - * - * @return \Illuminate\Database\Query\Builder - */ - public function trunks() - { - $grammar = $this->getConnection()->getQueryGrammar(); - - $rgtCol = $grammar->wrap($this->getQualifiedRightColumnName()); - $lftCol = $grammar->wrap($this->getQualifiedLeftColumnName()); - - return $this->descendants() - ->whereNotNull($this->getQualifiedParentColumnName()) - ->whereRaw($rgtCol . ' - ' . $lftCol . ' != 1'); - } - - /** - * Return all of its nested children which are trunks. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getTrunks($columns = ['*']) - { - return $this->trunks()->get($columns); - } - - /** - * Scope targeting itself and all of its nested children. - * - * @return \Illuminate\Database\Query\Builder - */ - public function descendantsAndSelf() - { - return $this->newQuery() - ->where($this->getLeftColumnName(), '>=', $this->getLeft()) - ->where($this->getLeftColumnName(), '<', $this->getRight()); - } - - /** - * Retrieve all nested children an self. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getDescendantsAndSelf($columns = ['*']) - { - if (is_array($columns)) { - return $this->descendantsAndSelf()->get($columns); - } - - $arguments = func_get_args(); - - $limit = intval(array_shift($arguments)); - $columns = array_shift($arguments) ?: ['*']; - - return $this->descendantsAndSelf()->limitDepth($limit)->get($columns); - } - - /** - * Set of all children & nested children. - * - * @return \Illuminate\Database\Query\Builder - */ - public function descendants() - { - return $this->descendantsAndSelf()->withoutSelf(); - } - - /** - * Retrieve all of its children & nested children. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getDescendants($columns = ['*']) - { - if (is_array($columns)) { - return $this->descendants()->get($columns); - } - - $arguments = func_get_args(); - - $limit = intval(array_shift($arguments)); - $columns = array_shift($arguments) ?: ['*']; - - return $this->descendants()->limitDepth($limit)->get($columns); - } - - /** - * Returns the first sibling to the left. - * - * @return NestedSet - */ - public function getLeftSibling() - { - return $this->siblings() - ->where($this->getLeftColumnName(), '<', $this->getLeft()) - ->orderBy($this->getOrderColumnName(), 'desc') - ->first(); - } - - /** - * Returns the first sibling to the right. - * - * @return NestedSet - */ - public function getRightSibling() - { - return $this->siblings() - ->where($this->getLeftColumnName(), '>', $this->getLeft()) - ->first(); - } - /** * Sets the depth attribute for the current node and all of its descendants. * From 0e9d4880d301d53791c1c03c5be513b4e3526c00 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Tue, 1 Sep 2020 17:19:11 +0200 Subject: [PATCH 44/62] Changes to composer.json (dev packages & php ver.) --- composer.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 4c5e73e8..7018f4d0 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ } ], "require": { - "php": "^7.1.3", + "php": "^7.1.3 || ^8.0", "illuminate/contracts": "~5.7.0|~5.8.0|^6.0|^7.0", "illuminate/database": "~5.7.0|~5.8.0|^6.0|^7.0", "illuminate/events": "~5.7.0|~5.8.0|^6.0|^7.0", @@ -22,7 +22,9 @@ "mockery/mockery": "^1.0", "phpunit/phpunit": "^7.5|^8.0", "psy/psysh": "@stable", - "squizlabs/php_codesniffer": "^3.4" + "squizlabs/php_codesniffer": "^3.4", + "illuminate/console": "~5.7.0|~5.8.0|^6.0|^7.0", + "illuminate/filesystem": "~5.7.0|~5.8.0|^6.0|^7.0" }, "autoload": { "psr-4": { From 07eb17639093a923408f898f5c536c0db22704a6 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Tue, 1 Sep 2020 19:07:55 +0200 Subject: [PATCH 45/62] Minor model & migration generator fixes. --- src/Console/MakeModelCommand.php | 16 +++++++++++++++- src/Console/MigrationCreator.php | 13 +++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Console/MakeModelCommand.php b/src/Console/MakeModelCommand.php index 11021cfe..e3139c8a 100644 --- a/src/Console/MakeModelCommand.php +++ b/src/Console/MakeModelCommand.php @@ -62,7 +62,7 @@ protected function getStub() */ protected function createMigration() { - $table = Str::snake(Str::pluralStudly(class_basename($this->argument('name')))); + $table = $this->getMigrationTableName(); $this->call('baum:make-migration', ['name' => "create_{$table}_table", '--create' => $table]); } @@ -80,4 +80,18 @@ protected function getOptions() ['migration', 'm', InputOption::VALUE_NONE, 'Create a new migration file for the model'], ]; } + + /** + * Get the migration table name. + * + * @return string + */ + public function getMigrationTableName() + { + if (method_exists(Str::class, 'pluralStudly')) { + return Str::snake(Str::pluralStudly(class_basename($this->argument('name')))); + } + + return Str::plural(Str::snake(class_basename($this->argument('name')))); + } } diff --git a/src/Console/MigrationCreator.php b/src/Console/MigrationCreator.php index 52ebaa18..62adbaab 100644 --- a/src/Console/MigrationCreator.php +++ b/src/Console/MigrationCreator.php @@ -2,10 +2,23 @@ namespace Baum\Console; +use Illuminate\Filesystem\Filesystem; use Illuminate\Database\Migrations\MigrationCreator as BaseMigrationCreator; class MigrationCreator extends BaseMigrationCreator { + /** + * Create a new migration creator instance. + * + * @param \Illuminate\Filesystem\Filesystem $files + * @param string|null $customStubPath + * @return void + */ + public function __construct(Filesystem $files, $customStubPath = null) + { + parent::__construct($files, $customStubPath); + } + /** * Get the migration stub file. * From fa6208a9db3524b35349fa147e1f99f82e714666 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Tue, 1 Sep 2020 19:08:17 +0200 Subject: [PATCH 46/62] Use big integer fields by default. Try to use the same defaults as the framework. --- src/Console/stubs/migration.stub | 12 ++++++------ src/Mixins/Blueprint.php | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Console/stubs/migration.stub b/src/Console/stubs/migration.stub index c859ee75..3adbd6da 100644 --- a/src/Console/stubs/migration.stub +++ b/src/Console/stubs/migration.stub @@ -14,7 +14,7 @@ class DummyClass extends Migration public function up() { Schema::create('DummyTable', function (Blueprint $table) { - $table->increments('id'); + $table->bigIncrements('id'); // We declare here the nested set structure columns/fields $table->nestedSet(); @@ -22,11 +22,11 @@ class DummyClass extends Migration // The previous `nestedSet` blueprint helper is equivalent to // the following column/field declarations: // - // $table->integer('parent_id')->unsigned()->nullable()->index(); - // $table->foreign('parent_id')->references('id')->on($table->getTable()); - // $table->integer('left')->unsigned()->nullable()->index(); - // $table->integer('right')->unsgined()->nullable()->index(); - // $table->integer('depth')->unsigned()->nullable()->index(); + // $table->unsignedBigInteger('parent_id')->nullable()->index(); + // $table->foreign('parent_id')->references('id')->on('DummyTable'); + // $table->unsignedBigInteger('left')->nullable()->index(); + // $table->unsignedBigInteger('right')->nullable()->index(); + // $table->unsignedInteger('depth')->nullable()->index(); // // Feel free to modify at your own will but note that all columns // *must be present* and initialized on the model accordingly. diff --git a/src/Mixins/Blueprint.php b/src/Mixins/Blueprint.php index e8d9f109..21d1e504 100644 --- a/src/Mixins/Blueprint.php +++ b/src/Mixins/Blueprint.php @@ -13,11 +13,11 @@ class Blueprint public function nestedSet() { return function () { - $this->integer('parent_id')->unsigned()->nullable()->index(); + $this->unsignedBigInteger('parent_id')->nullable()->index(); $this->foreign('parent_id')->references('id')->on($this->getTable()); - $this->integer('left')->unsigned()->nullable()->index(); - $this->integer('right')->unsgined()->nullable()->index(); - $this->integer('depth')->unsigned()->nullable()->index(); + $this->unsignedBigInteger('left')->nullable()->index(); + $this->unsignedBigInteger('right')->nullable()->index(); + $this->unsignedInteger('depth')->nullable()->index(); }; } } From 72229d11235dee7a40f403815414c272c776a393 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Tue, 1 Sep 2020 19:32:29 +0200 Subject: [PATCH 47/62] Add latest laravel support. --- composer.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 7018f4d0..9014dcd4 100644 --- a/composer.json +++ b/composer.json @@ -8,23 +8,23 @@ { "name": "Estanislau Trepat", "email": "estanis@etrepat.com", - "homepage": "http://etrepat.com" + "homepage": "https://etrepat.com" } ], "require": { "php": "^7.1.3 || ^8.0", - "illuminate/contracts": "~5.7.0|~5.8.0|^6.0|^7.0", - "illuminate/database": "~5.7.0|~5.8.0|^6.0|^7.0", - "illuminate/events": "~5.7.0|~5.8.0|^6.0|^7.0", - "illuminate/support": "~5.7.0|~5.8.0|^6.0|^7.0" + "illuminate/contracts": "~5.7.0|~5.8.0|^6.0|^7.0|^8.0", + "illuminate/database": "~5.7.0|~5.8.0|^6.0|^7.0|^8.0", + "illuminate/events": "~5.7.0|~5.8.0|^6.0|^7.0|^8.0", + "illuminate/support": "~5.7.0|~5.8.0|^6.0|^7.0|^8.0" }, "require-dev": { "mockery/mockery": "^1.0", "phpunit/phpunit": "^7.5|^8.0", "psy/psysh": "@stable", "squizlabs/php_codesniffer": "^3.4", - "illuminate/console": "~5.7.0|~5.8.0|^6.0|^7.0", - "illuminate/filesystem": "~5.7.0|~5.8.0|^6.0|^7.0" + "illuminate/console": "~5.7.0|~5.8.0|^6.0|^7.0|^8.0", + "illuminate/filesystem": "~5.7.0|~5.8.0|^6.0|^7.0|^8.0" }, "autoload": { "psr-4": { From 582dae08d28a2b80a9f8308c620b4114bdaebf3f Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Tue, 1 Sep 2020 19:33:03 +0200 Subject: [PATCH 48/62] Add laravel's develop branch to travis. --- .travis.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index 3cd6e0ad..e6190579 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,7 @@ env: - LARAVEL=5.8.* - LARAVEL=^6.0 - LARAVEL=^7.0 + - LARAVEL=dev-develop matrix: fast_finish: true @@ -24,6 +25,12 @@ matrix: env: LARAVEL=^6.0 - php: '7.1' env: LARAVEL=^7.0 + - php: '7.1' + env: LARAVEL=dev-develop + - php: '7.2' + env: LARAVEL=dev-develop + allow_failures: + - env: LARAVEL=dev-develop before_install: - phpenv config-rm xdebug.ini || true From 485d66e9ab7b83586992204f05a7304d1de45fe3 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Wed, 2 Sep 2020 12:18:42 +0200 Subject: [PATCH 49/62] Use ^8.0 branch alias for latest laravel on travis. --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index e6190579..5b4fb2a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ env: - LARAVEL=5.8.* - LARAVEL=^6.0 - LARAVEL=^7.0 - - LARAVEL=dev-develop + - LARAVEL=^8.0 matrix: fast_finish: true @@ -26,11 +26,11 @@ matrix: - php: '7.1' env: LARAVEL=^7.0 - php: '7.1' - env: LARAVEL=dev-develop + env: LARAVEL=^8.0 - php: '7.2' - env: LARAVEL=dev-develop + env: LARAVEL=^8.0 allow_failures: - - env: LARAVEL=dev-develop + - env: LARAVEL=^8.0 before_install: - phpenv config-rm xdebug.ini || true From fd3ff4de70bf8cc7b100d07d89223b2779ad60f0 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Wed, 2 Sep 2020 12:30:25 +0200 Subject: [PATCH 50/62] Allow latest phpunit version. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 9014dcd4..9c3f65e7 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ }, "require-dev": { "mockery/mockery": "^1.0", - "phpunit/phpunit": "^7.5|^8.0", + "phpunit/phpunit": "^7.5|^8.0|^9.0", "psy/psysh": "@stable", "squizlabs/php_codesniffer": "^3.4", "illuminate/console": "~5.7.0|~5.8.0|^6.0|^7.0|^8.0", From efb66126d7b920a4ca8c1adc5a3dbd7786ac4eb9 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Wed, 2 Sep 2020 17:49:21 +0200 Subject: [PATCH 51/62] Extract depth-related methods into their own trait. --- src/NestedSet/Concerns/HasDepth.php | 120 ++++++++++++++++++++++++++++ src/NestedSet/Node.php | 115 +------------------------- 2 files changed, 121 insertions(+), 114 deletions(-) create mode 100644 src/NestedSet/Concerns/HasDepth.php diff --git a/src/NestedSet/Concerns/HasDepth.php b/src/NestedSet/Concerns/HasDepth.php new file mode 100644 index 00000000..2fdf830d --- /dev/null +++ b/src/NestedSet/Concerns/HasDepth.php @@ -0,0 +1,120 @@ +getParentKey())) { + return 0; + } + + return $this->computeLevel(); + } + + /** + * Compute current node level. If could not move past ourseleves return + * our ancestor count, otherwhise get the first parent level + the computed + * nesting. + * + * @return integer + */ + protected function computeLevel() + { + list($node, $nesting) = $this->determineDepth($this); + + if ($node->equals($this)) { + return $this->ancestors()->count(); + } + + return $node->getLevel() + $nesting; + } + + /** + * Return an array with the last node we could reach and its nesting level + * + * @param Baum\Node $node + * @param integer $nesting + * @return array + */ + protected function determineDepth($node, $nesting = 0) + { + // Traverse back up the ancestry chain and add to the nesting level count + while ($parent = $node->parent()->first()) { + $nesting = $nesting + 1; + + $node = $parent; + } + + return [$node, $nesting]; + } + + /** + * Sets the depth attribute + * + * @return \Baum\Node + */ + public function setDepth() + { + $this->getConnection()->transaction(function () { + $this->refresh(); + + $level = $this->getLevel(); + + $this->newQuery()->where($this->getKeyName(), '=', $this->getKey())->update([$this->getDepthColumnName() => $level]); + $this->setAttribute($this->getDepthColumnName(), $level); + }); + + return $this; + } + + /** + * Provides a depth level limit for the query. + * + * @param query \Illuminate\Database\Query\Builder + * @param limit integer + * @return \Illuminate\Database\Query\Builder + */ + public function scopeLimitDepth($query, $limit) + { + $depth = $this->exists ? $this->getDepth() : $this->getLevel(); + $max = $depth + $limit; + $scopes = [$depth, $max]; + + return $query->whereBetween($this->getDepthColumnName(), [min($scopes), max($scopes)]); + } + + /** + * Sets the depth attribute for the current node and all of its descendants. + * + * @return \Baum\Node + */ + public function setDepthWithSubtree() + { + $this->getConnection()->transaction(function () { + $this->refresh(); + + $this->descendantsAndSelf()->select($this->getKeyName())->lockForUpdate()->get(); + + $oldDepth = !is_null($this->getDepth()) ? $this->getDepth() : 0; + + $newDepth = $this->getLevel(); + + $this->newQuery()->where($this->getKeyName(), '=', $this->getKey())->update([$this->getDepthColumnName() => $newDepth]); + $this->setAttribute($this->getDepthColumnName(), $newDepth); + + $diff = $newDepth - $oldDepth; + if (!$this->isLeaf() && $diff != 0) { + $this->descendants()->increment($this->getDepthColumnName(), $diff); + } + }); + + return $this; + } +} diff --git a/src/NestedSet/Node.php b/src/NestedSet/Node.php index 4e9934b4..f3c90a54 100644 --- a/src/NestedSet/Node.php +++ b/src/NestedSet/Node.php @@ -21,6 +21,7 @@ trait Node Concerns\WorksWithSoftDeletes, Concerns\CanBeScoped, Concerns\Relatable, + Concerns\HasDepth, Concerns\Movable, Concerns\Validatable, Concerns\Rebuildable, @@ -79,57 +80,6 @@ public static function roots() return $instance->newQuery()->whereNull($instance->getParentColumnName()); } - /** - * Returns the level of this node in the tree. - * Root level is 0. - * - * @return int - */ - public function getLevel() - { - if (is_null($this->getParentKey())) { - return 0; - } - - return $this->computeLevel(); - } - - /** - * Compute current node level. If could not move past ourseleves return - * our ancestor count, otherwhise get the first parent level + the computed - * nesting. - * - * @return integer - */ - protected function computeLevel() - { - list($node, $nesting) = $this->determineDepth($this); - - if ($node->equals($this)) { - return $this->ancestors()->count(); - } - - return $node->getLevel() + $nesting; - } - - /** - * Return an array with the last node we could reach and its nesting level - * - * @param Baum\Node $node - * @param integer $nesting - * @return array - */ - protected function determineDepth($node, $nesting = 0) - { - // Traverse back up the ancestry chain and add to the nesting level count - while ($parent = $node->parent()->first()) { - $nesting = $nesting + 1; - - $node = $parent; - } - - return [$node, $nesting]; - } /** * Sets default values for left and right fields. @@ -175,25 +125,6 @@ public function moveToNewParent() } } - /** - * Sets the depth attribute - * - * @return \Baum\Node - */ - public function setDepth() - { - $this->getConnection()->transaction(function () { - $this->refresh(); - - $level = $this->getLevel(); - - $this->newQuery()->where($this->getKeyName(), '=', $this->getKey())->update([$this->getDepthColumnName() => $level]); - $this->setAttribute($this->getDepthColumnName(), $level); - }); - - return $this; - } - /** * Prunes a branch off the tree, shifting all the elements on the right * back to the left so the counts work. @@ -301,22 +232,6 @@ public function scopeWithoutRoot($query) return $this->scopeWithoutNode($query, $this->getRoot()); } - /** - * Provides a depth level limit for the query. - * - * @param query \Illuminate\Database\Query\Builder - * @param limit integer - * @return \Illuminate\Database\Query\Builder - */ - public function scopeLimitDepth($query, $limit) - { - $depth = $this->exists ? $this->getDepth() : $this->getLevel(); - $max = $depth + $limit; - $scopes = [$depth, $max]; - - return $query->whereBetween($this->getDepthColumnName(), [min($scopes), max($scopes)]); - } - /** * Returns the root node starting at the current node. * @@ -337,34 +252,6 @@ public function getRoot() } } - /** - * Sets the depth attribute for the current node and all of its descendants. - * - * @return \Baum\Node - */ - public function setDepthWithSubtree() - { - $this->getConnection()->transaction(function () { - $this->refresh(); - - $this->descendantsAndSelf()->select($this->getKeyName())->lockForUpdate()->get(); - - $oldDepth = !is_null($this->getDepth()) ? $this->getDepth() : 0; - - $newDepth = $this->getLevel(); - - $this->newQuery()->where($this->getKeyName(), '=', $this->getKey())->update([$this->getDepthColumnName() => $newDepth]); - $this->setAttribute($this->getDepthColumnName(), $newDepth); - - $diff = $newDepth - $oldDepth; - if (!$this->isLeaf() && $diff != 0) { - $this->descendants()->increment($this->getDepthColumnName(), $diff); - } - }); - - return $this; - } - /** * Return an key-value array indicating the node's depth with $seperator * From 4787581fcd796d07a2e9babee7323e65baf9c595 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Wed, 2 Sep 2020 17:53:33 +0200 Subject: [PATCH 52/62] Recover 'immediateDescendants' alias. --- src/NestedSet/Concerns/Relatable.php | 29 +++++++++------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/src/NestedSet/Concerns/Relatable.php b/src/NestedSet/Concerns/Relatable.php index f3befa51..78623408 100644 --- a/src/NestedSet/Concerns/Relatable.php +++ b/src/NestedSet/Concerns/Relatable.php @@ -24,25 +24,15 @@ public function children() return $this->hasMany(get_class($this), $this->getParentColumnName()); } - // /** - // * Inmmediate descendants relation. Alias for "children". - // * - // * @return \Illuminate\Database\Eloquent\Relations\HasMany - // */ - // public function immediateDescendants() - // { - // return $this->children(); - // } - - // /** - // * Attribute alias so as to eager-load the proper relationship. - // * - // * @return mixed - // */ - // public function getImmediateDescendantsAttribute() - // { - // return $this->getRelationValue('children'); - // } + /** + * Inmmediate descendants relation. Alias for "children". + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function immediateDescendants() + { + return $this->children(); + } /** * Retrive all of its "immediate" descendants. @@ -52,7 +42,6 @@ public function children() */ public function getImmediateDescendants($columns = ['*']) { - // return $this->children()->get($columns); return $this->getRelationValue('children'); } From 1da4480ea23b23b6ac270b4bed93251e9059be41 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Wed, 2 Sep 2020 18:01:15 +0200 Subject: [PATCH 53/62] Add movement related utilities into corresponding trait. --- src/NestedSet/Concerns/Movable.php | 38 ++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/NestedSet/Concerns/Movable.php b/src/NestedSet/Concerns/Movable.php index fdbf6d03..448a772d 100644 --- a/src/NestedSet/Concerns/Movable.php +++ b/src/NestedSet/Concerns/Movable.php @@ -6,6 +6,13 @@ trait Movable { + /** + * Indicates whether we should move to a new parent. + * + * @var int + */ + protected static $moveToNewParentId = null; + /** * Main move method. Here we handle all node movements with the corresponding * lft/rgt index updates. @@ -132,4 +139,35 @@ public function makeRoot() { return $this->moveTo($this, 'root'); } + + /** + * Store the parent_id if the attribute is modified so as we are able to move + * the node to this new parent after saving. + * + * @return void + */ + public function storeNewParent() + { + if ($this->isDirty($this->getParentColumnName()) && ($this->exists || !$this->isRoot())) { + static::$moveToNewParentId = $this->getParentKey(); + } else { + static::$moveToNewParentId = false; + } + } + + /** + * Move to the new parent if appropiate. + * + * @return void + */ + public function moveToNewParent() + { + $pid = static::$moveToNewParentId; + + if (is_null($pid)) { + $this->makeRoot(); + } elseif ($pid !== false) { + $this->makeChildOf($pid); + } + } } From 7c317be953582531ce32a3bc364361c9c18b2bb1 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Wed, 2 Sep 2020 18:03:11 +0200 Subject: [PATCH 54/62] Move method to where it *should* be. --- src/NestedSet/Concerns/Relatable.php | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/NestedSet/Concerns/Relatable.php b/src/NestedSet/Concerns/Relatable.php index 78623408..bb388314 100644 --- a/src/NestedSet/Concerns/Relatable.php +++ b/src/NestedSet/Concerns/Relatable.php @@ -430,4 +430,38 @@ public function getRightSibling() ->where($this->getLeftColumnName(), '>', $this->getLeft()) ->first(); } + + /** + * Prunes a branch off the tree, shifting all the elements on the right + * back to the left so the counts work. + * + * @return void; + */ + public function destroyDescendants() + { + if (is_null($this->getRight()) || is_null($this->getLeft())) { + return; + } + + $this->getConnection()->transaction(function () { + $this->refresh(); + + $lftCol = $this->getQualifiedLeftColumnName(); + $rgtCol = $this->getQualifiedRightColumnName(); + $lft = $this->getLeft(); + $rgt = $this->getRight(); + + // Apply a lock to the rows which fall past the deletion point + $this->newQuery()->where($lftCol, '>=', $lft)->select($this->getQualifiedKeyName())->lockForUpdate()->get(); + + // Prune children + $this->newQuery()->where($lftCol, '>', $lft)->where($rgtCol, '<', $rgt)->delete(); + + // Update left and right indexes for the remaining nodes + $diff = $rgt - $lft + 1; + + $this->newQuery()->where($lftCol, '>', $rgt)->decrement($lftCol, $diff); + $this->newQuery()->where($rgtCol, '>', $rgt)->decrement($rgtCol, $diff); + }); + } } From 24c331d6fef50b0e112ea2854f7d88cd5f25a4dc Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Wed, 2 Sep 2020 18:03:19 +0200 Subject: [PATCH 55/62] Cleanup. --- src/NestedSet/Node.php | 73 ------------------------------------------ 1 file changed, 73 deletions(-) diff --git a/src/NestedSet/Node.php b/src/NestedSet/Node.php index f3c90a54..6736fba5 100644 --- a/src/NestedSet/Node.php +++ b/src/NestedSet/Node.php @@ -51,13 +51,6 @@ public function newQueryWithoutNestedSetScopes() ->withoutGlobalScope(Scopes\ScopedByScope::class); } - /** - * Indicates whether we should move to a new parent. - * - * @var int - */ - protected static $moveToNewParentId = null; - /** * Returns the first root node. * @@ -80,7 +73,6 @@ public static function roots() return $instance->newQuery()->whereNull($instance->getParentColumnName()); } - /** * Sets default values for left and right fields. * @@ -94,71 +86,6 @@ public function setDefaultLeftAndRight() $this->setAttribute($this->getRightColumnName(), $maxRgt + 2); } - /** - * Store the parent_id if the attribute is modified so as we are able to move - * the node to this new parent after saving. - * - * @return void - */ - public function storeNewParent() - { - if ($this->isDirty($this->getParentColumnName()) && ($this->exists || !$this->isRoot())) { - static::$moveToNewParentId = $this->getParentKey(); - } else { - static::$moveToNewParentId = false; - } - } - - /** - * Move to the new parent if appropiate. - * - * @return void - */ - public function moveToNewParent() - { - $pid = static::$moveToNewParentId; - - if (is_null($pid)) { - $this->makeRoot(); - } elseif ($pid !== false) { - $this->makeChildOf($pid); - } - } - - /** - * Prunes a branch off the tree, shifting all the elements on the right - * back to the left so the counts work. - * - * @return void; - */ - public function destroyDescendants() - { - if (is_null($this->getRight()) || is_null($this->getLeft())) { - return; - } - - $this->getConnection()->transaction(function () { - $this->refresh(); - - $lftCol = $this->getQualifiedLeftColumnName(); - $rgtCol = $this->getQualifiedRightColumnName(); - $lft = $this->getLeft(); - $rgt = $this->getRight(); - - // Apply a lock to the rows which fall past the deletion point - $this->newQuery()->where($lftCol, '>=', $lft)->select($this->getQualifiedKeyName())->lockForUpdate()->get(); - - // Prune children - $this->newQuery()->where($lftCol, '>', $lft)->where($rgtCol, '<', $rgt)->delete(); - - // Update left and right indexes for the remaining nodes - $diff = $rgt - $lft + 1; - - $this->newQuery()->where($lftCol, '>', $rgt)->decrement($lftCol, $diff); - $this->newQuery()->where($rgtCol, '>', $rgt)->decrement($rgtCol, $diff); - }); - } - /** * Static query scope. Returns a query scope with all nodes which are at * the end of a branch. From 4702589cee44b48f25743532855d87561cbee551 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Wed, 10 Aug 2022 17:16:35 +0200 Subject: [PATCH 56/62] Update dependencies. --- composer.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/composer.json b/composer.json index 9c3f65e7..ac492adb 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "baum/baum", "type": "library", "description": "Baum is an implementation of the Nested Set pattern for Eloquent models.", - "keywords": ["nested set", "laravel", "laravel 5", "eloquent", "database"], + "keywords": ["nested set", "laravel", "eloquent", "database", "tree", "hierarchy"], "license": "MIT", "authors": [ { @@ -12,19 +12,19 @@ } ], "require": { - "php": "^7.1.3 || ^8.0", - "illuminate/contracts": "~5.7.0|~5.8.0|^6.0|^7.0|^8.0", - "illuminate/database": "~5.7.0|~5.8.0|^6.0|^7.0|^8.0", - "illuminate/events": "~5.7.0|~5.8.0|^6.0|^7.0|^8.0", - "illuminate/support": "~5.7.0|~5.8.0|^6.0|^7.0|^8.0" + "php": "^7.3|^8.0", + "illuminate/contracts": "^8.0|^9.0", + "illuminate/database": "^8.0|^9.0", + "illuminate/events": "^8.0|^9.0", + "illuminate/support": "^8.0|^9.0" }, "require-dev": { - "mockery/mockery": "^1.0", - "phpunit/phpunit": "^7.5|^8.0|^9.0", + "mockery/mockery": "^1.4.4", + "phpunit/phpunit": "^8.5.19|^9.5.8", "psy/psysh": "@stable", - "squizlabs/php_codesniffer": "^3.4", - "illuminate/console": "~5.7.0|~5.8.0|^6.0|^7.0|^8.0", - "illuminate/filesystem": "~5.7.0|~5.8.0|^6.0|^7.0|^8.0" + "squizlabs/php_codesniffer": "*", + "illuminate/console": "^8.0|^9.0", + "illuminate/filesystem": "^8.0|^9.0" }, "autoload": { "psr-4": { From c0907c5e17939d5b10bf1259233925a93cc07025 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Wed, 10 Aug 2022 18:33:09 +0200 Subject: [PATCH 57/62] Replace travis w/github actions. --- .github/workflows/tests.yml | 51 +++++++++++++++++++++++++++++++++++++ .travis.yml | 42 ------------------------------ 2 files changed, 51 insertions(+), 42 deletions(-) create mode 100644 .github/workflows/tests.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..c5632b47 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,51 @@ +name: Tests + +on: + push: + pull_request: + schedule: + - cron: '0 0 * * *' + +jobs: + tests: + runs-on: ubuntu-20.04 + + strategy: + fail-fast: true + matrix: + php: [7.3, 7.4, 8.0, 8.1] + laravel: [8.0, 9.0] + exclude: + - php: 7.3 + laravel: 9.0 + - php: 7.4 + laravel: 9.0 + + name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + # Docs: https://github.com/shivammathur/setup-php + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: sqlite3 + ini-values: error_reporting=E_ALL + tools: composer:v2 + coverage: none + + - name: Setup problem matchers for PHP/PHPUnit + run: | + echo "::add-matcher::${{ runner.tool_cache }}/php.json" + echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Install dependencies + run: | + composer require "illuminate/contracts=^${{ matrix.laravel }}" --no-update + composer update --prefer-dist --no-interaction --no-progress + + - name: Execute test suite + run: vendor/bin/phpunit --verbose diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5b4fb2a0..00000000 --- a/.travis.yml +++ /dev/null @@ -1,42 +0,0 @@ -dist: bionic - -language: php - -sudo: false - -php: - - '7.1' - - '7.2' - - '7.3' - - '7.4' - -env: - matrix: - - LARAVEL=5.7.* - - LARAVEL=5.8.* - - LARAVEL=^6.0 - - LARAVEL=^7.0 - - LARAVEL=^8.0 - -matrix: - fast_finish: true - exclude: - - php: '7.1' - env: LARAVEL=^6.0 - - php: '7.1' - env: LARAVEL=^7.0 - - php: '7.1' - env: LARAVEL=^8.0 - - php: '7.2' - env: LARAVEL=^8.0 - allow_failures: - - env: LARAVEL=^8.0 - -before_install: - - phpenv config-rm xdebug.ini || true - -install: - - travis_retry composer require "illuminate/contracts=${LARAVEL}" --dev --prefer-dist --no-interaction --no-suggest - -script: - - vendor/bin/phpunit --verbose From 625816eeeaec1831d639457d0ed1d8076155569b Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Tue, 21 Feb 2023 19:04:12 +0100 Subject: [PATCH 58/62] Preliminar support for laravel 10.x --- .github/workflows/tests.yml | 14 +++++++++++--- composer.json | 14 +++++++------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c5632b47..2257e287 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,13 +13,21 @@ jobs: strategy: fail-fast: true matrix: - php: [7.3, 7.4, 8.0, 8.1] - laravel: [8.0, 9.0] + php: [7.3, 7.4, 8.0, 8.1, 8.2] + laravel: [8.0, 9.0, 10.0] exclude: - php: 7.3 laravel: 9.0 + - php: 7.3 + laravel: 10.0 - php: 7.4 laravel: 9.0 + - php: 7.4 + laravel: 10.0 + - php: 8.0 + laravel: 10.0 + - php: 8.2 + laravel: 8.0 name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} @@ -48,4 +56,4 @@ jobs: composer update --prefer-dist --no-interaction --no-progress - name: Execute test suite - run: vendor/bin/phpunit --verbose + run: vendor/bin/phpunit diff --git a/composer.json b/composer.json index ac492adb..612dfe5f 100644 --- a/composer.json +++ b/composer.json @@ -12,19 +12,19 @@ } ], "require": { - "php": "^7.3|^8.0", - "illuminate/contracts": "^8.0|^9.0", - "illuminate/database": "^8.0|^9.0", - "illuminate/events": "^8.0|^9.0", - "illuminate/support": "^8.0|^9.0" + "php": "^7.3|^8.0|^8.1", + "illuminate/contracts": "^8.0|^9.0|^10.0", + "illuminate/database": "^8.0|^9.0|^10.0", + "illuminate/events": "^8.0|^9.0|^10.0", + "illuminate/support": "^8.0|^9.0|^10.0" }, "require-dev": { "mockery/mockery": "^1.4.4", "phpunit/phpunit": "^8.5.19|^9.5.8", "psy/psysh": "@stable", "squizlabs/php_codesniffer": "*", - "illuminate/console": "^8.0|^9.0", - "illuminate/filesystem": "^8.0|^9.0" + "illuminate/console": "^8.0|^9.0|^10.0", + "illuminate/filesystem": "^8.0|^9.0|^10.0" }, "autoload": { "psr-4": { From bfb75d6375242087ce292a4834b1887aa2d624a9 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Tue, 11 Jun 2024 19:13:36 +0200 Subject: [PATCH 59/62] Add preliminar support for laravel 11.x --- .github/workflows/tests.yml | 10 +++++++++- .gitignore | 1 + composer.json | 18 +++++++++--------- phpunit.xml | 27 +++++++++++++-------------- 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2257e287..e6dba544 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,18 +14,26 @@ jobs: fail-fast: true matrix: php: [7.3, 7.4, 8.0, 8.1, 8.2] - laravel: [8.0, 9.0, 10.0] + laravel: [8.0, 9.0, 10.0, 11.0] exclude: - php: 7.3 laravel: 9.0 - php: 7.3 laravel: 10.0 + - php: 7.3 + laravel: 11.0 - php: 7.4 laravel: 9.0 - php: 7.4 laravel: 10.0 + - php: 7.4 + laravel: 11.0 - php: 8.0 laravel: 10.0 + - php: 8.0 + laravel: 11.0 + - php: 8.1 + laravel: 11.0 - php: 8.2 laravel: 8.0 diff --git a/.gitignore b/.gitignore index 04c3641c..60d5fca4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ composer.lock .vscode .phpunit.result.cache .php_cs.cache +.phpunit.cache diff --git a/composer.json b/composer.json index 612dfe5f..64126efd 100644 --- a/composer.json +++ b/composer.json @@ -12,19 +12,19 @@ } ], "require": { - "php": "^7.3|^8.0|^8.1", - "illuminate/contracts": "^8.0|^9.0|^10.0", - "illuminate/database": "^8.0|^9.0|^10.0", - "illuminate/events": "^8.0|^9.0|^10.0", - "illuminate/support": "^8.0|^9.0|^10.0" + "php": "^7.3|^8.0|^8.1|^8.2", + "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0", + "illuminate/database": "^8.0|^9.0|^10.0|^11.0", + "illuminate/events": "^8.0|^9.0|^10.0|^11.0", + "illuminate/support": "^8.0|^9.0|^10.0|^11.0" }, "require-dev": { - "mockery/mockery": "^1.4.4", - "phpunit/phpunit": "^8.5.19|^9.5.8", + "mockery/mockery": "^1.4", + "phpunit/phpunit": "^8.5.19|^9.5.8|^11.0.1", "psy/psysh": "@stable", "squizlabs/php_codesniffer": "*", - "illuminate/console": "^8.0|^9.0|^10.0", - "illuminate/filesystem": "^8.0|^9.0|^10.0" + "illuminate/console": "^8.0|^9.0|^10.0|^11.0", + "illuminate/filesystem": "^8.0|^9.0|^10.0|^11.0" }, "autoload": { "psr-4": { diff --git a/phpunit.xml b/phpunit.xml index 12d26272..b220c029 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,18 +1,17 @@ - + ./tests/suite From 40a7984f07c0cf4aaa4be67e5ae734fd11b1bb85 Mon Sep 17 00:00:00 2001 From: Estanislau Trepat Date: Tue, 11 Jun 2024 20:02:24 +0200 Subject: [PATCH 60/62] Update gh actions. --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e6dba544..ae1d1f47 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,14 +41,14 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Docs: https://github.com/shivammathur/setup-php - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: sqlite3 + extensions: curl, libxml, mbstring, sqlite3 ini-values: error_reporting=E_ALL tools: composer:v2 coverage: none From f44497ec21c29574ce6f0ca405929d11431c3b03 Mon Sep 17 00:00:00 2001 From: ELtd <36067295+ELtd@users.noreply.github.com> Date: Thu, 24 Apr 2025 08:50:03 +0200 Subject: [PATCH 61/62] Add preliminar support for laravel 12.x --- composer.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 64126efd..9102788c 100644 --- a/composer.json +++ b/composer.json @@ -13,18 +13,18 @@ ], "require": { "php": "^7.3|^8.0|^8.1|^8.2", - "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0", - "illuminate/database": "^8.0|^9.0|^10.0|^11.0", - "illuminate/events": "^8.0|^9.0|^10.0|^11.0", - "illuminate/support": "^8.0|^9.0|^10.0|^11.0" + "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/database": "^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/events": "^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0" }, "require-dev": { "mockery/mockery": "^1.4", - "phpunit/phpunit": "^8.5.19|^9.5.8|^11.0.1", + "phpunit/phpunit": "^8.5.19|^9.5.8|^11.0.1|^11.5.3", "psy/psysh": "@stable", "squizlabs/php_codesniffer": "*", - "illuminate/console": "^8.0|^9.0|^10.0|^11.0", - "illuminate/filesystem": "^8.0|^9.0|^10.0|^11.0" + "illuminate/console": "^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/filesystem": "^8.0|^9.0|^10.0|^11.0|^12.0" }, "autoload": { "psr-4": { From c5e0fe5067099aafbd9fca56e6232384069c8618 Mon Sep 17 00:00:00 2001 From: ELtd <36067295+ELtd@users.noreply.github.com> Date: Thu, 24 Apr 2025 08:57:12 +0200 Subject: [PATCH 62/62] Update tests matrix for Laravel 12 Newbie in this kind of config, I just used your previous changes to do mines. --- .github/workflows/tests.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ae1d1f47..0b2cbaac 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,8 +13,8 @@ jobs: strategy: fail-fast: true matrix: - php: [7.3, 7.4, 8.0, 8.1, 8.2] - laravel: [8.0, 9.0, 10.0, 11.0] + php: [7.3, 7.4, 8.0, 8.1, 8.2, 8.3] + laravel: [8.0, 9.0, 10.0, 11.0, 12.0] exclude: - php: 7.3 laravel: 9.0 @@ -22,18 +22,26 @@ jobs: laravel: 10.0 - php: 7.3 laravel: 11.0 + - php: 7.3 + laravel: 12.0 - php: 7.4 laravel: 9.0 - php: 7.4 laravel: 10.0 - php: 7.4 laravel: 11.0 + - php: 7.4 + laravel: 12.0 - php: 8.0 laravel: 10.0 - php: 8.0 laravel: 11.0 + - php: 8.0 + laravel: 12.0 - php: 8.1 laravel: 11.0 + - php: 8.1 + laravel: 12.0 - php: 8.2 laravel: 8.0