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