From 4c29e78ae7becafcaa4ba592e1dfe7d993755fb8 Mon Sep 17 00:00:00 2001 From: Giacomo Trezzi <giacomo.trezzi@gmail.com> Date: Thu, 30 Sep 2021 00:24:43 +0200 Subject: [PATCH 1/2] Callback parameter when checking or fixing the tree --- src/QueryBuilder.php | 56 +++++++++++++++------- tests/NodeTest.php | 98 +++++++++++++++++++++++++++++++++++++++ tests/models/Category.php | 5 ++ 3 files changed, 142 insertions(+), 17 deletions(-) diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 61aba6a..9cd0680 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -665,23 +665,25 @@ protected function columnPatch($col, array $params) * * @since 2.0 * + * @param null|Closure $callback + * * @return array */ - public function countErrors() + public function countErrors($callback = null) { $checks = []; // Check if lft and rgt values are ok - $checks['oddness'] = $this->getOdnessQuery(); + $checks['oddness'] = $this->getOdnessQuery($callback); // Check if lft and rgt values are unique - $checks['duplicates'] = $this->getDuplicatesQuery(); + $checks['duplicates'] = $this->getDuplicatesQuery($callback); // Check if parent_id is set correctly - $checks['wrong_parent'] = $this->getWrongParentQuery(); + $checks['wrong_parent'] = $this->getWrongParentQuery($callback); // Check for nodes that have missing parent - $checks['missing_parent' ] = $this->getMissingParentQuery(); + $checks['missing_parent' ] = $this->getMissingParentQuery($callback); $query = $this->query->newQuery(); @@ -695,12 +697,15 @@ public function countErrors() } /** + * @param null|Closure $callback + * * @return BaseQueryBuilder */ - protected function getOdnessQuery() + protected function getOdnessQuery($callback = null) { return $this->model ->newNestedSetQuery() + ->when($callback, $callback) ->toBase() ->whereNested(function (BaseQueryBuilder $inner) { list($lft, $rgt) = $this->wrappedColumns(); @@ -711,9 +716,11 @@ protected function getOdnessQuery() } /** + * @param null|Closure $callback + * * @return BaseQueryBuilder */ - protected function getDuplicatesQuery() + protected function getDuplicatesQuery($callback = null) { $table = $this->wrappedTable(); $keyName = $this->wrappedKey(); @@ -726,6 +733,7 @@ protected function getDuplicatesQuery() $query = $this->model ->newNestedSetQuery($firstAlias) + ->when($callback, $callback) ->toBase() ->from($this->query->raw("{$table} as {$waFirst}, {$table} {$waSecond}")) ->whereRaw("{$waFirst}.{$keyName} < {$waSecond}.{$keyName}") @@ -742,9 +750,11 @@ protected function getDuplicatesQuery() } /** + * @param null|Closure $callback + * * @return BaseQueryBuilder */ - protected function getWrongParentQuery() + protected function getWrongParentQuery($callback = null) { $table = $this->wrappedTable(); $keyName = $this->wrappedKey(); @@ -763,6 +773,7 @@ protected function getWrongParentQuery() $query = $this->model ->newNestedSetQuery('c') + ->when($callback, $callback) ->toBase() ->from($this->query->raw("{$table} as {$waChild}, {$table} as {$waParent}, $table as {$waInterm}")) ->whereRaw("{$waChild}.{$parentIdName}={$waParent}.{$keyName}") @@ -783,14 +794,17 @@ protected function getWrongParentQuery() } /** + * @param null|Closure $callback + * * @return $this */ - protected function getMissingParentQuery() + protected function getMissingParentQuery($callback = null) { return $this->model ->newNestedSetQuery() + ->when($callback, $callback) ->toBase() - ->whereNested(function (BaseQueryBuilder $inner) { + ->whereNested(function (BaseQueryBuilder $inner) use($callback) { $grammar = $this->query->getGrammar(); $table = $this->wrappedTable(); @@ -801,6 +815,7 @@ protected function getMissingParentQuery() $existsCheck = $this->model ->newNestedSetQuery() + ->when($callback, $callback) ->toBase() ->selectRaw('1') ->from($this->query->raw("{$table} as {$wrappedAlias}")) @@ -817,25 +832,29 @@ protected function getMissingParentQuery() /** * Get the number of total errors of the tree. * + * @param null|Closure $callback + * * @since 2.0 * * @return int */ - public function getTotalErrors() + public function getTotalErrors($callback = null) { - return array_sum($this->countErrors()); + return array_sum($this->countErrors($callback)); } /** * Get whether the tree is broken. * + * @param null|Closure $callback + * * @since 2.0 * * @return bool */ - public function isBroken() + public function isBroken($callback = null) { - return $this->getTotalErrors() > 0; + return $this->getTotalErrors($callback) > 0; } /** @@ -844,10 +863,11 @@ public function isBroken() * Nodes with invalid parent are saved as roots. * * @param null|NodeTrait|Model $root + * @param null|Closure $callback * * @return int The number of changed nodes */ - public function fixTree($root = null) + public function fixTree($root = null, $callback = null) { $columns = [ $this->model->getKeyName(), @@ -858,6 +878,7 @@ public function fixTree($root = null) $dictionary = $this->model ->newNestedSetQuery() + ->when($callback, $callback) ->when($root, function (self $query) use ($root) { return $query->whereDescendantOf($root); }) @@ -871,12 +892,13 @@ public function fixTree($root = null) /** * @param NodeTrait|Model $root + * @param null|Closure $callback * * @return int */ - public function fixSubtree($root) + public function fixSubtree($root, $callback = null) { - return $this->fixTree($root); + return $this->fixTree($root, $callback = null); } /** diff --git a/tests/NodeTest.php b/tests/NodeTest.php index 7c2fbae..7b0c9a8 100644 --- a/tests/NodeTest.php +++ b/tests/NodeTest.php @@ -2,6 +2,7 @@ use Illuminate\Database\Capsule\Manager as Capsule; use Kalnoy\Nestedset\NestedSet; +use Kalnoy\Nestedset\QueryBuilder; class NodeTest extends PHPUnit\Framework\TestCase { @@ -124,6 +125,21 @@ public function testTreeNotBroken() $this->assertFalse(Category::isBroken()); } + public function testTreeNotEvenWithGlobalScopesBroken() + { + $this->assertTreeNotBroken(); + + Category::addGlobalScope('aScope', function($query){ + $query->whereIn('categories.name', ['apple', 'samsung', 'sony']); + }); + + $this->assertFalse(Category::isBroken(function($query){ + $query->withoutGlobalScope('aScope'); + })); + + Category::removeScope('aScope'); + } + public function nodeValues($node) { return array($node->_lft, $node->_rgt, $node->parent_id); @@ -596,6 +612,35 @@ public function testCountsTreeErrors() $this->assertEquals(1, $errors['missing_parent']); } + public function testCountsTreeErrorsWithGlobalScopes() + { + $errors = Category::countErrors(); + + $this->assertEquals([ 'oddness' => 0, + 'duplicates' => 0, + 'wrong_parent' => 0, + 'missing_parent' => 0 ], $errors); + + Category::where('id', '=', 5)->update([ '_lft' => 14 ]); + Category::where('id', '=', 8)->update([ 'parent_id' => 2 ]); + Category::where('id', '=', 11)->update([ '_lft' => 20 ]); + Category::where('id', '=', 4)->update([ 'parent_id' => 24 ]); + + Category::addGlobalScope('aScope', function($query){ + $query->whereIn('categories.name', ['apple', 'samsung', 'sony']); + }); + + $errors = Category::countErrors(function($query){ + $query->withoutGlobalScope('aScope'); + }); + + $this->assertEquals(1, $errors['oddness']); + $this->assertEquals(2, $errors['duplicates']); + $this->assertEquals(1, $errors['missing_parent']); + + Category::removeScope('aScope'); + } + public function testCreatesNode() { $node = Category::create([ 'name' => 'test' ]); @@ -698,6 +743,59 @@ public function testTreeIsFixed() $this->assertEquals(null, $node->getParentId()); } + public function testTreeFixingWithCallback() + { + Category::where('id', '=', 5)->update([ '_lft' => 14 ]); + Category::where('id', '=', 8)->update([ 'parent_id' => 2 ]); + Category::where('id', '=', 11)->update([ '_lft' => 20 ]); + Category::where('id', '=', 2)->update([ 'parent_id' => 24 ]); + + $fixed = Category::fixTree(null, function (QueryBuilder $query){ + $this->assertIsObject($query); + }); + + $this->assertTrue($fixed > 0); + $this->assertTreeNotBroken(); + + $node = Category::find(8); + + $this->assertEquals(2, $node->getParentId()); + + $node = Category::find(2); + + $this->assertEquals(null, $node->getParentId()); + } + + public function testTreeFixingCallbackCanRemoveScopes() + { + Category::where('id', '=', 5)->update([ '_lft' => 14 ]); + Category::where('id', '=', 8)->update([ 'parent_id' => 2 ]); + Category::where('id', '=', 11)->update([ '_lft' => 20 ]); + Category::where('id', '=', 2)->update([ 'parent_id' => 24 ]); + + Category::addGlobalScope('aScope', function($query){ + $query->whereIn('name', ['apple', 'samsung', 'sony']); + }); + + $fixed = Category::fixTree(null, function(QueryBuilder $query){ + $query->withoutGlobalScope('aScope'); + }); + + Category::removeScope('aScope'); + + $this->assertTrue($fixed > 0); + $this->assertTreeNotBroken(); + + $node = Category::find(8); + + $this->assertEquals(2, $node->getParentId()); + + $node = Category::find(2); + + $this->assertEquals(null, $node->getParentId()); + + } + public function testSubtreeIsFixed() { Category::where('id', '=', 8)->update([ '_lft' => 11 ]); diff --git a/tests/models/Category.php b/tests/models/Category.php index bcce8e9..07ffb1b 100644 --- a/tests/models/Category.php +++ b/tests/models/Category.php @@ -14,4 +14,9 @@ public static function resetActionsPerformed() { static::$actionsPerformed = 0; } + + static public function removeScope($name) + { + unset(static::$globalScopes[static::class][$name]); + } } \ No newline at end of file From 02ef280ee3c6285658bf4023431b094d71ba6b55 Mon Sep 17 00:00:00 2001 From: Giacomo Trezzi <giacomo.trezzi@gmail.com> Date: Thu, 30 Sep 2021 00:47:57 +0200 Subject: [PATCH 2/2] improved tests --- tests/NodeTest.php | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/NodeTest.php b/tests/NodeTest.php index 7b0c9a8..f0abb0a 100644 --- a/tests/NodeTest.php +++ b/tests/NodeTest.php @@ -34,6 +34,8 @@ public function setUp() Category::resetActionsPerformed(); + Category::removeScope('testScope'); + date_default_timezone_set('America/Denver'); } @@ -129,15 +131,16 @@ public function testTreeNotEvenWithGlobalScopesBroken() { $this->assertTreeNotBroken(); - Category::addGlobalScope('aScope', function($query){ + Category::addGlobalScope('testScope', function($query){ $query->whereIn('categories.name', ['apple', 'samsung', 'sony']); }); $this->assertFalse(Category::isBroken(function($query){ - $query->withoutGlobalScope('aScope'); + $query->withoutGlobalScope('testScope'); })); - Category::removeScope('aScope'); + $this->expectException(Illuminate\Database\QueryException::class); + Category::isBroken(); } public function nodeValues($node) @@ -626,19 +629,17 @@ public function testCountsTreeErrorsWithGlobalScopes() Category::where('id', '=', 11)->update([ '_lft' => 20 ]); Category::where('id', '=', 4)->update([ 'parent_id' => 24 ]); - Category::addGlobalScope('aScope', function($query){ + Category::addGlobalScope('testScope', function($query){ $query->whereIn('categories.name', ['apple', 'samsung', 'sony']); }); $errors = Category::countErrors(function($query){ - $query->withoutGlobalScope('aScope'); + $query->withoutGlobalScope('testScope'); }); $this->assertEquals(1, $errors['oddness']); $this->assertEquals(2, $errors['duplicates']); $this->assertEquals(1, $errors['missing_parent']); - - Category::removeScope('aScope'); } public function testCreatesNode() @@ -773,15 +774,15 @@ public function testTreeFixingCallbackCanRemoveScopes() Category::where('id', '=', 11)->update([ '_lft' => 20 ]); Category::where('id', '=', 2)->update([ 'parent_id' => 24 ]); - Category::addGlobalScope('aScope', function($query){ + Category::addGlobalScope('testScope', function($query){ $query->whereIn('name', ['apple', 'samsung', 'sony']); }); $fixed = Category::fixTree(null, function(QueryBuilder $query){ - $query->withoutGlobalScope('aScope'); + $query->withoutGlobalScope('testScope'); }); - Category::removeScope('aScope'); + Category::removeScope('testScope'); $this->assertTrue($fixed > 0); $this->assertTreeNotBroken();