diff --git a/docs/working-with-transitions/01-configuring-transitions.md b/docs/working-with-transitions/01-configuring-transitions.md index 7ae0ddf..b5ac449 100644 --- a/docs/working-with-transitions/01-configuring-transitions.md +++ b/docs/working-with-transitions/01-configuring-transitions.md @@ -44,7 +44,37 @@ Transitions can then be used like so: $payment->state->transitionTo(Paid::class); ``` -This line will only work when a valid transition was configured. If the initial state of `$payment` already was `Paid`, a `\Spatie\ModelStates\Exceptions\TransitionNotFound` will be thrown instead of changing the state. +This line will only work when a valid transition was configured. If the initial state of `$payment` already was `Paid`, a `\Spatie\ModelStates\Exceptions\TransitionNotFound` will be thrown instead of changing the state. + +## Ignoring same state transitions + +In some cases you may want to handle transition to same state without manually setting `allowTransition`, you can call `ignoreSameState` + +Please note that the `StateChanged` event will fire anyway. + +```php +abstract class PaymentState extends State +{ + // … + + public static function config(): StateConfig + { + return parent::config() + ->ignoreSameState() + ->allowTransition([Created::class, Pending::class], Failed::class, ToFailed::class); + } +} +``` + +It also works with `IgnoreSameState` Attribute + +```php +#[IgnoreSameState] +abstract class PaymentState extends State +{ + //... +} +``` ## Allow multiple transitions at once diff --git a/src/Attributes/AttributeLoader.php b/src/Attributes/AttributeLoader.php index 61f140f..14ad204 100644 --- a/src/Attributes/AttributeLoader.php +++ b/src/Attributes/AttributeLoader.php @@ -35,6 +35,12 @@ public function load(StateConfig $stateConfig): StateConfig $stateConfig->default($defaultStateAttribute->defaultStateClass); } + + if ($this->reflectionClass->getAttributes(IgnoreSameState::class)[0] ?? null) { + /** @var \Spatie\ModelStates\Attributes\IgnoreSameState $transitionAttribute */ + + $stateConfig->ignoreSameState(); + } $registerStateAttributes = $this->reflectionClass->getAttributes(RegisterState::class); diff --git a/src/Attributes/IgnoreSameState.php b/src/Attributes/IgnoreSameState.php new file mode 100644 index 0000000..ee53051 --- /dev/null +++ b/src/Attributes/IgnoreSameState.php @@ -0,0 +1,8 @@ +shouldIgnoreSameState = true; + + return $this; + } + public function allowTransition($from, string $to, ?string $transition = null): StateConfig { if (is_array($from)) { @@ -72,6 +82,10 @@ public function allowTransitions(array $transitions): StateConfig public function isTransitionAllowed(string $fromMorphClass, string $toMorphClass): bool { + if($this->shouldIgnoreSameState && $fromMorphClass === $toMorphClass){ + return true; + } + $transitionKey = $this->createTransitionKey($fromMorphClass, $toMorphClass); return array_key_exists($transitionKey, $this->allowedTransitions); @@ -81,7 +95,11 @@ public function resolveTransitionClass(string $fromMorphClass, string $toMorphCl { $transitionKey = $this->createTransitionKey($fromMorphClass, $toMorphClass); - return $this->allowedTransitions[$transitionKey]; + if(array_key_exists($transitionKey, $this->allowedTransitions)) { + return $this->allowedTransitions[$transitionKey]; + } + + return null; } public function transitionableStates(string $fromMorphClass): array diff --git a/tests/Dummy/IgnoreSameStateModelState/IgnoreSameStateModelAttributeState.php b/tests/Dummy/IgnoreSameStateModelState/IgnoreSameStateModelAttributeState.php new file mode 100644 index 0000000..fce83d1 --- /dev/null +++ b/tests/Dummy/IgnoreSameStateModelState/IgnoreSameStateModelAttributeState.php @@ -0,0 +1,17 @@ +ignoreSameState() + ->allowTransition(IgnoreSameStateModelStateA::class, IgnoreSameStateModelStateB::class) + ->default(IgnoreSameStateModelStateA::class); + } +} diff --git a/tests/Dummy/IgnoreSameStateModelState/IgnoreSameStateModelStateA.php b/tests/Dummy/IgnoreSameStateModelState/IgnoreSameStateModelStateA.php new file mode 100644 index 0000000..9b03053 --- /dev/null +++ b/tests/Dummy/IgnoreSameStateModelState/IgnoreSameStateModelStateA.php @@ -0,0 +1,7 @@ + IgnoreSameStateModelState::class, + ]; +} diff --git a/tests/Dummy/TestModelIgnoresSameStateByAttribute.php b/tests/Dummy/TestModelIgnoresSameStateByAttribute.php new file mode 100644 index 0000000..0bc7652 --- /dev/null +++ b/tests/Dummy/TestModelIgnoresSameStateByAttribute.php @@ -0,0 +1,12 @@ + IgnoreSameStateModelAttributeState::class, + ]; +} diff --git a/tests/TransitionTest.php b/tests/TransitionTest.php index 19b251a..71ea507 100644 --- a/tests/TransitionTest.php +++ b/tests/TransitionTest.php @@ -1,10 +1,12 @@ state)->toBeInstanceOf(StateC::class); }); + +it('ignore transition to same state', function(){ + $model = TestModelIgnoresSameState::create([ + 'state' => IgnoreSameStateModelStateA::class + ]); + + expect($model->state->canTransitionTo(IgnoreSameStateModelStateA::class))->toBeTrue(); + + $model->state->transitionTo(IgnoreSameStateModelStateA::class); + + expect($model->state)->toBeInstanceOf(IgnoreSameStateModelStateA::class); +}); + +it('ignore transition to same state using Attribute', function(){ + $model = TestModelIgnoresSameStateByAttribute::create([ + 'state' => IgnoreSameStateModelAttributeStateA::class + ]); + + expect($model->state->canTransitionTo(IgnoreSameStateModelAttributeStateA::class))->toBeTrue(); + + $model->state->transitionTo(IgnoreSameStateModelAttributeStateA::class); + + expect($model->state)->toBeInstanceOf(IgnoreSameStateModelAttributeStateA::class); +}); \ No newline at end of file