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: | |
13: | |
14: | trait Orderable |
15: | { |
16: | use Groupable; |
17: | |
18: | |
19: | |
20: | |
21: | public static function bootOrderable() |
22: | { |
23: | static::creating(function ($model) { |
24: | |
25: | $orderColumn = $model->getOrderColumn(); |
26: | |
27: | |
28: | if ($model->$orderColumn === null) { |
29: | $model->setAttribute($orderColumn, $model->getNextPosition()); |
30: | } |
31: | }); |
32: | |
33: | static::updating(function ($model) { |
34: | |
35: | $groupColumn = $model->getGroupColumn(); |
36: | $orderColumn = $model->getOrderColumn(); |
37: | $query = $model->newQueryInSameGroup(); |
38: | |
39: | |
40: | |
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: | |
49: | |
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: | |
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: | |
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: | |
87: | |
88: | |
89: | |
90: | public function getMaxPosition() |
91: | { |
92: | return $this->newQueryInSameGroup()->max($this->getOrderColumn()); |
93: | } |
94: | |
95: | |
96: | |
97: | |
98: | |
99: | |
100: | public function getNextPosition() |
101: | { |
102: | return $this->getMaxPosition() + 1; |
103: | } |
104: | |
105: | |
106: | |
107: | |
108: | |
109: | |
110: | |
111: | public function scopeOrdered($query, $direction = 'asc') |
112: | { |
113: | $this->scopeUnordered($query); |
114: | $query->orderBy($this->getOrderColumn(), $direction); |
115: | } |
116: | |
117: | |
118: | |
119: | |
120: | |
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: | |
133: | |
134: | |
135: | |
136: | |
137: | |
138: | public function scopeForceOrderBy($query, $column, $direction = 'asc') |
139: | { |
140: | $this->scopeUnordered($query); |
141: | $query->orderBy($column, $direction); |
142: | } |
143: | |
144: | |
145: | |
146: | |
147: | |
148: | |
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: | |
178: | |
179: | |
180: | |
181: | public function moveToStart() |
182: | { |
183: | return $this->moveToOffset(0); |
184: | } |
185: | |
186: | |
187: | |
188: | |
189: | |
190: | public function moveToEnd() |
191: | { |
192: | return $this->moveToOffset(-1); |
193: | } |
194: | |
195: | |
196: | |
197: | |
198: | |
199: | public function moveToPosition($newPosition) |
200: | { |
201: | return $this->moveToOffset($newPosition - 1); |
202: | } |
203: | |
204: | |
205: | |
206: | |
207: | |
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: | |
224: | |
225: | |
226: | public function moveDown($positions = 1, $strict = true) |
227: | { |
228: | return $this->moveUp(-$positions, $strict); |
229: | } |
230: | |
231: | |
232: | |
233: | |
234: | |
235: | |
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: | |
264: | |
265: | |
266: | |
267: | |
268: | |
269: | public function moveAfter($entity) |
270: | { |
271: | return $this->move('moveAfter', $entity); |
272: | } |
273: | |
274: | |
275: | |
276: | |
277: | |
278: | |
279: | |
280: | |
281: | public function moveBefore($entity) |
282: | { |
283: | return $this->move('moveBefore', $entity); |
284: | } |
285: | |
286: | |
287: | |
288: | |
289: | |
290: | |
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'; |
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: | |
328: | |
329: | |
330: | |
331: | |
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: | |
348: | |
349: | |
350: | |
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: | |
368: | |
369: | |
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: | |
383: | |
384: | |
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: | |
398: | |
399: | |
400: | |
401: | |
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: | |
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: | } |
436: | |