From 3422a64681de6e14e707e9482c5d333e53215194 Mon Sep 17 00:00:00 2001 From: "Gustavo R. Gentil" Date: Fri, 31 Jul 2020 18:15:10 -0300 Subject: [PATCH] Sequencing strategy (#15) * feat: ensure the strategy can be set to always * test: ensure the strategy can be set to always * feat: ensure the strategy can be set to on create * test: ensure the strategy can be set to on create * feat: ensure the strategy can be set to on update * test: ensure the strategy can be set to on update * feat: ensure the strategy can be set to never * test: ensure the strategy can be set to never * refactor: move commands tests to a different folder * refactor: set last value equal to the max value in the sequence * refactor: group withoutSequencing tests * refactor: rename tests * refactor: rename test classes * feat: add strategy config variable * test: ensure strategy set to always works on create * test: ensure strategy set to always works on delete * test: ensure strategy set to always works on update * feat: do nothing if strategy is set to never * test: ensure strategy set to never works on create * feat: ensure it does update the sequence on delete if strategy is set to never * test: ensure strategy set to never works on delete * feat: ensure strategy set to never works on update * test: ensure strategy set to never works on update * refactor: move strategy check to another method * feat: ensure strategy set on_create and on_update work * test: ensure strategy set on_create and on_update work * feat: ensure on_create and on_update disable sequencing for delete operations * test: ensure on_create and on_update disable sequencing for delete operations * refactor: accept array of strategies as param * docs: add docblock * fix: rename test class * refactor: rename test method * refactor: remove comparison negation * test: change null assertion to equal * feat: ensure the next value is greater than the max * refactor: rename method * docs: add comments to config file * docs: update README * docs: update README * fix: group class name uppercase --- README.md | 5 +- config/config.php | 13 ++++ src/SequencingStrategy.php | 11 ++++ src/Traits/Sequenceable.php | 51 +++++++++++++-- .../FlushSequenceValuesCommandTest.php | 6 +- .../PopulateSequenceValuesCommandTest.php | 10 +-- .../DeleteObjectWithoutSequencingTest.php | 23 ------- ...tedTest.php => DeleteSequenceableTest.php} | 2 +- ... SequenceValueOutOfBoundsOnCreateTest.php} | 2 +- ... SequenceValueOutOfBoundsOnUpdateTest.php} | 4 +- tests/Unit/SequencingStrategyTest.php | 33 ++++++++++ tests/Unit/StrategyAlwaysTest.php | 62 +++++++++++++++++++ tests/Unit/StrategyNeverTest.php | 62 +++++++++++++++++++ tests/Unit/StrategyOnCreateTest.php | 62 +++++++++++++++++++ tests/Unit/StrategyOnUpdateTest.php | 62 +++++++++++++++++++ ...cingTest.php => WithoutSequencingTest.php} | 20 +++++- 16 files changed, 384 insertions(+), 44 deletions(-) create mode 100644 src/SequencingStrategy.php rename tests/Unit/{ => Commands}/FlushSequenceValuesCommandTest.php (93%) rename tests/Unit/{ => Commands}/PopulateSequenceValuesCommandTest.php (90%) delete mode 100644 tests/Unit/DeleteObjectWithoutSequencingTest.php rename tests/Unit/{RearrangeSequenceWhenAnObjectIsDeletedTest.php => DeleteSequenceableTest.php} (97%) rename tests/Unit/{CreateObjectWithSequenceValueOutOfBoundsTest.php => SequenceValueOutOfBoundsOnCreateTest.php} (96%) rename tests/Unit/{UpdateObjectWithSequenceValueOutOfBoundsTest.php => SequenceValueOutOfBoundsOnUpdateTest.php} (96%) create mode 100644 tests/Unit/SequencingStrategyTest.php create mode 100644 tests/Unit/StrategyAlwaysTest.php create mode 100644 tests/Unit/StrategyNeverTest.php create mode 100644 tests/Unit/StrategyOnCreateTest.php create mode 100644 tests/Unit/StrategyOnUpdateTest.php rename tests/Unit/{UpdateObjectWithoutSequencingTest.php => WithoutSequencingTest.php} (71%) diff --git a/README.md b/README.md index 20ccdc3..19013ef 100644 --- a/README.md +++ b/README.md @@ -20,15 +20,18 @@ php artisan vendor:publish --provider="Gurgentil\LaravelEloquentSequencer\Larave ### Configuration parameters -You can change the default colum name and the initial value for the sequences in `config/eloquentsequencer.php`: +You can change the default colum name, the initial value and the sequencing strategy in `config/eloquentsequencer.php`: ```php return [ 'column_name' => 'position', 'initial_value' => 1, + 'strategy' => 'always', ]; ``` +The `strategy` configuration determines when sequencing should be triggered and accepts one of the following values: `always`, `on_create`, `on_update` or `never`. + ### Model configuration The `$sequenceable` attribute specifies the sequence column name for the model: diff --git a/config/config.php b/config/config.php index 201d046..3045feb 100644 --- a/config/config.php +++ b/config/config.php @@ -1,6 +1,19 @@ 'position', + + /* + * The value sequences should start at. + */ 'initial_value' => 1, + + /* + * Determines when models should be sequenced. + * Possible values: always|never|on_create|on_update + */ + 'strategy' => 'always', ]; diff --git a/src/SequencingStrategy.php b/src/SequencingStrategy.php new file mode 100644 index 0000000..168a703 --- /dev/null +++ b/src/SequencingStrategy.php @@ -0,0 +1,11 @@ +getSequenceValue(); + if (static::strategyIn([ + SequencingStrategy::NEVER, + SequencingStrategy::ON_UPDATE, + ])) { + return; + } + if (is_null($value)) { $this->{static::getSequenceColumnName()} = $this->getNextSequenceValue(); } @@ -76,6 +84,13 @@ protected function handleSequenceableCreate(): void */ protected function handleSequenceableUpdate(): void { + if (static::strategyIn([ + SequencingStrategy::NEVER, + SequencingStrategy::ON_CREATE, + ])) { + return; + } + if (! $this->shouldBeSequenced) { $this->shouldBeSequenced = true; @@ -84,7 +99,7 @@ protected function handleSequenceableUpdate(): void $value = $this->getSequenceValue(); - if (! $this->isDirty(static::getSequenceColumnName()) || is_null($value)) { + if ($this->isClean(static::getSequenceColumnName()) || is_null($value)) { return; } @@ -102,6 +117,14 @@ protected function handleSequenceableUpdate(): void */ protected function handleSequenceableDelete(): void { + if (static::strategyIn([ + SequencingStrategy::NEVER, + SequencingStrategy::ON_CREATE, + SequencingStrategy::ON_UPDATE, + ])) { + return; + } + if (! $this->shouldBeSequenced) { $this->shouldBeSequenced = true; @@ -116,6 +139,17 @@ protected function handleSequenceableDelete(): void static::decrementSequenceValues($objects); } + /** + * Determine if strategy is in array. + * + * @param array $strategies + * @return bool + */ + protected static function strategyIn(array $strategies): bool + { + return in_array(config('eloquentsequencer.strategy'), $strategies); + } + /** * Determine if new sequence value is out of bounds. * @@ -279,11 +313,13 @@ public static function getSequenceColumnName(): string /** * Get sequence value of the last model in the sequence. * - * @return int + * @return int|null */ - protected function getLastSequenceValue(): int + protected function getLastSequenceValue(): ?int { - return $this->getNextSequenceValue() - 1; + $column = static::getSequenceColumnName(); + + return $this->getSequence()->max($column); } /** @@ -293,7 +329,12 @@ protected function getLastSequenceValue(): int */ public function getNextSequenceValue(): int { - return static::getInitialSequenceValue() + $this->getSequence()->count(); + $column = static::getSequenceColumnName(); + $maxSequenceValue = $this->getSequence()->max($column); + + return $this->getSequence()->count() === 0 + ? static::getInitialSequenceValue() + : $maxSequenceValue + 1; } /** diff --git a/tests/Unit/FlushSequenceValuesCommandTest.php b/tests/Unit/Commands/FlushSequenceValuesCommandTest.php similarity index 93% rename from tests/Unit/FlushSequenceValuesCommandTest.php rename to tests/Unit/Commands/FlushSequenceValuesCommandTest.php index 67c7212..45a60e8 100644 --- a/tests/Unit/FlushSequenceValuesCommandTest.php +++ b/tests/Unit/Commands/FlushSequenceValuesCommandTest.php @@ -1,6 +1,6 @@ create(); + $group = Factory::of('Group')->create(); $items = Factory::of('Item')->times(4)->create(['group_id' => $group->id]); @@ -48,7 +48,7 @@ public function the_flush_command_does_not_update_values_that_are_already_null() /** @test */ public function the_flush_command_flushes_sequence_values() { - $group = Factory::of('group')->create(); + $group = Factory::of('Group')->create(); $firstItem = Factory::of('Item')->create(['group_id' => $group->id]); $secondItem = Factory::of('Item')->create(['group_id' => $group->id]); diff --git a/tests/Unit/PopulateSequenceValuesCommandTest.php b/tests/Unit/Commands/PopulateSequenceValuesCommandTest.php similarity index 90% rename from tests/Unit/PopulateSequenceValuesCommandTest.php rename to tests/Unit/Commands/PopulateSequenceValuesCommandTest.php index 14e199b..6b88a47 100644 --- a/tests/Unit/PopulateSequenceValuesCommandTest.php +++ b/tests/Unit/Commands/PopulateSequenceValuesCommandTest.php @@ -1,6 +1,6 @@ create(); + $group = Factory::of('Group')->create(); $items = Factory::of('Item') ->times(4) @@ -48,7 +48,7 @@ public function the_populate_command_does_not_update_values_that_are_already_ass /** @test */ public function the_populate_command_populates_every_empty_sequence_value() { - $group = Factory::of('group')->create(); + $group = Factory::of('Group')->create(); $firstItem = Factory::of('Item')->create(['group_id' => $group->id]); $secondItem = Factory::of('Item')->create(['group_id' => $group->id]); @@ -58,7 +58,7 @@ public function the_populate_command_populates_every_empty_sequence_value() $thirdItem->update(['position' => null]); - $this->assertNotNull($firstItem->position); + $this->assertEquals(1, $firstItem->refresh()->position); $this->assertNull($secondItem->position); $this->assertNull($thirdItem->position); diff --git a/tests/Unit/DeleteObjectWithoutSequencingTest.php b/tests/Unit/DeleteObjectWithoutSequencingTest.php deleted file mode 100644 index fa2aaf2..0000000 --- a/tests/Unit/DeleteObjectWithoutSequencingTest.php +++ /dev/null @@ -1,23 +0,0 @@ -create(); - - $firstItem = Factory::of('Item')->create(['group_id' => $group->id]); - $secondItem = Factory::of('Item')->create(['group_id' => $group->id]); - - $firstItem->withoutSequencing() - ->delete(); - - $this->assertEquals(2, $secondItem->refresh()->position); - } -} diff --git a/tests/Unit/RearrangeSequenceWhenAnObjectIsDeletedTest.php b/tests/Unit/DeleteSequenceableTest.php similarity index 97% rename from tests/Unit/RearrangeSequenceWhenAnObjectIsDeletedTest.php rename to tests/Unit/DeleteSequenceableTest.php index 1dbb1be..9f3dd41 100644 --- a/tests/Unit/RearrangeSequenceWhenAnObjectIsDeletedTest.php +++ b/tests/Unit/DeleteSequenceableTest.php @@ -5,7 +5,7 @@ use Facades\Gurgentil\LaravelEloquentSequencer\Tests\Factories\Factory; use Gurgentil\LaravelEloquentSequencer\Tests\TestCase; -class RearrangeSequenceWhenAnObjectIsDeletedTest extends TestCase +class DeleteSequenceableTest extends TestCase { /** @test */ public function it_assigns_1_to_the_second_object_and_two_to_the_third_one_when_the_first_one_is_deleted() diff --git a/tests/Unit/CreateObjectWithSequenceValueOutOfBoundsTest.php b/tests/Unit/SequenceValueOutOfBoundsOnCreateTest.php similarity index 96% rename from tests/Unit/CreateObjectWithSequenceValueOutOfBoundsTest.php rename to tests/Unit/SequenceValueOutOfBoundsOnCreateTest.php index 172bc76..a9adbf4 100644 --- a/tests/Unit/CreateObjectWithSequenceValueOutOfBoundsTest.php +++ b/tests/Unit/SequenceValueOutOfBoundsOnCreateTest.php @@ -6,7 +6,7 @@ use Gurgentil\LaravelEloquentSequencer\Exceptions\SequenceValueOutOfBoundsException; use Gurgentil\LaravelEloquentSequencer\Tests\TestCase; -class CreateObjectWithSequenceValueOutOfBoundsTest extends TestCase +class SequenceValueOutOfBoundsOnCreateTest extends TestCase { /** @test */ public function it_throws_an_exception_when_the_object_created_has_a_sequence_value_that_is_negative() diff --git a/tests/Unit/UpdateObjectWithSequenceValueOutOfBoundsTest.php b/tests/Unit/SequenceValueOutOfBoundsOnUpdateTest.php similarity index 96% rename from tests/Unit/UpdateObjectWithSequenceValueOutOfBoundsTest.php rename to tests/Unit/SequenceValueOutOfBoundsOnUpdateTest.php index 2d86d5c..7c239a8 100644 --- a/tests/Unit/UpdateObjectWithSequenceValueOutOfBoundsTest.php +++ b/tests/Unit/SequenceValueOutOfBoundsOnUpdateTest.php @@ -6,7 +6,7 @@ use Gurgentil\LaravelEloquentSequencer\Exceptions\SequenceValueOutOfBoundsException; use Gurgentil\LaravelEloquentSequencer\Tests\TestCase; -class UpdateObjectWithSequenceValueOutOfBoundsText extends TestCase +class SequenceValueOutOfBoundsOnUpdateTest extends TestCase { /** @test */ public function it_throws_an_exception_when_the_object_updated_has_a_sequence_value_that_is_negative() @@ -72,7 +72,7 @@ public function it_throws_an_exception_when_the_updated_object_has_a_sequence_va $this->expectException(SequenceValueOutOfBoundsException::class); - $item->update(['position' => 3]); + $item->update(['position' => 4]); } /** @test */ diff --git a/tests/Unit/SequencingStrategyTest.php b/tests/Unit/SequencingStrategyTest.php new file mode 100644 index 0000000..0b635e2 --- /dev/null +++ b/tests/Unit/SequencingStrategyTest.php @@ -0,0 +1,33 @@ +assertEquals('always', SequencingStrategy::ALWAYS); + } + + /** @test **/ + public function it_can_be_set_to_on_create() + { + $this->assertEquals('on_create', SequencingStrategy::ON_CREATE); + } + + /** @test **/ + public function it_can_be_set_to_on_update() + { + $this->assertEquals('on_update', SequencingStrategy::ON_UPDATE); + } + + /** @test **/ + public function it_can_be_set_to_never() + { + $this->assertEquals('never', SequencingStrategy::NEVER); + } +} diff --git a/tests/Unit/StrategyAlwaysTest.php b/tests/Unit/StrategyAlwaysTest.php new file mode 100644 index 0000000..a2f4b56 --- /dev/null +++ b/tests/Unit/StrategyAlwaysTest.php @@ -0,0 +1,62 @@ + SequencingStrategy::ALWAYS]); + + $group = Factory::of('Group')->create(); + + $firstItem = Factory::of('Item')->create(['group_id' => $group->id]); + + $this->assertEquals(1, $firstItem->refresh()->position); + } + + /** + * @test + * @group Strategy + */ + public function strategy_set_to_always_works_on_update() + { + $group = Factory::of('group')->create(); + + $firstItem = Factory::of('item')->create(['group_id' => $group->id]); + $secondItem = Factory::of('item')->create(['group_id' => $group->id]); + + config(['eloquentsequencer.strategy' => SequencingStrategy::ALWAYS]); + + $secondItem->update(['position' => 1]); + + $this->assertEquals(2, $firstItem->refresh()->position); + $this->assertEquals(1, $secondItem->refresh()->position); + } + + /** + * @test + * @group Strategy + */ + public function strategy_set_to_always_works_on_delete() + { + $group = Factory::of('Group')->create(); + + $firstItem = Factory::of('Item')->create(['group_id' => $group->id]); + $secondItem = Factory::of('Item')->create(['group_id' => $group->id]); + + config(['eloquentsequencer.strategy' => SequencingStrategy::ALWAYS]); + + $firstItem->delete(); + + $this->assertEquals(1, $secondItem->refresh()->position); + } +} diff --git a/tests/Unit/StrategyNeverTest.php b/tests/Unit/StrategyNeverTest.php new file mode 100644 index 0000000..1c7c982 --- /dev/null +++ b/tests/Unit/StrategyNeverTest.php @@ -0,0 +1,62 @@ + SequencingStrategy::NEVER]); + + $group = Factory::of('Group')->create(); + + $firstItem = Factory::of('Item')->create(['group_id' => $group->id]); + + $this->assertNull($firstItem->refresh()->position); + } + + /** + * @test + * @group Strategy + */ + public function strategy_set_to_never_works_on_update() + { + $group = Factory::of('group')->create(); + + $firstItem = Factory::of('item')->create(['group_id' => $group->id]); + $secondItem = Factory::of('item')->create(['group_id' => $group->id]); + + config(['eloquentsequencer.strategy' => SequencingStrategy::NEVER]); + + $secondItem->update(['position' => 1]); + + $this->assertEquals(1, $firstItem->refresh()->position); + $this->assertEquals(1, $secondItem->refresh()->position); + } + + /** + * @test + * @group Strategy + */ + public function strategy_set_to_never_works_on_delete() + { + $group = Factory::of('Group')->create(); + + $firstItem = Factory::of('Item')->create(['group_id' => $group->id]); + $secondItem = Factory::of('Item')->create(['group_id' => $group->id]); + + config(['eloquentsequencer.strategy' => SequencingStrategy::NEVER]); + + $firstItem->delete(); + + $this->assertEquals(2, $secondItem->refresh()->position); + } +} diff --git a/tests/Unit/StrategyOnCreateTest.php b/tests/Unit/StrategyOnCreateTest.php new file mode 100644 index 0000000..1151927 --- /dev/null +++ b/tests/Unit/StrategyOnCreateTest.php @@ -0,0 +1,62 @@ + SequencingStrategy::ON_CREATE]); + + $group = Factory::of('Group')->create(); + + $firstItem = Factory::of('Item')->create(['group_id' => $group->id]); + + $this->assertEquals(1, $firstItem->refresh()->position); + } + + /** + * @test + * @group Strategy + */ + public function strategy_set_to_on_create_disables_sequencing_on_update() + { + $group = Factory::of('group')->create(); + + $firstItem = Factory::of('item')->create(['group_id' => $group->id]); + $secondItem = Factory::of('item')->create(['group_id' => $group->id]); + + config(['eloquentsequencer.strategy' => SequencingStrategy::ON_CREATE]); + + $secondItem->update(['position' => 1]); + + $this->assertEquals(1, $firstItem->refresh()->position); + $this->assertEquals(1, $secondItem->refresh()->position); + } + + /** + * @test + * @group Strategy + */ + public function strategy_set_to_on_create_disables_sequencing_on_delete() + { + $group = Factory::of('Group')->create(); + + $firstItem = Factory::of('Item')->create(['group_id' => $group->id]); + $secondItem = Factory::of('Item')->create(['group_id' => $group->id]); + + config(['eloquentsequencer.strategy' => SequencingStrategy::ON_CREATE]); + + $firstItem->delete(); + + $this->assertEquals(2, $secondItem->refresh()->position); + } +} diff --git a/tests/Unit/StrategyOnUpdateTest.php b/tests/Unit/StrategyOnUpdateTest.php new file mode 100644 index 0000000..083bda6 --- /dev/null +++ b/tests/Unit/StrategyOnUpdateTest.php @@ -0,0 +1,62 @@ +create(); + + $firstItem = Factory::of('item')->create(['group_id' => $group->id]); + $secondItem = Factory::of('item')->create(['group_id' => $group->id]); + + config(['eloquentsequencer.strategy' => SequencingStrategy::ON_UPDATE]); + + $secondItem->update(['position' => 1]); + + $this->assertEquals(2, $firstItem->refresh()->position); + $this->assertEquals(1, $secondItem->refresh()->position); + } + + /** + * @test + * @group Strategy + */ + public function strategy_set_to_on_update_disables_sequencing_on_create() + { + config(['eloquentsequencer.strategy' => SequencingStrategy::ON_UPDATE]); + + $group = Factory::of('Group')->create(); + + $firstItem = Factory::of('Item')->create(['group_id' => $group->id]); + + $this->assertNull($firstItem->refresh()->position); + } + + /** + * @test + * @group Strategy + */ + public function strategy_set_to_on_update_disables_sequencing_on_delete() + { + $group = Factory::of('Group')->create(); + + $firstItem = Factory::of('Item')->create(['group_id' => $group->id]); + $secondItem = Factory::of('Item')->create(['group_id' => $group->id]); + + config(['eloquentsequencer.strategy' => SequencingStrategy::ON_UPDATE]); + + $firstItem->delete(); + + $this->assertEquals(2, $secondItem->refresh()->position); + } +} diff --git a/tests/Unit/UpdateObjectWithoutSequencingTest.php b/tests/Unit/WithoutSequencingTest.php similarity index 71% rename from tests/Unit/UpdateObjectWithoutSequencingTest.php rename to tests/Unit/WithoutSequencingTest.php index 6b8257b..9657ed4 100644 --- a/tests/Unit/UpdateObjectWithoutSequencingTest.php +++ b/tests/Unit/WithoutSequencingTest.php @@ -5,10 +5,24 @@ use Facades\Gurgentil\LaravelEloquentSequencer\Tests\Factories\Factory; use Gurgentil\LaravelEloquentSequencer\Tests\TestCase; -class UpdateObjectWithoutSequencingTest extends TestCase +class WithoutSequencingTest extends TestCase { /** @test */ - public function sequencing_may_be_disabled_for_updates() + public function without_sequencing_works_on_deletes() + { + $group = Factory::of('Group')->create(); + + $firstItem = Factory::of('Item')->create(['group_id' => $group->id]); + $secondItem = Factory::of('Item')->create(['group_id' => $group->id]); + + $firstItem->withoutSequencing() + ->delete(); + + $this->assertEquals(2, $secondItem->refresh()->position); + } + + /** @test */ + public function without_sequencing_works_on_updates() { $group = Factory::of('Group')->create(); @@ -23,7 +37,7 @@ public function sequencing_may_be_disabled_for_updates() } /** @test */ - public function sequencing_may_be_disabled_for_only_the_following_update() + public function without_sequencing_should_not_affect_following_operations() { $group = Factory::of('Group')->create();