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