diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4f38912
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+.idea
+vendor
+composer.lock
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..0317c1a
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright for portions of project metrix/eloquent-sortable are held by Spatie bvba as part of spatie/eloquent-sortable.
+All other copyright for project metrix/eloquent-sortable are held by (c) Michael J. Pawlowsky 2018.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cc39218
--- /dev/null
+++ b/README.md
@@ -0,0 +1,176 @@
+# Sortable behaviour for Eloquent models
+
+This package provides a trait that adds sortable behaviour to an Eloquent model.
+
+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 or a subset group of records of that model + 1.
+
+The package also provides a query scope to fetch all the records in the right order.
+
+This package is a fork of the popular [spatie/eloquent-sortable](https://github.com/spatie/eloquent-sortable) with added functionality
+to allow sorting on subsets of models as well as moving a model to a specific position.
+
+Thank you to [Freek Van der Herten](https://murze.be) for sharing the original package.
+
+## Installation
+
+This package can be installed through Composer.
+
+```
+composer require metrix/eloquent-sortable
+```
+
+## Usage
+
+To add sortable behaviour to your model you must:
+1. Use the trait `Metrix\EloquentSortable\Sortable`.
+2. Optionally specify which column will be used as the order column. The default is `display_order`.
+
+### Examples
+
+*Simple ordered model*
+
+```php
+use Metrix\EloquentSortable\Sortable;
+
+class MyModel extends Eloquent
+{
+
+ use SortableTrait;
+
+ public $sortable = [
+ 'sort_on_creating' => true,
+ 'order_column' => 'display_order',
+ ];
+
+ ...
+}
+```
+
+*Ordered model with a grouping column*
+
+```php
+use Metrix\EloquentSortable\Sortable;
+
+class MyModel extends Eloquent
+{
+
+ use SortableTrait;
+
+ public $sortable = [
+ 'sort_on_creating' => true,
+ 'order_column' => 'display_order',
+ 'group_column' => 'group_id',
+ ];
+
+ ...
+}
+```
+
+*Ordered model grouped on multiple columns*
+
+```php
+use Metrix\EloquentSortable\Sortable;
+
+class MyModel extends Eloquent
+{
+
+ use SortableTrait;
+
+ public $sortable = [
+ 'sort_on_creating' => true,
+ 'order_column' => 'display_order',
+ 'group_column' => ['group_id','user_id'],
+ ];
+
+ ...
+}
+```
+
+If you don't set a value for `$sortable['order_column']` the package will assume an order column name of `display_order`.
+
+If you don't set a value `$sortable['sort_on_creating']` the package will automatically assign the next highest order value to the new model;
+
+Assuming that the db table for `MyModel` is empty:
+
+```php
+$myModel = new MyModel();
+$myModel->save(); // order_column for this record will be set to 1
+
+$myModel = new MyModel();
+$myModel->save(); // order_column for this record will be set to 2
+
+$myModel = new MyModel();
+$myModel->save(); // order_column for this record will be set to 3
+```
+
+The trait also provides an ordered query scope.
+All models will be returned ordered by 'group' and then 'display_order'
+if you have not applied a where() method for your group column on your query,
+
+```php
+$orderedRecords = MyModel::ordered()->get();
+
+$groupedOrderedRecords = MyModel::where('group_id', 2)->ordered()->get();
+
+$allRecords = MyModel::ordered()->get();
+```
+
+You can set a new order for all the records using the `setNewOrder`-method
+
+```php
+/**
+ * the record for model id 3 will have record_column value 1
+ * the record for model id 1 will have record_column value 2
+ * the record for model id 2 will have record_column value 3
+ */
+MyModel::setNewOrder([3,1,2]);
+```
+
+Optionally you can pass the starting order number as the second argument.
+
+```php
+/**
+ * the record for model id 3 will have record_column value 11
+ * the record for model id 1 will have record_column value 12
+ * the record for model id 2 will have record_column value 13
+ */
+MyModel::setNewOrder([3,1,2], 10);
+```
+
+You can also move a model up or down with these methods:
+
+```php
+$myModel->moveOrderDown();
+$myModel->moveOrderUp();
+```
+
+You can also move a model to the first or last position:
+
+```php
+$myModel->moveToStart();
+$myModel->moveToEnd();
+```
+
+You can swap the order of two models:
+
+```php
+MyModel::swapOrder($myModel, $anotherModel);
+```
+
+You can move a model to a specific position:
+
+```php
+$myModel->moveToPosition(4);
+```
+
+## Tests
+
+The package contains some integration/smoke tests, set up with Orchestra. The tests can be run via the ./bin/test.sh script from the root directory.
+
+```
+$ ./bin/test.sh
+```
+
+## License
+
+The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
diff --git a/bin/test.sh b/bin/test.sh
new file mode 100755
index 0000000..71efa6d
--- /dev/null
+++ b/bin/test.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+echo "PHP Lint"
+vendor/bin/parallel-lint --blame src
+
+echo "Fixing code sniffs"
+vendor/bin/phpcbf -p -s --standard=build/phpcs.xml
+
+echo "Running code sniffer"
+vendor/bin/phpcs -p -s --standard=build/phpcs.xml
+
+echo "Running Mess Detector"
+vendor/bin/phpmd app text build/phpmd.xml
+
+echo "Running Tests"
+vendor/bin/phpunit -d memory_limit=512M
diff --git a/build/phpcs.xml b/build/phpcs.xml
new file mode 100644
index 0000000..b0f0f3a
--- /dev/null
+++ b/build/phpcs.xml
@@ -0,0 +1,30 @@
+
+
+
+ The coding standard for PHP_CodeSniffer itself.
+
+ .././src
+
+ */Standards/*/Tests/*.(inc|css|js)
+
+
+
+
+
+
+
+
+ error
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build/phpmd.xml b/build/phpmd.xml
new file mode 100644
index 0000000..4f13dd2
--- /dev/null
+++ b/build/phpmd.xml
@@ -0,0 +1,29 @@
+
+
+
+ Honeyfund 2 Rule Set
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..a667846
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,48 @@
+{
+ "name": "metrix/laravel-sortable",
+ "description": "Sortable trait for Laravel Eloquent models",
+ "homepage": "https://github.com/metrix/laravel-sortable",
+ "authors": [
+ {
+ "name": "Michael J. Pawlowsky",
+ "email": "info@metrixinfo.com"
+ }
+ ],
+ "keywords":
+ [
+ "sort",
+ "sortable",
+ "eloquent",
+ "model",
+ "laravel",
+ "trait",
+ "display order"
+ ],
+ "license": "MIT",
+ "require": {
+ "php": ">=7.1",
+ "laravel/framework": "~5.5.0|~5.6.0|~5.7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit" : "^6.2|^7.0",
+ "orchestra/testbench": "~3.5.0|~3.6.0|~3.7.0",
+ "consistence/coding-standard": "^2.0",
+ "jakub-onderka/php-parallel-lint": "^0.9.2",
+ "pdepend/pdepend": "^2.5",
+ "phploc/phploc": "^4.0",
+ "phpmd/phpmd": "^2.6",
+ "phpunit/php-code-coverage": "^5.2",
+ "sebastian/phpcpd": "^3.0",
+ "squizlabs/php_codesniffer": "^3.2"
+ },
+ "autoload": {
+ "psr-4": {
+ "Metrix\\EloquentSortable\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Metrix\\EloquentSortable\\Test\\": "tests"
+ }
+ }
+}
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..9576993
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,29 @@
+
+
+
+
+ ./tests/
+
+
+
+
+ app/
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Sortable.php b/src/Sortable.php
new file mode 100644
index 0000000..325d3e7
--- /dev/null
+++ b/src/Sortable.php
@@ -0,0 +1,339 @@
+shouldSortWhenCreating()) {
+ $model->setHighestOrderValue();
+ }
+ });
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setHighestOrderValue(): void
+ {
+ $orderColumnName = $this->sortOrderColumnName();
+ $this->{$orderColumnName} = $this->getHighestOrderValue() + 1;
+ }
+
+ /**
+ * Determine the order value for the new record.
+ */
+ public function getHighestOrderValue(): int
+ {
+ return (int) $this->sortQuery()->max($this->sortOrderColumnName());
+ }
+
+ /**
+ * Determine the order value of a model at a specified Nth position.
+ *
+ * @param int $position The position of the model. Positions start at 1.
+ *
+ * @return int
+ */
+ public function getOrderValueAtPosition(int $position): int
+ {
+ $position--;
+ $position = max($position, 0);
+
+ return (int) $this->sortQuery()->orderBy($this->sortOrderColumnName())->skip($position)->limit(1)->value($this->sortOrderColumnName());
+ }
+
+ /**
+ * Provide an ordered scope.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @param string $direction
+ *
+ * @return \Illuminate\Database\Eloquent\Builder;
+ */
+ public function scopeOrdered(Builder $query, string $direction = 'asc'): Builder
+ {
+ $orderColumnName = $this->sortOrderColumnName();
+ $group_column = $this->sortOrderGroupColumnName();
+
+ if ($group_column) {
+ // Multiple Group Columns (array)
+ if (\is_array($group_column)) {
+ foreach ($group_column as $field) {
+ $query = $query->orderBy($field, $direction);
+ }
+ }
+
+ // Single Group Column
+ $query->orderBy($group_column, $direction);
+ }
+
+ return $query->orderBy($orderColumnName, $direction);
+ }
+
+ /**
+ * This function reorders the records: the record with the first id in the array
+ * will get order 1, the record with the second id will get order 2, ...
+ *
+ * A starting order number can be optionally supplied (defaults to 1).
+ *
+ * @param array|\ArrayAccess $ids
+ * @param int $startOrder
+ *
+ * @return void
+ */
+ public static function setNewOrder($ids, int $startOrder = 1): void
+ {
+ if (! \is_array($ids) && ! $ids instanceof ArrayAccess) {
+ throw new InvalidArgumentException('You must pass an array or ArrayAccess object to setNewOrder');
+ }
+
+ $model = new static;
+
+ $orderColumnName = $model->sortOrderColumnName();
+ $primaryKeyColumn = $model->getKeyName();
+
+ foreach ($ids as $id) {
+ static::withoutGlobalScope(SoftDeletingScope::class)
+ ->where($primaryKeyColumn, $id)
+ ->update([$orderColumnName => $startOrder++]);
+ }
+ }
+
+ /**
+ * Get the order column name.
+ */
+ protected function sortOrderColumnName(): string
+ {
+ return $this->sortable['order_column'] ?? 'display_order';
+ }
+
+ /**
+ * @return string|array|null
+ */
+ public function sortOrderGroupColumnName()
+ {
+ return $this->sortable['group_column'] ?? null;
+ }
+
+ /**
+ * Determine if the order column should be set when saving a new model instance.
+ */
+ public function shouldSortWhenCreating(): bool
+ {
+ return $this->sortable['sort_on_creating'] ?? true;
+ }
+
+ /**
+ * Swaps the order of this model with the model 'below' this model.
+ *
+ * @return $this
+ */
+ public function moveOrderDown(): self
+ {
+ $orderColumnName = $this->sortOrderColumnName();
+
+ $swapWithModel = $this->sortQuery()->limit(1)
+ ->ordered()
+ ->where($orderColumnName, '>', $this->{$orderColumnName})
+ ->first();
+
+ if (! $swapWithModel) {
+ return $this;
+ }
+
+ return $this->swapOrderWithModel($swapWithModel);
+ }
+
+ /**
+ * Swaps the order of this model with the model 'above' this model.
+ *
+ * @return $this
+ */
+ public function moveOrderUp(): self
+ {
+ $orderColumnName = $this->sortOrderColumnName();
+
+ $swapWithModel = $this->sortQuery()->limit(1)
+ ->ordered('desc')
+ ->where($orderColumnName, '<', $this->{$orderColumnName})
+ ->first();
+
+ if (! $swapWithModel) {
+ return $this;
+ }
+
+ return $this->swapOrderWithModel($swapWithModel);
+ }
+
+ /**
+ * Swap the order of this model with the order of another model.
+ *
+ * @param mixed $otherModel
+ *
+ * @return $this
+ */
+ public function swapOrderWithModel($otherModel): self
+ {
+ $orderColumnName = $this->sortOrderColumnName();
+
+ $oldOrderOfOtherModel = $otherModel->{$orderColumnName};
+
+ $otherModel->{$orderColumnName} = $this->{$orderColumnName};
+ $otherModel->save();
+
+ $this->{$orderColumnName} = $oldOrderOfOtherModel;
+ $this->save();
+
+ return $this;
+ }
+
+ /**
+ * Swap the order of two models.
+ *
+ * @param mixed $model
+ * @param mixed $otherModel
+ *
+ * @return void
+ */
+ public static function swapOrder($model, $otherModel): void
+ {
+ $model->swapOrderWithModel($otherModel);
+ }
+
+ /**
+ * Moves this model to the first position.
+ *
+ * @return $this
+ */
+ public function moveToStart(): self
+ {
+ $primary_key = $this->getKeyName();
+
+ $firstModel = $this->sortQuery()
+ ->limit(1)
+ ->ordered()
+ ->first();
+
+ if ($firstModel->{$primary_key} === $this->{$primary_key}) {
+ return $this;
+ }
+
+ $orderColumnName = $this->sortOrderColumnName();
+
+ $this->{$orderColumnName} = $firstModel->{$orderColumnName};
+ $this->save();
+
+ $this->sortQuery()->where($primary_key, '!=', $this->{$primary_key})->increment($orderColumnName);
+
+ return $this;
+ }
+
+ /**
+ * Moves this model to the last position.
+ *
+ * @return $this
+ */
+ public function moveToEnd(): self
+ {
+ $maxOrderValue = $this->getHighestOrderValue();
+ $orderColumnName = $this->sortOrderColumnName();
+ $primaryKey = $this->getKeyName();
+
+ if ($this->{$orderColumnName} === $maxOrderValue) {
+ return $this;
+ }
+
+ $oldOrder = $this->{$orderColumnName};
+
+ $this->{$orderColumnName} = $maxOrderValue;
+ $this->save();
+
+ $this->sortQuery()->where($primaryKey, '!=', $this->{$primaryKey})
+ ->where($orderColumnName, '>', $oldOrder)
+ ->decrement($orderColumnName);
+
+ return $this;
+ }
+
+ /**
+ * Move a model into a specified position
+ * Positions starts at 1. 0 would be the same as start.
+ *
+ * @param int $newPosition
+ *
+ * @return $this
+ */
+ public function moveToPosition(int $newPosition): self
+ {
+ $primaryKey = $this->getKeyName();
+ $orderColumnName = $this->sortOrderColumnName();
+ $newPosition = max($newPosition, 0);
+ $currentPosition = (int) $this->{$orderColumnName};
+ $orderAtPosition = $this->getOrderValueAtPosition($newPosition);
+
+ // No need to do anything, it is already in the correct position
+ if ($currentPosition === $newPosition) {
+ return $this;
+ }
+
+ if ($newPosition > $currentPosition) {
+ // The model is moving up
+ $this->sortQuery()->where([[$primaryKey, '!=', $this->{$primaryKey}], [$orderColumnName, '>', $currentPosition], [$orderColumnName, '<=', $orderAtPosition]])->decrement($orderColumnName);
+ } else {
+ // The model is moving down
+ $this->sortQuery()->where([[$primaryKey, '!=', $this->{$primaryKey}], [$orderColumnName, '<', $currentPosition], [$orderColumnName, '>=', $orderAtPosition]])->increment($orderColumnName);
+ }
+
+ $this->{$orderColumnName} = $orderAtPosition;
+ $this->save();
+
+ return $this;
+ }
+
+ /**
+ * Get eloquent builder for sortable.
+ *
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function sortQuery(): Builder
+ {
+ $group_column = $this->sortOrderGroupColumnName();
+
+ if ($group_column) {
+
+ /** @var Builder $query */
+ $query = static::query();
+
+ // Multiple Group Columns (array)
+ if (\is_array($group_column)) {
+ foreach ($group_column as $field) {
+ $query = $query->where($field, $this->{$field});
+ }
+ return $query;
+ }
+
+ // Single Group Column
+ return $query->where($group_column, $this->{$group_column});
+ }
+
+ // No group column
+ return static::query();
+ }
+}
diff --git a/tests/Dummy.php b/tests/Dummy.php
new file mode 100644
index 0000000..02a0d63
--- /dev/null
+++ b/tests/Dummy.php
@@ -0,0 +1,37 @@
+ 'display_order',
+ 'sort_on_creating' => true,
+ 'group_column' => 'group_id'
+ ];
+
+}
diff --git a/tests/DummyWithSoftDeletes.php b/tests/DummyWithSoftDeletes.php
new file mode 100644
index 0000000..5a7f353
--- /dev/null
+++ b/tests/DummyWithSoftDeletes.php
@@ -0,0 +1,16 @@
+assertEquals($dummy->name, $dummy->display_order);
+ }
+ }
+
+ /**
+ * @test
+ */
+ public function it_sets_the_display_order_on_creation_with_grouped_models(): void
+ {
+ $this->setUpGroups();
+
+ $all = DummyWithGroups::where('group_id', 2)->get()->all();
+
+ foreach ($all as $dummy) {
+ $this->assertEquals($dummy->name, $dummy->display_order);
+ }
+ }
+
+ /**
+ * @test
+ */
+ public function it_can_get_the_highest_sort_order_value(): void
+ {
+ $this->assertEquals(Dummy::all()->count(), (new Dummy())->getHighestOrderValue());
+ }
+
+ /**
+ * @test
+ */
+ public function it_can_get_the_highest_sort_order_value_with_grouped_models(): void
+ {
+ $this->setUpGroups();
+
+ $this->assertEquals(DummyWithGroups::where('group_id', 1)->count(), DummyWithGroups::make(['name'=>21, 'group_id'=>1])->getHighestOrderValue());
+ $this->assertEquals(DummyWithGroups::where('group_id', 2)->count(), DummyWithGroups::make(['name'=>21, 'group_id'=>2])->getHighestOrderValue());
+ $this->assertEquals(DummyWithGroups::where('group_id', 3)->count(), DummyWithGroups::make(['name'=>21, 'group_id'=>3])->getHighestOrderValue());
+ }
+
+ /**
+ * @test
+ */
+ public function it_can_get_the_order_number_at_a_specific_position(): void
+ {
+ $all = Dummy::all();
+
+ $first = $all->first();
+ $this->assertEquals($first->display_order, (new Dummy())->getOrderValueAtPosition(0));
+
+ $fourth = $all->slice(3, 1)->first();
+ $this->assertEquals($fourth->display_order, (new Dummy())->getOrderValueAtPosition(4));
+
+ $seventh = $all->slice(6, 1)->first();
+ $this->assertEquals($seventh->display_order, (new Dummy())->getOrderValueAtPosition(7));
+ }
+
+ /**
+ * @test
+ */
+ public function it_can_get_the_order_number_at_a_specific_position_with_grouped_models(): void
+ {
+
+ $this->setUpGroups();
+ $all = DummyWithGroups::all();
+
+ $first = $all->first();
+ $this->assertEquals($first->display_order, DummyWithGroups::make(['name'=>21, 'group_id'=>1])->getOrderValueAtPosition(0));
+
+ $fourth = $all->slice(3, 1)->first();
+ $this->assertEquals($fourth->display_order, DummyWithGroups::make(['name'=>21, 'group_id'=>1])->getOrderValueAtPosition(4));
+
+ $seventh = $all->slice(6, 1)->first();
+ $this->assertEquals($seventh->display_order, DummyWithGroups::make(['name'=>21, 'group_id'=>1])->getOrderValueAtPosition(7));
+
+ $first = $all->slice(20, 1)->first();
+ $this->assertEquals($first->display_order, DummyWithGroups::make(['name'=>21, 'group_id'=>2])->getOrderValueAtPosition(0));
+
+ $fourth = $all->slice(23, 1)->first();
+ $this->assertEquals($fourth->display_order, DummyWithGroups::make(['name'=>21, 'group_id'=>2])->getOrderValueAtPosition(4));
+
+ $seventh = $all->slice(26, 1)->first();
+ $this->assertEquals($seventh->display_order, DummyWithGroups::make(['name'=>21, 'group_id'=>2])->getOrderValueAtPosition(7));
+
+ $first = $all->slice(40, 1)->first();
+ $this->assertEquals($first->display_order, DummyWithGroups::make(['name'=>21, 'group_id'=>3])->getOrderValueAtPosition(0));
+
+ $fourth = $all->slice(43, 1)->first();
+ $this->assertEquals($fourth->display_order, DummyWithGroups::make(['name'=>21, 'group_id'=>3])->getOrderValueAtPosition(4));
+
+ $seventh = $all->slice(46, 1)->first();
+ $this->assertEquals($seventh->display_order, DummyWithGroups::make(['name'=>21, 'group_id'=>3])->getOrderValueAtPosition(7));
+ }
+
+ /**
+ * @test
+ */
+ public function it_provides_an_ordered_trait(): void
+ {
+ $i=1;
+
+ foreach (Dummy::ordered()->get()->pluck('display_order') as $order) {
+ $this->assertEquals($i++, $order);
+ }
+ }
+
+ /**
+ * @test
+ */
+ public function it_provides_an_ordered_trait_with_a_single__set_of_grouped_models(): void
+ {
+ $this->setUpGroups();
+
+ $i=1;
+
+ foreach (DummyWithGroups::where('group_id', 2)->ordered()->get()->pluck('display_order') as $order ) {
+ $this->assertEquals($i++, $order);
+ }
+
+ }
+
+ /**
+ * @test
+ */
+ public function it_provides_an_ordered_trait_with_multiple_sets_of_grouped_models(): void
+ {
+ $this->setUpGroups();
+
+ $i=1;
+ $group_id = 1;
+
+ foreach (DummyWithGroups::ordered()->get() as $order ) {
+ $this->assertEquals($i++, $order->display_order);
+ $this->assertEquals($group_id, $order->group_id);
+ if ( $i > 20 ){
+ $group_id++;
+ $i=1;
+ }
+ }
+
+ }
+
+ /**
+ * @test
+ */
+ public function it_can_get_the_highest_order_number_with_trashed_models(): void
+ {
+ $this->setUpSoftDeletes();
+
+ DummyWithSoftDeletes::first()->delete();
+
+ $this->assertEquals(DummyWithSoftDeletes::withTrashed()->count(), (new DummyWithSoftDeletes())->getHighestOrderValue());
+ }
+
+ /** @test */
+ public function it_can_get_the_order_number_at_a_specific_position_with_trashed_models(): void
+ {
+ $this->setUpSoftDeletes();
+ $all = DummyWithSoftDeletes::all();
+
+ $first = $all->first();
+ $this->assertEquals($first->display_order, (new Dummy())->getOrderValueAtPosition(1));
+
+ $fourth = $all->slice(3, 1)->first();
+ $this->assertEquals($fourth->display_order, (new Dummy())->getOrderValueAtPosition(4));
+
+ $seventh = $all->slice(6, 1)->first();
+ $this->assertEquals($seventh->display_order, (new Dummy())->getOrderValueAtPosition(7));
+ }
+
+ /** @test */
+ public function it_can_set_a_new_order(): void
+ {
+ $newOrder = Collection::make(Dummy::all()->pluck('id'))->shuffle()->toArray();
+
+ Dummy::setNewOrder($newOrder);
+
+ foreach (Dummy::orderBy('display_order')->get() as $i => $dummy) {
+ $this->assertEquals($newOrder[$i], $dummy->id);
+ }
+ }
+
+ /** @test */
+ public function it_can_set_a_new_order_from_collection(): void
+ {
+ $newOrder = Collection::make(Dummy::all()->pluck('id'))->shuffle();
+
+ Dummy::setNewOrder($newOrder);
+
+ foreach (Dummy::orderBy('display_order')->get() as $i => $dummy) {
+ $this->assertEquals($newOrder[$i], $dummy->id);
+ }
+ }
+
+ /** @test */
+ public function it_can_set_a_new_order_with_trashed_models(): void
+ {
+ $this->setUpSoftDeletes();
+
+ $dummies = DummyWithSoftDeletes::all();
+
+ $dummies->random()->delete();
+
+ $newOrder = Collection::make($dummies->pluck('id'))->shuffle();
+
+ DummyWithSoftDeletes::setNewOrder($newOrder);
+
+ foreach (DummyWithSoftDeletes::withTrashed()->orderBy('display_order')->get() as $i => $dummy) {
+ $this->assertEquals($newOrder[$i], $dummy->id);
+ }
+ }
+
+ /** @test */
+ public function it_can_set_a_new_order_without_trashed_models(): void
+ {
+ $this->setUpSoftDeletes();
+
+ DummyWithSoftDeletes::first()->delete();
+
+ $newOrder = Collection::make(DummyWithSoftDeletes::pluck('id'))->shuffle();
+
+ DummyWithSoftDeletes::setNewOrder($newOrder);
+
+ foreach (DummyWithSoftDeletes::orderBy('display_order')->get() as $i => $dummy) {
+ $this->assertEquals($newOrder[$i], $dummy->id);
+ }
+ }
+
+ /** @test */
+ public function it_will_determine_to_sort_when_creating_if_sortable_attribute_does_not_exist(): void
+ {
+ $model = new Dummy();
+
+ $this->assertTrue($model->shouldSortWhenCreating());
+ }
+
+ /** @test */
+ public function it_will_determine_to_sort_when_creating_if_sort_when_creating_setting_does_not_exist(): void
+ {
+ $model = new class extends Dummy {
+ public $sortable = [];
+ };
+
+ $this->assertTrue($model->shouldSortWhenCreating());
+ }
+
+ /** @test */
+ public function it_will_respect_the_sort_when_creating_setting(): void
+ {
+ $model = new class extends Dummy {
+ public $sortable = ['sort_on_creating' => true];
+ };
+
+ $this->assertTrue($model->shouldSortWhenCreating());
+
+ $model = new class extends Dummy {
+ public $sortable = ['sort_on_creating' => false];
+ };
+
+ $this->assertFalse($model->shouldSortWhenCreating());
+ }
+
+
+ /** @test */
+ public function it_can_move_the_order_down(): void
+ {
+ $firstModel = Dummy::find(3);
+ $secondModel = Dummy::find(4);
+
+ $this->assertEquals($firstModel->display_order, 3);
+ $this->assertEquals($secondModel->display_order, 4);
+
+ $this->assertNotFalse($firstModel->moveOrderDown());
+
+ $firstModel = Dummy::find(3);
+ $secondModel = Dummy::find(4);
+
+ $this->assertEquals($firstModel->display_order, 4);
+ $this->assertEquals($secondModel->display_order, 3);
+ }
+
+ /** @test */
+ public function it_will_not_fail_when_it_cant_move_the_order_down(): void
+ {
+ $lastModel = Dummy::all()->last();
+
+ $this->assertEquals($lastModel->display_order, 20);
+ $this->assertEquals($lastModel, $lastModel->moveOrderDown());
+ }
+
+ /** @test */
+ public function it_can_move_the_order_up(): void
+ {
+ $firstModel = Dummy::find(3);
+ $secondModel = Dummy::find(4);
+
+ $this->assertEquals($firstModel->display_order, 3);
+ $this->assertEquals($secondModel->display_order, 4);
+
+ $this->assertNotFalse($secondModel->moveOrderUp());
+
+ $firstModel = Dummy::find(3);
+ $secondModel = Dummy::find(4);
+
+ $this->assertEquals($firstModel->display_order, 4);
+ $this->assertEquals($secondModel->display_order, 3);
+ }
+
+ /** @test */
+ public function it_will_not_break_when_it_cant_move_the_order_up(): void
+ {
+ $lastModel = Dummy::first();
+
+ $this->assertEquals($lastModel->display_order, 1);
+ $this->assertEquals($lastModel, $lastModel->moveOrderUp());
+ }
+
+ /** @test */
+ public function it_can_swap_the_position_of_two_given_models(): void
+ {
+ $firstModel = Dummy::find(3);
+ $secondModel = Dummy::find(4);
+
+ $this->assertEquals($firstModel->display_order, 3);
+ $this->assertEquals($secondModel->display_order, 4);
+
+ Dummy::swapOrder($firstModel, $secondModel);
+
+ $this->assertEquals($firstModel->display_order, 4);
+ $this->assertEquals($secondModel->display_order, 3);
+ }
+
+ /** @test */
+ public function it_can_swap_itself_with_another_model(): void
+ {
+ $firstModel = Dummy::find(3);
+ $secondModel = Dummy::find(4);
+
+ $this->assertEquals($firstModel->display_order, 3);
+ $this->assertEquals($secondModel->display_order, 4);
+
+ $firstModel->swapOrderWithModel($secondModel);
+
+ $this->assertEquals($firstModel->display_order, 4);
+ $this->assertEquals($secondModel->display_order, 3);
+ }
+
+ /** @test */
+ public function it_can_move_a_model_to_the_first_place(): void
+ {
+ $position = 3;
+
+ $oldModels = Dummy::whereNot('id', $position)->get();
+
+ $model = Dummy::find($position);
+
+ $this->assertEquals(3, $model->display_order);
+
+ $model = $model->moveToStart();
+
+ $this->assertEquals(1, $model->display_order);
+
+ $oldModels = $oldModels->pluck('display_order', 'id');
+ $newModels = Dummy::whereNot('id', $position)->get()->pluck('display_order', 'id');
+
+ foreach ($oldModels as $key => $oldModel) {
+ $this->assertEquals($oldModel + 1, $newModels[$key]);
+ }
+ }
+
+ /**
+ * @test
+ */
+ public function it_can_move_a_model_to_the_last_place(): void
+ {
+ $position = 3;
+
+ $oldModels = Dummy::whereNot('id', $position)->get();
+
+ $model = Dummy::find($position);
+
+ $this->assertNotEquals(20, $model->display_order);
+
+ $model = $model->moveToEnd();
+
+ $this->assertEquals(20, $model->display_order);
+
+ $oldModels = $oldModels->pluck('display_order', 'id');
+
+ $newModels = Dummy::whereNot('id', $position)->get()->pluck('display_order', 'id');
+
+ foreach ($oldModels as $key => $order) {
+ if ($order > $position) {
+ $this->assertEquals($order - 1, $newModels[$key]);
+ } else {
+ $this->assertEquals($order, $newModels[$key]);
+ }
+ }
+ }
+
+ /**
+ * @test
+ */
+ public function it_can_move_a_model_to_a_specified_position(): void
+ {
+ // Move the model up
+ $originalPosition = 3;
+ $newPosition = 7;
+
+ $model = Dummy::find($originalPosition);
+ $modelAtPosition = Dummy::find($newPosition);
+
+ $this->assertEquals($originalPosition, $model->display_order);
+ $this->assertEquals($newPosition, $modelAtPosition->display_order);
+
+ $model = $model->moveToPosition($newPosition);
+
+ $this->assertEquals($newPosition, $model->display_order);
+
+ $modelAtPosition->refresh();
+ $this->assertEquals($newPosition - 1, $modelAtPosition->display_order);
+
+ $all = Dummy::all()->sortBy('display_order');
+ $all->values()->all();
+ $count = $all->where('display_order', '<=', $newPosition)->count();
+ $this->assertEquals($newPosition, $count);
+
+ $counter = 1;
+ foreach ($all as $m) {
+ $this->assertEquals($m->display_order, $counter);
+ $counter++;
+ }
+
+ // Move the model down
+ $originalPosition = 10;
+ $newPosition = 2;
+
+ $model = Dummy::find($originalPosition);
+ $modelAtPosition = Dummy::find($newPosition);
+
+ $this->assertEquals($originalPosition, $model->display_order);
+ $this->assertEquals($newPosition, $modelAtPosition->display_order);
+
+ $model = $model->moveToPosition($newPosition);
+
+ $this->assertEquals($newPosition, $model->display_order);
+
+ $modelAtPosition->refresh();
+ $this->assertEquals($newPosition + 1, $modelAtPosition->display_order);
+
+ $all = Dummy::all()->sortBy('display_order');
+ $all->values()->all();
+ $count = $all->where('display_order', '<=', $newPosition)->count();
+ $this->assertEquals($newPosition, $count);
+
+ $counter = 1;
+ foreach ($all as $m) {
+ $this->assertEquals($m->display_order, $counter);
+ $counter++;
+ }
+ }
+
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 0000000..7216649
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,95 @@
+setUpDatabase();
+ }
+
+ /**
+ * @param \Illuminate\Foundation\Application $app
+ */
+ protected function getEnvironmentSetUp($app)
+ {
+ $app['config']->set('database.default', 'sqlite');
+ $app['config']->set('database.connections.sqlite', [
+ 'driver' => 'sqlite',
+ 'database' => ':memory:',
+ 'prefix' => '',
+ ]);
+ }
+
+ /**
+ * Database Migrations
+ *
+ * @return void
+ */
+ protected function setUpDatabase(): void
+ {
+ $this->app['db']->connection()->getSchemaBuilder()->create('dummies', function (Blueprint $table) {
+ $table->increments('id');
+ $table->string('name');
+ $table->integer('group_id', false, true);
+ $table->integer('display_order');
+ });
+
+ collect(range(1, 20))->each(function (int $i) {
+ Dummy::create([
+ 'name' => $i,
+ 'display_order' => $i,
+ 'group_id' => 1,
+ ]);
+ });
+
+ }
+
+ /**
+ * Add soft deletes to dummy data
+ */
+ protected function setUpSoftDeletes(): void
+ {
+ $this->app['db']->connection()->getSchemaBuilder()->table('dummies', function (Blueprint $table) {
+ $table->softDeletes();
+ });
+ }
+
+ /**
+ * Add group row to dummy data
+ */
+ protected function setUpGroups(): void
+ {
+
+ collect(range(1, 20))->each(function (int $i=1) {
+ DummyWithGroups::create([
+ 'name' => $i,
+ 'display_order' => $i,
+ 'group_id' => 2,
+ ]);
+ });
+
+ collect(range(1, 20))->each(function (int $i=1) {
+ DummyWithGroups::create([
+ 'name' => $i,
+ 'display_order' => $i,
+ 'group_id' => 3,
+ ]);
+ });
+
+ }
+
+}