diff --git a/docs/working-with-states/01-configuring-states.md b/docs/working-with-states/01-configuring-states.md index 597b027..979573c 100644 --- a/docs/working-with-states/01-configuring-states.md +++ b/docs/working-with-states/01-configuring-states.md @@ -100,4 +100,32 @@ abstract class PaymentState extends State } ``` +## Manually registering states +If you want to place your concrete state implementations in a different directory, you may do so and register them manually: + +```php +use Spatie\ModelStates\State; +use Spatie\ModelStates\StateConfig; + +use Your\Concrete\State\Class\Cancelled; // this may be wherever you want +use Your\Concrete\State\Class\ExampleOne; +use Your\Concrete\State\Class\ExampleTwo; + +abstract class PaymentState extends State +{ + abstract public function color(): string; + + public static function config(): StateConfig + { + return parent::config() + ->default(Pending::class) + ->allowTransition(Pending::class, Paid::class) + ->allowTransition(Pending::class, Failed::class) + ->registerState(Cancelled::class) + ->registerState([ExampleOne::class, ExampleTwo::class]) + ; + } +} +``` + Next up, we'll take a moment to discuss how state classes are serialized to the database. diff --git a/src/State.php b/src/State.php index 13dd79a..14c4ef3 100644 --- a/src/State.php +++ b/src/State.php @@ -300,6 +300,10 @@ private static function resolveStateMapping(): array $resolvedStates[$stateClass::getMorphClass()] = $stateClass; } + foreach ($stateConfig->registeredStates as $stateClass) { + $resolvedStates[$stateClass::getMorphClass()] = $stateClass; + } + return $resolvedStates; } } diff --git a/src/StateConfig.php b/src/StateConfig.php index 8c07de1..1ef8c3e 100644 --- a/src/StateConfig.php +++ b/src/StateConfig.php @@ -15,6 +15,9 @@ class StateConfig /** @var string[] */ public array $allowedTransitions = []; + /** @var string[] */ + public array $registeredStates = []; + public function __construct( string $baseStateClass ) { @@ -95,6 +98,25 @@ public function transitionableStates(string $fromMorphClass): array return $transitionableStates; } + public function registerState($stateClass): StateConfig + { + if (is_array($stateClass)) { + foreach ($stateClass as $state) { + $this->registerState($state); + } + + return $this; + } + + if (!is_subclass_of($stateClass, $this->baseStateClass)) { + throw InvalidConfig::doesNotExtendBaseClass($stateClass, $this->baseStateClass); + } + + $this->registeredStates[] = $stateClass; + + return $this; + } + /** * @param string|\Spatie\ModelStates\State $from * @param string|\Spatie\ModelStates\State $to diff --git a/tests/Dummy/ModelStates/AnotherDirectory/StateF.php b/tests/Dummy/ModelStates/AnotherDirectory/StateF.php new file mode 100644 index 0000000..52d1058 --- /dev/null +++ b/tests/Dummy/ModelStates/AnotherDirectory/StateF.php @@ -0,0 +1,9 @@ +allowTransition(StateA::class, StateB::class) ->allowTransition([StateA::class, StateB::class], StateC::class) ->allowTransition(StateA::class, StateD::class) + ->allowTransition(StateA::class, StateF::class) + ->registerState(StateF::class) + ->registerState([StateG::class, StateH::class]) ->default(StateA::class); } } diff --git a/tests/StateCastingTest.php b/tests/StateCastingTest.php index 0bcc5b1..c0820fc 100644 --- a/tests/StateCastingTest.php +++ b/tests/StateCastingTest.php @@ -6,6 +6,8 @@ use Spatie\ModelStates\Tests\Dummy\ModelStates\ModelState; use Spatie\ModelStates\Tests\Dummy\ModelStates\StateA; use Spatie\ModelStates\Tests\Dummy\ModelStates\StateC; +use Spatie\ModelStates\Tests\Dummy\ModelStates\AnotherDirectory\StateF; +use Spatie\ModelStates\Tests\Dummy\ModelStates\AnotherDirectory\StateG; use Spatie\ModelStates\Tests\Dummy\TestModel; class StateCastingTest extends TestCase @@ -24,6 +26,20 @@ public function state_without_alias_is_serialized_on_create() ]); } + /** @test */ + public function custom_registered_state_without_alias_is_serialized_on_create() + { + $model = TestModel::create([ + 'state' => StateF::class, + ]); + + $this->assertInstanceOf(StateF::class, $model->state); + + $this->assertDatabaseHas($model->getTable(), [ + 'state' => StateF::getMorphClass(), + ]); + } + /** @test */ public function state_with_alias_is_serialized_on_create_when_using_class_name() { @@ -38,6 +54,20 @@ public function state_with_alias_is_serialized_on_create_when_using_class_name() ]); } + /** @test */ + public function custom_registered_state_with_alias_is_serialized_on_create_when_using_class_name() + { + $model = TestModel::create([ + 'state' => StateG::class, + ]); + + $this->assertInstanceOf(StateG::class, $model->state); + + $this->assertDatabaseHas($model->getTable(), [ + 'state' => StateG::getMorphClass(), + ]); + } + /** @test */ public function state_with_alias_is_serialized_on_create_when_using_alias() { @@ -52,6 +82,20 @@ public function state_with_alias_is_serialized_on_create_when_using_alias() ]); } + /** @test */ + public function custom_registered_state_with_alias_is_serialized_on_create_when_using_alias() + { + $model = TestModel::create([ + 'state' => StateG::getMorphClass(), + ]); + + $this->assertInstanceOf(StateG::class, $model->state); + + $this->assertDatabaseHas($model->getTable(), [ + 'state' => StateG::getMorphClass(), + ]); + } + /** @test */ public function state_is_immediately_unserialized_on_property_set() { diff --git a/tests/StateTest.php b/tests/StateTest.php index 0ee1cac..27c260b 100644 --- a/tests/StateTest.php +++ b/tests/StateTest.php @@ -9,6 +9,9 @@ use Spatie\ModelStates\Tests\Dummy\ModelStates\StateC; use Spatie\ModelStates\Tests\Dummy\ModelStates\StateD; use Spatie\ModelStates\Tests\Dummy\ModelStates\StateE; +use Spatie\ModelStates\Tests\Dummy\ModelStates\AnotherDirectory\StateF; +use Spatie\ModelStates\Tests\Dummy\ModelStates\AnotherDirectory\StateG; +use Spatie\ModelStates\Tests\Dummy\ModelStates\AnotherDirectory\StateH; use Spatie\ModelStates\Tests\Dummy\OtherModelStates\StateX; use Spatie\ModelStates\Tests\Dummy\OtherModelStates\StateY; use Spatie\ModelStates\Tests\Dummy\TestModel; @@ -30,6 +33,11 @@ public function test_resolve_state_class() $this->assertEquals(StateD::class, ModelState::resolveStateClass(StateD::$name)); $this->assertEquals(StateE::class, ModelState::resolveStateClass(StateE::class)); $this->assertEquals(StateE::class, ModelState::resolveStateClass(StateE::getMorphClass())); + $this->assertEquals(StateF::class, ModelState::resolveStateClass(StateF::getMorphClass())); + $this->assertEquals(StateG::class, ModelState::resolveStateClass(StateG::getMorphClass())); + $this->assertEquals(StateG::class, ModelState::resolveStateClass(StateG::getMorphClass())); + $this->assertEquals(StateH::class, ModelState::resolveStateClass(StateH::getMorphClass())); + $this->assertEquals(StateH::class, ModelState::resolveStateClass(StateH::getMorphClass())); } /** @test */ @@ -41,6 +49,7 @@ public function transitionable_states() StateB::getMorphClass(), StateC::getMorphClass(), StateD::getMorphClass(), + StateF::getMorphClass(), ], $modelA->state->transitionableStates()); $modelB = TestModelWithDefault::create([ @@ -85,6 +94,7 @@ public function test_can_transition_to() $this->assertTrue($state->canTransitionTo(StateB::class)); $this->assertTrue($state->canTransitionTo(StateC::class)); + $this->assertTrue($state->canTransitionTo(StateF::class)); $state = new StateB(new TestModel()); $state->setField('state'); @@ -106,6 +116,9 @@ public function test_get_states() StateC::getMorphClass(), StateD::getMorphClass(), StateE::getMorphClass(), + StateF::getMorphClass(), + StateG::getMorphClass(), + StateH::getMorphClass(), ], ], $states->toArray() @@ -124,6 +137,9 @@ public function test_get_states_for() StateC::getMorphClass(), StateD::getMorphClass(), StateE::getMorphClass(), + StateF::getMorphClass(), + StateG::getMorphClass(), + StateH::getMorphClass(), ], $states->toArray() ); @@ -175,6 +191,9 @@ public function test_all() StateC::getMorphClass() => StateC::class, StateD::getMorphClass() => StateD::class, StateE::getMorphClass() => StateE::class, + StateF::getMorphClass() => StateF::class, + StateG::getMorphClass() => StateG::class, + StateH::getMorphClass() => StateH::class, ], ModelState::all()->toArray()); }