1: <?php
2:
3: namespace Baril\Bonsai\Concerns;
4:
5: use Baril\Bonsai\TreeException;
6:
7: trait ManagesClosures
8: {
9: /**
10: * Create, update and delete the closures when the model is saved.
11: */
12: public static function bootManagesClosures()
13: {
14: static::saving(function ($item) {
15: $item->checkIfParentIdIsValid();
16: });
17:
18: static::created(function ($item) {
19: $item->createSelfClosure();
20: $item->attachSubtree();
21: });
22:
23: static::updated(function ($item) {
24: // If the parent has changed, update the closures
25: // for the node and its descendants:
26: if ($item->isDirty($item->getParentForeignKeyName())) {
27: $item->detachSubtree();
28: $item->attachSubtree();
29: }
30: });
31:
32: static::deleting(function ($item) {
33: $item->checkIfDeletable();
34: });
35:
36: static::deleted(function ($item) {
37: // Delete the node's closures:
38: // @todo replace with !static::isSoftDeletable()
39: if (!property_exists($item, 'forceDeleting') || $item->forceDeleting) {
40: $item->deleteClosures();
41: }
42: });
43: }
44:
45: /**
46: * Check if the node can be deleted.
47: *
48: * @throws \Baril\Bonsai\TreeException
49: * @return void
50: */
51: protected function checkIfDeletable()
52: {
53: if ($this->children()->exists()) {
54: throw new TreeException('Can\'t delete an item with children!');
55: }
56: }
57:
58: /**
59: * Check that the parent is not the model itself or one of
60: * its descendants (which triggers an exception).
61: *
62: * @throws \Baril\Bonsai\TreeException
63: * @return void
64: */
65: protected function checkIfParentIdIsValid()
66: {
67: if (is_null($parentKey = $this->getParentKey())) {
68: return;
69: }
70:
71: if (!$this->parent()->exists()) {
72: throw new TreeException('The item\'s parent doesn\'t exist!');
73: }
74:
75: if (
76: $parentKey == $this->getKey()
77: || $this->descendingClosures()->whereDescendant($parentKey)->exists()
78: ) {
79: throw new TreeException(
80: 'Redundancy error! The item\'s parent can\'t be the item itself or one of its descendants.'
81: );
82: }
83: }
84:
85: /**
86: * Insert the self-closure for the model.
87: *
88: * @return int
89: */
90: protected function createSelfClosure()
91: {
92: return $this->newClosureQuery()->insert([
93: 'ancestor_id' => $this->getKey(),
94: 'descendant_id' => $this->getKey(),
95: 'depth' => 0,
96: ]);
97: }
98:
99: /**
100: * Create the closures to attach the model and its subtree
101: * to the rest of the tree (ie. the model's ancestors).
102: * This assumes that the "internal" closures of the subtree
103: * already exist.
104: *
105: * @return int
106: */
107: protected function attachSubtree()
108: {
109: if (! $parentKey = $this->getParentKey()) {
110: return;
111: }
112:
113: $connection = $this->getConnection();
114: $grammar = $connection->getQueryGrammar();
115:
116: // The new closures are all the possible combinations
117: // between the model's descending closures (including the self-closure)
118: // and the new parent's ascending closures (including the self-closure),
119: // with a depth that's the sum of both depths + 1.
120:
121: // INSERT INTO $closureTable (descendant_id, ancestor_id, depth)
122: // SELECT descendants.descendant_id, ancestors.ancestor_id, descendants.depth + ancestors.depth + 1
123: // FROM $closureTable AS descendants
124: // CROSS JOIN $closureTable AS ancestors
125: // WHERE descendants.ancestor_id = $id
126: // AND ancestors.descendant_id = $newParentId
127:
128: $select = $this->newClosureQuery('descendants')
129: ->selfCrossJoin('ancestors')
130: ->where('descendants.ancestor_id', $this->getKey())
131: ->where('ancestors.descendant_id', $parentKey)
132: ->select(
133: 'descendants.descendant_id',
134: 'ancestors.ancestor_id',
135: $connection->raw(sprintf(
136: '%s + %s + 1',
137: $grammar->wrap('descendants.depth'),
138: $grammar->wrap('ancestors.depth')
139: ))
140: );
141:
142: return $this->newClosureQuery()
143: ->insertUsing(['descendant_id', 'ancestor_id', 'depth'], $select);
144: }
145:
146: /**
147: * Delete the closures attaching the model and its subtree
148: * to the rest of the tree (ie. the model's ancestors),
149: * but not the "internal" closures of the subtree.
150: *
151: * @return int
152: */
153: protected function detachSubtree()
154: {
155: return $this->deleteClosures('>');
156: }
157:
158: /**
159: * Delete the ascending closures of the model and optionally its
160: * descendants.
161: *
162: * @param string|null $operator - '>' to detach the subtree (including the node) from the main tree
163: * - '>=' to detach the descendants from the node
164: * - null to delete all the subtree closures
165: * @return int
166: */
167: protected function deleteClosures($operator = null)
168: {
169: // DELETE FROM closures USING $closureTable AS closures
170: // INNER JOIN $closureTable AS descendants
171: // ON closures.descendant_id = descendants.descendant_id
172: // WHERE descendants.ancestor_id = $id
173: // AND closures.depth > descendants.depth
174:
175: $query = $this->newClosureQuery('closures_to_delete')
176: ->selfJoin('descendants', 'descendant_id')
177: ->where('descendants.ancestor_id', $this->id)
178: // This condition preserves the internal closures:
179: ->when($operator, function ($query, $operator) {
180: $query->whereColumn('closures_to_delete.depth', $operator, 'descendants.depth');
181: });
182:
183: return $query->delete();
184: }
185: }
186: