From fdfa22b272d3d494db4f78b17881f0a7494f05dc Mon Sep 17 00:00:00 2001
From: Silvio Gratani <s.gratani@zerouno.io>
Date: Tue, 8 Oct 2019 11:19:36 +0200
Subject: [PATCH 1/3] added scopes to NodeTrait to filter nodes with query
 builder

---
 src/NodeTrait.php | 104 ++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 86 insertions(+), 18 deletions(-)

diff --git a/src/NodeTrait.php b/src/NodeTrait.php
index 0b985ab..7a4f5a9 100644
--- a/src/NodeTrait.php
+++ b/src/NodeTrait.php
@@ -88,11 +88,13 @@ protected function callPendingAction()
     {
         $this->moved = false;
 
-        if ( ! $this->pending && ! $this->exists) {
+        if (! $this->pending && ! $this->exists) {
             $this->makeRoot();
         }
 
-        if ( ! $this->pending) return;
+        if (! $this->pending) {
+            return;
+        }
 
         $method = 'action'.ucfirst(array_shift($this->pending));
         $parameters = $this->pending;
@@ -132,7 +134,7 @@ protected function actionRaw()
     protected function actionRoot()
     {
         // Simplest case that do not affect other nodes.
-        if ( ! $this->exists) {
+        if (! $this->exists) {
             $cut = $this->getLowerBound() + 1;
 
             $this->setLft($cut);
@@ -168,7 +170,7 @@ protected function actionAppendOrPrepend(self $parent, $prepend = false)
 
         $cut = $prepend ? $parent->getLft() + 1 : $parent->getRgt();
 
-        if ( ! $this->insertAt($cut)) {
+        if (! $this->insertAt($cut)) {
             return false;
         }
 
@@ -212,7 +214,9 @@ protected function actionBeforeOrAfter(self $node, $after = false)
      */
     public function refreshNode()
     {
-        if ( ! $this->exists || static::$actionsPerformed === 0) return;
+        if (! $this->exists || static::$actionsPerformed === 0) {
+            return;
+        }
 
         $attributes = $this->newNestedSetQuery()->getNodeData($this->getKey());
 
@@ -468,7 +472,7 @@ public function beforeOrAfterNode(self $node, $after = false)
             ->assertNotDescendant($node)
             ->assertSameScope($node);
 
-        if ( ! $this->isSiblingOf($node)) {
+        if (! $this->isSiblingOf($node)) {
             $this->setParent($node->getRelationValue('parent'));
         }
 
@@ -498,7 +502,9 @@ public function insertAfterNode(self $node)
      */
     public function insertBeforeNode(self $node)
     {
-        if ( ! $this->beforeNode($node)->save()) return false;
+        if (! $this->beforeNode($node)->save()) {
+            return false;
+        }
 
         // We'll update the target node since it will be moved
         $node->refreshNode();
@@ -534,7 +540,9 @@ public function up($amount = 1)
             ->skip($amount - 1)
             ->first();
 
-        if ( ! $sibling) return false;
+        if (! $sibling) {
+            return false;
+        }
 
         return $this->insertBeforeNode($sibling);
     }
@@ -553,7 +561,9 @@ public function down($amount = 1)
             ->skip($amount - 1)
             ->first();
 
-        if ( ! $sibling) return false;
+        if (! $sibling) {
+            return false;
+        }
 
         return $this->insertAfterNode($sibling);
     }
@@ -590,7 +600,9 @@ protected function moveNode($position)
         $updated = $this->newNestedSetQuery()
                 ->moveNode($this->getKey(), $position) > 0;
 
-        if ($updated) $this->refreshNode();
+        if ($updated) {
+            $this->refreshNode();
+        }
 
         return $updated;
     }
@@ -698,17 +710,20 @@ public function newScopedQuery($table = null)
      */
     public function applyNestedSetScope($query, $table = null)
     {
-        if ( ! $scoped = $this->getScopeAttributes()) {
+        if (! $scoped = $this->getScopeAttributes()) {
             return $query;
         }
 
-        if ( ! $table) {
+        if (! $table) {
             $table = $this->getTable();
         }
 
         foreach ($scoped as $attribute) {
-            $query->where($table.'.'.$attribute, '=',
-                          $this->getAttributeValue($attribute));
+            $query->where(
+                $table.'.'.$attribute,
+                '=',
+                $this->getAttributeValue($attribute)
+            );
         }
 
         return $query;
@@ -784,7 +799,9 @@ public static function create(array $attributes = [], self $parent = null)
      */
     public function getNodeHeight()
     {
-        if ( ! $this->exists) return 2;
+        if (! $this->exists) {
+            return 2;
+        }
 
         return $this->getRgt() - $this->getLft() + 1;
     }
@@ -810,7 +827,9 @@ public function getDescendantCount()
      */
     public function setParentIdAttribute($value)
     {
-        if ($this->getParentId() == $value) return;
+        if ($this->getParentId() == $value) {
+            return;
+        }
 
         if ($value) {
             $this->appendToNode($this->newScopedQuery()->findOrFail($value));
@@ -1178,7 +1197,7 @@ protected function assertNotDescendant(self $node)
      */
     protected function assertNodeExists(self $node)
     {
-        if ( ! $node->getLft() || ! $node->getRgt()) {
+        if (! $node->getLft() || ! $node->getRgt()) {
             throw new LogicException('Node must exists.');
         }
 
@@ -1190,7 +1209,7 @@ protected function assertNodeExists(self $node)
      */
     protected function assertSameScope(self $node)
     {
-        if ( ! $scoped = $this->getScopeAttributes()) {
+        if (! $scoped = $this->getScopeAttributes()) {
             return;
         }
 
@@ -1218,4 +1237,53 @@ public function replicate(array $except = null)
 
         return parent::replicate($except);
     }
+
+    public function scopeWithChildren(Builder $query) : Builder
+    {
+        return $query->whereHas('children');
+    }
+
+    public function scopeWithoutChildren(Builder $query) : Builder
+    {
+        // @see https://laravel.com/api/5.7/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.html#method_whereHas
+        return $query->whereHas('children', null, '=', 0);
+    }
+
+    public function scopeWithDescendants(Builder $query) : Builder
+    {
+        return $query->whereHas('descendants');
+    }
+
+    public function scopeWithoutDescendants(Builder $query) : Builder
+    {
+        // @see https://laravel.com/api/5.7/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.html#method_whereHas
+        return $query->whereHas('descendants', null, '=', 0);
+    }
+
+    public function scopeWithSiblings(Builder $query) : Builder
+    {
+        return $query->whereHas('siblings');
+    }
+
+    public function scopeWithoutSiblings(Builder $query) : Builder
+    {
+        // @see https://laravel.com/api/5.7/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.html#method_whereHas
+        return $query->whereHas('siblings', null, '=', 0);
+    }
+
+    public function scopeWithAncestors(Builder $query) : Builder
+    {
+        return $query->whereHas('ancestors');
+    }
+
+    public function scopeWithoutAncestors(Builder $query) : Builder
+    {
+        // @see https://laravel.com/api/5.7/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.html#method_whereHas
+        return $query->whereHas('ancestors', null, '=', 0);
+    }
+
+    public function scopeRoots(Builder $query) : Builder
+    {
+        return $query->withoutAncestors();
+    }
 }

From 66773ac972b589f980bdecae3922f79ceeb9a788 Mon Sep 17 00:00:00 2001
From: Silvio Gratani <s.gratani@zerouno.io>
Date: Tue, 8 Oct 2019 11:36:08 +0200
Subject: [PATCH 2/3] added missing 'leaves' scope

---
 src/NodeTrait.php | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/NodeTrait.php b/src/NodeTrait.php
index 7a4f5a9..e311fab 100644
--- a/src/NodeTrait.php
+++ b/src/NodeTrait.php
@@ -1286,4 +1286,9 @@ public function scopeRoots(Builder $query) : Builder
     {
         return $query->withoutAncestors();
     }
+
+    public function scopeLeaves(Builder $query) : Builder
+    {
+        return $query->withoutDescendants();
+    }
 }

From 07673f551216451d7abd14df86d0fa163cf51224 Mon Sep 17 00:00:00 2001
From: Silvio Gratani <s.gratani@zerouno.io>
Date: Tue, 8 Oct 2019 11:51:31 +0200
Subject: [PATCH 3/3] added descendantsOf scope

---
 src/NodeTrait.php | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/NodeTrait.php b/src/NodeTrait.php
index e311fab..76abf98 100644
--- a/src/NodeTrait.php
+++ b/src/NodeTrait.php
@@ -1291,4 +1291,10 @@ public function scopeLeaves(Builder $query) : Builder
     {
         return $query->withoutDescendants();
     }
+
+    public function scopeDescendantsOf(Builder $query, self $other_category) : Builder
+    {
+        return $query->where('_lft', '>', $other_category->_lft)
+            ->where('_lft', '<', $other_category->_rgt);
+    }
 }