1: <?php
2:
3: namespace Baril\Bonsai\Concerns;
4:
5: use Illuminate\Database\Eloquent\Builder;
6: use Illuminate\Database\Eloquent\Model;
7:
8: trait 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: }
238: