1: | <?php |
2: | |
3: | namespace Baril\Bonsai\Concerns; |
4: | |
5: | use Baril\Bonsai\Relations\Closure; |
6: | use Baril\Bonsai\TreeException; |
7: | use Illuminate\Database\Eloquent\Model; |
8: | use Illuminate\Support\Str; |
9: | |
10: | trait BelongsToTree |
11: | { |
12: | |
13: | |
14: | |
15: | public static function bootBelongsToTree() |
16: | { |
17: | static::saving(function ($item) { |
18: | $item->checkIfParentIdIsValid(); |
19: | }); |
20: | static::created(function ($item) { |
21: | $item->refreshClosures(false); |
22: | }); |
23: | static::updated(function ($item) { |
24: | if ($item->isDirty($item->getParentForeignKeyName())) { |
25: | $item->refreshClosures(true); |
26: | } |
27: | }); |
28: | static::deleting(function ($item) { |
29: | if ($item->children()->exists()) { |
30: | throw new TreeException('Can\'t delete an item with children!'); |
31: | } |
32: | $item->deleteClosuresForLeaf(); |
33: | }); |
34: | } |
35: | |
36: | |
37: | |
38: | |
39: | |
40: | |
41: | public function getParentForeignKeyName() |
42: | { |
43: | return property_exists($this, 'parentForeignKey') ? $this->parentForeignKey : 'parent_id'; |
44: | } |
45: | |
46: | |
47: | |
48: | |
49: | |
50: | |
51: | public function getClosureTable() |
52: | { |
53: | return isset($this->closureTable) ? $this->closureTable : Str::snake(class_basename($this)) . '_tree'; |
54: | } |
55: | |
56: | |
57: | |
58: | |
59: | |
60: | |
61: | |
62: | |
63: | public function deleteNode() |
64: | { |
65: | $parent = $this->parent; |
66: | if ($parent) { |
67: | $parent->children()->saveMany($this->children); |
68: | } else { |
69: | $this->children->each(function ($child) { |
70: | $child->parent()->dissociate(); |
71: | $child->save(); |
72: | }); |
73: | } |
74: | return $this->delete(); |
75: | } |
76: | |
77: | |
78: | |
79: | |
80: | |
81: | |
82: | |
83: | |
84: | public function deleteTree() |
85: | { |
86: | $this->getConnection()->transaction(function () { |
87: | $this->descendants()->includingSelf()->update([$this->getParentForeignKeyName() => null]); |
88: | $this->descendants()->includingSelf()->delete(); |
89: | }); |
90: | } |
91: | |
92: | |
93: | |
94: | |
95: | |
96: | |
97: | |
98: | |
99: | |
100: | public function parent() |
101: | { |
102: | return $this->belongsTo(static::class, $this->getParentForeignKeyName()); |
103: | } |
104: | |
105: | |
106: | |
107: | |
108: | |
109: | |
110: | |
111: | |
112: | |
113: | |
114: | |
115: | |
116: | |
117: | |
118: | |
119: | |
120: | |
121: | |
122: | |
123: | |
124: | |
125: | public function children() |
126: | { |
127: | return $this->hasMany(static::class, $this->getParentForeignKeyName()); |
128: | } |
129: | |
130: | |
131: | |
132: | |
133: | public function ancestors() |
134: | { |
135: | $instance = $this->newRelatedInstance(static::class); |
136: | return (new Closure( |
137: | $instance->newQuery(), |
138: | $this, |
139: | $this->getClosureTable(), |
140: | 'descendant_id', |
141: | 'ancestor_id', |
142: | $this->getKeyName(), |
143: | $instance->getKeyName(), |
144: | 'ancestors' |
145: | ))->as('closure')->withPivot('depth')->excludingSelf(); |
146: | } |
147: | |
148: | |
149: | |
150: | |
151: | public function descendants() |
152: | { |
153: | $instance = $this->newRelatedInstance(static::class); |
154: | return (new Closure( |
155: | $instance->newQuery(), |
156: | $this, |
157: | $this->getClosureTable(), |
158: | 'ancestor_id', |
159: | 'descendant_id', |
160: | $this->getKeyName(), |
161: | $instance->getKeyName(), |
162: | 'descendants' |
163: | ))->as('closure')->withPivot('depth')->excludingSelf(); |
164: | } |
165: | |
166: | |
167: | |
168: | |
169: | |
170: | |
171: | |
172: | public function setRelation($relation, $value) |
173: | { |
174: | switch ($relation) { |
175: | case 'descendants': |
176: | $this->setChildrenFromDescendants($value); |
177: | break; |
178: | case 'ancestors': |
179: | $this->setParentsFromAncestors($value); |
180: | break; |
181: | } |
182: | return parent::setRelation($relation, $value); |
183: | } |
184: | |
185: | |
186: | |
187: | |
188: | |
189: | |
190: | |
191: | |
192: | protected function setChildrenFromDescendants($descendants) |
193: | { |
194: | $descendants = $descendants->keyBy($this->primaryKey); |
195: | $parentKey = $this->getParentForeignKeyName(); |
196: | |
197: | $descendants->each(function ($item, $key) use ($descendants, $parentKey) { |
198: | if ($descendants->has($item->$parentKey)) { |
199: | if (!$descendants[$item->$parentKey]->relationLoaded('children')) { |
200: | $descendants[$item->$parentKey]->setRelation('children', collect([])); |
201: | } |
202: | $descendants[$item->$parentKey]->children->push($item); |
203: | } |
204: | }); |
205: | |
206: | |
207: | $descendants->each(function ($item, $key) { |
208: | if (!$item->relationLoaded('children') && $item->closure->_remaining_depth !== 0) { |
209: | $item->setRelation('children', collect([])); |
210: | } |
211: | }); |
212: | |
213: | return $this->setRelation('children', $descendants->values()->filter(function ($item) use ($parentKey) { |
214: | return $item->$parentKey == $this->getKey(); |
215: | })->values()); |
216: | } |
217: | |
218: | |
219: | |
220: | |
221: | |
222: | |
223: | |
224: | |
225: | protected function setParentsFromAncestors($ancestors) |
226: | { |
227: | if (!$ancestors->count()) { |
228: | return; |
229: | } |
230: | |
231: | $parentKey = $this->getParentForeignKeyName(); |
232: | $keyedAncestors = $ancestors->keyBy($this->primaryKey); |
233: | |
234: | $ancestors->merge([$this])->each(function ($model) use ($keyedAncestors, $parentKey) { |
235: | if (null === $model->$parentKey) { |
236: | $model->setRelation('parent', null); |
237: | } elseif ($keyedAncestors->has($model->$parentKey)) { |
238: | $model->setRelation('parent', $keyedAncestors->get($model->$parentKey)); |
239: | } |
240: | }); |
241: | } |
242: | |
243: | |
244: | |
245: | |
246: | |
247: | |
248: | |
249: | |
250: | |
251: | public function isRoot() |
252: | { |
253: | $parentKey = $this->getParentForeignKeyName(); |
254: | return $this->$parentKey === null; |
255: | } |
256: | |
257: | |
258: | |
259: | |
260: | |
261: | public function isLeaf() |
262: | { |
263: | return !$this->children()->exists(); |
264: | } |
265: | |
266: | |
267: | |
268: | |
269: | |
270: | public function hasChildren() |
271: | { |
272: | return $this->children()->exists(); |
273: | } |
274: | |
275: | |
276: | |
277: | |
278: | |
279: | public function isChildOf($item) |
280: | { |
281: | $parentKey = $this->getParentForeignKeyName(); |
282: | return $item->getKey() == $this->$parentKey; |
283: | } |
284: | |
285: | |
286: | |
287: | |
288: | |
289: | |
290: | public function isParentOf($item) |
291: | { |
292: | return $item->isChildOf($this); |
293: | } |
294: | |
295: | |
296: | |
297: | |
298: | |
299: | |
300: | public function isDescendantOf($item) |
301: | { |
302: | return $this->ancestors()->whereKey($item->getKey())->exists(); |
303: | } |
304: | |
305: | |
306: | |
307: | |
308: | |
309: | |
310: | public function isAncestorOf($item) |
311: | { |
312: | return $item->isDescendantOf($this); |
313: | } |
314: | |
315: | |
316: | |
317: | |
318: | |
319: | |
320: | public function isSiblingOf($item) |
321: | { |
322: | $parentKey = $this->getParentForeignKeyName(); |
323: | return $item->$parentKey == $this->$parentKey; |
324: | } |
325: | |
326: | |
327: | |
328: | |
329: | |
330: | |
331: | |
332: | |
333: | |
334: | public function findCommonAncestorWith($item) |
335: | { |
336: | return $this->ancestors() |
337: | ->includingSelf() |
338: | ->whereIsAncestorOf($item->getKey(), null, true) |
339: | ->orderByDepth() |
340: | ->first(); |
341: | } |
342: | |
343: | |
344: | |
345: | |
346: | |
347: | |
348: | |
349: | |
350: | |
351: | |
352: | public function getDistanceTo($item) |
353: | { |
354: | $commonAncestor = $this->findCommonAncestorWith($item); |
355: | if (!$commonAncestor) { |
356: | throw new TreeException('The items have no common ancestor!'); |
357: | } |
358: | $depths = $commonAncestor->descendants()->includingSelf()->whereKey([$this->getKey(), $item->getKey()]) |
359: | ->toBase()->select($this->getClosureTable() . '.depth') |
360: | ->get()->pluck('depth'); |
361: | return $depths->sum(); |
362: | } |
363: | |
364: | |
365: | |
366: | |
367: | |
368: | |
369: | public function getDepth() |
370: | { |
371: | return $this->ancestors()->count(); |
372: | } |
373: | |
374: | |
375: | |
376: | |
377: | |
378: | |
379: | public function getSubtreeDepth() |
380: | { |
381: | return (int) $this->descendants()->orderByDepth('desc')->value('depth'); |
382: | } |
383: | |
384: | |
385: | |
386: | |
387: | |
388: | |
389: | protected function scopeWithClosure($query, $relation, $depth = null, $constraints = null) |
390: | { |
391: | $query->with([$relation => function ($query) use ($depth, $constraints) { |
392: | if ($depth !== null) { |
393: | $query->upToDepth($depth)->orderByDepth(); |
394: | } |
395: | if ($constraints !== null) { |
396: | $constraints($query); |
397: | } |
398: | }]); |
399: | } |
400: | |
401: | public function scopeWithAncestors($query, $depth = null, $constraints = null) |
402: | { |
403: | $this->scopeWithClosure($query, 'ancestors', $depth, $constraints); |
404: | } |
405: | |
406: | public function scopeWithDescendants($query, $depth = null, $constraints = null) |
407: | { |
408: | $this->scopeWithClosure($query, 'descendants', $depth, $constraints); |
409: | } |
410: | |
411: | public function scopeWithDepth($query, $as = 'depth') |
412: | { |
413: | $query->withCount('ancestors as ' . $as); |
414: | } |
415: | |
416: | public function scopeWhereIsRoot($query, $bool = true) |
417: | { |
418: | $query->where($this->getParentForeignKeyName(), ($bool ? '=' : '!='), null); |
419: | } |
420: | |
421: | public function scopeWhereIsLeaf($query, $bool = true) |
422: | { |
423: | if ($bool) { |
424: | $query->has('descendants', '=', 0); |
425: | } else { |
426: | $query->has('descendants'); |
427: | } |
428: | } |
429: | |
430: | public function scopeWhereHasChildren($query, $bool = true) |
431: | { |
432: | $this->scopeWhereIsLeaf($query, !$bool); |
433: | } |
434: | |
435: | public function scopeWhereIsDescendantOf($query, $ancestor, $maxDepth = null, $includingSelf = false) |
436: | { |
437: | $ancestorId = ($ancestor instanceof Model) ? $ancestor->getKey() : $ancestor; |
438: | $closureTable = $this->getClosureTable(); |
439: | $alias = $closureTable . uniqid(); |
440: | $query->join( |
441: | $closureTable . ' as ' . $alias, |
442: | function ($join) use ($ancestorId, $maxDepth, $alias, $includingSelf) { |
443: | $join->on($alias . '.descendant_id', '=', $this->getQualifiedKeyName()); |
444: | $join->where($alias . '.ancestor_id', '=', $ancestorId); |
445: | if (!$includingSelf) { |
446: | $join->where($alias . '.depth', '>', 0); |
447: | } |
448: | if ($maxDepth !== null) { |
449: | $join->where($alias . '.depth', '<=', $maxDepth); |
450: | } |
451: | } |
452: | ); |
453: | $query->where($alias . '.ancestor_id', '!=', null); |
454: | } |
455: | |
456: | public function scopeWhereIsAncestorOf($query, $descendant, $maxDepth = null, $includingSelf = false) |
457: | { |
458: | $descendantId = ($descendant instanceof Model) ? $descendant->getKey() : $descendant; |
459: | $closureTable = $this->getClosureTable(); |
460: | $alias = $closureTable . uniqid(); |
461: | $query->join( |
462: | $closureTable . ' as ' . $alias, |
463: | function ($join) use ($descendantId, $maxDepth, $alias, $includingSelf) { |
464: | $join->on($alias . '.ancestor_id', '=', $this->getQualifiedKeyName()); |
465: | $join->where($alias . '.descendant_id', '=', $descendantId); |
466: | if (!$includingSelf) { |
467: | $join->where($alias . '.depth', '>', 0); |
468: | } |
469: | if ($maxDepth !== null) { |
470: | $join->where($alias . '.depth', '<=', $maxDepth); |
471: | } |
472: | } |
473: | ); |
474: | $query->where($alias . '.ancestor_id', '!=', null); |
475: | } |
476: | |
477: | |
478: | |
479: | |
480: | |
481: | |
482: | |
483: | |
484: | |
485: | |
486: | |
487: | |
488: | public static function getTree($depth = null) |
489: | { |
490: | return static::query()->whereIsRoot()->withDescendants($depth)->get(); |
491: | } |
492: | |
493: | |
494: | |
495: | |
496: | |
497: | |
498: | public static function getTreeDepth() |
499: | { |
500: | $instance = new static(); |
501: | return $instance->getConnection()->table($instance->getClosureTable())->max('depth'); |
502: | } |
503: | |
504: | |
505: | |
506: | |
507: | |
508: | |
509: | |
510: | |
511: | |
512: | |
513: | |
514: | |
515: | protected function checkIfParentIdIsValid() |
516: | { |
517: | $parentKey = $this->getParentForeignKeyName(); |
518: | |
519: | if (is_null($this->$parentKey)) { |
520: | return; |
521: | } |
522: | if ( |
523: | $this->$parentKey == $this->getKey() |
524: | || $this->newQuery()->whereKey($this->$parentKey)->whereIsDescendantOf($this->getKey())->exists() |
525: | ) { |
526: | throw new TreeException( |
527: | 'Redundancy error! The item\'s parent can\'t be the item itself or one of its descendants.' |
528: | ); |
529: | } |
530: | } |
531: | |
532: | |
533: | |
534: | |
535: | |
536: | |
537: | public function refreshClosures($deleteOldClosures = true) |
538: | { |
539: | $connection = $this->getConnection(); |
540: | $grammar = $connection->getQueryGrammar(); |
541: | |
542: | $connection->transaction(function () use ($deleteOldClosures, $connection, $grammar) { |
543: | |
544: | $closureTable = $this->getClosureTable(); |
545: | $parentKey = $this->getParentForeignKeyName(); |
546: | $id = $this->getKey(); |
547: | $newParentId = $this->$parentKey; |
548: | |
549: | |
550: | if ($deleteOldClosures) { |
551: | |
552: | |
553: | |
554: | |
555: | |
556: | |
557: | |
558: | |
559: | $connection->table($closureTable, 'closures') |
560: | ->join("$closureTable as descendants", 'closures.descendant_id', '=', 'descendants.descendant_id') |
561: | ->join("$closureTable as ancestors", 'closures.ancestor_id', '=', 'ancestors.ancestor_id') |
562: | ->where('descendants.ancestor_id', $id) |
563: | ->where('ancestors.descendant_id', $id) |
564: | ->where('closures.depth', '>', $connection->raw($grammar->wrap('descendants.depth'))) |
565: | ->delete(); |
566: | } |
567: | |
568: | |
569: | $connection->table($closureTable)->insertOrIgnore([ |
570: | 'ancestor_id' => $id, |
571: | 'descendant_id' => $id, |
572: | 'depth' => 0, |
573: | ]); |
574: | |
575: | |
576: | if ($newParentId) { |
577: | |
578: | |
579: | |
580: | |
581: | |
582: | |
583: | $select = $connection |
584: | ->table($closureTable, 'ancestors') |
585: | ->crossJoin("$closureTable AS descendants") |
586: | ->where('ancestors.descendant_id', '=', $newParentId) |
587: | ->where('descendants.ancestor_id', '=', $id) |
588: | ->select( |
589: | 'ancestors.ancestor_id', |
590: | 'descendants.descendant_id', |
591: | $connection->raw(sprintf( |
592: | '%s + %s + 1', |
593: | $grammar->wrap('ancestors.depth'), |
594: | $grammar->wrap('descendants.depth') |
595: | )) |
596: | ); |
597: | $connection->table($closureTable)->insertUsing(['ancestor_id', 'descendant_id', 'depth'], $select); |
598: | } |
599: | }); |
600: | } |
601: | |
602: | |
603: | |
604: | |
605: | protected function deleteClosuresForLeaf() |
606: | { |
607: | $closureTable = $this->getClosureTable(); |
608: | $this->getConnection()->table($closureTable)->where('descendant_id', $this->getKey())->delete(); |
609: | } |
610: | } |
611: | |