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: // protected $parentForeignKey Name of the foreign key for the "parent/children" relation (defaults to parent_id)
13: // protected $closureTable Name of the closure table (defaults to [snake_cased_model_name]_tree)
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: * Returns the name of the "parent_id" column.
38: *
39: * @return string
40: */
41: public function getParentForeignKeyName()
42: {
43: return property_exists($this, 'parentForeignKey') ? $this->parentForeignKey : 'parent_id';
44: }
45:
46: /**
47: * Returns the name of the closure table.
48: *
49: * @return string
50: */
51: public function getClosureTable()
52: {
53: return isset($this->closureTable) ? $this->closureTable : Str::snake(class_basename($this)) . '_tree';
54: }
55:
56: /**
57: * Deletes the model after having attached its children to its parent.
58: *
59: * @return bool|null
60: *
61: * @throws \Exception
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: * Deletes the model and its descendants from the database.
79: *
80: * @return bool|null
81: *
82: * @throws \Exception
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: // RELATIONS
94: // =========================================================================
95:
96: /**
97: *
98: * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
99: */
100: public function parent()
101: {
102: return $this->belongsTo(static::class, $this->getParentForeignKeyName());
103: }
104:
105: // /**
106: // * Requires the package baril/octopus.
107: // *
108: // * @return \Baril\Octopus\Relations\HasManySiblings
109: // */
110: // public function siblings()
111: // {
112: // $parentForeignKey = $this->getParentForeignKeyName();
113: // return new \Baril\Octopus\Relations\HasManySiblings(
114: // $this->newInstance()->newQuery(),
115: // $this,
116: // $this->table . '.' . $parentForeignKey,
117: // $parentForeignKey
118: // );
119: // }
120:
121: /**
122: *
123: * @return \Illuminate\Database\Eloquent\Relations\HasMany
124: */
125: public function children()
126: {
127: return $this->hasMany(static::class, $this->getParentForeignKeyName());
128: }
129:
130: /**
131: * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
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: * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
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: * @param string $relation
169: * @param mixed $value
170: * @return $this
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: * Automatically sets the "children" (for the current object and each of
187: * its loaded descendants) when the "descendants" relationship is loaded.
188: *
189: * @param \Illuminate\Database\Eloquent\Collection $descendants
190: * @return $this
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: // Prevents an unneeded query in case we try to access the children of a leaf.
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: * Automatically sets the "parent" (for the current object and each of
220: * its loaded ancestors) when the "ancestors" relationship is loaded.
221: *
222: * @param \Illuminate\Database\Eloquent\Collection $ancestors
223: * @return $this
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: // MODEL METHODS
245: // =========================================================================
246:
247: /**
248: *
249: * @return bool
250: */
251: public function isRoot()
252: {
253: $parentKey = $this->getParentForeignKeyName();
254: return $this->$parentKey === null;
255: }
256:
257: /**
258: *
259: * @return bool
260: */
261: public function isLeaf()
262: {
263: return !$this->children()->exists();
264: }
265:
266: /**
267: *
268: * @return bool
269: */
270: public function hasChildren()
271: {
272: return $this->children()->exists();
273: }
274:
275: /**
276: *
277: * @return bool
278: */
279: public function isChildOf($item)
280: {
281: $parentKey = $this->getParentForeignKeyName();
282: return $item->getKey() == $this->$parentKey;
283: }
284:
285: /**
286: *
287: * @param static $item
288: * @return bool
289: */
290: public function isParentOf($item)
291: {
292: return $item->isChildOf($this);
293: }
294:
295: /**
296: *
297: * @param static $item
298: * @return bool
299: */
300: public function isDescendantOf($item)
301: {
302: return $this->ancestors()->whereKey($item->getKey())->exists();
303: }
304:
305: /**
306: *
307: * @param static $item
308: * @return bool
309: */
310: public function isAncestorOf($item)
311: {
312: return $item->isDescendantOf($this);
313: }
314:
315: /**
316: *
317: * @param static $item
318: * @return bool
319: */
320: public function isSiblingOf($item)
321: {
322: $parentKey = $this->getParentForeignKeyName();
323: return $item->$parentKey == $this->$parentKey;
324: }
325:
326: /**
327: * Returns the closest common ancestor with the provided $item.
328: * May return null if the tree has multiple roots and the 2 items have no
329: * common ancestor.
330: *
331: * @param static $item
332: * @return static|null
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: * Returns the distance between $this and another $item.
345: * May throw an exception if the tree has multiple roots and the 2 items
346: * have no common ancestor.
347: *
348: * @param static $item
349: * @return int
350: * @throws TreeException
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: * Return the depth of $this in the tree (0 is $this is a root).
366: *
367: * @return int
368: */
369: public function getDepth()
370: {
371: return $this->ancestors()->count();
372: }
373:
374: /**
375: * Returns the depth of the subtree of which $this is a root.
376: *
377: * @return int
378: */
379: public function getSubtreeDepth()
380: {
381: return (int) $this->descendants()->orderByDepth('desc')->value('depth');
382: }
383:
384:
385: // =========================================================================
386: // QUERY SCOPES
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: // ADDITIONAL USEFUL METHODS
479: // =========================================================================
480:
481: /**
482: * Shortcut method that returns a collection of the tree roots, with their
483: * eager-loaded descendants.
484: *
485: * @param int $depth
486: * @return \Illuminate\Database\Eloquent\Collection
487: */
488: public static function getTree($depth = null)
489: {
490: return static::query()->whereIsRoot()->withDescendants($depth)->get();
491: }
492:
493: /**
494: * Return the depth of the tree (0 if the tree is flat).
495: *
496: * @return int
497: */
498: public static function getTreeDepth()
499: {
500: $instance = new static();
501: return $instance->getConnection()->table($instance->getClosureTable())->max('depth');
502: }
503:
504: // =========================================================================
505: // INSERTING AND UPDATING CLOSURES
506: // =========================================================================
507:
508: /**
509: * Check if the parent_id points to a descendant of the current object
510: * (and trigger an exception if it's the case).
511: *
512: * @throws TreeException
513: * @return void
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: * Re-calculate the closures when the parent_id has changed on a single item.
534: *
535: * @param bool $deleteOldClosures Can be set to false if it's a new item
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: // Delete old closures:
550: if ($deleteOldClosures) {
551: // DELETE FROM closures USING $closureTable AS closures
552: // INNER JOIN $closureTable AS descendants
553: // ON closures.descendant_id = descendants.descendant_id
554: // INNER JOIN $closureTable AS ancestors
555: // ON closures.ancestor_id = ancestors.ancestor_id
556: // WHERE descendants.ancestor_id = $id
557: // AND ancestors.descendant_id = $id
558: // AND closures.depth > descendants.depth
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: // Create self-closure if needed:
569: $connection->table($closureTable)->insertOrIgnore([
570: 'ancestor_id' => $id,
571: 'descendant_id' => $id,
572: 'depth' => 0,
573: ]);
574:
575: // Create new closures:
576: if ($newParentId) {
577: // INSERT INTO $closureTable (ancestor_id, descendant_id, depth)
578: // SELECT ancestors.ancestor_id, descendants.descendant_id, ancestors.depth + descendants.depth + 1
579: // FROM $closureTable AS ancestors
580: // CROSS JOIN $closureTable AS descendants
581: // WHERE ancestors.descendant_id = $newParentId
582: // AND descendants.ancestor_id = $id
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: * Deletes the closures for the item. Assumes that the item has no children.
604: */
605: protected function deleteClosuresForLeaf()
606: {
607: $closureTable = $this->getClosureTable();
608: $this->getConnection()->table($closureTable)->where('descendant_id', $this->getKey())->delete();
609: }
610: }
611: