Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
94.37% |
134 / 142 |
|
75.00% |
18 / 24 |
CRAP | |
0.00% |
0 / 1 |
Orderable | |
94.37% |
134 / 142 |
|
75.00% |
18 / 24 |
50.45 | |
0.00% |
0 / 1 |
bootOrderable | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
7 | |||
newCollection | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getOrderColumn | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getPosition | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getMaxPosition | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getNextPosition | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
scopeOrdered | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
scopeUnordered | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
scopeForceOrderBy | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
moveToOffset | |
92.31% |
12 / 13 |
|
0.00% |
0 / 1 |
6.02 | |||
moveToStart | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
moveToEnd | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
moveToPosition | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
moveUp | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
moveDown | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
swapWith | |
85.71% |
12 / 14 |
|
0.00% |
0 / 1 |
3.03 | |||
moveAfter | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
moveBefore | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
move | |
89.47% |
17 / 19 |
|
0.00% |
0 / 1 |
4.02 | |||
getNewPosition | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
newQueryBetween | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
previous | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
next | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
scopeSetOrder | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace Baril\Orderly\Concerns; |
4 | |
5 | use Baril\Orderly\GroupException; |
6 | use Baril\Orderly\OrderableCollection; |
7 | use Baril\Orderly\PositionException; |
8 | use Illuminate\Database\Eloquent\Model; |
9 | use Illuminate\Database\Eloquent\Builder; |
10 | |
11 | /** |
12 | * @property string $orderColumn |
13 | */ |
14 | trait Orderable |
15 | { |
16 | use Groupable; |
17 | |
18 | /** |
19 | * Adds position to model on creating event. |
20 | */ |
21 | public static function bootOrderable() |
22 | { |
23 | static::creating(function ($model) { |
24 | /** @var Model $model */ |
25 | $orderColumn = $model->getOrderColumn(); |
26 | |
27 | // only automatically calculate next position with max+1 when a position has not been set already |
28 | if ($model->$orderColumn === null) { |
29 | $model->setAttribute($orderColumn, $model->getNextPosition()); |
30 | } |
31 | }); |
32 | |
33 | static::updating(function ($model) { |
34 | /** @var Model $model */ |
35 | $groupColumn = $model->getGroupColumn(); |
36 | $orderColumn = $model->getOrderColumn(); |
37 | $query = $model->newQueryInSameGroup(); |
38 | |
39 | // only automatically calculate next position with max+1 when a position has not been set already, |
40 | // or if the group is changing |
41 | if ($model->$orderColumn === null || ($groupColumn && $model->isDirty($groupColumn))) { |
42 | $model->setAttribute($orderColumn, $query->max($orderColumn) + 1); |
43 | } |
44 | }); |
45 | |
46 | static::updated(function ($model) { |
47 | $groupColumn = $model->getGroupColumn(); |
48 | // If the group was changed, we need to refresh the position for the |
49 | // former group: |
50 | if ($groupColumn && $model->isDirty($groupColumn)) { |
51 | $group = $model->getGroup(true); |
52 | static::whereGroup($group)->ordered()->updateColumnWithRowNumber($model->getOrderColumn()); |
53 | } |
54 | }); |
55 | |
56 | static::deleting(function ($model) { |
57 | $model->getConnection()->beginTransaction(); |
58 | }); |
59 | |
60 | static::deleted(function ($model) { |
61 | /** @var Model $model */ |
62 | $model->next()->decrement($model->getOrderColumn()); |
63 | $model->getConnection()->commit(); |
64 | }); |
65 | } |
66 | |
67 | public function newCollection(array $models = []) |
68 | { |
69 | return new OrderableCollection($models); |
70 | } |
71 | |
72 | /** |
73 | * @return string |
74 | */ |
75 | public function getOrderColumn() |
76 | { |
77 | return property_exists($this, 'orderColumn') ? $this->orderColumn : 'position'; |
78 | } |
79 | |
80 | public function getPosition() |
81 | { |
82 | return $this->{$this->getOrderColumn()}; |
83 | } |
84 | |
85 | /** |
86 | * Returns the maximum possible position for the current model. |
87 | * |
88 | * @return int |
89 | */ |
90 | public function getMaxPosition() |
91 | { |
92 | return $this->newQueryInSameGroup()->max($this->getOrderColumn()); |
93 | } |
94 | |
95 | /** |
96 | * Returns the position for a newly inserted model. |
97 | * |
98 | * @return int |
99 | */ |
100 | public function getNextPosition() |
101 | { |
102 | return $this->getMaxPosition() + 1; |
103 | } |
104 | |
105 | /** |
106 | * @param Builder $query |
107 | * @param string $direction |
108 | * |
109 | * @return void |
110 | */ |
111 | public function scopeOrdered($query, $direction = 'asc') |
112 | { |
113 | $this->scopeUnordered($query); |
114 | $query->orderBy($this->getOrderColumn(), $direction); |
115 | } |
116 | |
117 | /** |
118 | * @param Builder $query |
119 | * |
120 | * @return void |
121 | */ |
122 | public function scopeUnordered($query) |
123 | { |
124 | $query->getQuery()->orders = collect($query->getQuery()->orders) |
125 | ->reject(function ($order) { |
126 | return isset($order['column']) |
127 | ? $order['column'] === $this->getOrderColumn() : false; |
128 | })->values()->all(); |
129 | } |
130 | |
131 | /** |
132 | * @param Builder $query |
133 | * @param mixed $column |
134 | * @param string $direction |
135 | * |
136 | * @return void |
137 | */ |
138 | public function scopeForceOrderBy($query, $column, $direction = 'asc') |
139 | { |
140 | $this->scopeUnordered($query); |
141 | $query->orderBy($column, $direction); |
142 | } |
143 | |
144 | /** |
145 | * |
146 | * @param int $newOffset |
147 | * |
148 | * @throws PositionException |
149 | */ |
150 | public function moveToOffset($newOffset) |
151 | { |
152 | $query = $this->newQueryInSameGroup(); |
153 | $count = $query->count(); |
154 | |
155 | if ($newOffset < 0) { |
156 | $newOffset = $count + $newOffset; |
157 | } |
158 | if ($newOffset < 0 || $newOffset >= $count) { |
159 | throw new PositionException("Invalid offset $newOffset!"); |
160 | } |
161 | |
162 | $oldOffset = $this->previous()->count(); |
163 | |
164 | if ($oldOffset === $newOffset) { |
165 | return $this; |
166 | } |
167 | |
168 | $entity = $query->ordered()->offset($newOffset)->first(); |
169 | if ($oldOffset < $newOffset) { |
170 | return $this->moveAfter($entity); |
171 | } else { |
172 | return $this->moveBefore($entity); |
173 | } |
174 | } |
175 | |
176 | /** |
177 | * Moves the current model to the first position. |
178 | * |
179 | * @return $this |
180 | */ |
181 | public function moveToStart() |
182 | { |
183 | return $this->moveToOffset(0); |
184 | } |
185 | |
186 | /** |
187 | * moves $this model to the last position. |
188 | * |
189 | */ |
190 | public function moveToEnd() |
191 | { |
192 | return $this->moveToOffset(-1); |
193 | } |
194 | |
195 | /** |
196 | * |
197 | * @param int $newPosition |
198 | */ |
199 | public function moveToPosition($newPosition) |
200 | { |
201 | return $this->moveToOffset($newPosition - 1); |
202 | } |
203 | |
204 | /** |
205 | * |
206 | * @param int $positions |
207 | * @param boolean $strict |
208 | */ |
209 | public function moveUp($positions = 1, $strict = true) |
210 | { |
211 | $orderColumn = $this->getOrderColumn(); |
212 | $currentPosition = $this->getAttribute($orderColumn); |
213 | $newPosition = $currentPosition - $positions; |
214 | if (!$strict) { |
215 | $newPosition = max(1, $newPosition); |
216 | $newPosition = min($this->getMaxPosition(), $newPosition); |
217 | } |
218 | return $this->moveToPosition($newPosition); |
219 | } |
220 | |
221 | /** |
222 | * |
223 | * @param int $positions |
224 | * @param boolean $strict |
225 | */ |
226 | public function moveDown($positions = 1, $strict = true) |
227 | { |
228 | return $this->moveUp(-$positions, $strict); |
229 | } |
230 | |
231 | /** |
232 | * |
233 | * @param static $entity |
234 | * |
235 | * @throws GroupException |
236 | */ |
237 | public function swapWith($entity) |
238 | { |
239 | if (!$this->isInSameGroupAs($entity)) { |
240 | throw new GroupException('Both models must be in the same group!'); |
241 | } |
242 | |
243 | $this->getConnection()->transaction(function () use ($entity) { |
244 | $orderColumn = $this->getOrderColumn(); |
245 | |
246 | $oldPosition = $this->getAttribute($orderColumn); |
247 | $newPosition = $entity->getAttribute($orderColumn); |
248 | |
249 | if ($oldPosition === $newPosition) { |
250 | return; |
251 | } |
252 | |
253 | $this->setAttribute($orderColumn, $newPosition); |
254 | $entity->setAttribute($orderColumn, $oldPosition); |
255 | |
256 | $this->save(); |
257 | $entity->save(); |
258 | }); |
259 | return $this; |
260 | } |
261 | |
262 | /** |
263 | * moves $this model after $entity model (and rearrange all entities). |
264 | * |
265 | * @param static $entity |
266 | * |
267 | * @throws GroupException |
268 | */ |
269 | public function moveAfter($entity) |
270 | { |
271 | return $this->move('moveAfter', $entity); |
272 | } |
273 | |
274 | /** |
275 | * moves $this model before $entity model (and rearrange all entities). |
276 | * |
277 | * @param static $entity |
278 | * |
279 | * @throws GroupException |
280 | */ |
281 | public function moveBefore($entity) |
282 | { |
283 | return $this->move('moveBefore', $entity); |
284 | } |
285 | |
286 | /** |
287 | * @param string $action moveAfter/moveBefore |
288 | * @param static $entity |
289 | * |
290 | * @throws GroupException |
291 | */ |
292 | protected function move($action, $entity) |
293 | { |
294 | if (!$this->isInSameGroupAs($entity)) { |
295 | throw new GroupException('Both models must be in same group!'); |
296 | } |
297 | |
298 | $this->getConnection()->transaction(function () use ($entity, $action) { |
299 | $orderColumn = $this->getOrderColumn(); |
300 | |
301 | $oldPosition = $this->getAttribute($orderColumn); |
302 | $newPosition = $entity->getAttribute($orderColumn); |
303 | |
304 | if ($oldPosition === $newPosition) { |
305 | return; |
306 | } |
307 | |
308 | $isMoveBefore = $action === 'moveBefore'; // otherwise moveAfter |
309 | $isMoveForward = $oldPosition < $newPosition; |
310 | |
311 | if ($isMoveForward) { |
312 | $this->newQueryBetween($oldPosition, $newPosition)->decrement($orderColumn); |
313 | } else { |
314 | $this->newQueryBetween($newPosition, $oldPosition)->increment($orderColumn); |
315 | } |
316 | |
317 | $this->setAttribute($orderColumn, $this->getNewPosition($isMoveBefore, $isMoveForward, $newPosition)); |
318 | $entity->setAttribute($orderColumn, $this->getNewPosition(!$isMoveBefore, $isMoveForward, $newPosition)); |
319 | |
320 | $this->save(); |
321 | $entity->save(); |
322 | }); |
323 | return $this; |
324 | } |
325 | |
326 | /** |
327 | * @param bool $isMoveBefore |
328 | * @param bool $isMoveForward |
329 | * @param $position |
330 | * |
331 | * @return mixed |
332 | */ |
333 | protected function getNewPosition($isMoveBefore, $isMoveForward, $position) |
334 | { |
335 | if (!$isMoveBefore) { |
336 | ++$position; |
337 | } |
338 | |
339 | if ($isMoveForward) { |
340 | --$position; |
341 | } |
342 | |
343 | return $position; |
344 | } |
345 | |
346 | /** |
347 | * @param $leftPosition |
348 | * @param $rightPosition |
349 | * |
350 | * @return Builder |
351 | */ |
352 | protected function newQueryBetween($leftPosition, $rightPosition) |
353 | { |
354 | $orderColumn = $this->getOrderColumn(); |
355 | $query = $this->newQueryInSameGroup(); |
356 | |
357 | if (!is_null($leftPosition)) { |
358 | $query->where($orderColumn, '>', $leftPosition); |
359 | } |
360 | if (!is_null($rightPosition)) { |
361 | $query->where($orderColumn, '<', $rightPosition); |
362 | } |
363 | return $query; |
364 | } |
365 | |
366 | /** |
367 | * @param int $limit |
368 | * |
369 | * @return Builder |
370 | */ |
371 | public function previous($limit = null) |
372 | { |
373 | $orderColumn = $this->getOrderColumn(); |
374 | $query = $this->newQueryBetween(null, $this->getAttribute($orderColumn))->ordered('desc'); |
375 | if ($limit) { |
376 | $query->limit($limit); |
377 | } |
378 | return $query; |
379 | } |
380 | |
381 | /** |
382 | * @param int $limit |
383 | * |
384 | * @return Builder |
385 | */ |
386 | public function next($limit = null) |
387 | { |
388 | $orderColumn = $this->getOrderColumn(); |
389 | $query = $this->newQueryBetween($this->getAttribute($orderColumn), null)->ordered(); |
390 | if ($limit) { |
391 | $query->limit($limit); |
392 | } |
393 | return $query; |
394 | } |
395 | |
396 | /** |
397 | * Reorders the elements based on their ids. |
398 | * |
399 | * @param Builder $query |
400 | * @param array $ids |
401 | * @return int |
402 | */ |
403 | public function scopeSetOrder($query, $ids) |
404 | { |
405 | $query = clone $query; |
406 | $query->setQuery($query->getQuery()->cloneWithout(['orders'])->cloneWithoutBindings(['order'])); |
407 | |
408 | $instance = $query->getModel(); |
409 | $pdo = $instance->getConnection()->getPdo(); |
410 | |
411 | // We're selecting only the necessary columns: |
412 | $orderColumn = $instance->getOrderColumn(); |
413 | $groupColumn = $instance->getGroupColumn(); |
414 | $columns = [ |
415 | $instance->getKeyName(), |
416 | $orderColumn, |
417 | ]; |
418 | if ($groupColumn) { |
419 | $columns = array_merge($columns, (array) $groupColumn); |
420 | } |
421 | |
422 | $collection = $query->orderByValues($instance->getKeyName(), $ids) |
423 | ->ordered() |
424 | ->select($columns) |
425 | ->get(); |
426 | |
427 | $oldPositions = $collection->pluck($orderColumn); |
428 | $collection->saveOrder(); |
429 | $newPositions = $collection->pluck($orderColumn); |
430 | |
431 | return $oldPositions->combine($newPositions)->map(function ($new, $old) { |
432 | return (int) ($old != $new); |
433 | })->sum(); |
434 | } |
435 | } |