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();