├── .github
├── FUNDING.yml
├── dependabot.yml
├── ISSUE_TEMPLATE
│ └── config.yml
└── workflows
│ ├── php-cs-fixer.yml
│ ├── update-changelog.yml
│ ├── dependabot-auto-merge.yml
│ └── run-tests.yml
├── .gitignore
├── .editorconfig
├── tests
├── DummyWithTimestamps.php
├── Dummy.php
├── DummyWithSoftDeletes.php
├── DummyWithGlobalScope.php
├── TestCase.php
└── SortableTest.php
├── src
├── EloquentSortableServiceProvider.php
├── EloquentModelSortedEvent.php
├── Sortable.php
└── SortableTrait.php
├── config
└── eloquent-sortable.php
├── phpunit.xml
├── LICENSE.md
├── .php_cs.dist.php
├── composer.json
├── CHANGELOG.md
└── README.md
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: spatie
2 | custom: https://spatie.be/open-source/support-us
3 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | updates:
4 |
5 | - package-ecosystem: "github-actions"
6 | directory: "/"
7 | schedule:
8 | interval: "weekly"
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .php_cs
3 | .php_cs.cache
4 | .phpunit.result.cache
5 | build
6 | composer.lock
7 | coverage
8 | docs
9 | phpunit.xml
10 | psalm.xml
11 | testbench.yaml
12 | vendor
13 | composer.phar
14 | tests/_output/*
15 | .php-cs-fixer.cache
16 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_size = 4
6 | indent_style = space
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
14 | [*.{yml,yaml}]
15 | indent_size = 2
16 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 | contact_links:
3 | - name: Feature Request
4 | url: https://github.com/spatie/eloquent-sortable/discussions/new?category=ideas
5 | about: Share ideas for new features
6 | - name: Ask a Question
7 | url: https://github.com/spatie/eloquent-sortable/discussions/new?category=q-a
8 | about: Ask the community for help
9 |
--------------------------------------------------------------------------------
/tests/DummyWithTimestamps.php:
--------------------------------------------------------------------------------
1 | name('eloquent-sortable')
14 | ->hasConfigFile();
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/DummyWithSoftDeletes.php:
--------------------------------------------------------------------------------
1 | model = $model;
14 | }
15 |
16 | public function isFor(Model|string $model): bool
17 | {
18 | if (is_string($model)) {
19 | return $model === $this->model;
20 | }
21 |
22 | return get_class($model) === $this->model;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/config/eloquent-sortable.php:
--------------------------------------------------------------------------------
1 | 'order_column',
8 |
9 | /*
10 | * Define if the models should sort when creating.
11 | * When true, the package will automatically assign the highest order number to a new model
12 | */
13 | 'sort_when_creating' => true,
14 |
15 | /*
16 | * Define if the timestamps should be ignored when sorting.
17 | * When true, updated_at will not be updated when using setNewOrder
18 | */
19 | 'ignore_timestamps' => false,
20 | ];
21 |
--------------------------------------------------------------------------------
/.github/workflows/php-cs-fixer.yml:
--------------------------------------------------------------------------------
1 | name: Check & fix styling
2 |
3 | on: [push]
4 |
5 | jobs:
6 | php-cs-fixer:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - name: Checkout code
11 | uses: actions/checkout@v5
12 | with:
13 | ref: ${{ github.head_ref }}
14 |
15 | - name: Run PHP CS Fixer
16 | uses: docker://oskarstark/php-cs-fixer-ga
17 | with:
18 | args: --config=.php_cs.dist.php --allow-risky=yes
19 |
20 | - name: Commit changes
21 | uses: stefanzweifel/git-auto-commit-action@v6
22 | with:
23 | commit_message: Fix styling
24 |
--------------------------------------------------------------------------------
/.github/workflows/update-changelog.yml:
--------------------------------------------------------------------------------
1 | name: "Update Changelog"
2 |
3 | on:
4 | release:
5 | types: [released]
6 |
7 | jobs:
8 | update:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - name: Checkout code
13 | uses: actions/checkout@v5
14 | with:
15 | ref: main
16 |
17 | - name: Update Changelog
18 | uses: stefanzweifel/changelog-updater-action@v1
19 | with:
20 | latest-version: ${{ github.event.release.name }}
21 | release-notes: ${{ github.event.release.body }}
22 |
23 | - name: Commit updated CHANGELOG
24 | uses: stefanzweifel/git-auto-commit-action@v6
25 | with:
26 | branch: main
27 | commit_message: Update CHANGELOG
28 | file_pattern: CHANGELOG.md
29 |
--------------------------------------------------------------------------------
/tests/DummyWithGlobalScope.php:
--------------------------------------------------------------------------------
1 | where('is_active', true);
29 | });
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | app/
6 |
7 |
8 |
9 |
10 | ./tests/
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/Sortable.php:
--------------------------------------------------------------------------------
1 | in([
5 | __DIR__ . '/src',
6 | __DIR__ . '/tests',
7 | ])
8 | ->name('*.php')
9 | ->notName('*.blade.php')
10 | ->ignoreDotFiles(true)
11 | ->ignoreVCS(true);
12 |
13 | return (new PhpCsFixer\Config())
14 | ->setRules([
15 | '@PSR12' => true,
16 | 'array_syntax' => ['syntax' => 'short'],
17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'],
18 | 'no_unused_imports' => true,
19 | 'not_operator_with_successor_space' => true,
20 | 'trailing_comma_in_multiline' => true,
21 | 'phpdoc_scalar' => true,
22 | 'unary_operator_spaces' => true,
23 | 'binary_operator_spaces' => true,
24 | 'blank_line_before_statement' => [
25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'],
26 | ],
27 | 'phpdoc_single_line_var_spacing' => true,
28 | 'phpdoc_var_without_name' => true,
29 | 'class_attributes_separation' => [
30 | 'elements' => [
31 | 'method' => 'one',
32 | ],
33 | ],
34 | 'method_argument_space' => [
35 | 'on_multiline' => 'ensure_fully_multiline',
36 | 'keep_multiple_spaces_after_comma' => true,
37 | ],
38 | 'single_trait_insert_per_statement' => true,
39 | ])
40 | ->setFinder($finder);
41 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "spatie/eloquent-sortable",
3 | "description": "Sortable behaviour for eloquent models",
4 | "homepage": "https://github.com/spatie/eloquent-sortable",
5 | "authors": [
6 | {
7 | "name": "Freek Van der Herten",
8 | "email": "freek@spatie.be"
9 | }
10 | ],
11 | "keywords": [
12 | "sort",
13 | "sortable",
14 | "eloquent",
15 | "model",
16 | "laravel",
17 | "behaviour"
18 | ],
19 | "license": "MIT",
20 | "require": {
21 | "php": "^8.1",
22 | "illuminate/database": "^9.31|^10.0|^11.0|^12.0",
23 | "illuminate/support": "^9.31|^10.0|^11.0|^12.0",
24 | "nesbot/carbon": "^2.63|^3.0",
25 | "spatie/laravel-package-tools": "^1.9"
26 | },
27 | "require-dev": {
28 | "phpunit/phpunit": "^9.5|^10.0|^11.5.3",
29 | "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0"
30 | },
31 | "autoload": {
32 | "psr-4": {
33 | "Spatie\\EloquentSortable\\": "src/"
34 | }
35 | },
36 | "autoload-dev": {
37 | "psr-4": {
38 | "Spatie\\EloquentSortable\\Test\\": "tests"
39 | }
40 | },
41 | "minimum-stability": "dev",
42 | "prefer-stable": true,
43 | "scripts": {
44 | "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes",
45 | "test": "vendor/bin/phpunit"
46 | },
47 | "extra": {
48 | "laravel": {
49 | "providers": [
50 | "Spatie\\EloquentSortable\\EloquentSortableServiceProvider"
51 | ]
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/.github/workflows/dependabot-auto-merge.yml:
--------------------------------------------------------------------------------
1 | name: dependabot-auto-merge
2 | on: pull_request_target
3 |
4 | permissions:
5 | pull-requests: write
6 | contents: write
7 |
8 | jobs:
9 | dependabot:
10 | runs-on: ubuntu-latest
11 | if: ${{ github.actor == 'dependabot[bot]' }}
12 | steps:
13 |
14 | - name: Dependabot metadata
15 | id: metadata
16 | uses: dependabot/fetch-metadata@v2.4.0
17 | with:
18 | github-token: "${{ secrets.GITHUB_TOKEN }}"
19 | compat-lookup: true
20 |
21 | - name: Auto-merge Dependabot PRs for semver-minor updates
22 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}}
23 | run: gh pr merge --auto --merge "$PR_URL"
24 | env:
25 | PR_URL: ${{github.event.pull_request.html_url}}
26 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
27 |
28 | - name: Auto-merge Dependabot PRs for semver-patch updates
29 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}}
30 | run: gh pr merge --auto --merge "$PR_URL"
31 | env:
32 | PR_URL: ${{github.event.pull_request.html_url}}
33 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
34 |
35 | - name: Auto-merge Dependabot PRs for Action major versions when compatibility is higher than 90%
36 | if: ${{steps.metadata.outputs.package-ecosystem == 'github_actions' && steps.metadata.outputs.update-type == 'version-update:semver-major' && steps.metadata.outputs.compatibility-score >= 90}}
37 | run: gh pr merge --auto --merge "$PR_URL"
38 | env:
39 | PR_URL: ${{github.event.pull_request.html_url}}
40 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
41 |
--------------------------------------------------------------------------------
/.github/workflows/run-tests.yml:
--------------------------------------------------------------------------------
1 | name: run-tests
2 |
3 | on:
4 | - push
5 | - pull_request
6 |
7 | jobs:
8 | test:
9 | runs-on: ${{ matrix.os }}
10 |
11 | strategy:
12 | fail-fast: true
13 | matrix:
14 | os: [ubuntu-latest]
15 | php: [8.3, 8.2, 8.1]
16 | laravel: ['9.*', '10.*', '11.*', '12.*']
17 | dependency-version: [prefer-lowest, prefer-stable]
18 | include:
19 | - laravel: 11.*
20 | testbench: 9.*
21 | - laravel: 10.*
22 | testbench: 8.*
23 | - laravel: 9.*
24 | testbench: 7.*
25 | - laravel: 12.*
26 | testbench: 10.*
27 | exclude:
28 | - laravel: 11.*
29 | php: 8.1
30 | - laravel: 12.*
31 | php: 8.1
32 |
33 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - ${{ matrix.os }}
34 |
35 | steps:
36 | - name: Checkout code
37 | uses: actions/checkout@v5
38 |
39 | - name: Setup PHP
40 | uses: shivammathur/setup-php@v2
41 | with:
42 | php-version: ${{ matrix.php }}
43 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick
44 | coverage: none
45 |
46 | - name: Setup problem matchers
47 | run: |
48 | echo "::add-matcher::${{ runner.tool_cache }}/php.json"
49 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
50 |
51 | - name: Install dependencies
52 | run: |
53 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
54 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction
55 |
56 | - name: Execute tests
57 | run: vendor/bin/phpunit
58 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | setUpDatabase();
15 | }
16 |
17 | /**
18 | * @param \Illuminate\Foundation\Application $app
19 | *
20 | * @return array
21 | */
22 | protected function getPackageProviders($app)
23 | {
24 | return [
25 |
26 | ];
27 | }
28 |
29 | /**
30 | * @param \Illuminate\Foundation\Application $app
31 | */
32 | protected function getEnvironmentSetUp($app)
33 | {
34 | $app['config']->set('database.default', 'sqlite');
35 | $app['config']->set('database.connections.sqlite', [
36 | 'driver' => 'sqlite',
37 | 'database' => ':memory:',
38 | 'prefix' => '',
39 | ]);
40 | }
41 |
42 | protected function setUpDatabase()
43 | {
44 | $this->app['db']->connection()->getSchemaBuilder()->create('dummies', function (Blueprint $table) {
45 | $table->increments('id');
46 | $table->string('name');
47 | $table->string('custom_column_sort');
48 | $table->integer('order_column');
49 | });
50 |
51 | collect(range(1, 20))->each(function (int $i) {
52 | Dummy::create([
53 | 'name' => $i,
54 | 'custom_column_sort' => rand(),
55 | ]);
56 | });
57 | }
58 |
59 | protected function setUpSoftDeletes()
60 | {
61 | $this->app['db']->connection()->getSchemaBuilder()->table('dummies', function (Blueprint $table) {
62 | $table->softDeletes();
63 | });
64 | }
65 |
66 | protected function setUpIsActiveFieldForGlobalScope()
67 | {
68 | $this->app['db']->connection()->getSchemaBuilder()->table('dummies', function (Blueprint $table) {
69 | $table->boolean('is_active')->default(false);
70 | });
71 | }
72 |
73 | protected function setUpTimestamps()
74 | {
75 | $this->app['db']->connection()->getSchemaBuilder()->table('dummies', function (Blueprint $table) {
76 | $table->timestamps();
77 | });
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/SortableTrait.php:
--------------------------------------------------------------------------------
1 | shouldSortWhenCreating()) {
17 | $model->setHighestOrderNumber();
18 | }
19 | });
20 | }
21 |
22 | public function setHighestOrderNumber(): void
23 | {
24 | $orderColumnName = $this->determineOrderColumnName();
25 |
26 | $this->$orderColumnName = $this->getHighestOrderNumber() + 1;
27 | }
28 |
29 | public function getHighestOrderNumber(): int
30 | {
31 | return (int)$this->buildSortQuery()->max($this->determineOrderColumnName());
32 | }
33 |
34 | public function getLowestOrderNumber(): int
35 | {
36 | return (int)$this->buildSortQuery()->min($this->determineOrderColumnName());
37 | }
38 |
39 | public function scopeOrdered(Builder $query, string $direction = 'asc')
40 | {
41 | return $query->orderBy($this->determineOrderColumnName(), $direction);
42 | }
43 |
44 | public static function setNewOrder(
45 | $ids,
46 | int $startOrder = 1,
47 | ?string $primaryKeyColumn = null,
48 | ?callable $modifyQuery = null
49 | ): void {
50 | if (! is_array($ids) && ! $ids instanceof ArrayAccess) {
51 | throw new InvalidArgumentException('You must pass an array or ArrayAccess object to setNewOrder');
52 | }
53 |
54 | $model = new static();
55 |
56 | $orderColumnName = $model->determineOrderColumnName();
57 |
58 | if (is_null($primaryKeyColumn)) {
59 | $primaryKeyColumn = $model->getQualifiedKeyName();
60 | }
61 |
62 | if (config('eloquent-sortable.ignore_timestamps', false)) {
63 | static::$ignoreTimestampsOn = array_values(array_merge(static::$ignoreTimestampsOn, [static::class]));
64 | }
65 |
66 | foreach ($ids as $id) {
67 | static::withoutGlobalScope(SoftDeletingScope::class)
68 | ->when(is_callable($modifyQuery), function ($query) use ($modifyQuery) {
69 | return $modifyQuery($query);
70 | })
71 | ->where($primaryKeyColumn, $id)
72 | ->update([$orderColumnName => $startOrder++]);
73 | }
74 |
75 | Event::dispatch(new EloquentModelSortedEvent(static::class));
76 |
77 | if (config('eloquent-sortable.ignore_timestamps', false)) {
78 | static::$ignoreTimestampsOn = array_values(array_diff(static::$ignoreTimestampsOn, [static::class]));
79 | }
80 | }
81 |
82 | public static function setNewOrderByCustomColumn(string $primaryKeyColumn, $ids, int $startOrder = 1)
83 | {
84 | self::setNewOrder($ids, $startOrder, $primaryKeyColumn);
85 | }
86 |
87 | public function determineOrderColumnName(): string
88 | {
89 | return $this->sortable['order_column_name'] ?? config('eloquent-sortable.order_column_name', 'order_column');
90 | }
91 |
92 | /**
93 | * Determine if the order column should be set when saving a new model instance.
94 | */
95 | public function shouldSortWhenCreating(): bool
96 | {
97 | return $this->sortable['sort_when_creating'] ?? config('eloquent-sortable.sort_when_creating', true);
98 | }
99 |
100 | public function moveAfter(Sortable $model): void
101 | {
102 | $orderColumnName = $this->determineOrderColumnName();
103 |
104 | $this->buildSortQuery()
105 | ->ordered()
106 | ->where($orderColumnName, '>', $model->$orderColumnName)
107 | ->increment($orderColumnName);
108 |
109 | $this->$orderColumnName = $model->$orderColumnName + 1;
110 |
111 | $this->saveQuietly();
112 | }
113 |
114 | public function moveBefore(Sortable $model): void
115 | {
116 | $orderColumnName = $this->determineOrderColumnName();
117 |
118 | $this->buildSortQuery()
119 | ->ordered()
120 | ->where($orderColumnName, '>=', $model->$orderColumnName)
121 | ->increment($orderColumnName);
122 |
123 | $this->$orderColumnName = $model->$orderColumnName;
124 |
125 | $this->saveQuietly();
126 | }
127 |
128 | public function moveOrderDown(): static
129 | {
130 | $orderColumnName = $this->determineOrderColumnName();
131 |
132 | $swapWithModel = $this->buildSortQuery()->limit(1)
133 | ->ordered()
134 | ->where($orderColumnName, '>', $this->$orderColumnName)
135 | ->first();
136 |
137 | if (! $swapWithModel) {
138 | return $this;
139 | }
140 |
141 | return $this->swapOrderWithModel($swapWithModel);
142 | }
143 |
144 | public function moveOrderUp(): static
145 | {
146 | $orderColumnName = $this->determineOrderColumnName();
147 |
148 | $swapWithModel = $this->buildSortQuery()->limit(1)
149 | ->ordered('desc')
150 | ->where($orderColumnName, '<', $this->$orderColumnName)
151 | ->first();
152 |
153 | if (! $swapWithModel) {
154 | return $this;
155 | }
156 |
157 | return $this->swapOrderWithModel($swapWithModel);
158 | }
159 |
160 | public function swapOrderWithModel(Sortable $otherModel): static
161 | {
162 | $orderColumnName = $this->determineOrderColumnName();
163 |
164 | $oldOrderOfOtherModel = $otherModel->$orderColumnName;
165 |
166 | $otherModel->$orderColumnName = $this->$orderColumnName;
167 | $otherModel->save();
168 |
169 | $this->$orderColumnName = $oldOrderOfOtherModel;
170 | $this->save();
171 |
172 | return $this;
173 | }
174 |
175 | public static function swapOrder(Sortable $model, Sortable $otherModel): void
176 | {
177 | $model->swapOrderWithModel($otherModel);
178 | }
179 |
180 | public function moveToStart(): static
181 | {
182 | $firstModel = $this->buildSortQuery()->limit(1)
183 | ->ordered()
184 | ->first();
185 |
186 | if ($firstModel->getKey() === $this->getKey()) {
187 | return $this;
188 | }
189 |
190 | $orderColumnName = $this->determineOrderColumnName();
191 |
192 | $this->$orderColumnName = $firstModel->$orderColumnName;
193 | $this->save();
194 |
195 | $this->buildSortQuery()->where($this->getQualifiedKeyName(), '!=', $this->getKey())->increment(
196 | $orderColumnName
197 | );
198 |
199 | return $this;
200 | }
201 |
202 | public function moveToEnd(): static
203 | {
204 | $maxOrder = $this->getHighestOrderNumber();
205 |
206 | $orderColumnName = $this->determineOrderColumnName();
207 |
208 | if ($this->$orderColumnName === $maxOrder) {
209 | return $this;
210 | }
211 |
212 | $oldOrder = $this->$orderColumnName;
213 |
214 | $this->$orderColumnName = $maxOrder;
215 | $this->save();
216 |
217 | $this->buildSortQuery()->where($this->getQualifiedKeyName(), '!=', $this->getKey())
218 | ->where($orderColumnName, '>', $oldOrder)
219 | ->decrement($orderColumnName);
220 |
221 | return $this;
222 | }
223 |
224 | public function isLastInOrder(): bool
225 | {
226 | $orderColumnName = $this->determineOrderColumnName();
227 |
228 | return (int)$this->$orderColumnName === $this->getHighestOrderNumber();
229 | }
230 |
231 | public function isFirstInOrder(): bool
232 | {
233 | $orderColumnName = $this->determineOrderColumnName();
234 |
235 | return (int)$this->$orderColumnName === $this->getLowestOrderNumber();
236 | }
237 |
238 | public function buildSortQuery(): Builder
239 | {
240 | return static::query();
241 | }
242 | }
243 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to `eloquent-sortable` will be documented in this file
4 |
5 | ## 4.5.2 - 2025-08-25
6 |
7 | ### What's Changed
8 |
9 | * Revert "Check for parent::buildSortQuery() (#188)" by @phh in https://github.com/spatie/eloquent-sortable/pull/193
10 |
11 | ### New Contributors
12 |
13 | * @phh made their first contribution in https://github.com/spatie/eloquent-sortable/pull/193
14 |
15 | **Full Changelog**: https://github.com/spatie/eloquent-sortable/compare/4.5.1...4.5.2
16 |
17 | ## 4.5.1 - 2025-08-25
18 |
19 | ### What's Changed
20 |
21 | * Check for parent::buildSortQuery() by @JeroenHauser in https://github.com/spatie/eloquent-sortable/pull/188
22 | * Bump actions/checkout from 4 to 5 by @dependabot[bot] in https://github.com/spatie/eloquent-sortable/pull/191
23 | * Bump stefanzweifel/git-auto-commit-action from 5 to 6 by @dependabot[bot] in https://github.com/spatie/eloquent-sortable/pull/190
24 |
25 | ### New Contributors
26 |
27 | * @JeroenHauser made their first contribution in https://github.com/spatie/eloquent-sortable/pull/188
28 |
29 | **Full Changelog**: https://github.com/spatie/eloquent-sortable/compare/4.5.0...4.5.1
30 |
31 | ## 4.5.0 - 2025-06-03
32 |
33 | ### What's Changed
34 |
35 | * Bump dependabot/fetch-metadata from 2.3.0 to 2.4.0 by @dependabot in https://github.com/spatie/eloquent-sortable/pull/187
36 | * Add `moveAfter` and `moveBefore` methods for precise positioning by @SebastiaanKloos in https://github.com/spatie/eloquent-sortable/pull/189
37 |
38 | ### New Contributors
39 |
40 | * @SebastiaanKloos made their first contribution in https://github.com/spatie/eloquent-sortable/pull/189
41 |
42 | **Full Changelog**: https://github.com/spatie/eloquent-sortable/compare/4.4.2...4.5.0
43 |
44 | ## 4.4.2 - 2025-02-19
45 |
46 | ### What's Changed
47 |
48 | * Bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 by @dependabot in https://github.com/spatie/eloquent-sortable/pull/185
49 | * Laravel 12.x Compatibility by @laravel-shift in https://github.com/spatie/eloquent-sortable/pull/186
50 |
51 | ### New Contributors
52 |
53 | * @laravel-shift made their first contribution in https://github.com/spatie/eloquent-sortable/pull/186
54 |
55 | **Full Changelog**: https://github.com/spatie/eloquent-sortable/compare/4.4.1...4.4.2
56 |
57 | ## 4.4.1 - 2024-12-23
58 |
59 | ### What's Changed
60 |
61 | * Bump dependabot/fetch-metadata from 1.6.0 to 2.1.0 by @dependabot in https://github.com/spatie/eloquent-sortable/pull/176
62 | * Bump dependabot/fetch-metadata from 2.1.0 to 2.2.0 by @dependabot in https://github.com/spatie/eloquent-sortable/pull/181
63 | * Fix PHP 8.4 deprecation notice by @clarkewing in https://github.com/spatie/eloquent-sortable/pull/184
64 |
65 | ### New Contributors
66 |
67 | * @clarkewing made their first contribution in https://github.com/spatie/eloquent-sortable/pull/184
68 |
69 | **Full Changelog**: https://github.com/spatie/eloquent-sortable/compare/4.4.0...4.4.1
70 |
71 | ## 4.4.0 - 2024-06-04
72 |
73 | ### What's Changed
74 |
75 | * Dispatch event after performing a sort by @chrispage1 in https://github.com/spatie/eloquent-sortable/pull/178
76 |
77 | ### New Contributors
78 |
79 | * @chrispage1 made their first contribution in https://github.com/spatie/eloquent-sortable/pull/178
80 |
81 | **Full Changelog**: https://github.com/spatie/eloquent-sortable/compare/4.3.0...4.4.0
82 |
83 | ## 4.3.0 - 2024-05-02
84 |
85 | ### What's Changed
86 |
87 | * Use model's qualified key name for update queries by @JeremyDunn in https://github.com/spatie/eloquent-sortable/pull/175
88 |
89 | ### New Contributors
90 |
91 | * @JeremyDunn made their first contribution in https://github.com/spatie/eloquent-sortable/pull/175
92 |
93 | **Full Changelog**: https://github.com/spatie/eloquent-sortable/compare/4.2.0...4.3.0
94 |
95 | ## 4.2.0 - 2024-02-26
96 |
97 | ### What's Changed
98 |
99 | * Bump stefanzweifel/git-auto-commit-action from 4 to 5 by @dependabot in https://github.com/spatie/eloquent-sortable/pull/162
100 | * Support laravel 11 by @mokhosh in https://github.com/spatie/eloquent-sortable/pull/171
101 | * bump min laravel 9 version to support ignore timestamps by @mokhosh in https://github.com/spatie/eloquent-sortable/pull/173
102 |
103 | **Full Changelog**: https://github.com/spatie/eloquent-sortable/compare/4.1.1...4.2.0
104 |
105 | ## 4.1.1 - 2024-02-06
106 |
107 | ### What's Changed
108 |
109 | * Fix typo in eloquent-sortable.php by @dissto in https://github.com/spatie/eloquent-sortable/pull/168
110 | * Add ignore timestamp by @mokhosh in https://github.com/spatie/eloquent-sortable/pull/169
111 |
112 | ### New Contributors
113 |
114 | * @dissto made their first contribution in https://github.com/spatie/eloquent-sortable/pull/168
115 | * @mokhosh made their first contribution in https://github.com/spatie/eloquent-sortable/pull/169
116 |
117 | **Full Changelog**: https://github.com/spatie/eloquent-sortable/compare/4.1.0...4.1.1
118 |
119 | ## 4.0.2 - 2023-01-23
120 |
121 | ### What's Changed
122 |
123 | - Update readme about publishing config file by @patrickbrouwers in https://github.com/spatie/eloquent-sortable/pull/120
124 | - Add Dependabot Automation by @patinthehat in https://github.com/spatie/eloquent-sortable/pull/136
125 | - Bump actions/checkout from 2 to 3 by @dependabot in https://github.com/spatie/eloquent-sortable/pull/137
126 | - Add PHP 8.2 Support by @patinthehat in https://github.com/spatie/eloquent-sortable/pull/138
127 | - Laravel 10 support by @Okipa in https://github.com/spatie/eloquent-sortable/pull/142
128 |
129 | ### New Contributors
130 |
131 | - @patrickbrouwers made their first contribution in https://github.com/spatie/eloquent-sortable/pull/120
132 | - @dependabot made their first contribution in https://github.com/spatie/eloquent-sortable/pull/137
133 | - @Okipa made their first contribution in https://github.com/spatie/eloquent-sortable/pull/142
134 |
135 | **Full Changelog**: https://github.com/spatie/eloquent-sortable/compare/4.0.1...4.0.2
136 |
137 | ## 4.1.0 - 2023-01-24
138 |
139 | - add support for Laravel 10
140 | - drop support for Laravel 8
141 | - drop support for PHP 8.0
142 |
143 | ## 4.0.1 - 2022-01-21
144 |
145 | - support Laravel 9
146 |
147 | ## 4.0.0 - 2021-03-21
148 |
149 | - require PHP 8+
150 | - drop support for all PHP 7.x versions
151 | - use PHP 8 syntax
152 |
153 | ## 3.11.0 - 2021-01-18
154 |
155 | - add methods to determine whether element is the last or first in order (#102)
156 |
157 | ## 3.10.0 - 2020-11-25
158 |
159 | - add support for PHP 8.0
160 | - drop support for Laravel 5.8
161 |
162 | ## 3.9.0 - 2020-09-16
163 |
164 | - add config file
165 |
166 | ## 3.8.3 - 2020-09-08
167 |
168 | - add support for Laravel 8
169 |
170 | ## 3.8.2 - 2020-07-08
171 |
172 | - reduce dependency tree (#89)
173 |
174 | ## 3.8.1 - 2020-06-26
175 |
176 | - models don't always have an id as key, use `getKey` instead
177 |
178 | ## 3.8.0 - 2020-03-02
179 |
180 | - add support for Laravel 7
181 |
182 | ## 3.7.0 - 2019-09-04
183 |
184 | - add support for Laravel 6
185 |
186 | ## 3.6.0 - 2019-04-01
187 |
188 | - allow `setNewOrder` to accept a custom sort column
189 |
190 | ## 3.5.0 - 2019-02-27
191 |
192 | - drop support for L5.7 and below, PHP 7.1 and PHPUnit 7
193 |
194 | ## 3.4.4 - 2019-02-27
195 |
196 | - add support for Laravel 5.8
197 |
198 | ## 3.4.3 - 2018-08-24
199 |
200 | - add support for Laravel 5.7
201 |
202 | ## 3.4.2 - 2018-02-08
203 |
204 | - add support for L5.6
205 | - drop support for anything lower that L5.5
206 |
207 | ## 3.4.1
208 |
209 | - fix deps
210 |
211 | ## 3.4.0
212 |
213 | - add compatibility with Laravel 5.5
214 |
215 | ## 3.3.0 - 2017-04-16
216 |
217 | - add `buildSortQuery()`
218 |
219 | ## 3.2.1 - 2017-01-23
220 |
221 | - release without changes. Made to kickstart Packagist.
222 |
223 | ## 3.2.0 - 2017-01-23
224 |
225 | - add compatibility with Laravel 5.4
226 |
227 | ## 3.1.0 - 2016-11-20
228 |
229 | - added support for `SoftDeletes`
230 |
231 | ## 3.0.0 - 2016-10-22
232 |
233 | - removed the need for a service provider
234 | - some cleanup
235 |
236 | ## 2.3.0 - 2016-10-19
237 |
238 | - added support for collections passed to `setNewOrder`
239 |
240 | ## 2.2.0 - 2016-10-19
241 |
242 | - added `moveToStart`, `moveToEnd` and `swapOrder`
243 |
244 | ## 2.1.1 - 2016-03-21
245 |
246 | - Fixed a bug in `moveOrderUp` (see #13)
247 |
248 | ## 2.1.0
249 |
250 | - Added `moveOrderUp`- and `moveOrderDown`-methods
251 |
252 | ## 2.0.1
253 |
254 | - Fixed typehinting on scope
255 |
256 | ## 2.0.0
257 |
258 | - SortableInterface is now Sortable
259 | - Sortable is now SortableTrait
260 | - getHighestOrderNumber() now retrieves the highest existing order number (not a new one)
261 | - setHighestOrderNumber() no longer requires a Sortable object parameter
262 | - sort_when_creating option
263 | - Added shouldSortWhenCreating function
264 | - Added test coverage
265 |
266 | ## 1.1.2
267 |
268 | - Removed typehinting on scope in interface.
269 |
270 | ## 1.1.1 (non-functional version!)
271 |
272 | - Removed typehinting on scope
273 |
274 | ## 1.1.0
275 |
276 | - Added an argument to `setNewOrder` to specify the starting order
277 | - Adopted psr-2 and psr-4
278 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
Sortable behaviour for Eloquent models
10 |
11 | [](https://github.com/spatie/eloquent-sortable/releases)
12 | 
13 | [](LICENSE.md)
14 | [](https://packagist.org/packages/spatie/eloquent-sortable)
15 |
16 |
17 |
18 | This package provides a trait that adds sortable behaviour to an Eloquent model.
19 |
20 | The value of the order column of a new record of a model is determined by the maximum value of the order column of all records of that model + 1.
21 |
22 | The package also provides a query scope to fetch all the records in the right order.
23 |
24 | Spatie is a webdesign agency in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource).
25 |
26 | ## Support us
27 |
28 | Learn how to create a package like this one, by watching our premium video course:
29 |
30 | [](https://laravelpackage.training)
31 |
32 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us).
33 |
34 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards).
35 |
36 | ## Installation
37 |
38 | > For Laravel 6.x or PHP 7.x, use version 3.x of this package.
39 |
40 | This package can be installed through Composer.
41 |
42 | ```
43 | composer require spatie/eloquent-sortable
44 | ```
45 |
46 | In Laravel 5.5 and above the service provider will automatically get registered. In older versions of the framework just add the service provider in `config/app.php` file:
47 |
48 | ```php
49 | 'providers' => [
50 | ...
51 | Spatie\EloquentSortable\EloquentSortableServiceProvider::class,
52 | ];
53 | ```
54 |
55 | Optionally you can publish the config file with:
56 |
57 | ```bash
58 | php artisan vendor:publish --tag=eloquent-sortable-config
59 | ```
60 |
61 | This is the content of the file that will be published in `config/eloquent-sortable.php`
62 |
63 | ```php
64 | return [
65 | /*
66 | * The name of the column that will be used to sort models.
67 | */
68 | 'order_column_name' => 'order_column',
69 |
70 | /*
71 | * Define if the models should sort when creating. When true, the package
72 | * will automatically assign the highest order number to a new model
73 | */
74 | 'sort_when_creating' => true,
75 |
76 | /*
77 | * Define if the timestamps should be ignored when sorting.
78 | * When true, updated_at will not be updated when using setNewOrder
79 | */
80 | 'ignore_timestamps' => false,
81 | ];
82 | ```
83 |
84 | ## Usage
85 |
86 | To add sortable behaviour to your model you must:
87 | 1. Implement the `Spatie\EloquentSortable\Sortable` interface.
88 | 2. Use the trait `Spatie\EloquentSortable\SortableTrait`.
89 | 3. Optionally specify which column will be used as the order column. The default is `order_column`.
90 |
91 | ### Example
92 |
93 | ```php
94 | use Spatie\EloquentSortable\Sortable;
95 | use Spatie\EloquentSortable\SortableTrait;
96 |
97 | class MyModel extends Model implements Sortable
98 | {
99 | use SortableTrait;
100 |
101 | public $sortable = [
102 | 'order_column_name' => 'order_column',
103 | 'sort_when_creating' => true,
104 | ];
105 |
106 | // ...
107 | }
108 | ```
109 |
110 | If you don't set a value `$sortable['order_column_name']` the package will assume that your order column name will be named `order_column`.
111 |
112 | If you don't set a value `$sortable['sort_when_creating']` the package will automatically assign the highest order number to a new model;
113 |
114 | Assuming that the db-table for `MyModel` is empty:
115 |
116 | ```php
117 | $myModel = new MyModel();
118 | $myModel->save(); // order_column for this record will be set to 1
119 |
120 | $myModel = new MyModel();
121 | $myModel->save(); // order_column for this record will be set to 2
122 |
123 | $myModel = new MyModel();
124 | $myModel->save(); // order_column for this record will be set to 3
125 |
126 |
127 | //the trait also provides the ordered query scope
128 | $orderedRecords = MyModel::ordered()->get();
129 | ```
130 |
131 | You can set a new order for all the records using the `setNewOrder`-method
132 |
133 | ```php
134 | /**
135 | * the record for model id 3 will have order_column value 1
136 | * the record for model id 1 will have order_column value 2
137 | * the record for model id 2 will have order_column value 3
138 | */
139 | MyModel::setNewOrder([3,1,2]);
140 | ```
141 |
142 | Optionally you can pass the starting order number as the second argument.
143 |
144 | ```php
145 | /**
146 | * the record for model id 3 will have order_column value 11
147 | * the record for model id 1 will have order_column value 12
148 | * the record for model id 2 will have order_column value 13
149 | */
150 | MyModel::setNewOrder([3,1,2], 10);
151 | ```
152 |
153 | You can modify the query that will be executed by passing a closure as the fourth argument.
154 |
155 | ```php
156 | /**
157 | * the record for model id 3 will have order_column value 11
158 | * the record for model id 1 will have order_column value 12
159 | * the record for model id 2 will have order_column value 13
160 | */
161 | MyModel::setNewOrder([3,1,2], 10, null, function($query) {
162 | $query->withoutGlobalScope(new ActiveScope);
163 | });
164 | ```
165 |
166 |
167 | To sort using a column other than the primary key, use the `setNewOrderByCustomColumn`-method.
168 |
169 | ```php
170 | /**
171 | * the record for model uuid '7a051131-d387-4276-bfda-e7c376099715' will have order_column value 1
172 | * the record for model uuid '40324562-c7ca-4c69-8018-aff81bff8c95' will have order_column value 2
173 | * the record for model uuid '5dc4d0f4-0c88-43a4-b293-7c7902a3cfd1' will have order_column value 3
174 | */
175 | MyModel::setNewOrderByCustomColumn('uuid', [
176 | '7a051131-d387-4276-bfda-e7c376099715',
177 | '40324562-c7ca-4c69-8018-aff81bff8c95',
178 | '5dc4d0f4-0c88-43a4-b293-7c7902a3cfd1'
179 | ]);
180 | ```
181 |
182 | As with `setNewOrder`, `setNewOrderByCustomColumn` will also accept an optional starting order argument.
183 |
184 | ```php
185 | /**
186 | * the record for model uuid '7a051131-d387-4276-bfda-e7c376099715' will have order_column value 10
187 | * the record for model uuid '40324562-c7ca-4c69-8018-aff81bff8c95' will have order_column value 11
188 | * the record for model uuid '5dc4d0f4-0c88-43a4-b293-7c7902a3cfd1' will have order_column value 12
189 | */
190 | MyModel::setNewOrderByCustomColumn('uuid', [
191 | '7a051131-d387-4276-bfda-e7c376099715',
192 | '40324562-c7ca-4c69-8018-aff81bff8c95',
193 | '5dc4d0f4-0c88-43a4-b293-7c7902a3cfd1'
194 | ], 10);
195 | ```
196 |
197 | You can also move a model up or down with these methods:
198 |
199 | ```php
200 | $myModel->moveOrderDown();
201 | $myModel->moveOrderUp();
202 | ```
203 |
204 | You can also move a model to the first or last position:
205 |
206 | ```php
207 | $myModel->moveToStart();
208 | $myModel->moveToEnd();
209 | ```
210 |
211 | You can determine whether an element is first or last in order:
212 |
213 | ```php
214 | $myModel->isFirstInOrder();
215 | $myModel->isLastInOrder();
216 | ```
217 |
218 | You can swap the order of two models:
219 |
220 | ```php
221 | MyModel::swapOrder($myModel, $anotherModel);
222 | ```
223 |
224 | ### Grouping
225 |
226 | If your model/table has a grouping field (usually a foreign key): `id, `**`user_id`**`, title, order_column`
227 | and you'd like the above methods to take it into considerations, you can create a `buildSortQuery` method at your model:
228 | ```php
229 | // MyModel.php
230 |
231 | public function buildSortQuery()
232 | {
233 | return static::query()->where('user_id', $this->user_id);
234 | }
235 | ```
236 | This will restrict the calculations to fields value of the model instance.
237 |
238 | ### Dispatched events
239 |
240 | Once a sort has been completed, an event (`Spatie\EloquentSortable\EloquentModelSortedEvent`) is dispatched that you
241 | can listen for. This can be useful for running post-sorting logic such as clearing caches or other actions that
242 | need to be taken after a sort.
243 |
244 | The event has an `isFor` helper which allows you to conveniently check the Eloquent class that has been sorted.
245 |
246 | Below is an example of how you can listen for this event:
247 |
248 | ```php
249 | use Spatie\EloquentSortable\EloquentModelSortedEvent as SortEvent;
250 |
251 | class SortingListener
252 | {
253 | public function handle(SortEvent $event): void {
254 | if ($event->isFor(MyClass::class)) {
255 | // ToDo: flush our cache
256 | }
257 | }
258 | }
259 | ```
260 |
261 | ## Tests
262 |
263 | The package contains some integration/smoke tests, set up with Orchestra. The tests can be run via phpunit.
264 |
265 | ```bash
266 | vendor/bin/phpunit
267 | ```
268 |
269 | ## Changelog
270 |
271 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
272 |
273 | ## Contributing
274 |
275 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details.
276 |
277 | ## Security Vulnerabilities
278 |
279 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities.
280 |
281 | ## Credits
282 |
283 | - [Freek Van der Herten](https://github.com/freekmurze)
284 | - [All Contributors](../../contributors)
285 |
286 | ## Alternatives
287 | - [Listify](https://github.com/lookitsatravis/listify)
288 | - [Rutorike-sortable](https://github.com/boxfrommars/rutorika-sortable)
289 |
290 | ## License
291 |
292 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
293 |
--------------------------------------------------------------------------------
/tests/SortableTest.php:
--------------------------------------------------------------------------------
1 | assertEquals($dummy->name, $dummy->order_column);
16 | }
17 | }
18 |
19 | /** @test */
20 | public function it_can_get_the_highest_order_number()
21 | {
22 | $this->assertEquals(Dummy::all()->count(), (new Dummy())->getHighestOrderNumber());
23 | }
24 |
25 | /** @test */
26 | public function it_can_get_the_highest_order_number_with_trashed_models()
27 | {
28 | $this->setUpSoftDeletes();
29 |
30 | DummyWithSoftDeletes::first()->delete();
31 |
32 | $this->assertEquals(DummyWithSoftDeletes::withTrashed()->count(), (new DummyWithSoftDeletes())->getHighestOrderNumber());
33 | }
34 |
35 | /** @test */
36 | public function it_can_set_a_new_order()
37 | {
38 |
39 | Event::fake(EloquentModelSortedEvent::class);
40 |
41 | $newOrder = Collection::make(Dummy::all()->pluck('id'))->shuffle()->toArray();
42 |
43 | Dummy::setNewOrder($newOrder);
44 |
45 | foreach (Dummy::orderBy('order_column')->get() as $i => $dummy) {
46 | $this->assertEquals($newOrder[$i], $dummy->id);
47 | }
48 |
49 | Event::assertDispatched(EloquentModelSortedEvent::class, function (EloquentModelSortedEvent $event) {
50 | return $event->isFor(Dummy::class);
51 | });
52 | }
53 |
54 | /** @test */
55 | public function it_can_touch_timestamps_when_setting_a_new_order()
56 | {
57 | $this->setUpTimestamps();
58 | DummyWithTimestamps::query()->update(['updated_at' => now()]);
59 | $originalTimestamps = DummyWithTimestamps::all()->pluck('updated_at');
60 |
61 | $this->travelTo(now()->addMinute());
62 |
63 | config()->set('eloquent-sortable.ignore_timestamps', false);
64 | $newOrder = Collection::make(DummyWithTimestamps::all()->pluck('id'))->shuffle()->toArray();
65 | DummyWithTimestamps::setNewOrder($newOrder);
66 |
67 | foreach (DummyWithTimestamps::orderBy('order_column')->get() as $i => $dummy) {
68 | $this->assertNotEquals($originalTimestamps[$i], $dummy->updated_at);
69 | }
70 | }
71 |
72 | /** @test */
73 | public function it_can_set_a_new_order_without_touching_timestamps()
74 | {
75 | $this->setUpTimestamps();
76 | DummyWithTimestamps::query()->update(['updated_at' => now()]);
77 | $originalTimestamps = DummyWithTimestamps::all()->pluck('updated_at');
78 |
79 | $this->travelTo(now()->addMinute());
80 |
81 | config()->set('eloquent-sortable.ignore_timestamps', true);
82 | $newOrder = Collection::make(DummyWithTimestamps::all()->pluck('id'))->shuffle()->toArray();
83 | DummyWithTimestamps::setNewOrder($newOrder);
84 |
85 | foreach (DummyWithTimestamps::orderBy('order_column')->get() as $i => $dummy) {
86 | $this->assertEquals($originalTimestamps[$i], $dummy->updated_at);
87 | }
88 | }
89 |
90 | /** @test */
91 | public function it_can_set_a_new_order_by_custom_column()
92 | {
93 | $newOrder = Collection::make(Dummy::all()->pluck('custom_column_sort'))->shuffle()->toArray();
94 |
95 | Dummy::setNewOrderByCustomColumn('custom_column_sort', $newOrder);
96 |
97 | foreach (Dummy::orderBy('order_column')->get() as $i => $dummy) {
98 | $this->assertEquals($newOrder[$i], $dummy->custom_column_sort);
99 | }
100 | }
101 |
102 | /** @test */
103 | public function it_can_set_a_new_order_from_collection()
104 | {
105 | $newOrder = Collection::make(Dummy::all()->pluck('id'))->shuffle();
106 |
107 | Dummy::setNewOrder($newOrder);
108 |
109 | foreach (Dummy::orderBy('order_column')->get() as $i => $dummy) {
110 | $this->assertEquals($newOrder[$i], $dummy->id);
111 | }
112 | }
113 |
114 | /** @test */
115 | public function it_can_set_a_new_order_by_custom_column_from_collection()
116 | {
117 | $newOrder = Collection::make(Dummy::all()->pluck('custom_column_sort'))->shuffle();
118 |
119 | Dummy::setNewOrderByCustomColumn('custom_column_sort', $newOrder);
120 |
121 | foreach (Dummy::orderBy('order_column')->get() as $i => $dummy) {
122 | $this->assertEquals($newOrder[$i], $dummy->custom_column_sort);
123 | }
124 | }
125 |
126 | /** @test */
127 | public function it_can_set_new_order_without_global_scopes_models()
128 | {
129 | $this->setUpIsActiveFieldForGlobalScope();
130 |
131 | $newOrder = Collection::make(Dummy::all()->pluck('id'))->shuffle()->toArray();
132 |
133 | DummyWithGlobalScope::setNewOrder($newOrder, 1, null, function ($query) {
134 | $query->withoutGlobalScope('ActiveScope');
135 | });
136 |
137 | foreach (Dummy::orderBy('order_column')->get() as $i => $dummy) {
138 | $this->assertEquals($newOrder[$i], $dummy->id);
139 | }
140 | }
141 |
142 | /** @test */
143 | public function it_can_set_a_new_order_with_trashed_models()
144 | {
145 | $this->setUpSoftDeletes();
146 |
147 | $dummies = DummyWithSoftDeletes::all();
148 |
149 | $dummies->random()->delete();
150 |
151 | $newOrder = Collection::make($dummies->pluck('id'))->shuffle();
152 |
153 | DummyWithSoftDeletes::setNewOrder($newOrder);
154 |
155 | foreach (DummyWithSoftDeletes::withTrashed()->orderBy('order_column')->get() as $i => $dummy) {
156 | $this->assertEquals($newOrder[$i], $dummy->id);
157 | }
158 | }
159 |
160 | /** @test */
161 | public function it_can_set_a_new_order_by_custom_column_with_trashed_models()
162 | {
163 | $this->setUpSoftDeletes();
164 |
165 | $dummies = DummyWithSoftDeletes::all();
166 |
167 | $dummies->random()->delete();
168 |
169 | $newOrder = Collection::make($dummies->pluck('custom_column_sort'))->shuffle();
170 |
171 | DummyWithSoftDeletes::setNewOrderByCustomColumn('custom_column_sort', $newOrder);
172 |
173 | foreach (DummyWithSoftDeletes::withTrashed()->orderBy('order_column')->get() as $i => $dummy) {
174 | $this->assertEquals($newOrder[$i], $dummy->custom_column_sort);
175 | }
176 | }
177 |
178 | /** @test */
179 | public function it_can_set_a_new_order_without_trashed_models()
180 | {
181 | $this->setUpSoftDeletes();
182 |
183 | DummyWithSoftDeletes::first()->delete();
184 |
185 | $newOrder = Collection::make(DummyWithSoftDeletes::pluck('id'))->shuffle();
186 |
187 | DummyWithSoftDeletes::setNewOrder($newOrder);
188 |
189 | foreach (DummyWithSoftDeletes::orderBy('order_column')->get() as $i => $dummy) {
190 | $this->assertEquals($newOrder[$i], $dummy->id);
191 | }
192 | }
193 |
194 | /** @test */
195 | public function it_can_set_a_new_order_by_custom_column_without_trashed_models()
196 | {
197 | $this->setUpSoftDeletes();
198 |
199 | DummyWithSoftDeletes::first()->delete();
200 |
201 | $newOrder = Collection::make(DummyWithSoftDeletes::pluck('custom_column_sort'))->shuffle();
202 |
203 | DummyWithSoftDeletes::setNewOrderByCustomColumn('custom_column_sort', $newOrder);
204 |
205 | foreach (DummyWithSoftDeletes::orderBy('order_column')->get() as $i => $dummy) {
206 | $this->assertEquals($newOrder[$i], $dummy->custom_column_sort);
207 | }
208 | }
209 |
210 | /** @test */
211 | public function it_will_determine_to_sort_when_creating_if_sortable_attribute_does_not_exist()
212 | {
213 | $model = new Dummy();
214 |
215 | $this->assertTrue($model->shouldSortWhenCreating());
216 | }
217 |
218 | /** @test */
219 | public function it_will_determine_to_sort_when_creating_if_sort_when_creating_setting_does_not_exist()
220 | {
221 | $model = new class () extends Dummy {
222 | public $sortable = [];
223 | };
224 |
225 | $this->assertTrue($model->shouldSortWhenCreating());
226 | }
227 |
228 | /** @test */
229 | public function it_will_respect_the_sort_when_creating_setting()
230 | {
231 | $model = new class () extends Dummy {
232 | public $sortable = ['sort_when_creating' => true];
233 | };
234 |
235 | $this->assertTrue($model->shouldSortWhenCreating());
236 |
237 | $model = new class () extends Dummy {
238 | public $sortable = ['sort_when_creating' => false];
239 | };
240 | $this->assertFalse($model->shouldSortWhenCreating());
241 | }
242 |
243 | /** @test */
244 | public function it_provides_an_ordered_trait()
245 | {
246 | $i = 1;
247 |
248 | foreach (Dummy::ordered()->get()->pluck('order_column') as $order) {
249 | $this->assertEquals($i++, $order);
250 | }
251 | }
252 |
253 | /** @test */
254 | public function it_can_move_after()
255 | {
256 | $firstModel = Dummy::find(3);
257 | $secondModel = Dummy::find(4);
258 | $newModel = Dummy::create(['name' => 'New dummy', 'custom_column_sort' => rand()]);
259 |
260 | $newModel->moveAfter($firstModel);
261 |
262 | $newModel->refresh();
263 | $secondModel->refresh();
264 |
265 | $this->assertEquals($newModel->order_column, 4);
266 | $this->assertEquals($secondModel->order_column, 5);
267 | }
268 |
269 | /** @test */
270 | public function it_can_move_before()
271 | {
272 | $firstModel = Dummy::find(4);
273 | $newModel = Dummy::create(['name' => 'New dummy', 'custom_column_sort' => rand()]);
274 |
275 | $newModel->moveBefore($firstModel);
276 |
277 | $firstModel->refresh();
278 | $newModel->refresh();
279 |
280 | $this->assertEquals($newModel->order_column, 4);
281 | $this->assertEquals($firstModel->order_column, 5);
282 | }
283 |
284 | /** @test */
285 | public function it_can_move_the_order_down()
286 | {
287 | $firstModel = Dummy::find(3);
288 | $secondModel = Dummy::find(4);
289 |
290 | $this->assertEquals($firstModel->order_column, 3);
291 | $this->assertEquals($secondModel->order_column, 4);
292 |
293 | $this->assertNotFalse($firstModel->moveOrderDown());
294 |
295 | $firstModel = Dummy::find(3);
296 | $secondModel = Dummy::find(4);
297 |
298 | $this->assertEquals($firstModel->order_column, 4);
299 | $this->assertEquals($secondModel->order_column, 3);
300 | }
301 |
302 | /** @test */
303 | public function it_will_not_fail_when_it_cant_move_the_order_down()
304 | {
305 | $lastModel = Dummy::all()->last();
306 |
307 | $this->assertEquals($lastModel->order_column, 20);
308 | $this->assertEquals($lastModel, $lastModel->moveOrderDown());
309 | }
310 |
311 | /** @test */
312 | public function it_can_move_the_order_up()
313 | {
314 | $firstModel = Dummy::find(3);
315 | $secondModel = Dummy::find(4);
316 |
317 | $this->assertEquals($firstModel->order_column, 3);
318 | $this->assertEquals($secondModel->order_column, 4);
319 |
320 | $this->assertNotFalse($secondModel->moveOrderUp());
321 |
322 | $firstModel = Dummy::find(3);
323 | $secondModel = Dummy::find(4);
324 |
325 | $this->assertEquals($firstModel->order_column, 4);
326 | $this->assertEquals($secondModel->order_column, 3);
327 | }
328 |
329 | /** @test */
330 | public function it_will_not_break_when_it_cant_move_the_order_up()
331 | {
332 | $lastModel = Dummy::first();
333 |
334 | $this->assertEquals($lastModel->order_column, 1);
335 | $this->assertEquals($lastModel, $lastModel->moveOrderUp());
336 | }
337 |
338 | /** @test */
339 | public function it_can_swap_the_position_of_two_given_models()
340 | {
341 | $firstModel = Dummy::find(3);
342 | $secondModel = Dummy::find(4);
343 |
344 | $this->assertEquals($firstModel->order_column, 3);
345 | $this->assertEquals($secondModel->order_column, 4);
346 |
347 | Dummy::swapOrder($firstModel, $secondModel);
348 |
349 | $this->assertEquals($firstModel->order_column, 4);
350 | $this->assertEquals($secondModel->order_column, 3);
351 | }
352 |
353 | /** @test */
354 | public function it_can_swap_itself_with_another_model()
355 | {
356 | $firstModel = Dummy::find(3);
357 | $secondModel = Dummy::find(4);
358 |
359 | $this->assertEquals($firstModel->order_column, 3);
360 | $this->assertEquals($secondModel->order_column, 4);
361 |
362 | $firstModel->swapOrderWithModel($secondModel);
363 |
364 | $this->assertEquals($firstModel->order_column, 4);
365 | $this->assertEquals($secondModel->order_column, 3);
366 | }
367 |
368 | /** @test */
369 | public function it_can_move_a_model_to_the_first_place()
370 | {
371 | $position = 3;
372 |
373 | $oldModels = Dummy::whereNot('id', $position)->get();
374 |
375 | $model = Dummy::find($position);
376 |
377 | $this->assertEquals(3, $model->order_column);
378 |
379 | $model = $model->moveToStart();
380 |
381 | $this->assertEquals(1, $model->order_column);
382 |
383 | $oldModels = $oldModels->pluck('order_column', 'id');
384 | $newModels = Dummy::whereNot('id', $position)->get()->pluck('order_column', 'id');
385 |
386 | foreach ($oldModels as $key => $oldModel) {
387 | $this->assertEquals($oldModel + 1, $newModels[$key]);
388 | }
389 | }
390 |
391 | /**
392 | * @test
393 | */
394 | public function it_can_move_a_model_to_the_last_place()
395 | {
396 | $position = 3;
397 |
398 | $oldModels = Dummy::whereNot('id', $position)->get();
399 |
400 | $model = Dummy::find($position);
401 |
402 | $this->assertNotEquals(20, $model->order_column);
403 |
404 | $model = $model->moveToEnd();
405 |
406 | $this->assertEquals(20, $model->order_column);
407 |
408 | $oldModels = $oldModels->pluck('order_column', 'id');
409 |
410 | $newModels = Dummy::whereNot('id', $position)->get()->pluck('order_column', 'id');
411 |
412 | foreach ($oldModels as $key => $order) {
413 | if ($order > $position) {
414 | $this->assertEquals($order - 1, $newModels[$key]);
415 | } else {
416 | $this->assertEquals($order, $newModels[$key]);
417 | }
418 | }
419 | }
420 |
421 | /** @test */
422 | public function it_can_use_config_properties()
423 | {
424 | config([
425 | 'eloquent-sortable.order_column_name' => 'order_column',
426 | 'eloquent-sortable.sort_when_creating' => true,
427 | ]);
428 |
429 | $model = new class () extends Dummy {
430 | public $sortable = [];
431 | };
432 |
433 | $this->assertEquals(config('eloquent-sortable.order_column_name'), $model->determineOrderColumnName());
434 | $this->assertEquals(config('eloquent-sortable.sort_when_creating'), $model->shouldSortWhenCreating());
435 | }
436 |
437 | /** @test */
438 | public function it_can_override_config_properties()
439 | {
440 | $model = new class () extends Dummy {
441 | public $sortable = [
442 | 'order_column_name' => 'my_custom_order_column',
443 | 'sort_when_creating' => false,
444 | ];
445 | };
446 |
447 | $this->assertEquals($model->determineOrderColumnName(), 'my_custom_order_column');
448 | $this->assertFalse($model->shouldSortWhenCreating());
449 | }
450 |
451 | /** @test */
452 | public function it_can_tell_if_element_is_first_in_order()
453 | {
454 | $model = (new Dummy())->buildSortQuery()->get();
455 | $this->assertTrue($model[0]->isFirstInOrder());
456 | $this->assertFalse($model[1]->isFirstInOrder());
457 | }
458 |
459 | /** @test */
460 | public function it_can_tell_if_element_is_last_in_order()
461 | {
462 | $model = (new Dummy())->buildSortQuery()->get();
463 | $this->assertTrue($model[$model->count() - 1]->isLastInOrder());
464 | $this->assertFalse($model[$model->count() - 2]->isLastInOrder());
465 | }
466 | }
467 |
--------------------------------------------------------------------------------