Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.02% covered (success)
93.02%
40 / 43
83.33% covered (warning)
83.33%
15 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
HasDescendants
93.02% covered (success)
93.02%
40 / 43
83.33% covered (warning)
83.33%
15 / 18
19.12
0.00% covered (danger)
0.00%
0 / 1
 children
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 descendants
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 descendingClosures
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 scopeWithDescendants
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 scopeWithHeight
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 scopeWhereIsLeaf
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 scopeOnlyLeaves
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 scopeWithoutLeaves
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 scopeWhereHasChildren
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 scopeHasChildren
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 scopeWhereIsAncestorOf
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 scopeAncestorsOf
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 isLeaf
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasChildren
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isParentOf
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isAncestorOf
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getSubtreeDepth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHeight
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace Baril\Bonsai\Concerns;
4
5use Illuminate\Database\Eloquent\Builder;
6use Illuminate\Database\Eloquent\Model;
7
8trait HasDescendants
9{
10    // ========================================================================
11    // RELATIONS
12    // ========================================================================
13
14    /**
15     * One-to-many relation to the children nodes.
16     *
17     * @todo add chaperone in v4
18     *
19     * @return \Illuminate\Database\Eloquent\Relations\HasMany
20     */
21    public function children()
22    {
23        return $this->hasMany(static::class, $this->getParentForeignKeyName());
24    }
25
26    /**
27     * Many-to-many relation to the descendants through the closure table.
28     *
29     * @return \Baril\Bonsai\Relations\BelongsToManyThroughClosures<static::class, $this, \Illuminate\Database\Eloquent\Relations\Pivot>
30     */
31    public function descendants()
32    {
33        return
34            $this->belongsToManyThroughClosures(
35                static::class,
36                $this->getClosureTable(),
37                'ancestor_id',
38                'descendant_id',
39                'descendants'
40            )
41            ->withoutSelf()
42            ->closes('children');
43    }
44
45    /**
46     * One-to-many relationships to the descending closures.
47     *
48     * @return \Illuminate\Database\Eloquent\Relations\HasMany<\Baril\Bonsai\Closure, $this>
49     */
50    public function descendingClosures()
51    {
52        return $this->hasManyClosuresWith(
53            static::class,
54            $this->getClosureTable(),
55            'ancestor_id',
56            'descendant_id'
57        );
58    }
59
60    // =========================================================================
61    // QUERY SCOPES
62    // =========================================================================
63
64    /**
65     * @deprecated Use ->with() instead
66     *
67     * @param  \Illuminate\Database\Eloquent\Builder  $query
68     * @param  int|null  $depth
69     * @param  callable|null  $constraints
70     * @return void
71     */
72    public function scopeWithDescendants(Builder $query, $depth = null, $constraints = null)
73    {
74        $this->scopeWithManyThroughClosures($query, 'descendants', $depth, $constraints);
75    }
76
77    /**
78     * @param  \Illuminate\Database\Eloquent\Builder  $query
79     * @param  string  $as
80     * @return void
81     */
82    public function scopeWithHeight(Builder $query, $as = 'height')
83    {
84        $query->withMax("descendants as $as", $this->getClosureTable() . '.depth');
85    }
86
87    /**
88     * @deprecated Use ->onlyLeaves() instead
89     *
90     * @param  \Illuminate\Database\Eloquent\Builder  $query
91     * @param  bool  $bool
92     * @return void
93     */
94    public function scopeWhereIsLeaf(Builder $query, $bool = true)
95    {
96        $this->scopeHasChildren($query, !$bool);
97    }
98
99    /**
100     * @param  \Illuminate\Database\Eloquent\Builder  $query
101     * @return void
102     */
103    public function scopeOnlyLeaves(Builder $query)
104    {
105        $this->scopeHasChildren($query, false);
106    }
107
108    /**
109     * @param  \Illuminate\Database\Eloquent\Builder  $query
110     * @return void
111     */
112    public function scopeWithoutLeaves(Builder $query)
113    {
114        $this->scopeHasChildren($query);
115    }
116
117    /**
118     * @deprecated
119     *
120     * @param  \Illuminate\Database\Eloquent\Builder  $query
121     * @param  bool  $bool
122     * @return void
123     */
124    public function scopeWhereHasChildren(Builder $query, $bool = true)
125    {
126        $this->scopeHasChildren($query, $bool);
127    }
128
129    /**
130     * @param  \Illuminate\Database\Eloquent\Builder  $query
131     * @param  bool  $bool
132     * @return void
133     */
134    public function scopeHasChildren(Builder $query, $bool = true)
135    {
136        if ($bool) {
137            $query->has('children');
138        } else {
139            $query->doesntHave('children');
140        }
141    }
142
143    /**
144     * @deprecated
145     *
146     * @param  \Illuminate\Database\Eloquent\Builder  $query
147     * @param  mixed|\Illuminate\Database\Eloquent\Model  $descendant
148     * @param  int|null  $maxDepth
149     * @param  bool  $withSelf
150     * @return void
151     */
152    public function scopeWhereIsAncestorOf(Builder $query, $descendant, $maxDepth = null, $withSelf = false)
153    {
154        $this->scopeAncestorsOf($query, $descendant, $maxDepth, $withSelf);
155    }
156
157    /**
158     * @param  \Illuminate\Database\Eloquent\Builder  $query
159     * @param  mixed|\Illuminate\Database\Eloquent\Model  $descendant
160     * @param  int|null  $maxDepth
161     * @param  bool  $withSelf
162     * @return void
163     */
164    public function scopeAncestorsOf(Builder $query, $descendant, $maxDepth = null, $withSelf = false)
165    {
166        $this->scopeWhereHasClosuresWith(
167            $query,
168            $descendant,
169            'descendingClosures',
170            'whereDescendant',
171            $maxDepth,
172            $withSelf
173        );
174    }
175
176    // =========================================================================
177    // MODEL METHODS
178    // =========================================================================
179
180    /**
181     * @return bool
182     */
183    public function isLeaf()
184    {
185        return !$this->hasChildren();
186    }
187
188    /**
189     * @return bool
190     */
191    public function hasChildren()
192    {
193        return $this->children()->exists();
194    }
195
196    /**
197     * @param  \Illuminate\Database\Eloquent\Model  $node
198     * @return bool
199     */
200    public function isParentOf(Model $node)
201    {
202        return $node->isChildOf($this);
203    }
204
205    /**
206     * @param  mixed|\Illuminate\Database\Eloquent\Model  $node
207     * @return bool
208     */
209    public function isAncestorOf($node)
210    {
211        return $this->descendingClosures()
212            ->whereDescendant($node)
213            ->exists();
214    }
215
216    /**
217     * @deprecated use getHeight() instead
218     *
219     * Returns the depth of the subtree of which $this is a root.
220     *
221     * @return int
222     */
223    public function getSubtreeDepth()
224    {
225        return $this->getHeight();
226    }
227
228    /**
229     * Returns the depth of the subtree of which $this is a root.
230     *
231     * @return int
232     */
233    public function getHeight()
234    {
235        return (int) $this->descendants()->max('depth');
236    }
237}