From add717751a239d65f9e38efbedb414a0c2c3919e Mon Sep 17 00:00:00 2001 From: Richard McDaniel Date: Mon, 23 Feb 2026 09:14:37 -0600 Subject: [PATCH 1/2] State changed event --- src/Events/StateChanged.php | 38 ++++++++++++++++++ src/States/State.php | 14 +++++-- src/States/StateConfig.php | 10 ++--- tests/Feature/StateChangedEventTest.php | 40 +++++++++++++++++++ tests/Unit/States/StateInfrastructureTest.php | 16 ++++++++ 5 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 src/Events/StateChanged.php create mode 100644 tests/Feature/StateChangedEventTest.php diff --git a/src/Events/StateChanged.php b/src/Events/StateChanged.php new file mode 100644 index 00000000..3a743dc6 --- /dev/null +++ b/src/Events/StateChanged.php @@ -0,0 +1,38 @@ +initialState = $initialState; + $this->finalState = $finalState; + $this->model = $model; + $this->field = $field; + } +} diff --git a/src/States/State.php b/src/States/State.php index af395296..6e46aeb6 100644 --- a/src/States/State.php +++ b/src/States/State.php @@ -9,6 +9,7 @@ use InvalidArgumentException; use JsonSerializable; use ReflectionClass; +use Workflow\Events\StateChanged; use Workflow\Exceptions\TransitionNotFound; abstract class State implements Castable, JsonSerializable @@ -140,14 +141,21 @@ public function transitionTo($newState, ...$transitionArgs) $this->model->{$this->field} = $newState; $this->model->save(); - - $currentState = $this->model->{$this->field}; + $model = $this->model; + $currentState = $model->{$this->field} ?? null; if ($currentState instanceof self) { $currentState->setField($this->field); } - return $this->model; + event(new StateChanged( + $this, + $currentState instanceof self ? $currentState : null, + $this->model, + $this->field + )); + + return $model; } public function canTransitionTo($newState, ...$transitionArgs): bool diff --git a/src/States/StateConfig.php b/src/States/StateConfig.php index 8f62f7c8..d302d0f2 100644 --- a/src/States/StateConfig.php +++ b/src/States/StateConfig.php @@ -13,7 +13,7 @@ final class StateConfig public ?string $defaultStateClass = null; /** - * @var array + * @var array */ public array $allowedTransitions = []; @@ -43,11 +43,11 @@ public function ignoreSameState(): self return $this; } - public function allowTransition($from, string $to, ?string $transition = null): self + public function allowTransition($from, string $to): self { if (is_array($from)) { foreach ($from as $fromState) { - $this->allowTransition($fromState, $to, $transition); + $this->allowTransition($fromState, $to); } return $this; @@ -61,7 +61,7 @@ public function allowTransition($from, string $to, ?string $transition = null): throw new InvalidArgumentException("{$to} does not extend {$this->baseStateClass}."); } - $this->allowedTransitions[$this->createTransitionKey($from, $to)] = $transition; + $this->allowedTransitions[$this->createTransitionKey($from, $to)] = true; return $this; } @@ -72,7 +72,7 @@ public function allowTransition($from, string $to, ?string $transition = null): public function allowTransitions(array $transitions): self { foreach ($transitions as $transition) { - $this->allowTransition($transition[0], $transition[1], $transition[2] ?? null); + $this->allowTransition($transition[0], $transition[1]); } return $this; diff --git a/tests/Feature/StateChangedEventTest.php b/tests/Feature/StateChangedEventTest.php new file mode 100644 index 00000000..fe8f3c2e --- /dev/null +++ b/tests/Feature/StateChangedEventTest.php @@ -0,0 +1,40 @@ + TestWorkflow::class, + ]); + $storedWorkflow = StoredWorkflow::findOrFail($storedWorkflow->id); + + Event::fake([StateChanged::class]); + + $initialState = $storedWorkflow->status; + $storedWorkflow->status->transitionTo(WorkflowPendingStatus::class); + + Event::assertDispatched(StateChanged::class, static function (StateChanged $event) use ( + $storedWorkflow, + $initialState + ) { + return $event->initialState === $initialState + && $event->initialState instanceof WorkflowCreatedStatus + && $event->finalState instanceof WorkflowPendingStatus + && $event->model->is($storedWorkflow) + && $event->field === 'status'; + }); + } +} diff --git a/tests/Unit/States/StateInfrastructureTest.php b/tests/Unit/States/StateInfrastructureTest.php index 1a1ea703..adbf7972 100644 --- a/tests/Unit/States/StateInfrastructureTest.php +++ b/tests/Unit/States/StateInfrastructureTest.php @@ -5,8 +5,10 @@ namespace Tests\Unit\States; use Exception; +use Illuminate\Support\Facades\Event; use InvalidArgumentException; use Tests\TestCase; +use Workflow\Events\StateChanged; use Workflow\Exceptions\TransitionNotFound; use Workflow\States\State; use Workflow\States\StateCaster; @@ -196,6 +198,8 @@ public function testStateUtilityMethods(): void public function testStateTransitionsAndEqualityChecks(): void { + Event::fake([StateChanged::class]); + $model = new StateInfraModel(); $state = new StateInfraInitialState($model); $state->setField('status'); @@ -213,6 +217,12 @@ public function testStateTransitionsAndEqualityChecks(): void $this->assertTrue($model->saved); $this->assertInstanceOf(StateInfraNextState::class, $model->status); $this->assertSame('status', $model->status->getField()); + Event::assertDispatched(StateChanged::class, static function (StateChanged $event) use ($model, $state) { + return $event->initialState === $state + && $event->finalState instanceof StateInfraNextState + && $event->model === $model + && $event->field === 'status'; + }); $model->saved = false; $nextState = $model->status; @@ -220,6 +230,12 @@ public function testStateTransitionsAndEqualityChecks(): void $this->assertTrue($model->saved); $this->assertInstanceOf(StateInfraTerminalState::class, $model->status); + Event::assertDispatched(StateChanged::class, static function (StateChanged $event) use ($model, $nextState) { + return $event->initialState === $nextState + && $event->finalState instanceof StateInfraTerminalState + && $event->model === $model + && $event->field === 'status'; + }); try { $model->status->transitionTo(StateInfraInitialState::class); From 5534dc871a0658b2c5c6dfc37bf72ea81a80c6a5 Mon Sep 17 00:00:00 2001 From: Richard McDaniel Date: Mon, 23 Feb 2026 09:20:10 -0600 Subject: [PATCH 2/2] Cleanup --- src/Events/StateChanged.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Events/StateChanged.php b/src/Events/StateChanged.php index 3a743dc6..78483954 100644 --- a/src/Events/StateChanged.php +++ b/src/Events/StateChanged.php @@ -24,12 +24,8 @@ class StateChanged * @param string|State|null $finalState * @param \Illuminate\Database\Eloquent\Model $model */ - public function __construct( - ?State $initialState, - ?State $finalState, - $model, - string $field - ) { + public function __construct(?State $initialState, ?State $finalState, $model, string $field) + { $this->initialState = $initialState; $this->finalState = $finalState; $this->model = $model;