From e97444507f2d872e7eaee7d648a5f57c994ca5b0 Mon Sep 17 00:00:00 2001 From: Metrix Information Solutions Date: Sun, 14 Oct 2018 13:36:59 -0400 Subject: [PATCH] Initial Commit --- .gitignore | 3 + LICENSE.md | 22 ++ README.md | 176 ++++++++++++ bin/test.sh | 16 ++ build/phpcs.xml | 30 +++ build/phpmd.xml | 29 ++ composer.json | 48 ++++ phpunit.xml | 29 ++ src/Sortable.php | 339 +++++++++++++++++++++++ tests/Dummy.php | 37 +++ tests/DummyWithGroups.php | 48 ++++ tests/DummyWithSoftDeletes.php | 16 ++ tests/SortableTest.php | 476 +++++++++++++++++++++++++++++++++ tests/TestCase.php | 95 +++++++ 14 files changed, 1364 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 README.md create mode 100755 bin/test.sh create mode 100644 build/phpcs.xml create mode 100644 build/phpmd.xml create mode 100644 composer.json create mode 100644 phpunit.xml create mode 100644 src/Sortable.php create mode 100644 tests/Dummy.php create mode 100644 tests/DummyWithGroups.php create mode 100644 tests/DummyWithSoftDeletes.php create mode 100644 tests/SortableTest.php create mode 100644 tests/TestCase.php 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, + ]); + }); + + } + +}