Sortable::moveToEnd()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 11
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 21
rs 9.9
1
<?php
2
3
namespace Squadron\Base\Models\Traits;
4
5
use ArrayAccess;
6
use InvalidArgumentException;
7
use Illuminate\Database\Eloquent\Builder;
8
use Illuminate\Database\Eloquent\SoftDeletingScope;
9
10
/**
11
 * @property int $sortOrder
12
 */
13
trait Sortable
14
{
15
    public static function bootSortable(): void
16
    {
17
        static::creating(function ($model) {
18
            $model->setHighestOrderNumber();
19
        });
20
    }
21
22
    /**
23
     * Let's be nice and provide an ordered scope.
24
     *
25
     * @param Builder $query
26
     * @param string  $direction
27
     *
28
     * @return Builder
29
     */
30
    public function scopeOrdered(Builder $query, string $direction = 'asc'): Builder
31
    {
32
        return $query->orderBy($this->determineOrderColumnName(), $direction);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->orderBy($...lumnName(), $direction) could return the type Illuminate\Database\Query\Builder which is incompatible with the type-hinted return Illuminate\Database\Eloquent\Builder. Consider adding an additional type-check to rule them out.
Loading history...
33
    }
34
35
    /**
36
     * Modify the order column value.
37
     */
38
    public function setHighestOrderNumber(): void
39
    {
40
        $orderColumnName = $this->determineOrderColumnName();
41
42
        $this->$orderColumnName = $this->getHighestOrderNumber() + 1;
43
    }
44
45
    /**
46
     * Determine the order value for the new record.
47
     */
48
    public function getHighestOrderNumber(): int
49
    {
50
        return (int) $this->buildSortQuery()->max($this->determineOrderColumnName());
51
    }
52
53
    /**
54
     * This function reorders the records: the record with the first id in the array
55
     * will get order 1, the record with the second it will get order 2, ...
56
     *
57
     * A starting order number can be optionally supplied (defaults to 1).
58
     *
59
     * @param array|\ArrayAccess $ids
60
     * @param int                $startOrder
61
     */
62
    public static function setNewOrder($ids, int $startOrder = 1): void
63
    {
64
        if (! \is_array($ids) && ! $ids instanceof ArrayAccess)
0 ignored issues
show
introduced by
$ids is always a sub-type of ArrayAccess.
Loading history...
65
        {
66
            throw new InvalidArgumentException('You must pass an array or ArrayAccess object to setNewOrder');
67
        }
68
69
        $model = new static;
70
71
        $orderColumnName = $model->determineOrderColumnName();
72
        $primaryKeyColumn = $model->getKeyName();
0 ignored issues
show
Bug introduced by
It seems like getKeyName() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

72
        /** @scrutinizer ignore-call */ 
73
        $primaryKeyColumn = $model->getKeyName();
Loading history...
73
74
        foreach ($ids as $id)
75
        {
76
            static::withoutGlobalScope(SoftDeletingScope::class)
77
                ->where($primaryKeyColumn, $id)
78
                ->update([$orderColumnName => $startOrder++]);
79
        }
80
    }
81
82
    /**
83
     * Swaps the order of this model with the model 'below' this model.
84
     *
85
     * @return $this
86
     */
87
    public function moveOrderDown(): self
88
    {
89
        $orderColumnName = $this->determineOrderColumnName();
90
91
        $swapWithModel = $this->buildSortQuery()->limit(1)->ordered()
92
                            ->where($orderColumnName, '>', $this->$orderColumnName)
93
                            ->first();
94
95
        if (! $swapWithModel)
96
        {
97
            return $this;
98
        }
99
100
        return $this->swapOrderWithModel($swapWithModel);
101
    }
102
103
    /**
104
     * Swaps the order of this model with the model 'above' this model.
105
     *
106
     * @return $this
107
     */
108
    public function moveOrderUp(): self
109
    {
110
        $orderColumnName = $this->determineOrderColumnName();
111
112
        $swapWithModel = $this->buildSortQuery()->limit(1)
113
            ->ordered('desc')
114
            ->where($orderColumnName, '<', $this->$orderColumnName)
115
            ->first();
116
117
        if (! $swapWithModel)
118
        {
119
            return $this;
120
        }
121
122
        return $this->swapOrderWithModel($swapWithModel);
123
    }
124
125
    /**
126
     * Swap the order of this model with the order of another model.
127
     *
128
     * @param $otherModel
129
     *
130
     * @return $this
131
     */
132
    public function swapOrderWithModel(Sortable $otherModel): self
133
    {
134
        $orderColumnName = $this->determineOrderColumnName();
135
136
        $oldOrderOfOtherModel = $otherModel->$orderColumnName;
137
138
        $otherModel->$orderColumnName = $this->$orderColumnName;
139
        $otherModel->save();
0 ignored issues
show
Bug introduced by
It seems like save() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

139
        $otherModel->/** @scrutinizer ignore-call */ 
140
                     save();
Loading history...
140
141
        $this->$orderColumnName = $oldOrderOfOtherModel;
142
        $this->save();
143
144
        return $this;
145
    }
146
147
    /**
148
     * Swap the order of two models.
149
     *
150
     * @param $model
151
     * @param $otherModel
152
     */
153
    public static function swapOrder($model, $otherModel): void
154
    {
155
        $model->swapOrderWithModel($otherModel);
156
    }
157
158
    /**
159
     * Moves this model to the first position.
160
     *
161
     * @return $this
162
     */
163
    public function moveToStart(): self
164
    {
165
        $firstModel = $this->buildSortQuery()->limit(1)
166
            ->ordered()
167
            ->first();
168
169
        if ($firstModel->getKey() === $this->getKey())
0 ignored issues
show
Bug introduced by
It seems like getKey() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

169
        if ($firstModel->getKey() === $this->/** @scrutinizer ignore-call */ getKey())
Loading history...
170
        {
171
            return $this;
172
        }
173
174
        $orderColumnName = $this->determineOrderColumnName();
175
176
        $this->$orderColumnName = $firstModel->$orderColumnName;
177
        $this->save();
178
179
        $this->buildSortQuery()->where($this->getKeyName(), '!=', $this->getKey())->increment($orderColumnName);
180
181
        return $this;
182
    }
183
184
    /**
185
     * Moves this model to the last position.
186
     *
187
     * @return $this
188
     */
189
    public function moveToEnd(): self
190
    {
191
        $maxOrder = $this->getHighestOrderNumber();
192
193
        $orderColumnName = $this->determineOrderColumnName();
194
195
        if ($this->$orderColumnName === $maxOrder)
196
        {
197
            return $this;
198
        }
199
200
        $oldOrder = $this->$orderColumnName;
201
202
        $this->$orderColumnName = $maxOrder;
203
        $this->save();
204
205
        $this->buildSortQuery()->where($this->getKeyName(), '!=', $this->getKey())
206
            ->where($orderColumnName, '>', $oldOrder)
207
            ->decrement($orderColumnName);
208
209
        return $this;
210
    }
211
212
    /**
213
     * Determine the column name of the order column.
214
     */
215
    protected function determineOrderColumnName(): string
216
    {
217
        return 'sortOrder';
218
    }
219
220
    /**
221
     * Determine the column name of the sorting key column (for filtering reasons).
222
     */
223
    protected function determineKeyColumnName(): string
224
    {
225
        return null;
0 ignored issues
show
Bug Best Practice introduced by
The expression return null returns the type null which is incompatible with the type-hinted return string.
Loading history...
226
    }
227
228
    /**
229
     * Build eloquent builder of sortable.
230
     *
231
     * @return Builder
232
     */
233
    public function buildSortQuery(): Builder
234
    {
235
        $keyColumn = $this->determineKeyColumnName();
236
237
        return $keyColumn !== null
0 ignored issues
show
introduced by
The condition $keyColumn !== null is always true.
Loading history...
238
                ? static::query()->where($keyColumn, $this->$keyColumn)
239
                : static::query();
240
    }
241
}
242