diff --git a/spec/SM/DummyEnumState.php b/spec/SM/DummyEnumState.php new file mode 100644 index 0000000..8eafe20 --- /dev/null +++ b/spec/SM/DummyEnumState.php @@ -0,0 +1,21 @@ +=')) { + enum DummyEnumState: string + { + case Checkout = 'checkout'; + case Pending = 'pending'; + case Confirmed = 'confirmed'; + case Cancelled = 'cancelled'; + } +} else { + class DummyEnumState + { + const Checkout = 'checkout'; + const Pending = 'pending'; + const Confirmed = 'confirmed'; + const Cancelled = 'cancelled'; + } +} diff --git a/spec/SM/StateMachine/StateMachineSpec.php b/spec/SM/StateMachine/StateMachineSpec.php index 8bee34d..7d6bf61 100644 --- a/spec/SM/StateMachine/StateMachineSpec.php +++ b/spec/SM/StateMachine/StateMachineSpec.php @@ -2,6 +2,7 @@ namespace spec\SM\StateMachine; +use PhpSpec\Exception\Example\SkippingException; use PhpSpec\ObjectBehavior; use Prophecy\Argument; use SM\Callback\CallbackFactoryInterface; @@ -208,4 +209,47 @@ function it_returns_possible_transitions($object, $callbackFactory, CallbackInte $this->getPossibleTransitions()->shouldReturn(array('create', 'confirm')); } + + + + function it_should_return_back_enum_value($object) + { + if (version_compare(PHP_VERSION, '8.1', '<')) { + return; + } + + $object->getState()->shouldBeCalled()->willReturn(\spec\SM\DummyEnumState::Checkout); + $this->setEnumClass(\spec\SM\DummyEnumState::class); + $state = \spec\SM\DummyEnumState::Checkout; + + if (property_exists($state, 'value')) { + $this->getState()->shouldReturn($state->value); + } + } + + function it_throws_exeception_if_enum_class_in_not_backed($object) + { + if (version_compare(PHP_VERSION, '8.1', '<')) { + return; + } + + $this->shouldThrow(SMException::class)->during('setEnumClass', array('dummy_string')); + } + + function it_not_throws_an_exception_during_apply_when_state_is_back_enum($object, $callbackFactory, CallbackInterface $guard, $dispatcher) + { + $this->checkPhpMinimumVersion('8.1'); + + $object->getState()->shouldBeCalled()->willReturn(\spec\SM\DummyEnumState::Checkout); + $this->setEnumClass(\spec\SM\DummyEnumState::class); + $dispatcher->dispatch(Argument::any())->shouldNotBeCalled(); + $this->shouldNotThrow(SMException::class)->during('apply', array(\spec\SM\DummyEnumState::Pending)); + } + + private function checkPhpMinimumVersion($version) + { + if (version_compare(PHP_VERSION, $version, '<')) { + throw new SkippingException(sprintf("Minimum php version: %s", $version)); + } + } } diff --git a/src/SM/StateMachine/StateMachine.php b/src/SM/StateMachine/StateMachine.php index 2868d4f..f711922 100644 --- a/src/SM/StateMachine/StateMachine.php +++ b/src/SM/StateMachine/StateMachine.php @@ -35,6 +35,9 @@ class StateMachine implements StateMachineInterface /** @var CallbackFactoryInterface */ protected $callbackFactory; + /** @var ?string */ + protected $enumClass; + /** * @param object $object Underlying object for the state machine * @param array $config Config array of the graph @@ -59,6 +62,8 @@ public function __construct( $this->config = $config; + $this->setEnumClass($config['enumClass'] ?? null); + // Test if the given object has the given state property path if (!$this->hasObjectProperty($object, $config['property_path'])) { throw new SMException(sprintf( @@ -147,7 +152,13 @@ public function apply(string $transition, $soft = false): bool public function getState(): string { $accessor = new PropertyAccessor(); - return $accessor->getValue($this->object, $this->config['property_path']); + $state = $accessor->getValue($this->object, $this->config['property_path']); + + if (($enumClass = $this->getEnumClass()) && is_a($state, $enumClass, true)) { + return (string) $state->value; + } + + return $state; } /** @@ -195,6 +206,10 @@ protected function setState($state): void )); } + if ($enumClass = $this->getEnumClass()) { + $state = $enumClass::from($state); + } + $accessor = new PropertyAccessor(); $accessor->setValue($this->object, $this->config['property_path'], $state); } @@ -223,4 +238,20 @@ protected function hasObjectProperty($object, string $property): bool { return (new PropertyAccessor())->isReadable($object, $property); } + + public function getEnumClass(): ?string + { + return $this->enumClass; + } + + public function setEnumClass(?string $class): self + { + if ($class && !is_a($class, \BackedEnum::class, true)) { + throw new SMException('Enum class must be a BackedEnum'); + } + + $this->enumClass = $class; + + return $this; + } }