Sequenceable::isAffectedByRepositioningOf()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 9
c 1
b 0
f 0
nc 5
nop 1
dl 0
loc 16
rs 9.6111
1
<?php
2
3
namespace Gurgentil\LaravelEloquentSequencer\Traits;
4
5
use Gurgentil\LaravelEloquentSequencer\Exceptions\SequenceValueOutOfBoundsException;
6
use Gurgentil\LaravelEloquentSequencer\SequencingStrategy;
7
use Illuminate\Database\Eloquent\Builder;
8
use Illuminate\Database\Eloquent\Model;
9
use Illuminate\Support\Collection;
10
11
trait Sequenceable
12
{
13
    /**
14
     * Indicates if the model should be sequenced.
15
     *
16
     * @var bool
17
     */
18
    protected $shouldBeSequenced = true;
19
20
    /**
21
     * Handle lifecycle hooks.
22
     *
23
     * @return void
24
     */
25
    public static function bootSequenceable(): void
26
    {
27
        static::creating(function ($model) {
28
            $model->handleSequenceableCreate();
29
        });
30
31
        static::updating(function ($model) {
32
            $model->handleSequenceableUpdate();
33
        });
34
35
        static::deleting(function ($model) {
36
            $model->handleSequenceableDelete();
37
        });
38
    }
39
40
    /**
41
     * Disable sequencing.
42
     *
43
     * @return self
44
     */
45
    public function withoutSequencing(): self
46
    {
47
        $this->shouldBeSequenced = false;
48
49
        return $this;
50
    }
51
52
    /**
53
     * Handle sequenceable creation.
54
     *
55
     * @return void
56
     */
57
    protected function handleSequenceableCreate(): void
58
    {
59
        $value = $this->getSequenceValue();
60
61
        if (static::strategyIn([
62
            SequencingStrategy::NEVER,
63
            SequencingStrategy::ON_UPDATE,
64
        ])) {
65
            return;
66
        }
67
68
        if (is_null($value)) {
69
            $this->{static::getSequenceColumnName()} = $this->getNextSequenceValue();
70
        }
71
72
        if ($this->isNewSequenceValueOutOfBounds()) {
73
            throw SequenceValueOutOfBoundsException::create($value);
74
        }
75
76
        static::updateSequenceablesAffectedBy($this);
0 ignored issues
show
Bug introduced by
$this of type Gurgentil\LaravelEloquen...cer\Traits\Sequenceable is incompatible with the type Illuminate\Database\Eloquent\Model expected by parameter $model of Gurgentil\LaravelEloquen...quenceablesAffectedBy(). ( Ignorable by Annotation )

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

76
        static::updateSequenceablesAffectedBy(/** @scrutinizer ignore-type */ $this);
Loading history...
77
    }
78
79
    /**
80
     * Handle sequenceable update.
81
     *
82
     * @return void
83
     */
84
    protected function handleSequenceableUpdate(): void
85
    {
86
        if (static::strategyIn([
87
            SequencingStrategy::NEVER,
88
            SequencingStrategy::ON_CREATE,
89
        ])) {
90
            return;
91
        }
92
93
        if (! $this->shouldBeSequenced) {
94
            $this->shouldBeSequenced = true;
95
96
            return;
97
        }
98
99
        $value = $this->getSequenceValue();
100
101
        if ($this->isClean(static::getSequenceColumnName()) || is_null($value)) {
102
            return;
103
        }
104
105
        if ($this->isUpdatedSequenceValueOutOfBounds()) {
106
            throw SequenceValueOutOfBoundsException::create($value);
107
        }
108
109
        static::updateSequenceablesAffectedBy($this);
0 ignored issues
show
Bug introduced by
$this of type Gurgentil\LaravelEloquen...cer\Traits\Sequenceable is incompatible with the type Illuminate\Database\Eloquent\Model expected by parameter $model of Gurgentil\LaravelEloquen...quenceablesAffectedBy(). ( Ignorable by Annotation )

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

109
        static::updateSequenceablesAffectedBy(/** @scrutinizer ignore-type */ $this);
Loading history...
110
    }
111
112
    /**
113
     * Handle sequenceable delete.
114
     *
115
     * @return void
116
     */
117
    protected function handleSequenceableDelete(): void
118
    {
119
        if (static::strategyIn([
120
            SequencingStrategy::NEVER,
121
            SequencingStrategy::ON_CREATE,
122
            SequencingStrategy::ON_UPDATE,
123
        ])) {
124
            return;
125
        }
126
127
        if (! $this->shouldBeSequenced) {
128
            $this->shouldBeSequenced = true;
129
130
            return;
131
        }
132
133
        $columnName = static::getSequenceColumnName();
134
135
        $objects = $this->getSequence()
136
            ->where($columnName, '>', $this->getSequenceValue());
137
138
        static::decrementSequenceValues($objects);
139
    }
140
141
    /**
142
     * Determine if strategy is in array.
143
     *
144
     * @param array $strategies
145
     *
146
     * @return bool
147
     */
148
    protected static function strategyIn(array $strategies): bool
149
    {
150
        return in_array(config('eloquentsequencer.strategy'), $strategies);
151
    }
152
153
    /**
154
     * Determine if new sequence value is out of bounds.
155
     *
156
     * @return bool
157
     */
158
    protected function isNewSequenceValueOutOfBounds(): bool
159
    {
160
        $newValue = $this->getSequenceValue();
161
162
        return $newValue < static::getInitialSequenceValue()
163
            || $newValue <= 0
164
            || $newValue > $this->getNextSequenceValue();
165
    }
166
167
    /**
168
     * Determine if updated sequence value is out of bounds.
169
     *
170
     * @return bool
171
     */
172
    protected function isUpdatedSequenceValueOutOfBounds(): bool
173
    {
174
        $newValue = $this->getSequenceValue();
175
        $originalValue = $this->getOriginalSequenceValue();
176
177
        return $newValue < static::getInitialSequenceValue()
178
            || ! is_null($originalValue) && $newValue > $this->getLastSequenceValue()
179
            || is_null($originalValue) && $newValue > $this->getNextSequenceValue();
180
    }
181
182
    /**
183
     * Decrement sequence values from a collection of sequenceable models.
184
     *
185
     * @param Collection $models
186
     *
187
     * @return void
188
     */
189
    protected static function decrementSequenceValues(Collection $models): void
190
    {
191
        $models->each(function ($model) {
192
            $model->decrement(static::getSequenceColumnName());
193
        });
194
    }
195
196
    /**
197
     * Increment sequence values from a collection of sequenceable models.
198
     *
199
     * @param Collection $models
200
     *
201
     * @return void
202
     */
203
    protected static function incrementSequenceValues(Collection $models): void
204
    {
205
        $models->each(function ($model) {
206
            $model->increment(static::getSequenceColumnName());
207
        });
208
    }
209
210
    /**
211
     * Get sequence value.
212
     *
213
     * @return int|null
214
     */
215
    public function getSequenceValue(): ?int
216
    {
217
        $value = $this->{static::getSequenceColumnName()};
218
219
        return is_numeric($value) ? (int) $value : null;
220
    }
221
222
    /**
223
     * Get original sequence value.
224
     *
225
     * @return int|null
226
     */
227
    protected function getOriginalSequenceValue(): ?int
228
    {
229
        $value = $this->getOriginal(static::getSequenceColumnName());
230
231
        return is_numeric($value) ? (int) $value : null;
232
    }
233
234
    /**
235
     * Update models affected by the repositioning of another model.
236
     *
237
     * @param Model $model
238
     *
239
     * @return void
240
     */
241
    protected static function updateSequenceablesAffectedBy(Model $model): void
242
    {
243
        $model->getConnection()->transaction(function () use ($model) {
244
            $modelsToUpdate = $model->getSequence()
245
                ->where('id', '!=', $model->id)
246
                ->filter(function ($sequenceModel) use ($model) {
247
                    return $sequenceModel->isAffectedByRepositioningOf($model);
248
                })
249
                ->each->withoutSequencing();
250
251
            if ($model->isMovingUpInSequence()) {
252
                static::decrementSequenceValues($modelsToUpdate);
253
            } else {
254
                static::incrementSequenceValues($modelsToUpdate);
255
            }
256
        });
257
    }
258
259
    /**
260
     * Determine if the model is moving up in the sequence.
261
     *
262
     * @return bool
263
     */
264
    protected function isMovingUpInSequence(): bool
265
    {
266
        $originalValue = $this->getOriginalSequenceValue();
267
268
        return ! is_null($originalValue) && $originalValue < $this->getSequenceValue();
269
    }
270
271
    /**
272
     * Determine if model is moving down in the sequence.
273
     *
274
     * @return bool
275
     */
276
    protected function isMovingDownInSequence(): bool
277
    {
278
        $originalValue = $this->getOriginalSequenceValue();
279
280
        return ! is_null($originalValue) && $originalValue > $this->getSequenceValue();
281
    }
282
283
    /**
284
     * Indicate if model is affected by the repositioning of another model in the sequence.
285
     *
286
     * @param Model $model
287
     *
288
     * @return bool
289
     */
290
    protected function isAffectedByRepositioningOf(Model $model): bool
291
    {
292
        $newValue = $model->getSequenceValue();
293
        $originalValue = $model->getOriginalSequenceValue();
294
295
        if ($model->isMovingDownInSequence()) {
296
            return $this->getSequenceValue() >= $newValue
297
                && $this->getSequenceValue() < $originalValue;
298
        }
299
300
        if ($model->isMovingUpInSequence()) {
301
            return $this->getSequenceValue() <= $newValue
302
                && $this->getSequenceValue() > $originalValue;
303
        }
304
305
        return $this->getSequenceValue() >= $newValue;
306
    }
307
308
    /**
309
     * Get name of the column that stores the sequence value.
310
     *
311
     * @return string
312
     */
313
    public static function getSequenceColumnName(): string
314
    {
315
        return (string) property_exists(static::class, 'sequenceable')
316
            ? static::$sequenceable
317
            : config('eloquentsequencer.column_name', 'position');
318
    }
319
320
    /**
321
     * Get sequence value of the last model in the sequence.
322
     *
323
     * @return int|null
324
     */
325
    protected function getLastSequenceValue(): ?int
326
    {
327
        $column = static::getSequenceColumnName();
328
329
        return $this->getSequence()->max($column);
330
    }
331
332
    /**
333
     * Get sequence value for the next model in the sequence.
334
     *
335
     * @return int
336
     */
337
    public function getNextSequenceValue(): int
338
    {
339
        $column = static::getSequenceColumnName();
340
        $maxSequenceValue = $this->getSequence()->max($column);
341
342
        return $this->getSequence()->count() === 0
343
            ? static::getInitialSequenceValue()
344
            : $maxSequenceValue + 1;
345
    }
346
347
    /**
348
     * Scope a query to order by sequence value.
349
     *
350
     * @param Builder $query
351
     *
352
     * @return Builder
353
     */
354
    public static function scopeSequenced(Builder $query): Builder
355
    {
356
        return $query->orderBy(static::getSequenceColumnName());
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->orderBy(s...etSequenceColumnName()) 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...
357
    }
358
359
    /**
360
     * Get a list of the models in the sequence.
361
     *
362
     * @return Collection
363
     */
364
    protected function getSequence(): Collection
365
    {
366
        $columnName = static::getSequenceColumnName();
367
368
        return static::where($this->getSequenceQueryConstraints())
369
            ->select($this->getSequenceQuerySelectColumns())
370
            ->where($columnName, '!=', null)
371
            ->orderBy($columnName)
372
            ->get();
373
    }
374
375
    /**
376
     * Get sequence query constraints.
377
     *
378
     * @return array
379
     */
380
    protected function getSequenceQueryConstraints(): array
381
    {
382
        $constraints = [];
383
384
        foreach ($this->getSequenceKeys() as $key) {
385
            $constraints[$key] = $this->$key;
386
        }
387
388
        return $constraints;
389
    }
390
391
    /**
392
     * Get keys to be selected in the query.
393
     *
394
     * @return array
395
     */
396
    protected function getSequenceQuerySelectColumns(): array
397
    {
398
        $primaryKey = $this->getKeyName();
399
        $sequenceColumnName = static::getSequenceColumnName();
400
401
        $columns = [
402
            $primaryKey,
403
            $sequenceColumnName,
404
        ];
405
406
        foreach ($this->getSequenceKeys() as $key) {
407
            array_push($columns, $key);
408
        }
409
410
        return $columns;
411
    }
412
413
    /**
414
     * Get sequence keys.
415
     *
416
     * @return array
417
     */
418
    protected function getSequenceKeys(): array
419
    {
420
        return property_exists(static::class, 'sequenceableKeys')
421
            ? (array) static::$sequenceableKeys
422
            : [];
423
    }
424
425
    /**
426
     * Get the value that sequences should start at.
427
     *
428
     * @return int
429
     */
430
    protected static function getInitialSequenceValue(): int
431
    {
432
        return (int) config('eloquentsequencer.initial_value', 1);
433
    }
434
435
    /**
436
     * Get the primary key for the model.
437
     *
438
     * @return string
439
     */
440
    abstract public function getKeyName();
441
442
    /**
443
     * Determine if the model and all the given attribute(s) have remained the same.
444
     *
445
     * @param array|string|null $attributes
446
     * @return bool
447
     */
448
    abstract public function isClean($attributes = null);
449
450
    /**
451
     * Get the model's original attribute values.
452
     *
453
     * @param  string|null  $key
454
     * @param  mixed  $default
455
     * @return mixed|array
456
     */
457
    abstract public function getOriginal($key = null, $default = null);
458
}
459