diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index fcc7247..a5bdcc5 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -2,9 +2,9 @@ name: Php state machine on: push: - branches: [ master ] + branches: [ master, release/* ] pull_request: - branches: [ master ] + branches: [ master, release/* ] jobs: phpunit: diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 51ee2db..65426d0 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,21 @@ # Release notes +# 3.3.0 + +This release is a deprecation release. Deprecation warning will be issued when using a changed feature. + +* Introduction of a `StateContext` to replace the context that could be mixed. + +Before 3.3.0, a context could be `"post"` or an object that was converted to a FQCN. + +Starting with 4.0, we'll only accept implementation of `StateContext`. You will need your context to + implement the interface, or use the adapters [StringAdapterContext](src/Context/StringAdapterContext.php) or + [ObjectAdapterContext](src/Context/ObjectAdapterContext.php) if you don't want to add new custom code. + +# 3.2.0 + +[3.2.0](https://github.com/yvoyer/php-state/releases/tag/3.2.0) + # 3.1.0 * [#30](https://github.com/yvoyer/php-state/pull/30) diff --git a/examples/CallbackStateTest.php b/examples/CallbackStateTest.php index a5aad3d..e6b3a60 100644 --- a/examples/CallbackStateTest.php +++ b/examples/CallbackStateTest.php @@ -9,6 +9,7 @@ use Star\Component\State\Callbacks\TransitionCallback; use Star\Component\State\InvalidStateTransitionException; use Star\Component\State\RegistryBuilder; +use Star\Component\State\StateContext; use Star\Component\State\StateMachine; use Star\Component\State\StateMetadata; use Star\Component\State\StateTransition; @@ -58,7 +59,7 @@ public function test_workflow(): void } } -final class TurnStill +final class TurnStill implements StateContext { /** * @var TurnStillState|StateMetadata @@ -75,6 +76,11 @@ public function __construct() $this->state = new TurnStillState('locked'); } + public function toStateContextIdentifier(): string + { + return 'turn-still'; + } + public function pay(int $coin): void { $this->state = $this->state->transit( @@ -94,7 +100,7 @@ public function pass(): void { $this->state = $this->state->transit( 'pass', - 'turn-still', + $this, new CallClosureOnFailure( function () { return $this->state->transit('alarm', $this)->getCurrent(); @@ -105,7 +111,7 @@ function () { public function reset(): void { - $this->state = $this->state->transit('reset', 'turn-still'); + $this->state = $this->state->transit('reset', $this); } public function isLocked(): bool diff --git a/examples/ContextUsingBuilderTest.php b/examples/ContextUsingBuilderTest.php index 1951dd7..b63fb9d 100644 --- a/examples/ContextUsingBuilderTest.php +++ b/examples/ContextUsingBuilderTest.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\TestCase; use Star\Component\State\Builder\StateBuilder; use Star\Component\State\InvalidStateTransitionException; +use Star\Component\State\StateContext; use Star\Component\State\StateMachine; final class ContextUsingBuilderTest extends TestCase @@ -187,7 +188,7 @@ public function test_it_should_allow_to_define_attributes_on_state(): void /** * Example of usage when using self contained workflow creation. */ -final class Post +final class Post implements StateContext { const ALIAS = 'post'; @@ -202,16 +203,18 @@ final class Post const ATTRIBUTE_ACTIVE = 'active'; const ATTRIBUTE_CLOSED = 'closed'; - /** - * @var string - */ - private $state; + private string $state; private function __construct(string $state) { $this->state = $state; } + public function toStateContextIdentifier(): string + { + return self::ALIAS; + } + public function isDraft(): bool { return $this->workflow()->isInState(self::STATE_DRAFT); @@ -239,23 +242,20 @@ public function isClosed(): bool public function moveToDraft(): void { - $this->state = $this->workflow()->transit(self::TRANSITION_TO_DRAFT, 'post'); + $this->state = $this->workflow()->transit(self::TRANSITION_TO_DRAFT, $this); } public function publish(): void { - $this->state = $this->workflow()->transit(self::TRANSITION_PUBLISH, 'post'); + $this->state = $this->workflow()->transit(self::TRANSITION_PUBLISH, $this); } public function archive(): void { - $this->state = $this->workflow()->transit(self::TRANSITION_ARCHIVE, 'post'); + $this->state = $this->workflow()->transit(self::TRANSITION_ARCHIVE, $this); } - /** - * @return Post - */ - public static function drafted() + public static function drafted(): self { return new self(self::STATE_DRAFT); } diff --git a/examples/ContextUsingCustomMetadataTest.php b/examples/ContextUsingCustomMetadataTest.php index cd15ff1..bb2e254 100644 --- a/examples/ContextUsingCustomMetadataTest.php +++ b/examples/ContextUsingCustomMetadataTest.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\TestCase; use Star\Component\State\Builder\StateBuilder; use Star\Component\State\RegistryBuilder; +use Star\Component\State\StateContext; use Star\Component\State\StateMetadata; use Star\Component\State\StateTransition; @@ -215,18 +216,20 @@ public function getDestinationState(): string } } -final class ContextStub +final class ContextStub implements StateContext { - /** - * @var MyStateWorkflow|StateMetadata - */ - public $state; + public MyStateWorkflow $state; public function __construct() { $this->state = new MyStateWorkflow(); } + public function toStateContextIdentifier(): string + { + return 'ContextStub'; + } + public function publish(): void { $this->state = $this->state->transit('publish', $this); diff --git a/examples/DoctrineMappedContextTest.php b/examples/DoctrineMappedContextTest.php index ff3759c..79ca559 100644 --- a/examples/DoctrineMappedContextTest.php +++ b/examples/DoctrineMappedContextTest.php @@ -11,6 +11,7 @@ use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\TestCase; use Star\Component\State\Builder\StateBuilder; +use Star\Component\State\StateContext; use Star\Component\State\StateMetadata; final class DoctrineMappedContextTest extends TestCase @@ -80,7 +81,7 @@ private function save(MyEntity $entity): MyEntity } #[ORM\Entity] -class MyEntity +class MyEntity implements StateContext { #[ORM\Id()] #[ORM\GeneratedValue(strategy: "AUTO")] @@ -95,6 +96,11 @@ public function __construct() $this->state = new MyState(); } + public function toStateContextIdentifier(): string + { + throw new \RuntimeException(__METHOD__ . ' is not implemented yet.'); + } + public function isLocked(): bool { return $this->state->isInState('locked'); diff --git a/src/Builder/StateBuilder.php b/src/Builder/StateBuilder.php index dbc9ca1..1caa39b 100644 --- a/src/Builder/StateBuilder.php +++ b/src/Builder/StateBuilder.php @@ -89,6 +89,7 @@ public static function build( ?TransitionRegistry $registry = null, ?EventRegistry $listeners = null, ): StateBuilder { + // todo deprecate explict class in favor of interface return new self($registry, $listeners); } } diff --git a/src/Callbacks/AlwaysReturnStateOnFailure.php b/src/Callbacks/AlwaysReturnStateOnFailure.php index f2f530e..f3fba76 100644 --- a/src/Callbacks/AlwaysReturnStateOnFailure.php +++ b/src/Callbacks/AlwaysReturnStateOnFailure.php @@ -3,6 +3,7 @@ namespace Star\Component\State\Callbacks; use Star\Component\State\InvalidStateTransitionException; +use Star\Component\State\StateContext; use Star\Component\State\StateMachine; final class AlwaysReturnStateOnFailure implements TransitionCallback @@ -14,31 +15,34 @@ public function __construct(string $to) $this->to = $to; } - /** - * @param mixed $context - * @param StateMachine $machine - */ - public function beforeStateChange($context, StateMachine $machine): void - { + public function beforeStateChange( + /* StateContext in 4.0 */ $context, + StateMachine $machine + ): void { } /** - * @param mixed $context + * @param string|object|StateContext $context * @param StateMachine $machine */ - public function afterStateChange($context, StateMachine $machine): void - { + public function afterStateChange( + /* StateContext in 4.0 */ $context, + StateMachine $machine + ): void { } /** * @param InvalidStateTransitionException $exception - * @param mixed $context + * @param string|object|StateContext $context * @param StateMachine $machine * * @return string */ - public function onFailure(InvalidStateTransitionException $exception, $context, StateMachine $machine): string - { + public function onFailure( + InvalidStateTransitionException $exception, + /* StateContext in 4.0 */ $context, + StateMachine $machine + ): string { return $this->to; } } diff --git a/src/Callbacks/AlwaysThrowExceptionOnFailure.php b/src/Callbacks/AlwaysThrowExceptionOnFailure.php index 6ab9e4e..1df6acb 100644 --- a/src/Callbacks/AlwaysThrowExceptionOnFailure.php +++ b/src/Callbacks/AlwaysThrowExceptionOnFailure.php @@ -11,16 +11,20 @@ final class AlwaysThrowExceptionOnFailure implements TransitionCallback * @param mixed $context * @param StateMachine $machine */ - public function beforeStateChange($context, StateMachine $machine): void - { + public function beforeStateChange( + /* StateContext in 4.0 */ $context, + StateMachine $machine + ): void { } /** * @param mixed $context * @param StateMachine $machine */ - public function afterStateChange($context, StateMachine $machine): void - { + public function afterStateChange( + /* StateContext in 4.0 */ $context, + StateMachine $machine + ): void { } /** @@ -31,8 +35,11 @@ public function afterStateChange($context, StateMachine $machine): void * @return string * @throws InvalidStateTransitionException */ - public function onFailure(InvalidStateTransitionException $exception, $context, StateMachine $machine): string - { + public function onFailure( + InvalidStateTransitionException $exception, + /* StateContext in 4.0 */ $context, + StateMachine $machine + ): string { throw $exception; } } diff --git a/src/Callbacks/BufferStateChanges.php b/src/Callbacks/BufferStateChanges.php index 94e36a6..dc0b9f7 100644 --- a/src/Callbacks/BufferStateChanges.php +++ b/src/Callbacks/BufferStateChanges.php @@ -2,8 +2,8 @@ namespace Star\Component\State\Callbacks; -use RuntimeException; use Star\Component\State\InvalidStateTransitionException; +use Star\Component\State\StateContext; use Star\Component\State\StateMachine; use Webmozart\Assert\Assert; use function get_class; @@ -16,29 +16,46 @@ final class BufferStateChanges implements TransitionCallback */ private array $buffer = []; - public function beforeStateChange($context, StateMachine $machine): void + /** + * @param mixed|StateContext $context + * @return string + */ + private function extractContextIdentifier($context): string { - if (is_object($context)) { - $context = get_class($context); + if (! $context instanceof StateContext) { + if (is_object($context)) { + $context = get_class($context); + } + } else { + $context = $context->toStateContextIdentifier(); } Assert::string($context, 'Context is expected to be a string. Got: %s'); - $this->buffer[$context][] = __FUNCTION__; + return $context; } - public function afterStateChange($context, StateMachine $machine): void - { - if (is_object($context)) { - $context = get_class($context); - } - Assert::string($context, 'Context is expected to be a string. Got: %s'); + public function beforeStateChange( + /* StateContext in 4.0 */ $context, + StateMachine $machine + ): void { + $this->buffer[$this->extractContextIdentifier($context)][] = __FUNCTION__; + } - $this->buffer[$context][] = __FUNCTION__; + public function afterStateChange( + /* StateContext in 4.0 */ $context, + StateMachine $machine + ): void { + $this->buffer[$this->extractContextIdentifier($context)][] = __FUNCTION__; } - public function onFailure(InvalidStateTransitionException $exception, $context, StateMachine $machine): string - { - throw new RuntimeException(__METHOD__ . ' is not implemented yet.'); + public function onFailure( + InvalidStateTransitionException $exception, + /* StateContext in 4.0 */ $context, + StateMachine $machine + ): string { + $this->buffer[$this->extractContextIdentifier($context)][] = get_class($exception); + + return ''; } /** diff --git a/src/Callbacks/CallClosureOnFailure.php b/src/Callbacks/CallClosureOnFailure.php index dad8d4d..f418500 100644 --- a/src/Callbacks/CallClosureOnFailure.php +++ b/src/Callbacks/CallClosureOnFailure.php @@ -22,16 +22,20 @@ public function __construct(Closure $callback) * @param mixed $context * @param StateMachine $machine */ - public function beforeStateChange($context, StateMachine $machine): void - { + public function beforeStateChange( + /* StateContext in 4.0 */ $context, + StateMachine $machine + ): void { } /** * @param mixed $context * @param StateMachine $machine */ - public function afterStateChange($context, StateMachine $machine): void - { + public function afterStateChange( + /* StateContext in 4.0 */ $context, + StateMachine $machine + ): void { } /** @@ -43,7 +47,7 @@ public function afterStateChange($context, StateMachine $machine): void */ public function onFailure( InvalidStateTransitionException $exception, - $context, + /* StateContext in 4.0 */ $context, StateMachine $machine ): string { $callback = $this->callback; diff --git a/src/Callbacks/CallContextMethodOnFailure.php b/src/Callbacks/CallContextMethodOnFailure.php index f244abc..f72b700 100644 --- a/src/Callbacks/CallContextMethodOnFailure.php +++ b/src/Callbacks/CallContextMethodOnFailure.php @@ -34,16 +34,20 @@ public function __construct( * @param mixed $context * @param StateMachine $machine */ - public function beforeStateChange($context, StateMachine $machine): void - { + public function beforeStateChange( + /* StateContext in 4.0 */ $context, + StateMachine $machine + ): void { } /** * @param mixed $context * @param StateMachine $machine */ - public function afterStateChange($context, StateMachine $machine): void - { + public function afterStateChange( + /* StateContext in 4.0 */ $context, + StateMachine $machine + ): void { } /** @@ -53,8 +57,11 @@ public function afterStateChange($context, StateMachine $machine): void * * @return string */ - public function onFailure(InvalidStateTransitionException $exception, $context, StateMachine $machine): string - { + public function onFailure( + InvalidStateTransitionException $exception, + /* StateContext in 4.0 */ $context, + StateMachine $machine + ): string { $closure = function (array $args) use ($context) { $context->{$this->method}(...$args); }; diff --git a/src/Callbacks/NullCallback.php b/src/Callbacks/NullCallback.php index 6b0b4db..8f960d6 100644 --- a/src/Callbacks/NullCallback.php +++ b/src/Callbacks/NullCallback.php @@ -11,16 +11,20 @@ final class NullCallback implements TransitionCallback * @param mixed $context * @param StateMachine $machine */ - public function beforeStateChange($context, StateMachine $machine): void - { + public function beforeStateChange( + /* StateContext in 4.0 */ $context, + StateMachine $machine + ): void { } /** * @param mixed $context * @param StateMachine $machine */ - public function afterStateChange($context, StateMachine $machine): void - { + public function afterStateChange( + /* StateContext in 4.0 */ $context, + StateMachine $machine + ): void { } /** @@ -30,8 +34,11 @@ public function afterStateChange($context, StateMachine $machine): void * * @return string */ - public function onFailure(InvalidStateTransitionException $exception, $context, StateMachine $machine): string - { + public function onFailure( + InvalidStateTransitionException $exception, + /* StateContext in 4.0 */ $context, + StateMachine $machine + ): string { throw new \RuntimeException('Method ' . __METHOD__ . ' should never be called.'); } } diff --git a/src/Callbacks/TransitionCallback.php b/src/Callbacks/TransitionCallback.php index de7107b..5d2d58d 100644 --- a/src/Callbacks/TransitionCallback.php +++ b/src/Callbacks/TransitionCallback.php @@ -3,28 +3,45 @@ namespace Star\Component\State\Callbacks; use Star\Component\State\InvalidStateTransitionException; +use Star\Component\State\StateContext; use Star\Component\State\StateMachine; interface TransitionCallback { /** - * @param mixed $context + * @param mixed|StateContext $context * @param StateMachine $machine + * @deprecated $context will expect a type of StateContext in 4.0, you need to update your implementations. + * @see StateContext */ - public function beforeStateChange($context, StateMachine $machine): void; + public function beforeStateChange( + /* StateContext in 4.0 */ $context, + StateMachine $machine, + ): void; /** - * @param mixed $context + * @param mixed|StateContext $context * @param StateMachine $machine + * @deprecated $context will expect a type of StateContext in 4.0, you need to update your implementations. + * @see StateContext */ - public function afterStateChange($context, StateMachine $machine): void; + public function afterStateChange( + /* StateContext in 4.0 */ $context, + StateMachine $machine, + ): void; /** * @param InvalidStateTransitionException $exception - * @param mixed $context + * @param mixed|StateContext $context * @param StateMachine $machine * * @return string The new state to move to on failure + * @deprecated $context will expect a type of StateContext in 4.0, you need to update your implementations. + * @see StateContext */ - public function onFailure(InvalidStateTransitionException $exception, $context, StateMachine $machine): string; + public function onFailure( + InvalidStateTransitionException $exception, + /* StateContext in 4.0 */ $context, + StateMachine $machine, + ): string; } diff --git a/src/Context/ObjectAdapterContext.php b/src/Context/ObjectAdapterContext.php new file mode 100644 index 0000000..d5819f5 --- /dev/null +++ b/src/Context/ObjectAdapterContext.php @@ -0,0 +1,52 @@ +object = $object; + + if ($triggerError) { + @trigger_error( + sprintf( + 'Passing an object of type "%s" that do not implement "%s" is deprecated. ' . + 'The object should implementing "%s" interface.', + get_class($this->object), + StateContext::class, + StateContext::class, + ), + E_USER_DEPRECATED, + ); + } + } + + public function toStateContextIdentifier(): string + { + $class = get_class($this->object); + $pos = strrpos($class, '\\'); + if ($pos > 0) { + $pos = $pos + 1; + } + + return substr($class, (int) $pos); + } +} diff --git a/src/Context/StringAdapterContext.php b/src/Context/StringAdapterContext.php new file mode 100644 index 0000000..592f03c --- /dev/null +++ b/src/Context/StringAdapterContext.php @@ -0,0 +1,41 @@ +context = $context; + if ($triggerError) { + @trigger_error( + sprintf( + 'Passing a string context "%s" is deprecated. ' . + 'You should provide your own class implementing "%s" interface.', + $this->context, + StateContext::class, + ), + E_USER_DEPRECATED, + ); + } + } + + public function toStateContextIdentifier(): string + { + return $this->context; + } +} diff --git a/src/Event/TransitionWasFailed.php b/src/Event/TransitionWasFailed.php index c74f13a..f6bd4e4 100644 --- a/src/Event/TransitionWasFailed.php +++ b/src/Event/TransitionWasFailed.php @@ -2,16 +2,29 @@ namespace Star\Component\State\Event; +use Star\Component\State\StateContext; use Symfony\Contracts\EventDispatcher\Event; +use Throwable; final class TransitionWasFailed extends Event implements StateEvent { private string $transition; - private \Throwable $exception; + private string $previousState; + private string $destinationState; + private StateContext $context; + private Throwable $exception; - public function __construct(string $transition, \Throwable $exception) - { + public function __construct( + string $transition, + string $previousState, + string $destinationState, + StateContext $context, + Throwable $exception + ) { $this->transition = $transition; + $this->previousState = $previousState; + $this->destinationState = $destinationState; + $this->context = $context; $this->exception = $exception; } @@ -20,6 +33,21 @@ public function transition(): string return $this->transition; } + public function getPreviousState(): string + { + return $this->previousState; + } + + public function getDestinationState(): string + { + return $this->destinationState; + } + + public function getContext(): StateContext + { + return $this->context; + } + public function exception(): \Throwable { return $this->exception; diff --git a/src/Event/TransitionWasRequested.php b/src/Event/TransitionWasRequested.php index 9b93983..e558ac4 100644 --- a/src/Event/TransitionWasRequested.php +++ b/src/Event/TransitionWasRequested.php @@ -2,19 +2,45 @@ namespace Star\Component\State\Event; +use Star\Component\State\StateContext; use Symfony\Contracts\EventDispatcher\Event; final class TransitionWasRequested extends Event implements StateEvent { private string $transition; + private string $previousState; + private string $destinationState; + private StateContext $context; - public function __construct(string $transition) - { + public function __construct( + string $transition, + string $previousState, + string $destinationState, + StateContext $context + ) { $this->transition = $transition; + $this->previousState = $previousState; + $this->destinationState = $destinationState; + $this->context = $context; } public function transition(): string { return $this->transition; } + + public function getPreviousState(): string + { + return $this->previousState; + } + + public function getDestinationState(): string + { + return $this->destinationState; + } + + public function getContext(): StateContext + { + return $this->context; + } } diff --git a/src/Event/TransitionWasSuccessful.php b/src/Event/TransitionWasSuccessful.php index 24be7dd..26fd9fe 100644 --- a/src/Event/TransitionWasSuccessful.php +++ b/src/Event/TransitionWasSuccessful.php @@ -2,19 +2,45 @@ namespace Star\Component\State\Event; +use Star\Component\State\StateContext; use Symfony\Contracts\EventDispatcher\Event; final class TransitionWasSuccessful extends Event implements StateEvent { private string $transition; + private string $previousState; + private string $destinationState; + private StateContext $context; - public function __construct(string $transition) - { + public function __construct( + string $transition, + string $previousState, + string $destinationState, + StateContext $context + ) { $this->transition = $transition; + $this->previousState = $previousState; + $this->destinationState = $destinationState; + $this->context = $context; } public function transition(): string { return $this->transition; } + + public function getPreviousState(): string + { + return $this->previousState; + } + + public function getDestinationState(): string + { + return $this->destinationState; + } + + public function getContext(): StateContext + { + return $this->context; + } } diff --git a/src/InvalidStateTransitionException.php b/src/InvalidStateTransitionException.php index 49d5c31..0ba254a 100644 --- a/src/InvalidStateTransitionException.php +++ b/src/InvalidStateTransitionException.php @@ -2,11 +2,16 @@ namespace Star\Component\State; +use Star\Component\State\Context\ObjectAdapterContext; +use Star\Component\State\Context\StringAdapterContext; +use function is_scalar; +use function sprintf; + final class InvalidStateTransitionException extends \Exception { /** * @param string $transition - * @param string|object $context + * @param string|object|StateContext $context * @param string $currentState * * @return static @@ -16,15 +21,19 @@ public static function notAllowedTransition( $context, string $currentState ): self { - if (\is_object($context)) { - $context = \get_class($context); + if (is_scalar($context)) { + $context = new StringAdapterContext((string) $context, true); + } + + if (!$context instanceof StateContext) { + $context = new ObjectAdapterContext($context, true); } - return new static( - \sprintf( + return new self( + sprintf( "The transition '%s' is not allowed when context '%s' is in state '%s'.", $transition, - $context, + $context->toStateContextIdentifier(), $currentState ) ); diff --git a/src/StateContext.php b/src/StateContext.php new file mode 100644 index 0000000..c51b19a --- /dev/null +++ b/src/StateContext.php @@ -0,0 +1,17 @@ +currentState; + $transition = $this->states->getTransition($transitionName); + $newState = $transition->getDestinationState(); $this->listeners->dispatch( StateEventStore::BEFORE_TRANSITION, - new TransitionWasRequested($transitionName) + new TransitionWasRequested( + $transitionName, + $previous, + $newState, + $context, + ) ); - $transition = $this->states->getTransition($transitionName); $callback->beforeStateChange($context, $this); - $newState = $transition->getDestinationState(); $allowed = $this->states->transitionStartsFrom($transitionName, $this->currentState); if (!$allowed) { $exception = InvalidStateTransitionException::notAllowedTransition( @@ -64,7 +80,13 @@ public function transit( $this->listeners->dispatch( StateEventStore::FAILURE_TRANSITION, - new TransitionWasFailed($transitionName, $exception) + new TransitionWasFailed( + $transitionName, + $previous, + $newState, + $context, + $exception, + ) ); $newState = $callback->onFailure($exception, $context, $this); @@ -76,7 +98,12 @@ public function transit( $this->listeners->dispatch( StateEventStore::AFTER_TRANSITION, - new TransitionWasSuccessful($transitionName) + new TransitionWasSuccessful( + $transitionName, + $previous, + $newState, + $context, + ) ); return $this->currentState; diff --git a/tests/Callbacks/BufferStateChangesTest.php b/tests/Callbacks/BufferStateChangesTest.php index 18ace69..9b37f46 100644 --- a/tests/Callbacks/BufferStateChangesTest.php +++ b/tests/Callbacks/BufferStateChangesTest.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\TestCase; use Star\Component\State\Builder\StateBuilder; -class BufferStateChangesTest extends TestCase +final class BufferStateChangesTest extends TestCase { public function test_it_should_buffer_context_as_object(): void { @@ -15,11 +15,11 @@ public function test_it_should_buffer_context_as_object(): void ->create(''); $buffer->beforeStateChange( - (object) [], + (object)[], $machine ); $buffer->afterStateChange( - (object) [], + (object)[], $machine ); diff --git a/tests/Context/TestStubContext.php b/tests/Context/TestStubContext.php new file mode 100644 index 0000000..34746ca --- /dev/null +++ b/tests/Context/TestStubContext.php @@ -0,0 +1,24 @@ +identifier = $identifier; + } + + public function toStateContextIdentifier(): string + { + return $this->identifier; + } +} diff --git a/tests/StateBuilderTest.php b/tests/StateBuilderTest.php index e4ee0c4..1e27d4d 100644 --- a/tests/StateBuilderTest.php +++ b/tests/StateBuilderTest.php @@ -17,7 +17,7 @@ public function test_it_should_allow_to_transition_to_next_state_when_multiple_s ->create('from'); self::assertTrue($machine->isInState('from')); - $machine->transit('t1', 'context'); + $machine->transit('t1', new TestContext()); self::assertTrue($machine->isInState('to')); } @@ -31,7 +31,7 @@ public function test_it_should_return_whether_the_current_state_has_attribute_af self::assertTrue($machine->isInState('from')); self::assertTrue($machine->hasAttribute('attr')); - $machine->transit('t1', 'context'); + $machine->transit('t1', new TestContext()); self::assertTrue($machine->isInState('to')); self::assertFalse($machine->hasAttribute('attr')); diff --git a/tests/StateMachineTest.php b/tests/StateMachineTest.php index 203ecb4..5feb8b0 100644 --- a/tests/StateMachineTest.php +++ b/tests/StateMachineTest.php @@ -5,10 +5,14 @@ use PHPUnit\Framework\TestCase; use Star\Component\State\Callbacks\BufferStateChanges; use Star\Component\State\Callbacks\TransitionCallback; +use Star\Component\State\Builder\StateBuilder; +use Star\Component\State\Context\ObjectAdapterContext; +use Star\Component\State\Context\StringAdapterContext; +use Star\Component\State\Context\TestStubContext; use Star\Component\State\Event\StateEventStore; use Star\Component\State\Event\TransitionWasFailed; -use Star\Component\State\Event\TransitionWasSuccessful; use Star\Component\State\Event\TransitionWasRequested; +use Star\Component\State\Event\TransitionWasSuccessful; use Star\Component\State\Stub\EventRegistrySpy; use Star\Component\State\Transitions\OneToOneTransition; use stdClass; @@ -75,7 +79,7 @@ public function test_it_should_throw_exception_with_class_context_when_transitio $this->expectExceptionMessage( "The transition 't' is not allowed when context 'stdClass' is in state 'current'." ); - $this->machine->transit('t', new stdClass); + $this->machine->transit('t', new ObjectAdapterContext(new stdClass, false)); } public function test_it_should_throw_exception_with_context_as_string_when_transition_not_allowed(): void @@ -87,7 +91,7 @@ public function test_it_should_throw_exception_with_context_as_string_when_trans $this->expectExceptionMessage( "The transition 'transition' is not allowed when context 'c' is in state 'current'." ); - $this->machine->transit('transition', 'c'); + $this->machine->transit('transition', new StringAdapterContext('c')); } public function test_state_can_have_attribute(): void @@ -101,6 +105,9 @@ public function test_it_should_visit_the_transitions(): void { $visitor = $this->createStub(TransitionVisitor::class); $registry = $this->createMock(StateRegistry::class); + $machine = new StateMachine('', $registry, $this->events); + $visitor = $this->createStub(TransitionVisitor::class); + $registry ->expects($this->once()) ->method('acceptTransitionVisitor') @@ -122,7 +129,7 @@ public function test_it_should_dispatch_an_event_before_a_transition_has_failed( { $this->registry->addTransition(new OneToOneTransition('t', 'from', 'to')); try { - $this->machine->transit('t', 'context'); + $this->machine->transit('t', new TestContext()); $this->fail('An exception should have been thrown'); } catch (Throwable $exception) { // silence it @@ -161,4 +168,70 @@ public function test_it_should_invoke_before_state_change_callback(): void $buffer->flushBuffer(), ); } + + public function test_it_should_allow_to_transit_using_state_context(): void + { + $machine = StateBuilder::build(null, $this->events) + ->allowTransition('activate', 'left', 'right') + ->create('left'); + $context = new TestStubContext('post'); + + $machine->transit( + 'activate', + $context, + $callback = new BufferStateChanges(), + ); + + self::assertSame( + [ + 'post' => [ + 'beforeStateChange', + 'afterStateChange', + ], + ], + $callback->flushBuffer() + ); + self::assertCount( + 1, + $this->events->getDispatchedEvents(StateEventStore::BEFORE_TRANSITION) + ); + self::assertCount( + 1, + $this->events->getDispatchedEvents(StateEventStore::AFTER_TRANSITION) + ); + } + + public function test_it_should_allow_handle_failure_with_state_context(): void + { + $machine = StateBuilder::build(null, $this->events) + ->allowTransition('activate', 'right', 'left') + ->create('left'); + $context = new TestStubContext('post'); + $callback = new BufferStateChanges(); + + $machine->transit( + 'activate', + $context, + $callback + ); + + self::assertSame( + [ + 'post' => [ + 'beforeStateChange', + InvalidStateTransitionException::class, + 'afterStateChange', + ], + ], + $callback->flushBuffer() + ); + self::assertCount( + 1, + $this->events->getDispatchedEvents(StateEventStore::BEFORE_TRANSITION) + ); + self::assertCount( + 1, + $this->events->getDispatchedEvents(StateEventStore::AFTER_TRANSITION) + ); + } } diff --git a/tests/StateMetadataTest.php b/tests/StateMetadataTest.php index af4df95..80a9515 100644 --- a/tests/StateMetadataTest.php +++ b/tests/StateMetadataTest.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\TestCase; use Star\Component\State\Builder\StateBuilder; use Star\Component\State\Callbacks\NullCallback; +use Star\Component\State\Context\TestStubContext; final class StateMetadataTest extends TestCase { @@ -24,7 +25,7 @@ public function test_it_should_check_if_has_attribute(): void public function test_it_should_transit(): void { $metadata = new CustomMetadata('from'); - $new = $metadata->transit('t1', 'context'); + $new = $metadata->transit('t1', new TestStubContext('context')); self::assertTrue($new->isInState('to')); } @@ -38,7 +39,7 @@ public function test_it_should_use_the_failure_callback_on_transit(): void ); $metadata->transit( 't1', - 'context', + new TestStubContext('context'), new NullCallback() ); } diff --git a/tests/TestContext.php b/tests/TestContext.php index a6f33ef..7b338f0 100644 --- a/tests/TestContext.php +++ b/tests/TestContext.php @@ -2,6 +2,14 @@ namespace Star\Component\State; -final class TestContext +/** + * @deprecated Will be removed in 4.0. You should use StateContext. + * @see StateContext + */ +final class TestContext implements StateContext { + public function toStateContextIdentifier(): string + { + return 'context'; + } }