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: }
436: