diff --git a/README.md b/README.md index 6c6d038..e0c6d87 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,10 @@ State Machines for Eloquent models using Enums. - [Register the State Definition file](#register-the-state-definition-file) - [The State Machine](#the-state-machine) - [Using the State Machine](#using-the-state-machine) - - [Transitioning](#transitioning) - - [Checking available transitions](#checking-available-transitions) + - [Transitioning](#transitioning-1) + - [Checking available transitions](#checking-available-transitions) - [Events](#events) - - [Listening to events using `$dispatchesEvents`](#listening-to-events-using-dispatchesEvents) + - [Listening to events using `$dispatchesEvents`](#listening-to-events-using-dispatchesevents) - [Listening to events using Closures](#listening-to-events-using-closures) - [Testing](#testing) - [Inspiration](#inspiration) @@ -347,8 +347,6 @@ The `transitioning($field, $callback)` and `transitioned($field, $callback)` met > Note that the first parameter must be the name of the field we want to listen to. ```php -use App\Events\TransitionedOrderFulfillment; -use App\Events\TransitioningOrderStatus; use Norotaro\Enumata\Traits\HasStateMachines; class Order extends Model diff --git a/src/StateMachine.php b/src/StateMachine.php index 4fe3680..06b84dc 100644 --- a/src/StateMachine.php +++ b/src/StateMachine.php @@ -13,6 +13,8 @@ class StateMachine implements Contracts\StateMachine { + protected bool $isTransitioning = false; + public function __construct( protected Model $model, protected string $field @@ -52,32 +54,41 @@ public function canBe(DefineStates&UnitEnum $state): bool * @throws InvalidArgumentException * @throws InvalidCastException */ - public function transitionTo(DefineStates&UnitEnum $state, bool $force = false): void + public function transitionTo(DefineStates&UnitEnum $to, bool $force = false): void { - if ($state === $this->currentState()) { + if ($to === $this->currentState()) { return; } /** TODO: unify the validation logic of transitions */ - if (!$force && !$this->canBe($state)) { + if (!$force && !$this->canBe($to)) { throw new TransitionNotAllowedException( $this->currentState(), - $state, + $to, $this->model ); } - $this->model->{$this->field} = $state; + $this->isTransitioning = true; + + $this->model->{$this->field} = $to; $this->model->fireTransitioningEvent($this->field); $this->model->save(); - $this->model->fireTransitionedEvent($this->field); + $this->model->fireTransitionedEvent($this->field, $to); + + $this->isTransitioning = false; } public function getField(): string { return $this->field; } + + public function isTransitioning(): bool + { + return $this->isTransitioning; + } } diff --git a/src/Traits/HasStateMachines.php b/src/Traits/HasStateMachines.php index 4f255e6..b9fe9ee 100644 --- a/src/Traits/HasStateMachines.php +++ b/src/Traits/HasStateMachines.php @@ -71,10 +71,12 @@ public static function bootHasStateMachines() } MacroableModels::addMacro(static::class, 'fireTransitioningEvent', function ($field) { + // fire Eloquent event $this->fireModelEvent("transitioning:$field", false); }); - MacroableModels::addMacro(static::class, 'fireTransitionedEvent', function ($field) { + MacroableModels::addMacro(static::class, 'fireTransitionedEvent', function ($field, $from) { + // fire Eloquent event $this->fireModelEvent("transitioned:$field", false); }); @@ -89,7 +91,7 @@ public static function bootHasStateMachines() foreach ($model->getStateMachines() as $stateMachine) { $field = $stateMachine->getField(); - if ($model->isDirty($field)) { + if ($model->isDirty($field) && !$stateMachine->isTransitioning()) { $from = $model->getOriginal($field); $to = $model->{$field}; diff --git a/tests/Traits/HasStateMachinesTest.php b/tests/Traits/HasStateMachinesTest.php index bb76c8f..45795c5 100644 --- a/tests/Traits/HasStateMachinesTest.php +++ b/tests/Traits/HasStateMachinesTest.php @@ -45,7 +45,15 @@ expect(MacroableModels::modelHasMacro($this->model::class, 'finish'))->toBe(true); }); + it('change status with transition methods', function () { + $this->model->save(); + $this->model->pay(); + + expect($this->model->status)->toBe(OrderStatus::Pending); + }); + it('creates transition methods that allows forced transitions', function () { + $this->model->save(); $this->model->end(force: true); expect($this->model->status)->toBe(OrderStatus::Finished);