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