Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.28% covered (success)
98.28%
57 / 58
94.74% covered (success)
94.74%
18 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
HasAncestors
98.28% covered (success)
98.28%
57 / 58
94.74% covered (success)
94.74%
18 / 19
22
0.00% covered (danger)
0.00%
0 / 1
 getParentForeignKeyName
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getParentKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parent
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 ancestors
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 ascendingClosures
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 siblings
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 hasManySiblings
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 scopeWithAncestors
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 scopeWithDepth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 scopeWhereIsDescendantOf
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 scopeDescendantsOf
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 isChildOf
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 isDescendantOf
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 isSiblingOf
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findCommonAncestorWith
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 hasCommonAncestorWith
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 newCommonAncestorQuery
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getDistanceTo
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getDepth
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 Baril\Bonsai\Relations\HasManySiblings;
6use Baril\Bonsai\TreeException;
7use Illuminate\Database\Eloquent\Builder;
8use Illuminate\Database\Eloquent\Model;
9
10trait HasAncestors
11{
12    /**
13     * Return the name of the "parent_id" column.
14     *
15     * @return string
16     */
17    public function getParentForeignKeyName()
18    {
19        return property_exists($this, 'parentForeignKey')
20            ? $this->parentForeignKey
21            : 'parent_id';
22    }
23
24    /**
25     * Return the value of the "parent_id" column.
26     *
27     * @return mixed
28     */
29    public function getParentKey()
30    {
31        return $this->parent()->getParentKey();
32    }
33
34    // ========================================================================
35    // RELATIONS
36    // ========================================================================
37
38    /**
39     * Many-to-one relation to the parent node.
40     *
41     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
42     */
43    public function parent()
44    {
45        return $this->belongsTo(
46            static::class,
47            $this->getParentForeignKeyName()
48        );
49    }
50
51    /**
52     * Many-to-many relation to the ancestors through the closure table.
53     *
54     * @return \Baril\Bonsai\Relations\BelongsToManyThroughClosures<static::class, $this, \Illuminate\Database\Eloquent\Relations\Pivot>
55     */
56    public function ancestors()
57    {
58        return
59            $this->belongsToManyThroughClosures(
60                static::class,
61                $this->getClosureTable()
62            )
63            ->withoutSelf()
64            ->closes('parent');
65    }
66
67    /**
68     * One-to-many relationships to the ascending closures.
69     *
70     * @return \Illuminate\Database\Eloquent\Relations\HasMany<\Baril\Bonsai\Closure, $this>
71     */
72    public function ascendingClosures()
73    {
74        return $this->hasManyClosuresWith(static::class, $this->getClosureTable());
75    }
76
77    /**
78     * One-to-many relationship to the siblings.
79     *
80     * @return \Baril\Bonsai\Relations\HasManySiblings<$this>
81     */
82    public function siblings()
83    {
84        return $this->hasManySiblings($this->getParentForeignKeyName())
85            ->withoutSelf();
86    }
87
88    /**
89     * Define a one-to-many relationship through a parent foreign key.
90     *
91     * @todo move to Octopus in v4
92     * @see \Illuminate\Database\Eloquent\Concerns\HasRelationships::hasMany()
93     *
94     * @param  string  $parentForeignKey
95     * @return \Baril\Bonsai\Relations\HasManySiblings<$this>
96     */
97    public function hasManySiblings($parentForeignKey = 'parent_id')
98    {
99        $instance = $this->newRelatedInstance(static::class);
100
101        return new HasManySiblings(
102            $instance->newQuery(),
103            $this,
104            $instance->qualifyColumn($parentForeignKey),
105            $parentForeignKey
106        );
107    }
108
109
110    // =========================================================================
111    // QUERY SCOPES
112    // =========================================================================
113
114    /**
115     * @deprecated Use ->with() instead
116     *
117     * @param  \Illuminate\Database\Eloquent\Builder  $query
118     * @param  int|null  $depth
119     * @param  callable|null  $constraints
120     * @return void
121     */
122    public function scopeWithAncestors(Builder $query, $depth = null, $constraints = null)
123    {
124        $this->scopeWithManyThroughClosures($query, 'ancestors', $depth, $constraints);
125    }
126
127    /**
128     * @param  \Illuminate\Database\Eloquent\Builder  $query
129     * @param  string  $as
130     * @return void
131     */
132    public function scopeWithDepth(Builder $query, $as = 'depth')
133    {
134        $query->withCount("ancestors as $as");
135    }
136
137    /**
138     * @deprecated
139     *
140     * @param  \Illuminate\Database\Eloquent\Builder  $query
141     * @param  mixed|\Illuminate\Database\Eloquent\Model  $ancestor
142     * @param  int|null  $maxDepth
143     * @param  bool  $withSelf
144     * @return void
145     */
146    public function scopeWhereIsDescendantOf(Builder $query, $ancestor, $maxDepth = null, $withSelf = false)
147    {
148        $this->scopeDescendantsOf($query, $ancestor, $maxDepth, $withSelf);
149    }
150
151    /**
152     * @param  \Illuminate\Database\Eloquent\Builder  $query
153     * @param  mixed|\Illuminate\Database\Eloquent\Model  $ancestor
154     * @param  int|null  $maxDepth
155     * @param  bool  $withSelf
156     * @return void
157     */
158    public function scopeDescendantsOf(Builder $query, $ancestor, $maxDepth = null, $withSelf = false)
159    {
160        $this->scopeWhereHasClosuresWith(
161            $query,
162            $ancestor,
163            'ascendingClosures',
164            'whereAncestor',
165            $maxDepth,
166            $withSelf
167        );
168    }
169
170    // =========================================================================
171    // MODEL METHODS
172    // =========================================================================
173
174    /**
175     * @param  mixed|\Illuminate\Database\Eloquent\Model  $node
176     * @return bool
177     */
178    public function isChildOf($node)
179    {
180        $nodeId = $node instanceof Model ? $node->getKey() : $node;
181
182        return $nodeId == $this->getParentKey();
183    }
184
185    /**
186     * @param  mixed|\Illuminate\Database\Eloquent\Model  $node
187     * @return bool
188     */
189    public function isDescendantOf($node)
190    {
191        return $this->ascendingClosures()
192            ->whereAncestor($node)
193            ->exists();
194    }
195
196    /**
197     * @param  \Illuminate\Database\Eloquent\Model  $node
198     * @return bool
199     */
200    public function isSiblingOf(Model $node)
201    {
202        return $node->getParentKey() == $this->getParentKey();
203    }
204
205    /**
206     * Returns the closest common ancestor with the provided $node.
207     * May return null if the tree has multiple roots and the 2 nodes have no
208     * common ancestor.
209     *
210     * @param  mixed|\Illuminate\Database\Eloquent\Model  $node
211     * @return \Illuminate\Database\Eloquent\Model|null
212     */
213    public function findCommonAncestorWith($node)
214    {
215        return $this->newCommonAncestorQuery($node)
216            ->orderByDepth()
217            ->first();
218    }
219
220    /**
221     * @param  mixed|\Illuminate\Database\Eloquent\Model  $node
222     * @return bool
223     */
224    public function hasCommonAncestorWith($node)
225    {
226        return $this->newCommonAncestorQuery($node)
227            ->exists();
228    }
229
230    /**
231     * @param  mixed|\Illuminate\Database\Eloquent\Model  $node
232     * @return \Baril\Bonsai\Relations\BelongsToManyThroughClosures
233     */
234    protected function newCommonAncestorQuery($node)
235    {
236        return $this->ancestors()
237            ->withSelf()
238            ->ancestorsOf($node, null, true);
239    }
240
241    /**
242     * Returns the distance between $this and another $item.
243     * May throw an exception if the tree has multiple roots and the 2 items
244     * have no common ancestor.
245     *
246     * @param  mixed|\Illuminate\Database\Eloquent\Model  $node
247     * @return int
248     * @throws \Baril\Bonsai\TreeException
249     */
250    public function getDistanceTo($node)
251    {
252        $commonAncestor = $this->findCommonAncestorWith($node);
253        if (!$commonAncestor) {
254            throw new TreeException('The items have no common ancestor!');
255        }
256
257        return $commonAncestor
258            ->descendingClosures()
259            ->whereDescendant([$this, $node])
260            ->sum('depth');
261    }
262
263    /**
264     * Return the depth of $this in the tree (0 is $this is a root).
265     *
266     * @return int
267     */
268    public function getDepth()
269    {
270        return $this->ancestors()->count();
271    }
272}