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->getPosition() === 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: | $currentPosition = $this->getPosition(); |
212: | $newPosition = $currentPosition - $positions; |
213: | if (!$strict) { |
214: | $newPosition = max(1, $newPosition); |
215: | $newPosition = min($this->getMaxPosition(), $newPosition); |
216: | } |
217: | return $this->moveToPosition($newPosition); |
218: | } |
219: | |
220: | |
221: | |
222: | |
223: | |
224: | |
225: | public function moveDown($positions = 1, $strict = true) |
226: | { |
227: | return $this->moveUp(-$positions, $strict); |
228: | } |
229: | |
230: | |
231: | |
232: | |
233: | |
234: | |
235: | |
236: | public function swapWith($entity) |
237: | { |
238: | if (!$this->isInSameGroupAs($entity)) { |
239: | throw new GroupException('Both models must be in the same group!'); |
240: | } |
241: | |
242: | $this->getConnection()->transaction(function () use ($entity) { |
243: | $orderColumn = $this->getOrderColumn(); |
244: | |
245: | $oldPosition = $this->getPosition(); |
246: | $newPosition = $entity->getPosition(); |
247: | |
248: | if ($oldPosition === $newPosition) { |
249: | return; |
250: | } |
251: | |
252: | $this->setAttribute($orderColumn, $newPosition); |
253: | $entity->setAttribute($orderColumn, $oldPosition); |
254: | |
255: | $this->save(); |
256: | $entity->save(); |
257: | }); |
258: | return $this; |
259: | } |
260: | |
261: | |
262: | |
263: | |
264: | |
265: | |
266: | |
267: | |
268: | public function moveAfter($entity) |
269: | { |
270: | return $this->move('moveAfter', $entity); |
271: | } |
272: | |
273: | |
274: | |
275: | |
276: | |
277: | |
278: | |
279: | |
280: | public function moveBefore($entity) |
281: | { |
282: | return $this->move('moveBefore', $entity); |
283: | } |
284: | |
285: | |
286: | |
287: | |
288: | |
289: | |
290: | |
291: | protected function move($action, $entity) |
292: | { |
293: | if (!$this->isInSameGroupAs($entity)) { |
294: | throw new GroupException('Both models must be in same group!'); |
295: | } |
296: | |
297: | $this->getConnection()->transaction(function () use ($entity, $action) { |
298: | $orderColumn = $this->getOrderColumn(); |
299: | |
300: | $oldPosition = $this->getPosition(); |
301: | $newPosition = $entity->getPosition(); |
302: | |
303: | if ($oldPosition === $newPosition) { |
304: | return; |
305: | } |
306: | |
307: | $isMoveBefore = $action === 'moveBefore'; |
308: | $isMoveForward = $oldPosition < $newPosition; |
309: | |
310: | if ($isMoveForward) { |
311: | $this->newQueryBetween($oldPosition, $newPosition)->decrement($orderColumn); |
312: | } else { |
313: | $this->newQueryBetween($newPosition, $oldPosition)->increment($orderColumn); |
314: | } |
315: | |
316: | $this->setAttribute($orderColumn, $this->getNewPosition($isMoveBefore, $isMoveForward, $newPosition)); |
317: | $entity->setAttribute($orderColumn, $this->getNewPosition(!$isMoveBefore, $isMoveForward, $newPosition)); |
318: | |
319: | $this->save(); |
320: | $entity->save(); |
321: | }); |
322: | return $this; |
323: | } |
324: | |
325: | |
326: | |
327: | |
328: | |
329: | |
330: | |
331: | |
332: | protected function getNewPosition($isMoveBefore, $isMoveForward, $position) |
333: | { |
334: | if (!$isMoveBefore) { |
335: | ++$position; |
336: | } |
337: | |
338: | if ($isMoveForward) { |
339: | --$position; |
340: | } |
341: | |
342: | return $position; |
343: | } |
344: | |
345: | |
346: | |
347: | |
348: | |
349: | |
350: | |
351: | protected function newQueryBetween($leftPosition, $rightPosition) |
352: | { |
353: | $orderColumn = $this->getOrderColumn(); |
354: | $query = $this->newQueryInSameGroup(); |
355: | |
356: | if (!is_null($leftPosition)) { |
357: | $query->where($orderColumn, '>', $leftPosition); |
358: | } |
359: | if (!is_null($rightPosition)) { |
360: | $query->where($orderColumn, '<', $rightPosition); |
361: | } |
362: | return $query; |
363: | } |
364: | |
365: | |
366: | |
367: | |
368: | |
369: | |
370: | public function previous($limit = null) |
371: | { |
372: | $query = $this->newQueryBetween(null, $this->getPosition())->ordered('desc'); |
373: | if ($limit) { |
374: | $query->limit($limit); |
375: | } |
376: | return $query; |
377: | } |
378: | |
379: | |
380: | |
381: | |
382: | |
383: | |
384: | public function next($limit = null) |
385: | { |
386: | $query = $this->newQueryBetween($this->getPosition(), null)->ordered(); |
387: | if ($limit) { |
388: | $query->limit($limit); |
389: | } |
390: | return $query; |
391: | } |
392: | |
393: | |
394: | |
395: | |
396: | |
397: | |
398: | |
399: | |
400: | public function scopeSetOrder($query, $ids) |
401: | { |
402: | $query = clone $query; |
403: | $query->setQuery($query->getQuery()->cloneWithout(['orders'])->cloneWithoutBindings(['order'])); |
404: | |
405: | $instance = $query->getModel(); |
406: | $pdo = $instance->getConnection()->getPdo(); |
407: | |
408: | |
409: | $orderColumn = $instance->getOrderColumn(); |
410: | $groupColumn = $instance->getGroupColumn(); |
411: | $columns = [ |
412: | $instance->getKeyName(), |
413: | $orderColumn, |
414: | ]; |
415: | if ($groupColumn) { |
416: | $columns = array_merge($columns, (array) $groupColumn); |
417: | } |
418: | |
419: | $collection = $query->orderByValues($instance->getKeyName(), $ids) |
420: | ->ordered() |
421: | ->select($columns) |
422: | ->get(); |
423: | |
424: | $oldPositions = $collection->pluck($orderColumn); |
425: | $collection->saveOrder(); |
426: | $newPositions = $collection->pluck($orderColumn); |
427: | |
428: | return $oldPositions->combine($newPositions)->map(function ($new, $old) { |
429: | return (int) ($old != $new); |
430: | })->sum(); |
431: | } |
432: | } |
433: | |