Skip to content

Commit 339cccc

Browse files
committed
Rework move methods to remove reliance on delete listener
1 parent c65f464 commit 339cccc

8 files changed

Lines changed: 109 additions & 158 deletions

File tree

README.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@
1111
This package is an extension of Spatie's excellent [Eloquent Sortable](https://github.com/spatie/eloquent-sortable). It adds the following additional features:
1212

1313
* New methods
14-
- `$model->moveBefore(Sortable $target): void`
15-
- `$model->moveAfter(Sortable $target): void`
16-
- `$model->moveBetween(Sortable $before = null, Sortable $after = null): void`
14+
- `$model->moveBefore(int|Sortable $target): void`
15+
- `$model->moveAfter(int|Sortable $target): void`
16+
- `$model->moveBetween(int|Sortable $before = null, Sortable $after = null): void`
1717
- `$model->moveTo(int|Sortable $newPosition): void`
18-
* New features
19-
- Automatic re-sorting after deleting a model
18+
19+
__Note:__ The move methods rebuild the entire sequence so this package is not recommended for very large datasets.
2020

2121
## Installation
2222

@@ -42,19 +42,27 @@ $model = MyModel::create(['title' => 'foo']);
4242

4343
# Move the model before the 5th item in the list
4444
$model->moveBefore(MyModel::where('order_column', 5)->first());
45+
// OR
46+
$model->moveBefore(5);
4547

4648
# Move the model after the 5th item in the list
4749
$model->moveAfter(MyModel::where('order_column', 5)->first());
50+
// OR
51+
$model->moveAfter(5);
4852

4953
# Move the model between the 5th and 6th item in the list
5054
$model->moveBetween(
5155
MyModel::where('order_column', 5)->first(),
5256
MyModel::where('order_column', 6)->first()
5357
);
58+
// OR
59+
$model->moveBetween(5, 6);
5460
# This is useful when using a javacript library that provides the node before
5561
# and after the location an item is dropped
5662

5763
# Move the model to the 5th item in the list
64+
$model->moveTo(MyModel::where('order_column', 5)->first());
65+
// OR
5866
$model->moveTo(5);
5967
```
6068

composer.json

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,6 @@
5858
"phpstan/extension-installer": true
5959
}
6060
},
61-
"extra": {
62-
"laravel": {
63-
"providers": [
64-
"Oddvalue\\EloquentSortable\\EloquentSortableServiceProvider"
65-
],
66-
"aliases": {
67-
"EloquentSortable": "Oddvalue\\EloquentSortable\\Facades\\EloquentSortable"
68-
}
69-
}
70-
},
7161
"minimum-stability": "dev",
7262
"prefer-stable": true
7363
}

src/EloquentSortableServiceProvider.php

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/Sortable.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,4 @@
44

55
interface Sortable extends \Spatie\EloquentSortable\Sortable
66
{
7-
/**
8-
* Determine if the siblings should resorted when deleting.
9-
*/
10-
public function shouldSortWhenDeleting(): bool;
117
}

src/SortableTrait.php

Lines changed: 33 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,63 +2,40 @@
22

33
namespace Oddvalue\EloquentSortable;
44

5-
use Illuminate\Database\Eloquent\Builder;
6-
use Spatie\EloquentSortable\Sortable;
7-
85
trait SortableTrait
96
{
107
use \Spatie\EloquentSortable\SortableTrait;
118

12-
public static function bootSortableTrait(): void
13-
{
14-
static::deleted([static::class, 'resortRowsDeleted']);
15-
}
16-
179
protected function getSortingValue(): int
1810
{
1911
return $this->{$this->determineOrderColumnName()};
2012
}
2113

22-
protected function setSortingValue(int $value): void
14+
public function moveBetween(Sortable|int $before = null, Sortable|int $after = null): void
2315
{
24-
$this->{$this->determineOrderColumnName()} = $value;
25-
}
16+
$from = $this->getSortingValue();
17+
$to = null;
2618

27-
public static function resortRowsDeleted(Sortable $instance): void
28-
{
29-
if (! $instance->shouldSortWhenDeleting()) {
30-
return;
19+
if ($before instanceof Sortable) {
20+
$before = $before->getSortingValue();
3121
}
32-
$instance->newQuery()
33-
->modifySortingQuery($instance)
34-
->where($instance->determineOrderColumnName(), '>', $instance->getSortingValue())
35-
->decrementSort();
36-
}
37-
38-
public function shouldSortWhenDeleting(): bool
39-
{
40-
return $this->sortable['sort_when_deleting'] ?? config('eloquent-sortable.sort_when_deleting', true);
41-
}
4222

43-
public function moveBetween(Sortable $before = null, Sortable $after = null): void
44-
{
45-
$from = $this->getSortingValue();
46-
$to = null;
23+
if ($after instanceof Sortable) {
24+
$after = $after->getSortingValue();
25+
}
4726

4827
if ($before) {
4928
// if the node has a sibling before it, insert after it
50-
$beforePosition = $before->getSortingValue();
51-
if ($beforePosition === $from) {
29+
if ($before === $from) {
5230
return;
5331
}
54-
$to = $beforePosition + ($from >= $beforePosition ? 1 : 0);
32+
$to = $before + ($from >= $before ? 1 : 0);
5533
} elseif ($after) {
5634
// if the node has a sibling after it, insert before it
57-
$afterPosition = $after->getSortingValue();
58-
if ($afterPosition === $from) {
35+
if ($after === $from) {
5936
return;
6037
}
61-
$to = $afterPosition - ($from <= $afterPosition ? 1 : 0);
38+
$to = $after - ($from <= $after ? 1 : 0);
6239
}
6340

6441
if ($to === null) {
@@ -68,12 +45,12 @@ public function moveBetween(Sortable $before = null, Sortable $after = null): vo
6845
$this->moveTo($to);
6946
}
7047

71-
public function moveBefore(Sortable $target): void
48+
public function moveBefore(Sortable|int $target): void
7249
{
7350
$this->moveBetween(after: $target);
7451
}
7552

76-
public function moveAfter(Sortable $target): void
53+
public function moveAfter(Sortable|int $target): void
7754
{
7855
$this->moveBetween(before: $target);
7956
}
@@ -84,38 +61,30 @@ public function moveTo(int|Sortable $newPosition): void
8461
$newPosition = $newPosition->getSortingValue();
8562
}
8663

87-
$from = $this->getSortingValue();
88-
89-
$difference = $from - $newPosition;
64+
$difference = $this->getSortingValue() - $newPosition;
9065

9166
if ($difference === 0) {
9267
return;
9368
}
94-
$query = $this->buildSortQuery()->whereBetween(
95-
$this->determineOrderColumnName(),
96-
[min($from, $newPosition), max($from, $newPosition)]
97-
);
98-
99-
if ($difference > 0) {
100-
$query->incrementSort();
101-
} else {
102-
$query->decrementSort();
103-
}
104-
$this->setSortingValue($newPosition);
105-
$this->save();
106-
}
10769

108-
public function scopeIncrementSort(Builder $query): void
109-
{
110-
// Increment is called on the raw query builder to avoid touching the
111-
// updated timestamp
112-
$query->getQuery()->increment($this->determineOrderColumnName());
113-
}
70+
$sequence = $this->buildSortQuery()
71+
->ordered()
72+
->where($this->getKeyName(), '!=', $this->getKey())
73+
->getQuery()
74+
->get([$this->determineOrderColumnName(), $this->getKeyName()]);
75+
76+
[$before, $after] = $sequence->partition(function ($item) use ($newPosition, $difference) {
77+
if ($difference > 0) {
78+
return $item->{$this->determineOrderColumnName()} < $newPosition;
79+
} else {
80+
return $item->{$this->determineOrderColumnName()} <= $newPosition;
81+
}
82+
});
11483

115-
public function scopeDecrementSort(Builder $query): void
116-
{
117-
// Decrement is called on the raw query builder to avoid touching the
118-
// updated timestamp
119-
$query->getQuery()->decrement($this->determineOrderColumnName());
84+
static::setNewOrder([
85+
...$before->pluck($this->getKeyName()),
86+
$this->getKey(),
87+
...$after->pluck($this->getKeyName()),
88+
]);
12089
}
12190
}

tests/Dummy.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,14 @@ class Dummy extends \Illuminate\Database\Eloquent\Model implements \Oddvalue\Elo
1111
protected $guarded = [];
1212

1313
public $timestamps = false;
14+
15+
public array $sortable = [
16+
'order_column' => 'order_column',
17+
'sort_when_deleting' => true,
18+
];
19+
20+
protected $casts = [
21+
'order_column' => 'integer',
22+
'original_position' => 'integer',
23+
];
1424
}

tests/SortableTest.php

Lines changed: 50 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,88 +3,86 @@
33
use Oddvalue\EloquentSortable\Tests\Dummy;
44

55
it('can_be_move_records', function (int $from, int $to, array $expected) {
6-
$featuredProducts = Dummy::all();
6+
$this->assertEquals(range(1, 10), Dummy::pluck('order_column')->all());
77

8-
$this->assertEquals(range(1, 10), $featuredProducts->pluck('order_column')->all());
8+
Dummy::where('order_column', $from)->first()->moveTo(Dummy::find($to));
99

10-
$featuredProducts->firstWhere('order_column', $from)->moveTo($to);
11-
12-
$this->assertEquals($expected, $featuredProducts->fresh()->pluck('order_column')->all());
10+
$this->assertEquals($expected, Dummy::ordered()->pluck('original_position')->all());
1311
})->with([
1412
[1, 2, [2, 1, 3, 4, 5, 6, 7, 8, 9, 10]],
15-
[2, 6, [1, 6, 2, 3, 4, 5, 7, 8, 9, 10]],
16-
[3, 10, [1, 2, 10, 3, 4, 5, 6, 7, 8, 9]],
17-
[4, 1, [2, 3, 4, 1, 5, 6, 7, 8, 9, 10]],
13+
[2, 6, [1, 3, 4, 5, 6, 2, 7, 8, 9, 10]],
14+
[3, 10, [1, 2, 4, 5, 6, 7, 8, 9, 10, 3]],
15+
[4, 1, [4, 1, 2, 3, 5, 6, 7, 8, 9, 10]],
1816
[5, 5, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]],
19-
[6, 10, [1, 2, 3, 4, 5, 10, 6, 7, 8, 9]],
20-
[7, 1, [2, 3, 4, 5, 6, 7, 1, 8, 9, 10]],
21-
[8, 5, [1, 2, 3, 4, 6, 7, 8, 5, 9, 10]],
22-
[9, 3, [1, 2, 4, 5, 6, 7, 8, 9, 3, 10]],
23-
[10, 1, [2, 3, 4, 5, 6, 7, 8, 9, 10, 1]],
17+
[6, 10, [1, 2, 3, 4, 5, 7, 8, 9, 10, 6]],
18+
[7, 1, [7, 1, 2, 3, 4, 5, 6, 8, 9, 10]],
19+
[8, 5, [1, 2, 3, 4, 8, 5, 6, 7, 9, 10]],
20+
[9, 3, [1, 2, 9, 3, 4, 5, 6, 7, 8, 10]],
21+
[10, 1, [10, 1, 2, 3, 4, 5, 6, 7, 8, 9]],
2422
]);
2523

2624
it('can_move_items_between', function (int $from, array $to, array $expected) {
27-
$featuredProducts = Dummy::all();
28-
2925
[$before, $after] = $to;
3026

31-
$featuredProducts->firstWhere('order_column', $from)
27+
Dummy::where('order_column', $from)->first()
3228
->moveBetween(
33-
$featuredProducts->firstWhere('order_column', $before),
34-
$featuredProducts->firstWhere('order_column', $after)
29+
Dummy::where('order_column', $before)->first(),
30+
Dummy::where('order_column', $after)->first()
3531
);
3632

37-
$this->assertEquals($expected, $featuredProducts->fresh()->pluck('order_column')->all());
33+
$this->assertEquals($expected, Dummy::ordered()->pluck('original_position')->all());
3834
})->with([
3935
[1, [1, 2], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]],
40-
[2, [5, 6], [1, 5, 2, 3, 4, 6, 7, 8, 9, 10]],
41-
[3, [10, null], [1, 2, 10, 3, 4, 5, 6, 7, 8, 9]],
42-
[4, [null, 1], [2, 3, 4, 1, 5, 6, 7, 8, 9, 10]],
36+
[2, [5, 6], [1, 3, 4, 5, 2, 6, 7, 8, 9, 10]],
37+
[3, [10, null], [1, 2, 4, 5, 6, 7, 8, 9, 10, 3]],
38+
[4, [null, 1], [4, 1, 2, 3, 5, 6, 7, 8, 9, 10]],
4339
[5, [4, 5], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]],
44-
[6, [10, null], [1, 2, 3, 4, 5, 10, 6, 7, 8, 9]],
45-
[7, [null, 1], [2, 3, 4, 5, 6, 7, 1, 8, 9, 10]],
46-
[8, [4, 5], [1, 2, 3, 4, 6, 7, 8, 5, 9, 10]],
40+
[6, [10, null], [1, 2, 3, 4, 5, 7, 8, 9, 10, 6]],
41+
[7, [null, 1], [7, 1, 2, 3, 4, 5, 6, 8, 9, 10]],
42+
[8, [4, 5], [1, 2, 3, 4, 8, 5, 6, 7, 9, 10]],
4743
[9, [10, null], [1, 2, 3, 4, 5, 6, 7, 8, 10, 9]],
48-
[10, [1, null], [1, 3, 4, 5, 6, 7, 8, 9, 10, 2]],
44+
[10, [3, 4], [1, 2, 3, 10, 4, 5, 6, 7, 8, 9]],
45+
[10, [100, 100], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]],
4946
]);
5047

5148
it('can_move_items_before', function ($from, $to, $expected) {
52-
$featuredProducts = Dummy::all();
53-
54-
$featuredProducts->firstWhere('order_column', $from)->moveBefore($featuredProducts->firstWhere('order_column', $to));
49+
Dummy::where('order_column', $from)->first()->moveBefore($to);
5550

56-
$this->assertEquals($expected, $featuredProducts->fresh()->pluck('order_column')->all());
51+
$this->assertEquals($expected, Dummy::ordered()->pluck('original_position')->all());
5752
})->with([
5853
[1, 2, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]],
59-
[2, 6, [1, 5, 2, 3, 4, 6, 7, 8, 9, 10]],
60-
[3, 10, [1, 2, 9, 3, 4, 5, 6, 7, 8, 10]],
61-
[4, 1, [2, 3, 4, 1, 5, 6, 7, 8, 9, 10]],
54+
[2, 6, [1, 3, 4, 5, 2, 6, 7, 8, 9, 10]],
55+
[3, 10, [1, 2, 4, 5, 6, 7, 8, 9, 3, 10]],
56+
[4, 1, [4, 1, 2, 3, 5, 6, 7, 8, 9, 10]],
6257
[5, 5, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]],
6358
[6, 8, [1, 2, 3, 4, 5, 7, 6, 8, 9, 10]],
64-
[7, 2, [1, 3, 4, 5, 6, 7, 2, 8, 9, 10]],
65-
[8, 5, [1, 2, 3, 4, 6, 7, 8, 5, 9, 10]],
66-
[9, 3, [1, 2, 4, 5, 6, 7, 8, 9, 3, 10]],
67-
[10, 1, [2, 3, 4, 5, 6, 7, 8, 9, 10, 1]],
59+
[7, 2, [1, 7, 2, 3, 4, 5, 6, 8, 9, 10]],
60+
[8, 5, [1, 2, 3, 4, 8, 5, 6, 7, 9, 10]],
61+
[9, 3, [1, 2, 9, 3, 4, 5, 6, 7, 8, 10]],
62+
[10, 1, [10, 1, 2, 3, 4, 5, 6, 7, 8, 9]],
6863
]);
6964

70-
/**
71-
* @dataProvider itemsToMoveAfter
72-
*/
7365
it('can_move_items_after', function ($from, $to, $expected) {
74-
$featuredProducts = Dummy::all();
66+
Dummy::where('order_column', $from)->first()->moveAfter($to);
7567

76-
$featuredProducts->firstWhere('order_column', $from)->moveAfter($featuredProducts->firstWhere('order_column', $to));
77-
78-
$this->assertEquals($expected, $featuredProducts->fresh()->pluck('order_column')->all());
68+
$this->assertEquals($expected, Dummy::ordered()->pluck('original_position')->all());
7969
})->with([
8070
[1, 2, [2, 1, 3, 4, 5, 6, 7, 8, 9, 10]],
81-
[2, 6, [1, 6, 2, 3, 4, 5, 7, 8, 9, 10]],
82-
[3, 10, [1, 2, 10, 3, 4, 5, 6, 7, 8, 9]],
83-
[4, 1, [1, 3, 4, 2, 5, 6, 7, 8, 9, 10]],
71+
[2, 6, [1, 3, 4, 5, 6, 2, 7, 8, 9, 10]],
72+
[3, 10, [1, 2, 4, 5, 6, 7, 8, 9, 10, 3]],
73+
[4, 1, [1, 4, 2, 3, 5, 6, 7, 8, 9, 10]],
8474
[5, 5, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]],
85-
[6, 8, [1, 2, 3, 4, 5, 8, 6, 7, 9, 10]],
86-
[7, 2, [1, 2, 4, 5, 6, 7, 3, 8, 9, 10]],
87-
[8, 5, [1, 2, 3, 4, 5, 7, 8, 6, 9, 10]],
88-
[9, 3, [1, 2, 3, 5, 6, 7, 8, 9, 4, 10]],
89-
[10, 1, [1, 3, 4, 5, 6, 7, 8, 9, 10, 2]],
75+
[6, 8, [1, 2, 3, 4, 5, 7, 8, 6, 9, 10]],
76+
[7, 2, [1, 2, 7, 3, 4, 5, 6, 8, 9, 10]],
77+
[8, 5, [1, 2, 3, 4, 5, 8, 6, 7, 9, 10]],
78+
[9, 3, [1, 2, 3, 9, 4, 5, 6, 7, 8, 10]],
79+
[10, 1, [1, 10, 2, 3, 4, 5, 6, 7, 8, 9]],
9080
]);
81+
82+
it('performs_acceptably', function () {
83+
Dummy::query()->delete();
84+
collect(range(1, 5000))->each(fn ($i) => Dummy::create());
85+
$start = microtime(true);
86+
Dummy::first()->moveAfter(Dummy::inRandomOrder()->first());
87+
expect(microtime(true) - $start)->toBeLessThanOrEqual(2);
88+
});

0 commit comments

Comments
 (0)