Skip to content

Commit

Permalink
Merge pull request #270 from zayedadel/feature-ignore-same-state
Browse files Browse the repository at this point in the history
Added test to ensure same-state transition fails when not allowed
  • Loading branch information
freekmurze authored Dec 30, 2024
2 parents d17896a + 9204103 commit 308f155
Show file tree
Hide file tree
Showing 13 changed files with 192 additions and 5 deletions.
32 changes: 31 additions & 1 deletion docs/working-with-transitions/01-configuring-transitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions src/Attributes/AttributeLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
8 changes: 8 additions & 0 deletions src/Attributes/IgnoreSameState.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Spatie\ModelStates\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS)]
class IgnoreSameState {}
20 changes: 19 additions & 1 deletion src/StateConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ class StateConfig
/** @var string[] */
public array $registeredStates = [];

/** @var bool */
public bool $shouldIgnoreSameState = false;

public string $stateChangedEvent = StateChanged::class;

public function __construct(
Expand All @@ -36,6 +39,13 @@ public function default(string $defaultStateClass): StateConfig
return $this;
}

public function ignoreSameState(): StateConfig
{
$this->shouldIgnoreSameState = true;

return $this;
}

public function allowTransition($from, string $to, ?string $transition = null): StateConfig
{
if (is_array($from)) {
Expand Down Expand Up @@ -74,6 +84,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);
Expand All @@ -83,7 +97,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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Spatie\ModelStates\Tests\Dummy\IgnoreSameStateModelState;

use Spatie\ModelStates\Attributes\AllowTransition;
use Spatie\ModelStates\Attributes\DefaultState;
use Spatie\ModelStates\Attributes\IgnoreSameState;
use Spatie\ModelStates\State;

#[
DefaultState(IgnoreSameStateModelAttributeStateA::class),
AllowTransition(IgnoreSameStateModelAttributeStateA::class, IgnoreSameStateModelAttributeStateB::class),
IgnoreSameState
]
abstract class IgnoreSameStateModelAttributeState extends State
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Spatie\ModelStates\Tests\Dummy\IgnoreSameStateModelState;


class IgnoreSameStateModelAttributeStateA extends IgnoreSameStateModelAttributeState
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace Spatie\ModelStates\Tests\Dummy\IgnoreSameStateModelState;

class IgnoreSameStateModelAttributeStateB extends IgnoreSameStateModelAttributeState
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Spatie\ModelStates\Tests\Dummy\IgnoreSameStateModelState;

use Spatie\ModelStates\State;
use Spatie\ModelStates\StateConfig;

abstract class IgnoreSameStateModelState extends State
{
public static function config(): StateConfig
{
return parent::config()
->ignoreSameState()
->allowTransition(IgnoreSameStateModelStateA::class, IgnoreSameStateModelStateB::class)
->default(IgnoreSameStateModelStateA::class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace Spatie\ModelStates\Tests\Dummy\IgnoreSameStateModelState;

class IgnoreSameStateModelStateA extends IgnoreSameStateModelState
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace Spatie\ModelStates\Tests\Dummy\IgnoreSameStateModelState;

class IgnoreSameStateModelStateB extends IgnoreSameStateModelState
{
}
12 changes: 12 additions & 0 deletions tests/Dummy/TestModelIgnoresSameState.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Spatie\ModelStates\Tests\Dummy;

use Spatie\ModelStates\Tests\Dummy\IgnoreSameStateModelState\IgnoreSameStateModelState;

class TestModelIgnoresSameState extends TestModel
{
protected $casts = [
'state' => IgnoreSameStateModelState::class,
];
}
12 changes: 12 additions & 0 deletions tests/Dummy/TestModelIgnoresSameStateByAttribute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Spatie\ModelStates\Tests\Dummy;

use Spatie\ModelStates\Tests\Dummy\IgnoreSameStateModelState\IgnoreSameStateModelAttributeState;

class TestModelIgnoresSameStateByAttribute extends TestModel
{
protected $casts = [
'state' => IgnoreSameStateModelAttributeState::class,
];
}
44 changes: 41 additions & 3 deletions tests/TransitionTest.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<?php

use Spatie\ModelStates\Tests\Dummy\IgnoreSameStateModelState\IgnoreSameStateModelAttributeStateA;
use Illuminate\Support\Facades\Event;
use Spatie\ModelStates\DefaultTransition;
use Spatie\ModelStates\Events\StateChanged;
use Spatie\ModelStates\Exceptions\TransitionNotAllowed;
use Spatie\ModelStates\Exceptions\TransitionNotFound;
use Spatie\ModelStates\Tests\Dummy\IgnoreSameStateModelState\IgnoreSameStateModelStateA;
use Spatie\ModelStates\Tests\Dummy\ModelStates\StateA;
use Spatie\ModelStates\Tests\Dummy\ModelStates\StateB;
use Spatie\ModelStates\Tests\Dummy\ModelStates\StateC;
Expand All @@ -13,6 +15,8 @@
use Spatie\ModelStates\Tests\Dummy\OtherModelStates\StateY;
use Spatie\ModelStates\Tests\Dummy\OtherModelStates\StateZ;
use Spatie\ModelStates\Tests\Dummy\TestModel;
use Spatie\ModelStates\Tests\Dummy\TestModelIgnoresSameState;
use Spatie\ModelStates\Tests\Dummy\TestModelIgnoresSameStateByAttribute;
use Spatie\ModelStates\Tests\Dummy\TestModelWithCustomTransition;
use Spatie\ModelStates\Tests\Dummy\TestModelWithTransitionsFromArray;
use Spatie\ModelStates\Tests\Dummy\Transitions\CustomInvalidTransition;
Expand Down Expand Up @@ -86,6 +90,16 @@
$model->state->transitionTo(StateA::class);
});

it('fails transition to same state when not allowed', function () {
$model = TestModel::create([
'state' => StateA::class,
]);

$this->expectException(TransitionNotFound::class);

$model->state->transitionTo(StateA::class);
});

it('custom transition test', function () {
$model = TestModelWithCustomTransition::create([
'state' => StateX::class,
Expand Down Expand Up @@ -153,9 +167,9 @@

Event::assertDispatched(StateChanged::class, function (StateChanged $event) use ($model) {
return $event->transition instanceof DefaultTransition
&& $event->initialState instanceof StateA
&& $event->finalState instanceof StateB
&& $event->model->is($model);
&& $event->initialState instanceof StateA
&& $event->finalState instanceof StateB
&& $event->model->is($model);
});
});

Expand All @@ -171,3 +185,27 @@

expect($model->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);
});

0 comments on commit 308f155

Please sign in to comment.