1 | <?php |
||
2 | |||
3 | namespace Encima\Albero; |
||
4 | |||
5 | use Illuminate\Database\Eloquent\Model; |
||
6 | use Illuminate\Contracts\Events\Dispatcher; |
||
7 | |||
8 | /** |
||
9 | * Move |
||
10 | */ |
||
11 | class Move |
||
12 | { |
||
13 | /** @var \Illuminate\Database\Eloquent\Model */ |
||
14 | protected $node = null; |
||
15 | |||
16 | /** @var \Illuminate\Database\Eloquent\Model|int */ |
||
17 | protected $target = null; |
||
18 | |||
19 | /** @var string|null */ |
||
20 | protected $position = null; |
||
21 | |||
22 | /** @var int|null */ |
||
23 | protected $_bound1 = null; |
||
24 | |||
25 | /** @var int|null */ |
||
26 | protected $_bound2 = null; |
||
27 | |||
28 | /** @var array|null */ |
||
29 | protected $_boundaries = null; |
||
30 | |||
31 | /** @var \Illuminate\Events\Dispatcher */ |
||
32 | protected static $dispatcher; |
||
33 | |||
34 | /** |
||
35 | * Create a new Move class instance. |
||
36 | * |
||
37 | * @param \Illuminate\Database\Eloquent\Model $node |
||
38 | * @param \Illuminate\Database\Eloquent\Model|int $target |
||
39 | * @param string $position |
||
40 | * @return void |
||
41 | */ |
||
42 | public function __construct(Model $node, $target, string $position) |
||
43 | { |
||
44 | $this->node = $node; |
||
45 | $this->target = $this->resolveNode($target); |
||
46 | $this->position = $position; |
||
47 | $this->setEventDispatcher($node->getEventDispatcher()); |
||
48 | } |
||
49 | |||
50 | /** |
||
51 | * Easy static accessor for performing a move operation. |
||
52 | * |
||
53 | * @param \Illuminate\Database\Eloquent\Model $node |
||
54 | * @param \Illuminate\Database\Eloquent\Model|int $target |
||
55 | * @param string $position |
||
56 | * @return \Illuminate\Database\Eloquent\Model |
||
57 | */ |
||
58 | public static function to(Model $node, $target, string $position): Model |
||
59 | { |
||
60 | $instance = new static($node, $target, $position); |
||
61 | |||
62 | return $instance->perform(); |
||
63 | } |
||
64 | |||
65 | /** |
||
66 | * Perform the move operation. |
||
67 | * |
||
68 | * @return \Illuminate\Database\Eloquent\Model |
||
69 | */ |
||
70 | public function perform(): Model |
||
71 | { |
||
72 | $this->guardAgainstImpossibleMove(); |
||
73 | |||
74 | if ($this->fireMoveEvent('moving') === false) { |
||
75 | return $this->node; |
||
76 | } |
||
77 | |||
78 | if ($this->hasChange()) { |
||
79 | $self = $this; |
||
80 | |||
81 | $this->node->getConnection()->transaction(function () use ($self) { |
||
82 | $self->updateStructure(); |
||
83 | }); |
||
84 | |||
85 | $this->target->reload(); |
||
86 | |||
87 | $this->node->setDepthWithSubtree(); |
||
88 | |||
89 | $this->node->reload(); |
||
90 | } |
||
91 | |||
92 | $this->fireMoveEvent('moved', false); |
||
93 | |||
94 | return $this->node; |
||
95 | } |
||
96 | |||
97 | /** |
||
98 | * Runs the SQL query associated with the update of the indexes affected |
||
99 | * by the move operation. |
||
100 | * |
||
101 | * @return int |
||
102 | */ |
||
103 | public function updateStructure(): int |
||
104 | { |
||
105 | list($a, $b, $c, $d) = $this->boundaries(); |
||
106 | |||
107 | // select the rows between the leftmost & the rightmost boundaries and apply a lock |
||
108 | $this->applyLockBetween($a, $d); |
||
109 | |||
110 | $connection = $this->node->getConnection(); |
||
111 | $grammar = $connection->getQueryGrammar(); |
||
112 | |||
113 | $currentId = $this->quoteIdentifier($this->node->getKey()); |
||
114 | $parentId = $this->quoteIdentifier($this->parentId()); |
||
115 | $leftColumn = $this->node->getLeftColumnName(); |
||
116 | $rightColumn = $this->node->getRightColumnName(); |
||
117 | $parentColumn = $this->node->getParentColumnName(); |
||
118 | $wrappedLeft = $grammar->wrap($leftColumn); |
||
119 | $wrappedRight = $grammar->wrap($rightColumn); |
||
120 | $wrappedParent = $grammar->wrap($parentColumn); |
||
121 | $wrappedId = $grammar->wrap($this->node->getKeyName()); |
||
122 | |||
123 | $lftSql = "CASE |
||
124 | WHEN ${wrappedLeft} BETWEEN ${a} AND ${b} THEN ${wrappedLeft} + ${d} - ${b} |
||
125 | WHEN ${wrappedLeft} BETWEEN ${c} AND ${d} THEN ${wrappedLeft} + ${a} - ${c} |
||
126 | ELSE ${wrappedLeft} END"; |
||
127 | |||
128 | $rgtSql = "CASE |
||
129 | WHEN ${wrappedRight} BETWEEN ${a} AND ${b} THEN ${wrappedRight} + ${d} - ${b} |
||
130 | WHEN ${wrappedRight} BETWEEN ${c} AND ${d} THEN ${wrappedRight} + ${a} - ${c} |
||
131 | ELSE ${wrappedRight} END"; |
||
132 | |||
133 | $parentSql = "CASE |
||
134 | WHEN ${wrappedId} = ${currentId} THEN ${parentId} |
||
135 | ELSE ${wrappedParent} END"; |
||
136 | |||
137 | $updateConditions = [ |
||
138 | $leftColumn => $connection->raw($lftSql), |
||
139 | $rightColumn => $connection->raw($rgtSql), |
||
140 | $parentColumn => $connection->raw($parentSql), |
||
141 | ]; |
||
142 | |||
143 | if ($this->node->timestamps) { |
||
144 | $updateConditions[$this->node->getUpdatedAtColumn()] = $this->node->freshTimestamp(); |
||
145 | } |
||
146 | |||
147 | return $this->node |
||
148 | ->newNestedSetQuery() |
||
149 | ->where(function ($query) use ($leftColumn, $rightColumn, $a, $d) { |
||
150 | $query->whereBetween($leftColumn, [$a, $d]) |
||
151 | ->orWhereBetween($rightColumn, [$a, $d]); |
||
152 | }) |
||
153 | ->update($updateConditions); |
||
154 | } |
||
155 | |||
156 | /** |
||
157 | * Resolves suplied node. Basically returns the node unchanged if |
||
158 | * supplied parameter is an instance of \Illuminate\Database\Eloquent\Model. Otherwise it will try |
||
159 | * to find the node in the database. |
||
160 | * |
||
161 | * @param \Illuminate\Database\Eloquent\Model|int |
||
162 | * @return \Illuminate\Database\Eloquent\Model |
||
163 | */ |
||
164 | protected function resolveNode($node): ?Model |
||
165 | { |
||
166 | if (is_object($node)) { |
||
167 | return $node->reload(); |
||
168 | } |
||
169 | |||
170 | return $this->node->newNestedSetQuery()->find($node); |
||
171 | } |
||
172 | |||
173 | /** |
||
174 | * Check wether the current move is possible and if not, rais an exception. |
||
175 | * |
||
176 | * @return void |
||
177 | */ |
||
178 | protected function guardAgainstImpossibleMove(): void |
||
179 | { |
||
180 | if (!$this->node->exists) { |
||
181 | throw new MoveNotPossibleException('A new node cannot be moved.'); |
||
182 | } |
||
183 | |||
184 | if (array_search($this->position, ['child', 'left', 'right', 'root']) === false) { |
||
185 | throw new MoveNotPossibleException("Position should be one of ['child', 'left', 'right'] but is {$this->position}."); |
||
186 | } |
||
187 | |||
188 | if (!$this->promotingToRoot()) { |
||
189 | if (is_null($this->target)) { |
||
190 | if ($this->position === 'left' || $this->position === 'right') { |
||
191 | throw new MoveNotPossibleException("Could not resolve target node. This node cannot move any further to the {$this->position}."); |
||
192 | } |
||
193 | |||
194 | throw new MoveNotPossibleException('Could not resolve target node.'); |
||
195 | } |
||
196 | |||
197 | if ($this->node->equals($this->target)) { |
||
198 | throw new MoveNotPossibleException('A node cannot be moved to itself.'); |
||
199 | } |
||
200 | |||
201 | if ($this->target->insideSubtree($this->node)) { |
||
202 | throw new MoveNotPossibleException('A node cannot be moved to a descendant of itself (inside moved tree).'); |
||
203 | } |
||
204 | |||
205 | if (!$this->node->inSameScope($this->target)) { |
||
206 | throw new MoveNotPossibleException('A node cannot be moved to a different scope.'); |
||
207 | } |
||
208 | } |
||
209 | } |
||
210 | |||
211 | /** |
||
212 | * Computes the boundary. |
||
213 | * |
||
214 | * @return int |
||
215 | */ |
||
216 | protected function bound1(): int |
||
217 | { |
||
218 | if (!is_null($this->_bound1)) { |
||
219 | return $this->_bound1; |
||
220 | } |
||
221 | |||
222 | switch ($this->position) { |
||
223 | case 'child': |
||
224 | $this->_bound1 = $this->target->getRight(); |
||
0 ignored issues
–
show
|
|||
225 | |||
226 | break; |
||
227 | |||
228 | case 'left': |
||
229 | $this->_bound1 = $this->target->getLeft(); |
||
230 | |||
231 | break; |
||
232 | |||
233 | case 'right': |
||
234 | $this->_bound1 = $this->target->getRight() + 1; |
||
235 | |||
236 | break; |
||
237 | |||
238 | case 'root': |
||
239 | $this->_bound1 = $this->node->newNestedSetQuery()->max($this->node->getRightColumnName()) + 1; |
||
240 | |||
241 | break; |
||
242 | } |
||
243 | |||
244 | $this->_bound1 = (($this->_bound1 > $this->node->getRight()) ? $this->_bound1 - 1 : $this->_bound1); |
||
245 | |||
246 | return $this->_bound1; |
||
247 | } |
||
248 | |||
249 | /** |
||
250 | * Computes the other boundary. |
||
251 | * TODO: Maybe find a better name for this... ¿? |
||
252 | * |
||
253 | * @return int |
||
254 | */ |
||
255 | protected function bound2(): int |
||
256 | { |
||
257 | if (!is_null($this->_bound2)) { |
||
258 | return $this->_bound2; |
||
259 | } |
||
260 | |||
261 | $this->_bound2 = (($this->bound1() > $this->node->getRight()) ? $this->node->getRight() + 1 : $this->node->getLeft() - 1); |
||
262 | |||
263 | return $this->_bound2; |
||
264 | } |
||
265 | |||
266 | /** |
||
267 | * Computes the boundaries array. |
||
268 | * |
||
269 | * @return array |
||
270 | */ |
||
271 | protected function boundaries(): array |
||
272 | { |
||
273 | if (!is_null($this->_boundaries)) { |
||
274 | return $this->_boundaries; |
||
275 | } |
||
276 | |||
277 | // we have defined the boundaries of two non-overlapping intervals, |
||
278 | // so sorting puts both the intervals and their boundaries in order |
||
279 | $this->_boundaries = [ |
||
280 | $this->node->getLeft(), |
||
281 | $this->node->getRight(), |
||
282 | $this->bound1(), |
||
283 | $this->bound2(), |
||
284 | ]; |
||
285 | sort($this->_boundaries); |
||
286 | |||
287 | return $this->_boundaries; |
||
288 | } |
||
289 | |||
290 | /** |
||
291 | * Computes the new parent id for the node being moved. |
||
292 | * |
||
293 | * @return int|string|null |
||
294 | */ |
||
295 | protected function parentId() |
||
296 | { |
||
297 | switch ($this->position) { |
||
298 | case 'root': |
||
299 | return null; |
||
300 | |||
301 | case 'child': |
||
302 | return $this->target->getKey(); |
||
303 | |||
304 | default: |
||
305 | return $this->target->getParentId(); |
||
306 | } |
||
307 | } |
||
308 | |||
309 | /** |
||
310 | * Check wether there should be changes in the downward tree structure. |
||
311 | * |
||
312 | * @return boolean |
||
313 | */ |
||
314 | protected function hasChange(): bool |
||
315 | { |
||
316 | return !($this->bound1() == $this->node->getRight() || $this->bound1() == $this->node->getLeft()); |
||
317 | } |
||
318 | |||
319 | /** |
||
320 | * Check if we are promoting the provided instance to a root node. |
||
321 | * |
||
322 | * @return boolean |
||
323 | */ |
||
324 | protected function promotingToRoot(): bool |
||
325 | { |
||
326 | return ($this->position == 'root'); |
||
327 | } |
||
328 | |||
329 | /** |
||
330 | * Get the event dispatcher instance. |
||
331 | * |
||
332 | * @return \Illuminate\Contracts\Events\Dispatcher |
||
333 | */ |
||
334 | public static function getEventDispatcher(): Dispatcher |
||
335 | { |
||
336 | return static::$dispatcher; |
||
337 | } |
||
338 | |||
339 | /** |
||
340 | * Set the event dispatcher instance. |
||
341 | * |
||
342 | * @param \Illuminate\Events\Dispatcher |
||
343 | * @return void |
||
344 | */ |
||
345 | public static function setEventDispatcher(Dispatcher $dispatcher): void |
||
346 | { |
||
347 | static::$dispatcher = $dispatcher; |
||
0 ignored issues
–
show
$dispatcher is of type Illuminate\Contracts\Events\Dispatcher , but the property $dispatcher was declared to be of type Illuminate\Events\Dispatcher . Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly. Either this assignment is in error or an instanceof check should be added for that assignment. class Alien {}
class Dalek extends Alien {}
class Plot
{
/** @var Dalek */
public $villain;
}
$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
$plot->villain = $alien;
}
![]() |
|||
348 | } |
||
349 | |||
350 | /** |
||
351 | * Fire the given move event for the model. |
||
352 | * |
||
353 | * @param string $event |
||
354 | * @param bool $halt |
||
355 | * @return mixed |
||
356 | */ |
||
357 | protected function fireMoveEvent(string $event, bool $halt = true) |
||
358 | { |
||
359 | if (!isset(static::$dispatcher)) { |
||
360 | return true; |
||
361 | } |
||
362 | |||
363 | // Basically the same as \Illuminate\Database\Eloquent\Model->fireModelEvent |
||
364 | // but we relay the event into the node instance. |
||
365 | $event = "eloquent.{$event}: ".get_class($this->node); |
||
366 | |||
367 | $method = $halt ? 'until' : 'dispatch'; |
||
368 | // dd(static::$dispatcher); |
||
369 | |||
370 | return static::$dispatcher->{$method}($event, $this->node); |
||
371 | } |
||
372 | |||
373 | /** |
||
374 | * Quotes an identifier for being used in a database query. |
||
375 | * |
||
376 | * @param mixed $value |
||
377 | * @return string |
||
378 | */ |
||
379 | protected function quoteIdentifier($value): string |
||
380 | { |
||
381 | if (is_null($value)) { |
||
382 | return 'NULL'; |
||
383 | } |
||
384 | |||
385 | $connection = $this->node->getConnection(); |
||
386 | |||
387 | $pdo = $connection->getPdo(); |
||
388 | |||
389 | return $pdo->quote($value); |
||
390 | } |
||
391 | |||
392 | /** |
||
393 | * Applies a lock to the rows between the supplied index boundaries. |
||
394 | * |
||
395 | * @param int $lft |
||
396 | * @param int $rgt |
||
397 | * @return void |
||
398 | */ |
||
399 | protected function applyLockBetween(int $lft, int $rgt): void |
||
400 | { |
||
401 | $this->node->newQuery() |
||
402 | ->where($this->node->getLeftColumnName(), '>=', $lft) |
||
403 | ->where($this->node->getRightColumnName(), '<=', $rgt) |
||
404 | ->select($this->node->getKeyName()) |
||
405 | ->lockForUpdate() |
||
406 | ->get(); |
||
407 | } |
||
408 | } |
||
409 |
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.