├── docker
├── php.ini.dist
└── Dockerfile
├── .gitignore
├── docker-compose.yml.dist
├── .github
└── PULL_REQUEST_TEMPLATE.md
├── .travis.yml
├── .php_cs
├── tests
├── bootstrap.php
└── StateMachine
│ ├── StateMachineBuilderTest.php
│ ├── ActionLogger.php
│ ├── ForkAndJoinTest.php
│ └── StateMachineTest.php
├── phpunit.xml
├── src
├── StateMachine
│ ├── ConstraintException.php
│ ├── StateNotFoundException.php
│ ├── StateMachineNotStartedException.php
│ ├── StateMachineAlreadyShutdownException.php
│ ├── StateMachineAlreadyStartedException.php
│ ├── StateMachineEvents.php
│ ├── TransitionLog.php
│ ├── StateMachineEvent.php
│ ├── StateMachineInterface.php
│ ├── StateMachineBuilder.php
│ └── StateMachine.php
├── Event
│ ├── EventInterface.php
│ └── Event.php
├── State
│ ├── StateInterface.php
│ ├── AutomaticTransitionInterface.php
│ ├── TransitionalStateInterface.php
│ ├── ParentStateInterface.php
│ ├── StateActionInterface.php
│ ├── FinalState.php
│ ├── TransitionalStateTrait.php
│ ├── ForkState.php
│ ├── JoinState.php
│ ├── AutomaticTransitionTrait.php
│ ├── StateCollection.php
│ ├── StateActionTrait.php
│ ├── InitialState.php
│ └── State.php
├── Transition
│ ├── ActionRunnerInterface.php
│ ├── GuardEvaluatorInterface.php
│ ├── TransitionInterface.php
│ └── Transition.php
└── Resources
│ └── config
│ └── serialization.xml
├── composer.json
├── LICENSE
└── README.md
/docker/php.ini.dist:
--------------------------------------------------------------------------------
1 | ;memory_limit=256M
2 | ;xdebug.remote_autostart=on
3 | ;xdebug.remote_port=9000
4 | ;xdebug.remote_host=172.17.0.1
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.buildpath
2 | /.composer/
3 | /.idea/
4 | /.php_cs.cache
5 | /.project
6 | /.settings/
7 | /composer.lock
8 | /composer.phar
9 | /docker-compose.yml
10 | /docker/php.ini
11 | /php-cs-fixer-v2.phar
12 | /vendor/
13 | *.iml
14 |
--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM phpmentors/php-app:php72
2 |
3 | RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
4 | RUN apt-get update -y
5 | RUN apt-get upgrade -y
6 |
7 | # Other tools
8 | RUN apt-get install -y less unzip
9 |
--------------------------------------------------------------------------------
/docker-compose.yml.dist:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | app:
4 | build: docker
5 | network_mode: bridge
6 | volumes:
7 | - .:/var/app
8 | working_dir: /var/app
9 | environment:
10 | TZ: "Asia/Tokyo"
11 | LANG: "ja_JP.UTF-8"
12 | # PHP_INI: "docker/php.ini"
13 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | | Q | A
2 | | ------------- | ---
3 | | Branch? | master for new features / 2.5 for fixes
4 | | Bug fix? | yes/no
5 | | New feature? | yes/no
6 | | BC breaks? | yes/no
7 | | Deprecations? | yes/no
8 | | Tests pass? | yes/no
9 | | Fixed tickets | comma-separated list of tickets fixed by the PR, if any
10 | | License | BSD-2-Clause
11 |
12 | [the description of this PR]
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | sudo: false
4 |
5 | cache:
6 | directories:
7 | - $HOME/.composer/cache
8 |
9 | php:
10 | - 7.0
11 | - 7.1
12 | - 7.2
13 | - 7.3
14 |
15 | matrix:
16 | fast_finish: true
17 |
18 | before_script:
19 | - rm -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini
20 | - composer self-update
21 | - COMPOSER_MEMORY_LIMIT=-1 travis_retry composer install --no-interaction
22 |
23 | script:
24 | - ./vendor/bin/phpunit
25 |
--------------------------------------------------------------------------------
/.php_cs:
--------------------------------------------------------------------------------
1 | in(__DIR__.'/src')
4 | ->in(__DIR__.'/tests')
5 | ;
6 |
7 | return PhpCsFixer\Config::create()
8 | ->setRules([
9 | '@Symfony' => true,
10 | 'no_useless_return' => false,
11 | 'blank_line_after_opening_tag' => false,
12 | 'ordered_imports' => true,
13 | 'phpdoc_no_empty_return' => false,
14 | 'yoda_style' => false,
15 | ])
16 | ->setFinder($finder)
17 | ;
18 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | error_reporting(E_ALL);
14 |
15 | require dirname(__DIR__).'/vendor/autoload.php';
16 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 | ./tests
11 |
12 |
13 |
14 |
15 |
16 | ./src
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/StateMachine/ConstraintException.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\StateMachine;
14 |
15 | /**
16 | * @since Class available since Release 3.0.0
17 | */
18 | class ConstraintException extends \LogicException
19 | {
20 | }
21 |
--------------------------------------------------------------------------------
/src/StateMachine/StateNotFoundException.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\StateMachine;
14 |
15 | /**
16 | * @since Class available since Release 2.0.0
17 | */
18 | class StateNotFoundException extends \LogicException
19 | {
20 | }
21 |
--------------------------------------------------------------------------------
/src/StateMachine/StateMachineNotStartedException.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\StateMachine;
14 |
15 | /**
16 | * @since Class available since Release 2.1.0
17 | */
18 | class StateMachineNotStartedException extends \LogicException
19 | {
20 | }
21 |
--------------------------------------------------------------------------------
/src/StateMachine/StateMachineAlreadyShutdownException.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\StateMachine;
14 |
15 | /**
16 | * @since Class available since Release 2.0.0
17 | */
18 | class StateMachineAlreadyShutdownException extends \RuntimeException
19 | {
20 | }
21 |
--------------------------------------------------------------------------------
/src/StateMachine/StateMachineAlreadyStartedException.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\StateMachine;
14 |
15 | /**
16 | * @since Class available since Release 2.1.0
17 | */
18 | class StateMachineAlreadyStartedException extends \LogicException
19 | {
20 | }
21 |
--------------------------------------------------------------------------------
/src/Event/EventInterface.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\Event;
14 |
15 | /**
16 | * @since Class available since Release 2.0.0
17 | */
18 | interface EventInterface
19 | {
20 | /**
21 | * Gets the ID of the event.
22 | *
23 | * @return string
24 | */
25 | public function getEventId();
26 | }
27 |
--------------------------------------------------------------------------------
/src/State/StateInterface.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\State;
14 |
15 | /**
16 | * @since Class available since Release 2.0.0
17 | */
18 | interface StateInterface
19 | {
20 | /**
21 | * Gets the ID of the state.
22 | *
23 | * @return string
24 | */
25 | public function getStateId();
26 | }
27 |
--------------------------------------------------------------------------------
/src/State/AutomaticTransitionInterface.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\State;
14 |
15 | /**
16 | * @since Interface available since Release 3.0.0
17 | */
18 | interface AutomaticTransitionInterface
19 | {
20 | /**
21 | * @return EventInterface|null
22 | */
23 | public function getAutomaticTransitionEvent();
24 | }
25 |
--------------------------------------------------------------------------------
/src/StateMachine/StateMachineEvents.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\StateMachine;
14 |
15 | /**
16 | * @since Class available since Release 2.1.0
17 | */
18 | final class StateMachineEvents
19 | {
20 | const EVENT_PROCESS = 'statemachine.process';
21 | const EVENT_EXIT = 'statemachine.exit';
22 | const EVENT_TRANSITION = 'statemachine.transition';
23 | const EVENT_ENTRY = 'statemachine.entry';
24 | const EVENT_DO = 'statemachine.do';
25 | }
26 |
--------------------------------------------------------------------------------
/src/Event/Event.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\Event;
14 |
15 | /**
16 | * @since Class available since Release 3.0.0
17 | */
18 | class Event implements EventInterface
19 | {
20 | /**
21 | * @var string
22 | */
23 | private $eventId;
24 |
25 | /**
26 | * @param string $eventId
27 | */
28 | public function __construct(string $eventId)
29 | {
30 | $this->eventId = $eventId;
31 | }
32 |
33 | /**
34 | * {@inheritdoc}
35 | */
36 | public function getEventId()
37 | {
38 | return $this->eventId;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "phpmentors/stagehand-fsm",
3 | "type": "library",
4 | "description": "A finite state machine",
5 | "keywords": ["fsm", "state machine"],
6 | "homepage": "https://github.com/phpmentors-jp/stagehand-fsm/wiki",
7 | "license": "BSD-2-Clause",
8 | "authors": [
9 | {
10 | "name": "KUBO Atsuhiro",
11 | "email": "kubo@iteman.jp"
12 | }
13 | ],
14 | "require": {
15 | "php": ">=7.0.8",
16 | "symfony/event-dispatcher": "~2.8|~3.0|~4.0"
17 | },
18 | "require-dev": {
19 | "phpunit/phpunit": "~6.5"
20 | },
21 | "autoload": {
22 | "psr-4": {
23 | "Stagehand\\FSM\\": "src/"
24 | }
25 | },
26 | "autoload-dev": {
27 | "psr-4": {
28 | "Stagehand\\FSM\\": "tests/"
29 | }
30 | },
31 | "extra": {
32 | "branch-alias": {
33 | "dev-master": "3.0.x-dev"
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/State/TransitionalStateInterface.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\State;
14 |
15 | use Stagehand\FSM\Event\EventInterface;
16 |
17 | /**
18 | * @since Class available since Release 2.2.0
19 | */
20 | interface TransitionalStateInterface extends StateInterface
21 | {
22 | /**
23 | * @param EventInterface $event
24 | */
25 | public function addTransitionEvent(EventInterface $event);
26 |
27 | /**
28 | * @param string $eventId
29 | *
30 | * @return EventInterface|null
31 | *
32 | * @since Method available since Release 3.0.0
33 | */
34 | public function getTransitionEvent($eventId);
35 | }
36 |
--------------------------------------------------------------------------------
/src/Transition/ActionRunnerInterface.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\Transition;
14 |
15 | use Stagehand\FSM\Event\EventInterface;
16 | use Stagehand\FSM\StateMachine\StateMachineInterface;
17 |
18 | /**
19 | * @since Class available since Release 3.0.0
20 | */
21 | interface ActionRunnerInterface
22 | {
23 | /**
24 | * @param EventInterface $event
25 | * @param mixed $payload
26 | * @param StateMachineInterface $stateMachine
27 | * @param TransitionInterface|null $transition
28 | */
29 | public function run(EventInterface $event, $payload, StateMachineInterface $stateMachine, TransitionInterface $transition = null);
30 | }
31 |
--------------------------------------------------------------------------------
/src/Transition/GuardEvaluatorInterface.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\Transition;
14 |
15 | use Stagehand\FSM\Event\EventInterface;
16 | use Stagehand\FSM\StateMachine\StateMachineInterface;
17 |
18 | /**
19 | * @since Class available since Release 3.0.0
20 | */
21 | interface GuardEvaluatorInterface
22 | {
23 | /**
24 | * @param EventInterface $event
25 | * @param mixed $payload
26 | * @param StateMachineInterface $stateMachine
27 | * @param TransitionInterface $transition
28 | *
29 | * @return bool
30 | */
31 | public function evaluate(EventInterface $event, $payload, StateMachineInterface $stateMachine, TransitionInterface $transition);
32 | }
33 |
--------------------------------------------------------------------------------
/src/Transition/TransitionInterface.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\Transition;
14 |
15 | use Stagehand\FSM\Event\EventInterface;
16 | use Stagehand\FSM\State\StateInterface;
17 | use Stagehand\FSM\State\TransitionalStateInterface;
18 |
19 | /**
20 | * @since Class available since Release 3.0.0
21 | */
22 | interface TransitionInterface
23 | {
24 | /**
25 | * @return StateInterface
26 | */
27 | public function getToState(): StateInterface;
28 |
29 | /**
30 | * @return TransitionalStateInterface
31 | */
32 | public function getFromState(): TransitionalStateInterface;
33 |
34 | /**
35 | * @return EventInterface
36 | */
37 | public function getEvent(): EventInterface;
38 | }
39 |
--------------------------------------------------------------------------------
/src/State/ParentStateInterface.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\State;
14 |
15 | use Stagehand\FSM\StateMachine\StateMachineInterface;
16 |
17 | interface ParentStateInterface
18 | {
19 | /**
20 | * @return bool
21 | */
22 | public function hasChildren();
23 |
24 | /**
25 | * @param StateMachineInterface $stateMachine
26 | */
27 | public function addChild(StateMachineInterface $stateMachine);
28 |
29 | /**
30 | * @param string $stateMachineId
31 | *
32 | * @return StateMachineInterface|null
33 | */
34 | public function getChild($stateMachineId);
35 |
36 | /**
37 | * @return StateMachineInterface[]
38 | */
39 | public function getChildren();
40 | }
41 |
--------------------------------------------------------------------------------
/src/State/StateActionInterface.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\State;
14 |
15 | use Stagehand\FSM\Event\EventInterface;
16 |
17 | /**
18 | * @since Interface available since Release 3.0.0
19 | */
20 | interface StateActionInterface
21 | {
22 | const EVENT_ENTRY = '__ENTRY__';
23 | const EVENT_EXIT = '__EXIT__';
24 | const EVENT_DO = '__DO__';
25 |
26 | /**
27 | * @return EventInterface
28 | */
29 | public function getEntryEvent(): EventInterface;
30 |
31 | /**
32 | * @return EventInterface
33 | */
34 | public function getExitEvent(): EventInterface;
35 |
36 | /**
37 | * @return EventInterface
38 | */
39 | public function getDoEvent(): EventInterface;
40 | }
41 |
--------------------------------------------------------------------------------
/src/State/FinalState.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\State;
14 |
15 | /**
16 | * @since Class available since Release 2.0.0
17 | */
18 | class FinalState implements StateInterface
19 | {
20 | /**
21 | * @var string
22 | *
23 | * @since Property available since Release 3.0.0
24 | */
25 | private $stateId;
26 |
27 | /**
28 | * @param string $stateId
29 | *
30 | * @since Method available since Release 2.1.0
31 | */
32 | public function __construct(string $stateId)
33 | {
34 | $this->stateId = $stateId;
35 | }
36 |
37 | /**
38 | * {@inheritdoc}
39 | */
40 | public function getStateId()
41 | {
42 | return $this->stateId;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/State/TransitionalStateTrait.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\State;
14 |
15 | use Stagehand\FSM\Event\EventInterface;
16 |
17 | /**
18 | * @since Trait available since Release 3.0.0
19 | */
20 | trait TransitionalStateTrait
21 | {
22 | /**
23 | * @var array
24 | */
25 | protected $events = [];
26 |
27 | /**
28 | * @param string $eventId
29 | *
30 | * @return EventInterface|null
31 | */
32 | public function getTransitionEvent($eventId)
33 | {
34 | return $this->events[$eventId] ?? null;
35 | }
36 |
37 | /**
38 | * @param EventInterface $event
39 | */
40 | public function addTransitionEvent(EventInterface $event)
41 | {
42 | $this->events[$event->getEventId()] = $event;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/State/ForkState.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\State;
14 |
15 | /**
16 | * @since Class available since Release 3.0.0
17 | */
18 | class ForkState implements TransitionalStateInterface, AutomaticTransitionInterface, StateActionInterface
19 | {
20 | use AutomaticTransitionTrait;
21 | use StateActionTrait;
22 |
23 | /**
24 | * @var string
25 | */
26 | private $stateId;
27 |
28 | /**
29 | * @param string $stateId
30 | */
31 | public function __construct(string $stateId)
32 | {
33 | $this->stateId = $stateId;
34 | $this->initializeStateActionEvents();
35 | }
36 |
37 | /**
38 | * {@inheritdoc}
39 | */
40 | public function getStateId()
41 | {
42 | return $this->stateId;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/State/JoinState.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\State;
14 |
15 | /**
16 | * @since Class available since Release 3.0.0
17 | */
18 | class JoinState implements TransitionalStateInterface, AutomaticTransitionInterface, StateActionInterface
19 | {
20 | use AutomaticTransitionTrait;
21 | use StateActionTrait;
22 |
23 | /**
24 | * @var string
25 | */
26 | private $stateId;
27 |
28 | /**
29 | * @param string $stateId
30 | */
31 | public function __construct(string $stateId)
32 | {
33 | $this->stateId = $stateId;
34 | $this->initializeStateActionEvents();
35 | }
36 |
37 | /**
38 | * {@inheritdoc}
39 | */
40 | public function getStateId()
41 | {
42 | return $this->stateId;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2006-2008, 2011-2017 KUBO Atsuhiro ,
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice,
8 | this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above copyright
10 | notice, this list of conditions and the following disclaimer in the
11 | documentation and/or other materials provided with the distribution.
12 |
13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
14 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
17 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
23 | POSSIBILITY OF SUCH DAMAGE.
24 |
--------------------------------------------------------------------------------
/src/State/AutomaticTransitionTrait.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\State;
14 |
15 | use Stagehand\FSM\Event\EventInterface;
16 |
17 | /**
18 | * @since Trait available since Release 3.0.0
19 | */
20 | trait AutomaticTransitionTrait
21 | {
22 | /**
23 | * @var EventInterface
24 | */
25 | private $transitionEvent;
26 |
27 | /**
28 | * @param EventInterface $event
29 | */
30 | public function addTransitionEvent(EventInterface $event)
31 | {
32 | $this->transitionEvent = $event;
33 | }
34 |
35 | /**
36 | * @param string $eventId
37 | *
38 | * @return EventInterface|null
39 | */
40 | public function getTransitionEvent($eventId)
41 | {
42 | if ($this->transitionEvent === null) {
43 | return null;
44 | } else {
45 | return $this->transitionEvent->getEventId() == $eventId ? $this->transitionEvent : null;
46 | }
47 | }
48 |
49 | /**
50 | * @return EventInterface|null
51 | */
52 | public function getAutomaticTransitionEvent()
53 | {
54 | return $this->transitionEvent;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/State/StateCollection.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\State;
14 |
15 | /**
16 | * @since Class available since Release 2.2.0
17 | */
18 | class StateCollection
19 | {
20 | /**
21 | * @var array
22 | */
23 | private $states = [];
24 |
25 | /**
26 | * @param array $states
27 | */
28 | public function __construct(array $states = [])
29 | {
30 | $this->states = $states;
31 | }
32 |
33 | public function add(StateInterface $entity)
34 | {
35 | $this->states[$entity->getStateId()] = $entity;
36 | }
37 |
38 | public function get($key)
39 | {
40 | if (array_key_exists($key, $this->states)) {
41 | return $this->states[$key];
42 | } else {
43 | return null;
44 | }
45 | }
46 |
47 | public function remove(StateInterface $entity)
48 | {
49 | if (array_key_exists($entity->getStateId(), $this->states)) {
50 | }
51 | }
52 |
53 | public function count()
54 | {
55 | return count($this->states);
56 | }
57 |
58 | public function getIterator()
59 | {
60 | return new \ArrayIterator($this->states);
61 | }
62 |
63 | public function toArray()
64 | {
65 | return $this->states;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/State/StateActionTrait.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\State;
14 |
15 | use Stagehand\FSM\Event\Event;
16 | use Stagehand\FSM\Event\EventInterface;
17 |
18 | /**
19 | * @since Trait available since Release 3.0.0
20 | */
21 | trait StateActionTrait
22 | {
23 | /**
24 | * @var EventInterface
25 | */
26 | protected $entryEvent;
27 |
28 | /**
29 | * @var EventInterface
30 | */
31 | protected $exitEvent;
32 |
33 | /**
34 | * @var EventInterface
35 | */
36 | protected $doEvent;
37 |
38 | /**
39 | * @return EventInterface
40 | */
41 | public function getEntryEvent(): EventInterface
42 | {
43 | return $this->entryEvent;
44 | }
45 |
46 | /**
47 | * @return EventInterface
48 | */
49 | public function getExitEvent(): EventInterface
50 | {
51 | return $this->exitEvent;
52 | }
53 |
54 | /**
55 | * @return EventInterface
56 | */
57 | public function getDoEvent(): EventInterface
58 | {
59 | return $this->doEvent;
60 | }
61 |
62 | protected function initializeStateActionEvents()
63 | {
64 | $this->entryEvent = new Event(StateActionInterface::EVENT_ENTRY);
65 | $this->exitEvent = new Event(StateActionInterface::EVENT_EXIT);
66 | $this->doEvent = new Event(StateActionInterface::EVENT_DO);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/State/InitialState.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\State;
14 |
15 | use Stagehand\FSM\Event\EventInterface;
16 |
17 | /**
18 | * @since Class available since Release 2.0.0
19 | */
20 | class InitialState implements TransitionalStateInterface
21 | {
22 | /**
23 | * @var EventInterface
24 | *
25 | * @since Property available since Release 3.0.0
26 | */
27 | private $transitionEvent;
28 |
29 | /**
30 | * @var string
31 | *
32 | * @since Property available since Release 3.0.0
33 | */
34 | private $stateId;
35 |
36 | /**
37 | * @param string $stateId
38 | */
39 | public function __construct(string $stateId)
40 | {
41 | $this->stateId = $stateId;
42 | }
43 |
44 | /**
45 | * {@inheritdoc}
46 | */
47 | public function getTransitionEvent($eventId)
48 | {
49 | if ($this->transitionEvent === null) {
50 | return null;
51 | } else {
52 | return $this->transitionEvent->getEventId() == $eventId ? $this->transitionEvent : null;
53 | }
54 | }
55 |
56 | /**
57 | * {@inheritdoc}
58 | */
59 | public function getStateId()
60 | {
61 | return $this->stateId;
62 | }
63 |
64 | /**
65 | * {@inheritdoc}
66 | */
67 | public function addTransitionEvent(EventInterface $event)
68 | {
69 | $this->transitionEvent = $event;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Transition/Transition.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\Transition;
14 |
15 | use Stagehand\FSM\Event\EventInterface;
16 | use Stagehand\FSM\State\StateInterface;
17 | use Stagehand\FSM\State\TransitionalStateInterface;
18 |
19 | /**
20 | * @since Class available since Release 3.0.0
21 | */
22 | class Transition implements TransitionInterface
23 | {
24 | /**
25 | * @var StateInterface
26 | */
27 | private $toState;
28 |
29 | /**
30 | * @var TransitionalStateInterface
31 | */
32 | private $fromState;
33 |
34 | /**
35 | * @var EventInterface
36 | */
37 | private $event;
38 |
39 | /**
40 | * @param StateInterface $toState
41 | * @param TransitionalStateInterface $fromState
42 | * @param EventInterface $event
43 | */
44 | public function __construct(StateInterface $toState, TransitionalStateInterface $fromState, EventInterface $event)
45 | {
46 | $this->toState = $toState;
47 | $this->fromState = $fromState;
48 | $this->event = $event;
49 |
50 | $this->fromState->addTransitionEvent($event);
51 | }
52 |
53 | /**
54 | * {@inheritdoc}
55 | */
56 | public function getToState(): StateInterface
57 | {
58 | return $this->toState;
59 | }
60 |
61 | /**
62 | * {@inheritdoc}
63 | */
64 | public function getFromState(): TransitionalStateInterface
65 | {
66 | return $this->fromState;
67 | }
68 |
69 | /**
70 | * {@inheritdoc}
71 | */
72 | public function getEvent(): EventInterface
73 | {
74 | return $this->event;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/State/State.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\State;
14 |
15 | use Stagehand\FSM\StateMachine\StateMachineInterface;
16 |
17 | /**
18 | * @since Class available since Release 0.1.0
19 | */
20 | class State implements TransitionalStateInterface, StateActionInterface, ParentStateInterface
21 | {
22 | use StateActionTrait;
23 | use TransitionalStateTrait;
24 |
25 | /**
26 | * @var string
27 | */
28 | private $stateId;
29 |
30 | /**
31 | * @var StateMachineInterface[]
32 | */
33 | private $children = [];
34 |
35 | /**
36 | * @param string $stateId
37 | */
38 | public function __construct($stateId)
39 | {
40 | $this->stateId = $stateId;
41 | $this->initializeStateActionEvents();
42 | }
43 |
44 | /**
45 | * {@inheritdoc}
46 | */
47 | public function getStateId()
48 | {
49 | return $this->stateId;
50 | }
51 |
52 | /**
53 | * {@inheritdoc}
54 | */
55 | public function hasChildren()
56 | {
57 | return count($this->children) > 0;
58 | }
59 |
60 | /**
61 | * {@inheritdoc}
62 | */
63 | public function addChild(StateMachineInterface $stateMachine)
64 | {
65 | $this->children[$stateMachine->getStateMachineId()] = $stateMachine;
66 | }
67 |
68 | /**
69 | * {@inheritdoc}
70 | */
71 | public function getChild($stateMachineId)
72 | {
73 | return $this->children[$stateMachineId] ?? null;
74 | }
75 |
76 | /**
77 | * {@inheritdoc}
78 | */
79 | public function getChildren()
80 | {
81 | return $this->children;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/StateMachine/TransitionLog.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\StateMachine;
14 |
15 | use Stagehand\FSM\Event\EventInterface;
16 | use Stagehand\FSM\State\StateInterface;
17 | use Stagehand\FSM\State\TransitionalStateInterface;
18 | use Stagehand\FSM\Transition\TransitionInterface;
19 |
20 | /**
21 | * @since Class available since Release 2.3.0
22 | */
23 | class TransitionLog
24 | {
25 | /**
26 | * @var \DateTime
27 | */
28 | private $transitionDate;
29 |
30 | /**
31 | * @var TransitionInterface
32 | *
33 | * @since Property available since Release 3.0.0
34 | */
35 | private $transition;
36 |
37 | /**
38 | * @param TransitionInterface $transition
39 | * @param \DateTime $transitionDate
40 | */
41 | public function __construct(TransitionInterface $transition, \DateTime $transitionDate)
42 | {
43 | $this->transition = $transition;
44 | $this->transitionDate = $transitionDate;
45 | }
46 |
47 | /**
48 | * @return EventInterface
49 | */
50 | public function getEvent(): EventInterface
51 | {
52 | return $this->transition->getEvent();
53 | }
54 |
55 | /**
56 | * @return TransitionalStateInterface
57 | */
58 | public function getFromState(): TransitionalStateInterface
59 | {
60 | return $this->transition->getFromState();
61 | }
62 |
63 | /**
64 | * @return StateInterface
65 | */
66 | public function getToState(): StateInterface
67 | {
68 | return $this->transition->getToState();
69 | }
70 |
71 | /**
72 | * @return \DateTime
73 | */
74 | public function getTransitionDate()
75 | {
76 | return $this->transitionDate;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Resources/config/serialization.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 | default
10 |
11 |
12 | default
13 |
14 |
15 | default
16 |
17 |
18 | default
19 |
20 |
21 | default
22 |
23 |
24 | default
25 |
26 |
27 |
28 |
29 | default
30 |
31 |
32 |
33 |
34 | default
35 |
36 |
37 | default
38 |
39 |
40 |
41 |
42 | default
43 |
44 |
45 |
46 |
47 | default
48 |
49 |
50 |
51 |
52 | default
53 |
54 |
55 | default
56 |
57 |
58 |
59 |
60 | default
61 |
62 |
63 | default
64 |
65 |
66 | default
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/tests/StateMachine/StateMachineBuilderTest.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\StateMachine;
14 |
15 | use PHPUnit\Framework\TestCase;
16 |
17 | /**
18 | * @since Class available since Release 2.0.0
19 | */
20 | class StateMachineBuilderTest extends TestCase
21 | {
22 | /**
23 | * @test
24 | */
25 | public function raisesAnExceptionIfTheSourceStateIsNotFoundWhenAddingATransition()
26 | {
27 | $stateMachineBuilder = new StateMachineBuilder();
28 | $stateMachineBuilder->addState('baz');
29 |
30 | try {
31 | $stateMachineBuilder->addTransition('foo', 'baz', 'bar');
32 | } catch (StateNotFoundException $e) {
33 | $this->assertThat($e->getMessage(), $this->stringContains('"foo"'));
34 |
35 | return;
36 | }
37 |
38 | $this->fail('An expected exception has not been raised.');
39 | }
40 |
41 | /**
42 | * @test
43 | */
44 | public function raisesAnExceptionIfTheDestinationStateIsNotFoundWhenAddingATransition()
45 | {
46 | $stateMachineBuilder = new StateMachineBuilder();
47 | $stateMachineBuilder->addState('foo');
48 |
49 | try {
50 | $stateMachineBuilder->addTransition('foo', 'baz', 'bar');
51 | } catch (StateNotFoundException $e) {
52 | $this->assertThat($e->getMessage(), $this->stringContains('"baz"'));
53 |
54 | return;
55 | }
56 |
57 | $this->fail('An expected exception has not been raised.');
58 | }
59 |
60 | /**
61 | * @test
62 | *
63 | * @since Method available since Release 2.1.0
64 | */
65 | public function setsTheStateMachineObject()
66 | {
67 | $stateMachine1 = new StateMachine();
68 | $stateMachineBuilder = new StateMachineBuilder($stateMachine1);
69 | $stateMachine2 = $stateMachineBuilder->getStateMachine();
70 |
71 | $this->assertThat($stateMachine2, $this->identicalTo($stateMachine1));
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/StateMachine/StateMachineEvent.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\StateMachine;
14 |
15 | use Stagehand\FSM\Event\EventInterface;
16 | use Stagehand\FSM\State\StateInterface;
17 | use Stagehand\FSM\Transition\TransitionInterface;
18 | use Symfony\Component\EventDispatcher\Event;
19 |
20 | /**
21 | * @since Class available since Release 2.1.0
22 | */
23 | class StateMachineEvent extends Event
24 | {
25 | /**
26 | * @var StateMachine
27 | */
28 | private $stateMachine;
29 |
30 | /**
31 | * @var StateInterface
32 | */
33 | private $state;
34 |
35 | /**
36 | * @var EventInterface|null
37 | */
38 | private $event;
39 |
40 | /**
41 | * @var TransitionInterface|null
42 | *
43 | * @since Property available since Release 3.0.0
44 | */
45 | private $transition;
46 |
47 | /**
48 | * @param StateMachine $stateMachine
49 | * @param StateInterface|null $state
50 | * @param EventInterface|null $event
51 | * @param TransitionInterface|null $transition
52 | */
53 | public function __construct(StateMachine $stateMachine, StateInterface $state = null, EventInterface $event = null, TransitionInterface $transition = null)
54 | {
55 | $this->stateMachine = $stateMachine;
56 | $this->state = $state;
57 | $this->event = $event;
58 | $this->transition = $transition;
59 | }
60 |
61 | /**
62 | * @return StateMachine
63 | */
64 | public function getStateMachine()
65 | {
66 | return $this->stateMachine;
67 | }
68 |
69 | /**
70 | * @return StateInterface
71 | */
72 | public function getState()
73 | {
74 | return $this->state;
75 | }
76 |
77 | /**
78 | * @return EventInterface
79 | */
80 | public function getEvent()
81 | {
82 | return $this->event;
83 | }
84 |
85 | /**
86 | * @return TransitionInterface
87 | *
88 | * @since Method available since Release 3.0.0
89 | */
90 | public function getTransition()
91 | {
92 | return $this->transition;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Stagehand_FSM
2 |
3 | A finite state machine
4 |
5 | [](https://packagist.org/packages/piece/stagehand-fsm)
6 | [](https://packagist.org/packages/piece/stagehand-fsm)
7 | [](https://packagist.org/packages/phpmentors/stagehand-fsm)
8 | [](https://travis-ci.org/phpmentors-jp/stagehand-fsm)
9 |
10 | `Stagehand_FSM` is a [finite state machine](https://en.wikipedia.org/wiki/Finite-state_machine).
11 |
12 | Manual state management makes code complex, decreases intentionality. By using `Stagehand_FSM`, state management code can be declaratively represented in the form of FSM. This makes code simpler, increases intentionality.
13 |
14 | `Stagehand_FSM` can be used as an infrastructure for [domain-specific languages](http://en.wikipedia.org/wiki/Domain-specific_language) (DSLs). Examples are workflow engines such as [Workflower](https://github.com/phpmentors-jp/workflower), pageflow engines such as [PHPMentorsPageflowerBundle](https://github.com/phpmentors-jp/pageflower-bundle).
15 |
16 | ```php
17 | addState('locked');
22 | $stateMachineBuilder->addState('unlocked');
23 | $stateMachineBuilder->setStartState('locked');
24 | $stateMachineBuilder->addTransition('locked', 'insertCoin', 'unlocked');
25 | $stateMachineBuilder->addTransition('unlocked', 'pass', 'locked');
26 | $stateMachine = $stateMachineBuilder->getStateMachine();
27 |
28 | $stateMachine->start();
29 | echo $stateMachine->getCurrentState()->getStateID() . PHP_EOL; // "locked"
30 | $stateMachine->triggerEvent('insertCoin');
31 | echo $stateMachine->getCurrentState()->getStateID() . PHP_EOL; // "unlocked"
32 | $stateMachine->triggerEvent('pass');
33 | echo $stateMachine->getCurrentState()->getStateID() . PHP_EOL; // "locked"
34 | ```
35 |
36 | ## Features
37 |
38 | * Activities (do actions)
39 | * Entry actions
40 | * Exit actions
41 | * Transition actions
42 | * Transition logging
43 | * Guards
44 | * Initial pseudo state
45 | * Final state
46 | * User-defined payload
47 | * User-defined event dispatcher for the state machine events
48 |
49 | ## Installation
50 |
51 | `Stagehand_FSM` can be installed using [Composer](http://getcomposer.org/).
52 |
53 | Add the dependency to `piece/stagehand-fsm` into your `composer.json` file as the following:
54 |
55 | Stable version:
56 |
57 | ```
58 | composer require piece/stagehand-fsm "2.6.*"
59 | ```
60 |
61 | Development version:
62 |
63 | ```
64 | composer require phpmentors/stagehand-fsm "~3.0@dev"
65 | ```
66 |
67 | ## Support
68 |
69 | If you find a bug or have a question, or want to request a feature, create an issue or pull request for it on [Issues](https://github.com/phpmentors-jp/stagehand-fsm/issues).
70 |
71 | ## Copyright
72 |
73 | Copyright (c) 2006-2008, 2011-2018 KUBO Atsuhiro, All rights reserved.
74 |
75 | ## License
76 |
77 | [The BSD 2-Clause License](http://opensource.org/licenses/BSD-2-Clause)
78 |
--------------------------------------------------------------------------------
/tests/StateMachine/ActionLogger.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\StateMachine;
14 |
15 | use Stagehand\FSM\Event\EventInterface;
16 | use Stagehand\FSM\Transition\ActionRunnerInterface;
17 | use Stagehand\FSM\Transition\GuardEvaluatorInterface;
18 | use Stagehand\FSM\Transition\TransitionInterface;
19 |
20 | /**
21 | * @since Class available since Release 3.0.0
22 | */
23 | class ActionLogger implements ActionRunnerInterface, GuardEvaluatorInterface, \ArrayAccess, \Countable
24 | {
25 | /**
26 | * @var GuardEvaluatorInterface
27 | */
28 | private $guardEvaluator;
29 |
30 | /**
31 | * @var array
32 | */
33 | private $runActions = [];
34 |
35 | /**
36 | * @param GuardEvaluatorInterface $guardEvaluator
37 | */
38 | public function setGuardEvaluator(GuardEvaluatorInterface $guardEvaluator)
39 | {
40 | $this->guardEvaluator = $guardEvaluator;
41 | }
42 |
43 | /**
44 | * {@inheritdoc}
45 | */
46 | public function run(EventInterface $event, $payload, StateMachineInterface $stateMachine, TransitionInterface $transition = null)
47 | {
48 | $this->runActions[] = [
49 | 'stateMachine' => $stateMachine->getStateMachineId(),
50 | 'state' => $transition === null ? $stateMachine->getCurrentState()->getStateId() : $transition->getFromState()->getStateId(),
51 | 'event' => $event->getEventId(),
52 | 'calledBy' => 'runAction',
53 | ];
54 | }
55 |
56 | /**
57 | * {@inheritdoc}
58 | *
59 | * @return bool
60 | */
61 | public function evaluate(EventInterface $event, $payload, StateMachineInterface $stateMachine, TransitionInterface $transition)
62 | {
63 | $result = $this->guardEvaluator->evaluate($event, $payload, $stateMachine, $transition);
64 | $this->runActions[] = [
65 | 'stateMachine' => $stateMachine->getStateMachineId(),
66 | 'state' => $stateMachine->getCurrentState()->getStateId(),
67 | 'event' => $event->getEventId(),
68 | 'calledBy' => 'evaluateGuard',
69 | 'result' => $result,
70 | ];
71 |
72 | return $result;
73 | }
74 |
75 | /**
76 | * {@inheritdoc}
77 | */
78 | public function offsetExists($offset)
79 | {
80 | return isset($this->runActions[$offset]);
81 | }
82 |
83 | /**
84 | * {@inheritdoc}
85 | */
86 | public function offsetGet($offset)
87 | {
88 | return $this->runActions[$offset] ?? null;
89 | }
90 |
91 | /**
92 | * {@inheritdoc}
93 | */
94 | public function offsetSet($offset, $value)
95 | {
96 | if ($offset === null) {
97 | $this->runActions[] = $value;
98 | } else {
99 | $this->runActions[$offset] = $value;
100 | }
101 | }
102 |
103 | /**
104 | * {@inheritdoc}
105 | */
106 | public function offsetUnset($offset)
107 | {
108 | unset($this->runActions[$offset]);
109 | }
110 |
111 | /**
112 | * {@inheritdoc}
113 | */
114 | public function count()
115 | {
116 | return count($this->runActions);
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/StateMachine/StateMachineInterface.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\StateMachine;
14 |
15 | use Stagehand\FSM\State\StateInterface;
16 | use Stagehand\FSM\Transition\ActionRunnerInterface;
17 | use Stagehand\FSM\Transition\GuardEvaluatorInterface;
18 | use Stagehand\FSM\Transition\TransitionInterface;
19 |
20 | /**
21 | * @since Class available since Release 2.2.0
22 | */
23 | interface StateMachineInterface
24 | {
25 | /**
26 | * @since Constant available since Release 3.0.0
27 | */
28 | const STATE_INITIAL = '__INITIAL__';
29 |
30 | /**
31 | * @since Constant available since Release 3.0.0
32 | */
33 | const STATE_FINAL = '__FINAL__';
34 |
35 | /**
36 | * @since Constant available since Release 3.0.0
37 | */
38 | const EVENT_START = '__START__';
39 |
40 | /**
41 | * @since Constant available since Release 3.0.0
42 | */
43 | const EVENT_FORK = '__FORK__';
44 |
45 | /**
46 | * @since Constant available since Release 3.0.0
47 | */
48 | const EVENT_JOIN = '__JOIN__';
49 |
50 | /**
51 | * Sets the payload to the state machine.
52 | *
53 | * @param mixed $payload
54 | */
55 | public function setPayload($payload);
56 |
57 | /**
58 | * Gets the payload.
59 | *
60 | * @return mixed $payload
61 | */
62 | public function getPayload();
63 |
64 | /**
65 | * Adds a state to the state machine.
66 | *
67 | * @param StateInterface $state
68 | */
69 | public function addState(StateInterface $state);
70 |
71 | /**
72 | * Gets the state according to the given ID.
73 | *
74 | * @param string $stateId
75 | *
76 | * @return StateInterface
77 | */
78 | public function getState($stateId);
79 |
80 | /**
81 | * Gets the current state of the state machine.
82 | *
83 | * @return StateInterface
84 | */
85 | public function getCurrentState();
86 |
87 | /**
88 | * Gets the previous state of the state machine.
89 | *
90 | * @return StateInterface
91 | */
92 | public function getPreviousState();
93 |
94 | /**
95 | * Gets the ID of the state machine.
96 | *
97 | * @return string
98 | */
99 | public function getStateMachineId();
100 |
101 | /**
102 | * @param TransitionInterface $transition
103 | */
104 | public function addTransition(TransitionInterface $transition);
105 |
106 | /**
107 | * Starts the state machine.
108 | *
109 | * @param StateMachineInterface|null $parent
110 | */
111 | public function start(StateMachineInterface $parent = null);
112 |
113 | /**
114 | * Triggers an event in the current state.
115 | * Note: Do not call this method directly from actions..
116 | *
117 | * @param string $eventId
118 | *
119 | * @throws StateMachineAlreadyShutdownException
120 | * @throws StateMachineNotStartedException
121 | */
122 | public function triggerEvent($eventId);
123 |
124 | /**
125 | * Queues an event to the event queue.
126 | *
127 | * @param string $eventId
128 | *
129 | * @throws StateMachineAlreadyShutdownException
130 | * @throws StateMachineNotStartedException
131 | */
132 | public function queueEvent($eventId);
133 |
134 | /**
135 | * @return bool
136 | *
137 | * @since Method available since Release 2.4.0
138 | */
139 | public function isActive();
140 |
141 | /**
142 | * @return bool
143 | *
144 | * @since Method available since Release 2.4.0
145 | */
146 | public function isEnded();
147 |
148 | /**
149 | * @return TransitionLog[]
150 | *
151 | * @since Method available since Release 2.4.0
152 | */
153 | public function getTransitionLog();
154 |
155 | /**
156 | * @param ActionRunnerInterface $actionRunner
157 | *
158 | * @since Method available since Release 3.0.0
159 | */
160 | public function addActionRunner(ActionRunnerInterface $actionRunner);
161 |
162 | /**
163 | * @param GuardEvaluatorInterface $guardEvaluator
164 | *
165 | * @since Method available since Release 3.0.0
166 | */
167 | public function addGuardEvaluator(GuardEvaluatorInterface $guardEvaluator);
168 |
169 | /**
170 | * @return array
171 | *
172 | * @since Method available since Release 3.0.0
173 | */
174 | public function getTransitionMap();
175 | }
176 |
--------------------------------------------------------------------------------
/src/StateMachine/StateMachineBuilder.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\StateMachine;
14 |
15 | use Stagehand\FSM\Event\Event;
16 | use Stagehand\FSM\State\FinalState;
17 | use Stagehand\FSM\State\ForkState;
18 | use Stagehand\FSM\State\InitialState;
19 | use Stagehand\FSM\State\JoinState;
20 | use Stagehand\FSM\State\State;
21 | use Stagehand\FSM\State\TransitionalStateInterface;
22 | use Stagehand\FSM\Transition\Transition;
23 |
24 | /**
25 | * @since Class available since Release 2.0.0
26 | */
27 | class StateMachineBuilder
28 | {
29 | /**
30 | * @var StateMachineInterface
31 | */
32 | private $stateMachine;
33 |
34 | /**
35 | * @param string|StateMachine $stateMachineId
36 | */
37 | public function __construct($stateMachineId = null)
38 | {
39 | if ($stateMachineId instanceof StateMachineInterface) {
40 | $this->stateMachine = $stateMachineId;
41 | } else {
42 | $this->stateMachine = new StateMachine($stateMachineId);
43 | }
44 | }
45 |
46 | /**
47 | * @return StateMachine
48 | */
49 | public function getStateMachine()
50 | {
51 | return $this->stateMachine;
52 | }
53 |
54 | /**
55 | * Sets the given state as the start state of the state machine.
56 | *
57 | * @param string $stateId
58 | */
59 | public function setStartState($stateId)
60 | {
61 | $state = $this->stateMachine->getState(StateMachineInterface::STATE_INITIAL);
62 | if ($state === null) {
63 | $state = new InitialState(StateMachineInterface::STATE_INITIAL);
64 | $this->stateMachine->addState($state);
65 | }
66 |
67 | $this->addTransition(StateMachineInterface::STATE_INITIAL, $stateId, StateMachineInterface::EVENT_START);
68 | }
69 |
70 | /**
71 | * Sets the given state as an end state of the state machine.
72 | *
73 | * @param string $stateId
74 | * @param string $eventId
75 | */
76 | public function setEndState($stateId, $eventId)
77 | {
78 | if ($this->stateMachine->getState(StateMachineInterface::STATE_FINAL) === null) {
79 | $this->stateMachine->addState(new FinalState(StateMachineInterface::STATE_FINAL));
80 | }
81 |
82 | $this->addTransition($stateId, StateMachineInterface::STATE_FINAL, $eventId);
83 | }
84 |
85 | /**
86 | * Adds a state to the state machine.
87 | *
88 | * @param string $stateId
89 | */
90 | public function addState($stateId)
91 | {
92 | $state = new State($stateId);
93 | $this->stateMachine->addState($state);
94 | }
95 |
96 | /**
97 | * Adds an state transition to the state machine.
98 | *
99 | * @param string $stateId
100 | * @param string $nextStateId
101 | * @param string $eventId
102 | *
103 | * @throws StateNotFoundException
104 | */
105 | public function addTransition($stateId, $nextStateId, $eventId = null)
106 | {
107 | $state = $this->stateMachine->getState($stateId);
108 | if ($state === null) {
109 | throw new StateNotFoundException(sprintf('The state "%s" is not found.', $stateId));
110 | }
111 |
112 | if ($state instanceof ForkState) {
113 | $eventId = StateMachineInterface::EVENT_FORK;
114 | }
115 |
116 | $nextState = $this->stateMachine->getState($nextStateId);
117 | if ($nextState === null) {
118 | throw new StateNotFoundException(sprintf('The state "%s" is not found.', $nextStateId));
119 | }
120 |
121 | if ($nextState instanceof JoinState) {
122 | $eventId = StateMachineInterface::EVENT_JOIN;
123 | } elseif ($nextState instanceof ForkState) {
124 | $this->assertForkMustHaveExactlyOneIncomingTransition($nextState, $state);
125 | }
126 |
127 | $event = $state->getTransitionEvent($eventId);
128 | if ($event === null) {
129 | $event = new Event($eventId);
130 | }
131 |
132 | $this->stateMachine->addTransition(new Transition($nextState, $state, $event));
133 | }
134 |
135 | /**
136 | * @param string $stateId
137 | */
138 | public function addForkState($stateId)
139 | {
140 | $state = new ForkState($stateId);
141 | $this->stateMachine->addState($state);
142 | }
143 |
144 | /**
145 | * @param string $stateId
146 | */
147 | public function addJoinState($stateId)
148 | {
149 | $state = new JoinState($stateId);
150 | $this->stateMachine->addState($state);
151 | }
152 |
153 | /**
154 | * @param string $parentStateId
155 | * @param $stateMachineId
156 | * @param callable $callback
157 | */
158 | public function addChild($parentStateId, $stateMachineId, callable $callback)
159 | {
160 | $parentState = $this->stateMachine->getState($parentStateId);
161 | if ($parentState === null) {
162 | throw new StateNotFoundException(sprintf('The state "%s" is not found.', $parentStateId));
163 | }
164 |
165 | $builder = new static($stateMachineId);
166 | call_user_func($callback, $builder);
167 |
168 | $parentState->addChild($builder->getStateMachine());
169 | }
170 |
171 | /**
172 | * @param ForkState $toState
173 | * @param TransitionalStateInterface $fromState
174 | *
175 | * @since Method available since Release 3.0.0
176 | */
177 | private function assertForkMustHaveExactlyOneIncomingTransition(ForkState $toState, TransitionalStateInterface $fromState)
178 | {
179 | foreach ($this->stateMachine->getTransitionMap() as $fromStateId => $transitionsByEvents) {
180 | foreach ($transitionsByEvents as $eventId => $transition) { /* @var $transition TransitionInterface */
181 | if ($transition->getToState() === $toState) {
182 | throw new ConstraintException(sprintf(
183 | 'Failed to add a transition "[%s] -> [%s]" because another transition "[%s]:%s -> [%s]" already exists. A fork state must have exactly one incoming transition.',
184 | $fromState->getStateId(), $toState->getStateId(),
185 | $transition->getFromState()->getStateId(), $transition->getEvent()->getEventId(), $transition->getToState()->getStateId()
186 | ));
187 | }
188 | }
189 | }
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/tests/StateMachine/ForkAndJoinTest.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\StateMachine;
14 |
15 | use PHPUnit\Framework\TestCase;
16 | use Stagehand\FSM\State\StateActionInterface;
17 |
18 | /**
19 | * @since Class available since Release 3.0.0
20 | */
21 | class ForkAndJoinTest extends TestCase
22 | {
23 | /**
24 | * @var StateMachineBuilder
25 | */
26 | private $builder;
27 |
28 | /**
29 | * @var ActionLogger
30 | */
31 | private $actionLogger;
32 |
33 | /**
34 | * {@inheritdoc}
35 | */
36 | protected function setUp()
37 | {
38 | $this->builder = new StateMachineBuilder('ForkAndJoin');
39 | $this->builder->addState('FillOrder');
40 | $this->builder->addForkState('FORK');
41 | $this->builder->addState('ProcessOrder');
42 | $this->builder->addChild('ProcessOrder', 'AcceptOrder', function (StateMachineBuilder $builder) {
43 | $builder->addState('AcceptOrder1');
44 | $builder->addState('AcceptOrder2');
45 | $builder->setStartState('AcceptOrder1');
46 | $builder->setEndState('AcceptOrder2', 'next');
47 | $builder->addTransition('AcceptOrder1', 'AcceptOrder2', 'next');
48 | });
49 | $this->builder->addChild('ProcessOrder', 'ShipOrder', function (StateMachineBuilder $builder) {
50 | $builder->addState('ShipOrder1');
51 | $builder->addState('ShipOrder2');
52 | $builder->setStartState('ShipOrder1');
53 | $builder->setEndState('ShipOrder2', 'next');
54 | $builder->addTransition('ShipOrder1', 'ShipOrder2', 'next');
55 | });
56 | $this->builder->addJoinState('JOIN');
57 | $this->builder->addState('CloseOrder');
58 | $this->builder->setStartState('FillOrder');
59 | $this->builder->setEndState('CloseOrder', 'next');
60 | $this->builder->addTransition('FillOrder', 'FORK', 'next');
61 | $this->builder->addTransition('FORK', 'ProcessOrder');
62 | $this->builder->addTransition('ProcessOrder', 'JOIN');
63 | $this->builder->addTransition('JOIN', 'CloseOrder', 'next');
64 |
65 | $this->actionLogger = new ActionLogger();
66 | }
67 |
68 | /**
69 | * @test
70 | */
71 | public function forkAndJoin()
72 | {
73 | $stateMachine = $this->builder->getStateMachine();
74 | $stateMachine->addActionRunner($this->actionLogger);
75 | $stateMachine->start();
76 |
77 | $this->assertThat($stateMachine->getCurrentState()->getStateId(), $this->equalTo('FillOrder'));
78 |
79 | $stateMachine->triggerEvent('next');
80 |
81 | $this->assertThat($stateMachine->getCurrentState()->getStateId(), $this->equalTo('ProcessOrder'));
82 |
83 | $childStateMachine = $stateMachine->getCurrentState()->getChild('AcceptOrder');
84 |
85 | $this->assertThat($childStateMachine->getCurrentState()->getStateId(), $this->equalTo('AcceptOrder1'));
86 |
87 | $childStateMachine->triggerEvent('next');
88 |
89 | $this->assertThat($childStateMachine->getCurrentState()->getStateId(), $this->equalTo('AcceptOrder2'));
90 |
91 | $childStateMachine->triggerEvent('next');
92 |
93 | $this->assertThat($childStateMachine->isEnded(), $this->equalTo(true));
94 |
95 | $this->assertThat($stateMachine->getCurrentState()->getStateId(), $this->logicalNot($this->equalTo('CloseOrder')));
96 |
97 | $childStateMachine = $stateMachine->getCurrentState()->getChild('ShipOrder');
98 |
99 | $this->assertThat($childStateMachine->getCurrentState()->getStateId(), $this->equalTo('ShipOrder1'));
100 |
101 | $childStateMachine->triggerEvent('next');
102 |
103 | $this->assertThat($childStateMachine->getCurrentState()->getStateId(), $this->equalTo('ShipOrder2'));
104 |
105 | $childStateMachine->triggerEvent('next');
106 |
107 | $this->assertThat($childStateMachine->isEnded(), $this->equalTo(true));
108 |
109 | $this->assertThat($stateMachine->getCurrentState()->getStateId(), $this->equalTo('CloseOrder'));
110 |
111 | $this->assertThat(count($this->actionLogger), $this->equalTo(37));
112 | $this->assertThat($this->actionLogger[0]['stateMachine'], $this->equalTo('ForkAndJoin'));
113 | $this->assertThat($this->actionLogger[0]['state'], $this->equalTo(StateMachineInterface::STATE_INITIAL));
114 | $this->assertThat($this->actionLogger[0]['event'], $this->equalTo(StateMachineInterface::EVENT_START));
115 | $this->assertThat($this->actionLogger[1]['stateMachine'], $this->equalTo('ForkAndJoin'));
116 | $this->assertThat($this->actionLogger[1]['state'], $this->equalTo('FillOrder'));
117 | $this->assertThat($this->actionLogger[1]['event'], $this->equalTo(StateActionInterface::EVENT_ENTRY));
118 | $this->assertThat($this->actionLogger[2]['stateMachine'], $this->equalTo('ForkAndJoin'));
119 | $this->assertThat($this->actionLogger[2]['state'], $this->equalTo('FillOrder'));
120 | $this->assertThat($this->actionLogger[2]['event'], $this->equalTo(StateActionInterface::EVENT_DO));
121 | $this->assertThat($this->actionLogger[3]['stateMachine'], $this->equalTo('ForkAndJoin'));
122 | $this->assertThat($this->actionLogger[3]['state'], $this->equalTo('FillOrder'));
123 | $this->assertThat($this->actionLogger[3]['event'], $this->equalTo(StateActionInterface::EVENT_EXIT));
124 | $this->assertThat($this->actionLogger[4]['stateMachine'], $this->equalTo('ForkAndJoin'));
125 | $this->assertThat($this->actionLogger[4]['state'], $this->equalTo('FillOrder'));
126 | $this->assertThat($this->actionLogger[4]['event'], $this->equalTo('next'));
127 | $this->assertThat($this->actionLogger[5]['stateMachine'], $this->equalTo('ForkAndJoin'));
128 | $this->assertThat($this->actionLogger[5]['state'], $this->equalTo('FORK'));
129 | $this->assertThat($this->actionLogger[5]['event'], $this->equalTo(StateActionInterface::EVENT_ENTRY));
130 | $this->assertThat($this->actionLogger[6]['stateMachine'], $this->equalTo('ForkAndJoin'));
131 | $this->assertThat($this->actionLogger[6]['state'], $this->equalTo('FORK'));
132 | $this->assertThat($this->actionLogger[6]['event'], $this->equalTo(StateActionInterface::EVENT_DO));
133 | $this->assertThat($this->actionLogger[7]['stateMachine'], $this->equalTo('ForkAndJoin'));
134 | $this->assertThat($this->actionLogger[7]['state'], $this->equalTo('FORK'));
135 | $this->assertThat($this->actionLogger[7]['event'], $this->equalTo(StateActionInterface::EVENT_EXIT));
136 | $this->assertThat($this->actionLogger[8]['stateMachine'], $this->equalTo('ForkAndJoin'));
137 | $this->assertThat($this->actionLogger[8]['state'], $this->equalTo('FORK'));
138 | $this->assertThat($this->actionLogger[8]['event'], $this->equalTo(StateMachineInterface::EVENT_FORK));
139 | $this->assertThat($this->actionLogger[9]['stateMachine'], $this->equalTo('ForkAndJoin'));
140 | $this->assertThat($this->actionLogger[9]['state'], $this->equalTo('ProcessOrder'));
141 | $this->assertThat($this->actionLogger[9]['event'], $this->equalTo(StateActionInterface::EVENT_ENTRY));
142 | $this->assertThat($this->actionLogger[10]['stateMachine'], $this->equalTo('ForkAndJoin'));
143 | $this->assertThat($this->actionLogger[10]['state'], $this->equalTo('ProcessOrder'));
144 | $this->assertThat($this->actionLogger[10]['event'], $this->equalTo(StateActionInterface::EVENT_DO));
145 | $this->assertThat($this->actionLogger[11]['stateMachine'], $this->equalTo('AcceptOrder'));
146 | $this->assertThat($this->actionLogger[11]['state'], $this->equalTo(StateMachineInterface::STATE_INITIAL));
147 | $this->assertThat($this->actionLogger[11]['event'], $this->equalTo(StateMachineInterface::EVENT_START));
148 | $this->assertThat($this->actionLogger[12]['stateMachine'], $this->equalTo('AcceptOrder'));
149 | $this->assertThat($this->actionLogger[12]['state'], $this->equalTo('AcceptOrder1'));
150 | $this->assertThat($this->actionLogger[12]['event'], $this->equalTo(StateActionInterface::EVENT_ENTRY));
151 | $this->assertThat($this->actionLogger[13]['stateMachine'], $this->equalTo('AcceptOrder'));
152 | $this->assertThat($this->actionLogger[13]['state'], $this->equalTo('AcceptOrder1'));
153 | $this->assertThat($this->actionLogger[13]['event'], $this->equalTo(StateActionInterface::EVENT_DO));
154 | $this->assertThat($this->actionLogger[14]['stateMachine'], $this->equalTo('ShipOrder'));
155 | $this->assertThat($this->actionLogger[14]['state'], $this->equalTo(StateMachineInterface::STATE_INITIAL));
156 | $this->assertThat($this->actionLogger[14]['event'], $this->equalTo(StateMachineInterface::EVENT_START));
157 | $this->assertThat($this->actionLogger[15]['stateMachine'], $this->equalTo('ShipOrder'));
158 | $this->assertThat($this->actionLogger[15]['state'], $this->equalTo('ShipOrder1'));
159 | $this->assertThat($this->actionLogger[15]['event'], $this->equalTo(StateActionInterface::EVENT_ENTRY));
160 | $this->assertThat($this->actionLogger[16]['stateMachine'], $this->equalTo('ShipOrder'));
161 | $this->assertThat($this->actionLogger[16]['state'], $this->equalTo('ShipOrder1'));
162 | $this->assertThat($this->actionLogger[16]['event'], $this->equalTo(StateActionInterface::EVENT_DO));
163 | $this->assertThat($this->actionLogger[17]['stateMachine'], $this->equalTo('AcceptOrder'));
164 | $this->assertThat($this->actionLogger[17]['state'], $this->equalTo('AcceptOrder1'));
165 | $this->assertThat($this->actionLogger[17]['event'], $this->equalTo(StateActionInterface::EVENT_EXIT));
166 | $this->assertThat($this->actionLogger[18]['stateMachine'], $this->equalTo('AcceptOrder'));
167 | $this->assertThat($this->actionLogger[18]['state'], $this->equalTo('AcceptOrder1'));
168 | $this->assertThat($this->actionLogger[18]['event'], $this->equalTo('next'));
169 | $this->assertThat($this->actionLogger[19]['stateMachine'], $this->equalTo('AcceptOrder'));
170 | $this->assertThat($this->actionLogger[19]['state'], $this->equalTo('AcceptOrder2'));
171 | $this->assertThat($this->actionLogger[19]['event'], $this->equalTo(StateActionInterface::EVENT_ENTRY));
172 | $this->assertThat($this->actionLogger[20]['stateMachine'], $this->equalTo('AcceptOrder'));
173 | $this->assertThat($this->actionLogger[20]['state'], $this->equalTo('AcceptOrder2'));
174 | $this->assertThat($this->actionLogger[20]['event'], $this->equalTo(StateActionInterface::EVENT_DO));
175 | $this->assertThat($this->actionLogger[21]['stateMachine'], $this->equalTo('AcceptOrder'));
176 | $this->assertThat($this->actionLogger[21]['state'], $this->equalTo('AcceptOrder2'));
177 | $this->assertThat($this->actionLogger[21]['event'], $this->equalTo(StateActionInterface::EVENT_EXIT));
178 | $this->assertThat($this->actionLogger[22]['stateMachine'], $this->equalTo('AcceptOrder'));
179 | $this->assertThat($this->actionLogger[22]['state'], $this->equalTo('AcceptOrder2'));
180 | $this->assertThat($this->actionLogger[22]['event'], $this->equalTo('next'));
181 | $this->assertThat($this->actionLogger[23]['stateMachine'], $this->equalTo('ShipOrder'));
182 | $this->assertThat($this->actionLogger[23]['state'], $this->equalTo('ShipOrder1'));
183 | $this->assertThat($this->actionLogger[23]['event'], $this->equalTo(StateActionInterface::EVENT_EXIT));
184 | $this->assertThat($this->actionLogger[24]['stateMachine'], $this->equalTo('ShipOrder'));
185 | $this->assertThat($this->actionLogger[24]['state'], $this->equalTo('ShipOrder1'));
186 | $this->assertThat($this->actionLogger[24]['event'], $this->equalTo('next'));
187 | $this->assertThat($this->actionLogger[25]['stateMachine'], $this->equalTo('ShipOrder'));
188 | $this->assertThat($this->actionLogger[25]['state'], $this->equalTo('ShipOrder2'));
189 | $this->assertThat($this->actionLogger[25]['event'], $this->equalTo(StateActionInterface::EVENT_ENTRY));
190 | $this->assertThat($this->actionLogger[26]['stateMachine'], $this->equalTo('ShipOrder'));
191 | $this->assertThat($this->actionLogger[26]['state'], $this->equalTo('ShipOrder2'));
192 | $this->assertThat($this->actionLogger[26]['event'], $this->equalTo(StateActionInterface::EVENT_DO));
193 | $this->assertThat($this->actionLogger[27]['stateMachine'], $this->equalTo('ShipOrder'));
194 | $this->assertThat($this->actionLogger[27]['state'], $this->equalTo('ShipOrder2'));
195 | $this->assertThat($this->actionLogger[27]['event'], $this->equalTo(StateActionInterface::EVENT_EXIT));
196 | $this->assertThat($this->actionLogger[28]['stateMachine'], $this->equalTo('ShipOrder'));
197 | $this->assertThat($this->actionLogger[28]['state'], $this->equalTo('ShipOrder2'));
198 | $this->assertThat($this->actionLogger[28]['event'], $this->equalTo('next'));
199 | $this->assertThat($this->actionLogger[29]['stateMachine'], $this->equalTo('ForkAndJoin'));
200 | $this->assertThat($this->actionLogger[29]['state'], $this->equalTo('ProcessOrder'));
201 | $this->assertThat($this->actionLogger[29]['event'], $this->equalTo(StateActionInterface::EVENT_EXIT));
202 | $this->assertThat($this->actionLogger[30]['stateMachine'], $this->equalTo('ForkAndJoin'));
203 | $this->assertThat($this->actionLogger[30]['state'], $this->equalTo('ProcessOrder'));
204 | $this->assertThat($this->actionLogger[30]['event'], $this->equalTo(StateMachineInterface::EVENT_JOIN));
205 | $this->assertThat($this->actionLogger[31]['stateMachine'], $this->equalTo('ForkAndJoin'));
206 | $this->assertThat($this->actionLogger[31]['state'], $this->equalTo('JOIN'));
207 | $this->assertThat($this->actionLogger[31]['event'], $this->equalTo(StateActionInterface::EVENT_ENTRY));
208 | $this->assertThat($this->actionLogger[32]['stateMachine'], $this->equalTo('ForkAndJoin'));
209 | $this->assertThat($this->actionLogger[32]['state'], $this->equalTo('JOIN'));
210 | $this->assertThat($this->actionLogger[32]['event'], $this->equalTo(StateActionInterface::EVENT_DO));
211 | $this->assertThat($this->actionLogger[33]['stateMachine'], $this->equalTo('ForkAndJoin'));
212 | $this->assertThat($this->actionLogger[33]['state'], $this->equalTo('JOIN'));
213 | $this->assertThat($this->actionLogger[33]['event'], $this->equalTo(StateActionInterface::EVENT_EXIT));
214 | $this->assertThat($this->actionLogger[34]['stateMachine'], $this->equalTo('ForkAndJoin'));
215 | $this->assertThat($this->actionLogger[34]['state'], $this->equalTo('JOIN'));
216 | $this->assertThat($this->actionLogger[34]['event'], $this->equalTo('next'));
217 | $this->assertThat($this->actionLogger[35]['stateMachine'], $this->equalTo('ForkAndJoin'));
218 | $this->assertThat($this->actionLogger[35]['state'], $this->equalTo('CloseOrder'));
219 | $this->assertThat($this->actionLogger[35]['event'], $this->equalTo(StateActionInterface::EVENT_ENTRY));
220 | $this->assertThat($this->actionLogger[36]['stateMachine'], $this->equalTo('ForkAndJoin'));
221 | $this->assertThat($this->actionLogger[36]['state'], $this->equalTo('CloseOrder'));
222 | $this->assertThat($this->actionLogger[36]['event'], $this->equalTo(StateActionInterface::EVENT_DO));
223 | }
224 |
225 | /**
226 | * @test
227 | */
228 | public function forkMustHaveExactlyOneIncomingTransition()
229 | {
230 | $this->builder->addState('AnotherStateToFORK');
231 |
232 | try {
233 | $this->builder->addTransition('AnotherStateToFORK', 'FORK', 'next');
234 | } catch (ConstraintException $e) {
235 | $this->assertTrue(true);
236 |
237 | return;
238 | } catch (\Exception $e) {
239 | }
240 |
241 | $this->fail('An expected exception has not been raised.');
242 | }
243 | }
244 |
--------------------------------------------------------------------------------
/src/StateMachine/StateMachine.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\StateMachine;
14 |
15 | use Stagehand\FSM\Event\EventInterface;
16 | use Stagehand\FSM\State\AutomaticTransitionInterface;
17 | use Stagehand\FSM\State\ParentStateInterface;
18 | use Stagehand\FSM\State\StateActionInterface;
19 | use Stagehand\FSM\State\StateCollection;
20 | use Stagehand\FSM\State\StateInterface;
21 | use Stagehand\FSM\State\TransitionalStateInterface;
22 | use Stagehand\FSM\Transition\ActionRunnerInterface;
23 | use Stagehand\FSM\Transition\GuardEvaluatorInterface;
24 | use Stagehand\FSM\Transition\TransitionInterface;
25 | use Symfony\Component\EventDispatcher\EventDispatcherInterface;
26 |
27 | /**
28 | * @see http://en.wikipedia.org/wiki/Finite_state_machine
29 | * @see http://www.sparxsystems.com/resources/uml2_tutorial/uml2_statediagram.html
30 | * @see http://pear.php.net/package/FSM
31 | * @see http://www.generation5.org/content/2003/FSM_Tutorial.asp
32 | * @see https://sparxsystems.com/enterprise_architect_user_guide/14.0/model_simulation/example__fork_and_join.html
33 | * @see https://online.visual-paradigm.com/diagrams/tutorials/state-machine-diagram-tutorial/
34 | * @see https://www.uml-diagrams.org/state-machine-diagrams.html
35 | * @since Class available since Release 0.1.0
36 | */
37 | class StateMachine implements StateMachineInterface
38 | {
39 | /**
40 | * @var StateCollection
41 | *
42 | * @since Property available since Release 2.2.0
43 | */
44 | private $stateCollection;
45 |
46 | /**
47 | * @var string
48 | */
49 | private $stateMachineId;
50 |
51 | /**
52 | * @var mixed
53 | */
54 | private $payload;
55 |
56 | /**
57 | * @var array
58 | */
59 | private $eventQueue = [];
60 |
61 | /**
62 | * @var EventDispatcherInterface
63 | *
64 | * @since Property available since Release 2.1.0
65 | */
66 | private $eventDispatcher;
67 |
68 | /**
69 | * @var TransitionLog[]
70 | *
71 | * @since Property available since Release 2.4.0
72 | */
73 | private $transitionLog = [];
74 |
75 | /**
76 | * @var array
77 | *
78 | * @since Property available since Release 2.3.0
79 | */
80 | private $transitionMap = [];
81 |
82 | /**
83 | * @var ActionRunnerInterface[]
84 | *
85 | * @since Property available since Release 3.0.0
86 | */
87 | private $actionRunners;
88 |
89 | /**
90 | * @var GuardEvaluatorInterface[]
91 | *
92 | * @since Property available since Release 3.0.0
93 | */
94 | private $guardEvaluators;
95 |
96 | /**
97 | * @var StateInterface
98 | *
99 | * @since Property available since Release 3.0.0
100 | */
101 | private $currentState;
102 |
103 | /**
104 | * @var TransitionalStateInterface
105 | *
106 | * @since Property available since Release 3.0.0
107 | */
108 | private $previousState;
109 |
110 | /**
111 | * @var StateMachine
112 | *
113 | * @since Property available since Release 3.0.0
114 | */
115 | private $parent;
116 |
117 | /**
118 | * @param string $stateMachineId
119 | */
120 | public function __construct($stateMachineId = null)
121 | {
122 | $this->stateCollection = new StateCollection();
123 | $this->stateMachineId = $stateMachineId;
124 | }
125 |
126 | /**
127 | * @param EventDispatcherInterface $eventDispatcher
128 | *
129 | * @since Method available since Release 2.1.0
130 | */
131 | public function setEventDispatcher(EventDispatcherInterface $eventDispatcher = null)
132 | {
133 | $this->eventDispatcher = $eventDispatcher;
134 | }
135 |
136 | /**
137 | * {@inheritdoc}
138 | */
139 | public function start(StateMachineInterface $parent = null)
140 | {
141 | if ($this->currentState !== null) {
142 | throw new StateMachineAlreadyStartedException('The state machine is already started.');
143 | }
144 |
145 | $initialState = $this->getState(self::STATE_INITIAL);
146 | assert($initialState !== null);
147 |
148 | if ($parent !== null) {
149 | $this->parent = $parent;
150 | }
151 | $this->currentState = $initialState;
152 | $this->triggerEvent(self::EVENT_START);
153 | }
154 |
155 | /**
156 | * {@inheritdoc}
157 | */
158 | public function getCurrentState()
159 | {
160 | return $this->currentState;
161 | }
162 |
163 | /**
164 | * {@inheritdoc}
165 | */
166 | public function getPreviousState()
167 | {
168 | return $this->previousState;
169 | }
170 |
171 | /**
172 | * {@inheritdoc}
173 | */
174 | public function getPayload()
175 | {
176 | if ($this->parent !== null) {
177 | return $this->parent->getPayload();
178 | }
179 |
180 | return $this->payload;
181 | }
182 |
183 | /**
184 | * {@inheritdoc}
185 | */
186 | public function triggerEvent($eventId)
187 | {
188 | $this->queueEvent($eventId);
189 |
190 | do {
191 | if ($this->isEnded()) {
192 | throw new StateMachineAlreadyShutdownException('The state machine was already shutdown.');
193 | }
194 |
195 | $event = $this->currentState->getTransitionEvent(array_shift($this->eventQueue));
196 | if ($event !== null) {
197 | if ($this->eventDispatcher !== null) {
198 | $this->eventDispatcher->dispatch(StateMachineEvents::EVENT_PROCESS, new StateMachineEvent($this, $this->currentState, $event));
199 | }
200 |
201 | $fromState = $this->currentState;
202 | if ($fromState instanceof TransitionalStateInterface) {
203 | $transition = $this->getTransition($fromState, $event);
204 | if ($this->evaluateGuard($this, $event, $transition)) {
205 | $toState = $this->transition($transition);
206 | }
207 | }
208 | }
209 |
210 | if ($this->currentState instanceof StateActionInterface) {
211 | $doEvent = $this->currentState->getDoEvent();
212 | if ($doEvent !== null) {
213 | if ($this->eventDispatcher !== null) {
214 | $this->eventDispatcher->dispatch(StateMachineEvents::EVENT_DO, new StateMachineEvent($this, $this->currentState, $doEvent));
215 | }
216 |
217 | $this->runAction($this, $doEvent);
218 | }
219 | }
220 |
221 | if (isset($toState)) {
222 | if ($toState instanceof ParentStateInterface) {
223 | $this->fork($toState);
224 | }
225 |
226 | if ($toState->getStateId() == self::STATE_FINAL) {
227 | if ($this->parent != null) {
228 | $parentCurrentState = $this->parent->getCurrentState();
229 | if ($parentCurrentState instanceof ParentStateInterface) {
230 | $this->parent->join($parentCurrentState);
231 | }
232 | }
233 | }
234 | }
235 |
236 | if ($this->currentState instanceof AutomaticTransitionInterface) {
237 | $this->queueEvent($this->currentState->getAutomaticTransitionEvent()->getEventId());
238 | }
239 | } while (count($this->eventQueue) > 0);
240 | }
241 |
242 | /**
243 | * {@inheritdoc}
244 | *
245 | * @since Method available since Release 1.7.0
246 | */
247 | public function queueEvent($eventId)
248 | {
249 | if ($this->currentState === null) {
250 | throw $this->createStateMachineNotStartedException();
251 | }
252 |
253 | if ($this->currentState->getStateId() == self::STATE_FINAL) {
254 | throw new StateMachineAlreadyShutdownException('The state machine was already shutdown.');
255 | }
256 |
257 | $this->eventQueue[] = $eventId;
258 | }
259 |
260 | /**
261 | * {@inheritdoc}
262 | */
263 | public function getState($stateId)
264 | {
265 | return $this->stateCollection->get($stateId);
266 | }
267 |
268 | /**
269 | * {@inheritdoc}
270 | */
271 | public function addState(StateInterface $state)
272 | {
273 | $this->stateCollection->add($state);
274 | }
275 |
276 | /**
277 | * {@inheritdoc}
278 | */
279 | public function getStateMachineId()
280 | {
281 | return $this->stateMachineId;
282 | }
283 |
284 | /**
285 | * {@inheritdoc}
286 | */
287 | public function setPayload($payload)
288 | {
289 | if ($this->parent !== null) {
290 | return $this->parent->setPayload($payload);
291 | }
292 |
293 | $this->payload = $payload;
294 | }
295 |
296 | /**
297 | * {@inheritdoc}
298 | */
299 | public function addTransition(TransitionInterface $transition)
300 | {
301 | $this->transitionMap[$transition->getFromState()->getStateId()][$transition->getEvent()->getEventId()] = $transition;
302 | }
303 |
304 | /**
305 | * {@inheritdoc}
306 | */
307 | public function getTransitionLog()
308 | {
309 | return $this->transitionLog;
310 | }
311 |
312 | /**
313 | * {@inheritdoc}
314 | *
315 | * @since Method available since Release 2.3.0
316 | */
317 | public function isActive()
318 | {
319 | if ($this->currentState === null) {
320 | return false;
321 | }
322 |
323 | return $this->currentState->getStateId() != self::STATE_FINAL;
324 | }
325 |
326 | /**
327 | * {@inheritdoc}
328 | */
329 | public function isEnded()
330 | {
331 | if ($this->currentState === null) {
332 | return false;
333 | }
334 |
335 | return $this->currentState->getStateId() == self::STATE_FINAL;
336 | }
337 |
338 | /**
339 | * {@inheritdoc}
340 | */
341 | public function addActionRunner(ActionRunnerInterface $actionRunner)
342 | {
343 | if ($this->parent !== null) {
344 | return $this->parent->addActionRunner($actionRunner);
345 | }
346 |
347 | $this->actionRunners[] = $actionRunner;
348 | }
349 |
350 | /**
351 | * {@inheritdoc}
352 | */
353 | public function addGuardEvaluator(GuardEvaluatorInterface $guardEvaluator)
354 | {
355 | if ($this->parent !== null) {
356 | return $this->parent->addGuardEvaluator($guardEvaluator);
357 | }
358 |
359 | $this->guardEvaluators[] = $guardEvaluator;
360 | }
361 |
362 | /**
363 | * {@inheritdoc}
364 | */
365 | public function getTransitionMap()
366 | {
367 | return $this->transitionMap;
368 | }
369 |
370 | /**
371 | * Transitions to the next state.
372 | *
373 | * @param TransitionInterface $transition
374 | *
375 | * @return StateInterface
376 | */
377 | private function transition(TransitionInterface $transition)
378 | {
379 | if ($transition->getFromState() instanceof StateActionInterface) {
380 | $exitEvent = $transition->getFromState()->getExitEvent();
381 | if ($exitEvent !== null) {
382 | if ($this->eventDispatcher !== null) {
383 | $this->eventDispatcher->dispatch(StateMachineEvents::EVENT_EXIT, new StateMachineEvent($this, $transition->getFromState(), $exitEvent));
384 | }
385 |
386 | $this->runAction($this, $exitEvent);
387 | }
388 | }
389 |
390 | if ($this->eventDispatcher !== null) {
391 | $this->eventDispatcher->dispatch(StateMachineEvents::EVENT_TRANSITION, new StateMachineEvent($this, null, $transition->getEvent(), $transition));
392 | }
393 | $this->runAction($this, $transition->getEvent(), $transition);
394 | $this->previousState = $transition->getFromState();
395 | $this->currentState = $toState = $transition->getToState();
396 | $this->transitionLog[] = $this->createTransitionLogEntry($transition);
397 |
398 | if ($toState instanceof StateActionInterface) {
399 | $entryEvent = $toState->getEntryEvent();
400 | if ($entryEvent !== null) {
401 | if ($this->eventDispatcher !== null) {
402 | $this->eventDispatcher->dispatch(StateMachineEvents::EVENT_ENTRY, new StateMachineEvent($this, $toState, $entryEvent));
403 | }
404 |
405 | $this->runAction($this, $entryEvent);
406 | }
407 | }
408 |
409 | return $toState;
410 | }
411 |
412 | /**
413 | * Evaluates the guard for the given event.
414 | *
415 | * @param StateMachineInterface $stateMachine
416 | * @param EventInterface $event
417 | * @param TransitionInterface $transition
418 | *
419 | * @return bool
420 | *
421 | * @since Method available since Release 2.0.0
422 | */
423 | private function evaluateGuard(StateMachineInterface $stateMachine, EventInterface $event, TransitionInterface $transition)
424 | {
425 | if ($this->parent !== null) {
426 | return $this->parent->evaluateGuard($stateMachine, $event, $transition);
427 | }
428 |
429 | foreach ((array) $this->guardEvaluators as $guardEvaluator) {
430 | $result = call_user_func([$guardEvaluator, 'evaluate'], $event, $this->getPayload(), $stateMachine, $transition);
431 | if (!$result) {
432 | return false;
433 | }
434 | }
435 |
436 | return true;
437 | }
438 |
439 | /**
440 | * Runs the action for the given event.
441 | *
442 | * @param StateMachineInterface $stateMachine
443 | * @param EventInterface $event
444 | * @param TransitionInterface|null $transition
445 | *
446 | * @return bool
447 | *
448 | * @since Method available since Release 2.0.0
449 | */
450 | private function runAction(StateMachineInterface $stateMachine, EventInterface $event, TransitionInterface $transition = null)
451 | {
452 | if ($this->parent !== null) {
453 | return $this->parent->runAction($stateMachine, $event, $transition);
454 | }
455 |
456 | foreach ((array) $this->actionRunners as $actionRunner) {
457 | call_user_func([$actionRunner, 'run'], $event, $this->getPayload(), $stateMachine, $transition);
458 | }
459 | }
460 |
461 | /**
462 | * @param TransitionInterface $transition
463 | *
464 | * @return TransitionLog
465 | */
466 | private function createTransitionLogEntry(TransitionInterface $transition)
467 | {
468 | return new TransitionLog($transition, new \DateTime());
469 | }
470 |
471 | /**
472 | * @return StateMachineNotStartedException
473 | *
474 | * @since Method available since Release 2.3.0
475 | */
476 | private function createStateMachineNotStartedException()
477 | {
478 | return new StateMachineNotStartedException('The state machine is not started yet.');
479 | }
480 |
481 | /**
482 | * @param TransitionalStateInterface $state
483 | * @param EventInterface $event
484 | *
485 | * @return TransitionInterface
486 | *
487 | * @since Method available since Release 3.0.0
488 | */
489 | private function getTransition(TransitionalStateInterface $state, EventInterface $event): TransitionInterface
490 | {
491 | return $this->transitionMap[$state->getStateId()][$event->getEventId()];
492 | }
493 |
494 | /**
495 | * @param ParentStateInterface $parent
496 | */
497 | private function fork(ParentStateInterface $parent)
498 | {
499 | foreach ($parent->getChildren() as $child) {
500 | $child->start($this);
501 | }
502 | }
503 |
504 | /**
505 | * @param ParentStateInterface $parent
506 | */
507 | private function join(ParentStateInterface $parent)
508 | {
509 | foreach ($parent->getChildren() as $child) {
510 | if (!$child->isEnded()) {
511 | return;
512 | }
513 | }
514 |
515 | $this->triggerEvent(self::EVENT_JOIN);
516 | }
517 | }
518 |
--------------------------------------------------------------------------------
/tests/StateMachine/StateMachineTest.php:
--------------------------------------------------------------------------------
1 | ,
4 | * All rights reserved.
5 | *
6 | * This file is part of Stagehand_FSM.
7 | *
8 | * This program and the accompanying materials are made available under
9 | * the terms of the BSD 2-Clause License which accompanies this
10 | * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
11 | */
12 |
13 | namespace Stagehand\FSM\StateMachine;
14 |
15 | use PHPUnit\Framework\TestCase;
16 | use Stagehand\FSM\Event\EventInterface;
17 | use Stagehand\FSM\State\StateActionInterface;
18 | use Stagehand\FSM\Transition\ActionRunnerInterface;
19 | use Stagehand\FSM\Transition\GuardEvaluatorInterface;
20 | use Stagehand\FSM\Transition\TransitionInterface;
21 | use Symfony\Component\EventDispatcher\EventDispatcher;
22 | use Symfony\Component\EventDispatcher\EventDispatcherInterface;
23 |
24 | /**
25 | * @since Class available since Release 0.1.0
26 | */
27 | class StateMachineTest extends TestCase
28 | {
29 | /**
30 | * @var StateMachineBuilder
31 | *
32 | * @since Property available since Release 2.0.0
33 | */
34 | protected $stateMachineBuilder;
35 |
36 | /**
37 | * @var ActionLogger
38 | *
39 | * @since Property available since Release 3.0.0
40 | */
41 | private $actionLogger;
42 |
43 | /**
44 | * {@inheritdoc}
45 | */
46 | protected function setUp()
47 | {
48 | $this->stateMachineBuilder = new StateMachineBuilder('Registration');
49 | $this->stateMachineBuilder->addState('Input');
50 | $this->stateMachineBuilder->addState('Confirmation');
51 | $this->stateMachineBuilder->addState('Success');
52 | $this->stateMachineBuilder->addState('Validation');
53 | $this->stateMachineBuilder->addState('Registration');
54 | $this->stateMachineBuilder->setStartState('Input');
55 | $this->stateMachineBuilder->addTransition('Input', 'Validation', 'next');
56 | $this->stateMachineBuilder->addTransition('Validation', 'Confirmation', 'valid');
57 | $this->stateMachineBuilder->addTransition('Validation', 'Input', 'invalid');
58 | $this->stateMachineBuilder->addTransition('Confirmation', 'Registration', 'next');
59 | $this->stateMachineBuilder->addTransition('Confirmation', 'Input', 'prev');
60 | $this->stateMachineBuilder->addTransition('Registration', 'Success', 'next');
61 | $this->stateMachineBuilder->setEndState('Success', 'next');
62 |
63 | $this->actionLogger = new ActionLogger();
64 | }
65 |
66 | /**
67 | * @test
68 | *
69 | * @since Method available since Release 2.0.0
70 | */
71 | public function transitions()
72 | {
73 | $stateMachine = $this->stateMachineBuilder->getStateMachine();
74 | $stateMachine->addActionRunner($this->actionLogger);
75 | $stateMachine->start();
76 |
77 | $this->assertThat($stateMachine->getCurrentState()->getStateId(), $this->equalTo('Input'));
78 | $this->assertThat($stateMachine->getPreviousState()->getStateId(), $this->equalTo(StateMachineInterface::STATE_INITIAL));
79 |
80 | $this->assertThat(count($this->actionLogger), $this->equalTo(3));
81 | $this->assertThat($this->actionLogger[0]['state'], $this->equalTo(StateMachineInterface::STATE_INITIAL));
82 | $this->assertThat($this->actionLogger[0]['event'], $this->equalTo(StateMachineInterface::EVENT_START));
83 | $this->assertThat($this->actionLogger[1]['state'], $this->equalTo('Input'));
84 | $this->assertThat($this->actionLogger[1]['event'], $this->equalTo(StateActionInterface::EVENT_ENTRY));
85 | $this->assertThat($this->actionLogger[2]['state'], $this->equalTo('Input'));
86 | $this->assertThat($this->actionLogger[2]['event'], $this->equalTo(StateActionInterface::EVENT_DO));
87 |
88 | $stateMachine->triggerEvent('next');
89 |
90 | $this->assertThat($stateMachine->getCurrentState()->getStateId(), $this->equalTo('Validation'));
91 | $this->assertThat($stateMachine->getPreviousState()->getStateId(), $this->equalTo('Input'));
92 |
93 | $this->assertThat(count($this->actionLogger), $this->equalTo(7));
94 | $this->assertThat($this->actionLogger[3]['state'], $this->equalTo('Input'));
95 | $this->assertThat($this->actionLogger[3]['event'], $this->equalTo(StateActionInterface::EVENT_EXIT));
96 | $this->assertThat($this->actionLogger[4]['state'], $this->equalTo('Input'));
97 | $this->assertThat($this->actionLogger[4]['event'], $this->equalTo('next'));
98 | $this->assertThat($this->actionLogger[5]['state'], $this->equalTo('Validation'));
99 | $this->assertThat($this->actionLogger[5]['event'], $this->equalTo(StateActionInterface::EVENT_ENTRY));
100 | $this->assertThat($this->actionLogger[6]['state'], $this->equalTo('Validation'));
101 | $this->assertThat($this->actionLogger[6]['event'], $this->equalTo(StateActionInterface::EVENT_DO));
102 | }
103 |
104 | /**
105 | * @test
106 | *
107 | * @since Method available since Release 2.0.0
108 | */
109 | public function raisesAnExceptionWhenAnEventIsTriggeredOnTheFinalState()
110 | {
111 | $stateMachine = $this->stateMachineBuilder->getStateMachine();
112 | $stateMachine->start();
113 | $stateMachine->triggerEvent('next');
114 | $stateMachine->triggerEvent('valid');
115 | $stateMachine->triggerEvent('next');
116 | $stateMachine->triggerEvent('next');
117 | $stateMachine->triggerEvent('next');
118 |
119 | $this->assertThat($stateMachine->getCurrentState()->getStateId(), $this->equalTo(StateMachineInterface::STATE_FINAL));
120 |
121 | try {
122 | $stateMachine->triggerEvent('foo');
123 | } catch (StateMachineAlreadyShutdownException $e) {
124 | return;
125 | }
126 |
127 | $this->fail('An expected exception has not been raised.');
128 | }
129 |
130 | /**
131 | * @test
132 | *
133 | * @since Method available since Release 2.0.0
134 | */
135 | public function transitionsToTheNextStateWhenTheGuardConditionIsTrue()
136 | {
137 | $stateMachine = $this->stateMachineBuilder->getStateMachine();
138 | $stateMachine->addActionRunner($this->actionLogger);
139 | $stateMachine->addGuardEvaluator($this->actionLogger);
140 | $this->actionLogger->setGuardEvaluator(new class() implements GuardEvaluatorInterface {
141 | /**
142 | * {@inheritdoc}
143 | */
144 | public function evaluate(EventInterface $event, $payload, StateMachineInterface $stateMachine, TransitionInterface $transition)
145 | {
146 | if ($stateMachine->getCurrentState()->getStateId() == StateMachineInterface::STATE_INITIAL && $event->getEventId() == StateMachineInterface::EVENT_START) {
147 | return true;
148 | } elseif ($stateMachine->getCurrentState()->getStateId() == 'Input' && $event->getEventId() == 'next') {
149 | return true;
150 | } else {
151 | return false;
152 | }
153 | }
154 | });
155 | $stateMachine->start();
156 | $stateMachine->triggerEvent('next');
157 |
158 | $this->assertThat($stateMachine->getCurrentState()->getStateId(), $this->equalTo('Validation'));
159 | $this->assertThat($stateMachine->getPreviousState()->getStateId(), $this->equalTo('Input'));
160 | $this->assertThat(count($this->actionLogger), $this->equalTo(9));
161 |
162 | $this->assertThat($this->actionLogger[4]['state'], $this->equalTo('Input'));
163 | $this->assertThat($this->actionLogger[4]['event'], $this->equalTo('next'));
164 | $this->assertThat($this->actionLogger[4]['calledBy'], $this->equalTo('evaluateGuard'));
165 | $this->assertThat($this->actionLogger[4]['result'], $this->isTrue());
166 | $this->assertThat($this->actionLogger[5]['state'], $this->equalTo('Input'));
167 | $this->assertThat($this->actionLogger[5]['event'], $this->equalTo(StateActionInterface::EVENT_EXIT));
168 | $this->assertThat($this->actionLogger[5]['calledBy'], $this->equalTo('runAction'));
169 | }
170 |
171 | /**
172 | * @test
173 | *
174 | * @since Method available since Release 2.0.0
175 | */
176 | public function doesNotTransitionToTheNextStateWhenTheGuardConditionIsFalse()
177 | {
178 | $stateMachine = $this->stateMachineBuilder->getStateMachine();
179 | $stateMachine->addActionRunner($this->actionLogger);
180 | $stateMachine->addGuardEvaluator($this->actionLogger);
181 | $this->actionLogger->setGuardEvaluator(new class() implements GuardEvaluatorInterface {
182 | /**
183 | * {@inheritdoc}
184 | */
185 | public function evaluate(EventInterface $event, $payload, StateMachineInterface $stateMachine, TransitionInterface $transition)
186 | {
187 | if ($stateMachine->getCurrentState()->getStateId() == 'Input' && $event->getEventId() == 'next') {
188 | return false;
189 | }
190 |
191 | return true;
192 | }
193 | });
194 | $stateMachine->start();
195 | $stateMachine->triggerEvent('next');
196 |
197 | $this->assertThat($stateMachine->getCurrentState()->getStateId(), $this->equalTo('Input'));
198 | $this->assertThat($stateMachine->getPreviousState()->getStateId(), $this->equalTo(StateMachineInterface::STATE_INITIAL));
199 | $this->assertThat(count($this->actionLogger), $this->equalTo(6));
200 |
201 | $this->assertThat($this->actionLogger[4]['state'], $this->equalTo('Input'));
202 | $this->assertThat($this->actionLogger[4]['event'], $this->equalTo('next'));
203 | $this->assertThat($this->actionLogger[4]['calledBy'], $this->equalTo('evaluateGuard'));
204 | $this->assertThat($this->actionLogger[4]['result'], $this->isFalse());
205 | $this->assertThat($this->actionLogger[5]['state'], $this->equalTo('Input'));
206 | $this->assertThat($this->actionLogger[5]['event'], $this->equalTo(StateActionInterface::EVENT_DO));
207 | $this->assertThat($this->actionLogger[5]['calledBy'], $this->equalTo('runAction'));
208 | }
209 |
210 | /**
211 | * @test
212 | *
213 | * @since Method available since Release 2.0.0
214 | */
215 | public function passesTheUserDefinedPayloadToActions()
216 | {
217 | $stateMachine = $this->stateMachineBuilder->getStateMachine();
218 | $stateMachine->addActionRunner(new class() implements ActionRunnerInterface {
219 | /**
220 | * {@inheritdoc}
221 | */
222 | public function run(EventInterface $event, $payload, StateMachineInterface $stateMachine, TransitionInterface $transition = null)
223 | {
224 | if (($transition === null ? $stateMachine->getCurrentState() : $transition->getFromState())->getStateId() == 'Input' && $event->getEventId() == 'next') {
225 | $payload->foo = 'baz';
226 | }
227 | }
228 | });
229 | $payload = new \stdClass();
230 | $payload->foo = 'bar';
231 | $stateMachine = $this->stateMachineBuilder->getStateMachine();
232 | $stateMachine->setPayload($payload);
233 | $stateMachine->start();
234 | $stateMachine->triggerEvent('next');
235 |
236 | $this->assertThat($payload->foo, $this->equalTo('baz'));
237 | }
238 |
239 | /**
240 | * @test
241 | *
242 | * @since Method available since Release 2.0.0
243 | */
244 | public function passesTheUserDefinedPayloadToGuards()
245 | {
246 | $stateMachine = $this->stateMachineBuilder->getStateMachine();
247 | $stateMachine->addActionRunner($this->actionLogger);
248 | $stateMachine->addGuardEvaluator($this->actionLogger);
249 | $this->actionLogger->setGuardEvaluator(new class() implements GuardEvaluatorInterface {
250 | /**
251 | * {@inheritdoc}
252 | */
253 | public function evaluate(EventInterface $event, $payload, StateMachineInterface $stateMachine, TransitionInterface $transition)
254 | {
255 | if ($stateMachine->getCurrentState()->getStateId() == 'Input' && $event->getEventId() == 'next') {
256 | $payload->foo = 'baz';
257 | }
258 |
259 | return true;
260 | }
261 | });
262 | $payload = new \stdClass();
263 | $payload->foo = 'bar';
264 | $stateMachine = $this->stateMachineBuilder->getStateMachine();
265 | $stateMachine->setPayload($payload);
266 | $stateMachine->start();
267 | $stateMachine->triggerEvent('next');
268 |
269 | $this->assertThat($payload->foo, $this->equalTo('baz'));
270 | }
271 |
272 | /**
273 | * @test
274 | *
275 | * @since Method available since Release 2.0.0
276 | */
277 | public function getsTheIdOfTheStateMachine()
278 | {
279 | $stateMachine = $this->stateMachineBuilder->getStateMachine();
280 |
281 | $this->assertThat($stateMachine->getStateMachineId(), $this->equalTo('Registration'));
282 | }
283 |
284 | /**
285 | * @test
286 | */
287 | public function dispatchesSystemEventsToListenersIfTheEventDispatcherHasBeenSet()
288 | {
289 | $events = [];
290 | $eventDispatcher = new EventDispatcher();
291 | $eventDispatcher->addListener(StateMachineEvents::EVENT_PROCESS, function (StateMachineEvent $event, $eventName, EventDispatcherInterface $eventDispatcher) use (&$events) {
292 | $events[] = ['name' => $eventName, 'event' => $event];
293 | });
294 | $eventDispatcher->addListener(StateMachineEvents::EVENT_EXIT, function (StateMachineEvent $event, $eventName, EventDispatcherInterface $eventDispatcher) use (&$events) {
295 | $events[] = ['name' => $eventName, 'event' => $event];
296 | });
297 | $eventDispatcher->addListener(StateMachineEvents::EVENT_TRANSITION, function (StateMachineEvent $event, $eventName, EventDispatcherInterface $eventDispatcher) use (&$events) {
298 | $events[] = ['name' => $eventName, 'event' => $event];
299 | });
300 | $eventDispatcher->addListener(StateMachineEvents::EVENT_ENTRY, function (StateMachineEvent $event, $eventName, EventDispatcherInterface $eventDispatcher) use (&$events) {
301 | $events[] = ['name' => $eventName, 'event' => $event];
302 | });
303 | $eventDispatcher->addListener(StateMachineEvents::EVENT_DO, function (StateMachineEvent $event, $eventName, EventDispatcherInterface $eventDispatcher) use (&$events) {
304 | $events[] = ['name' => $eventName, 'event' => $event];
305 | });
306 | $stateMachineBuilder = new StateMachineBuilder();
307 | $stateMachineBuilder->addState('locked');
308 | $stateMachineBuilder->addState('unlocked');
309 | $stateMachineBuilder->setStartState('locked');
310 | $stateMachineBuilder->addTransition('locked', 'unlocked', 'insertCoin');
311 | $stateMachineBuilder->addTransition('unlocked', 'locked', 'pass');
312 | $stateMachine = $stateMachineBuilder->getStateMachine();
313 | $stateMachine->setEventDispatcher($eventDispatcher);
314 | $stateMachine->start();
315 | $stateMachine->triggerEvent('insertCoin');
316 |
317 | $this->assertThat(count($events), $this->equalTo(9));
318 |
319 | $this->assertThat($events[0]['name'], $this->equalTo(StateMachineEvents::EVENT_PROCESS));
320 | $this->assertThat($events[0]['event']->getStateMachine(), $this->identicalTo($stateMachine));
321 | $this->assertThat($events[0]['event']->getState()->getStateId(), $this->equalTo(StateMachineInterface::STATE_INITIAL));
322 | $this->assertThat($events[0]['event']->getEvent(), $this->isInstanceOf('Stagehand\FSM\Event\EventInterface'));
323 | $this->assertThat($events[0]['event']->getEvent()->getEventId(), $this->equalTo(StateMachineInterface::EVENT_START));
324 |
325 | $this->assertThat($events[1]['name'], $this->equalTo(StateMachineEvents::EVENT_TRANSITION));
326 | $this->assertThat($events[1]['event']->getStateMachine(), $this->identicalTo($stateMachine));
327 | $this->assertThat(($events[1]['event']->getTransition() === null ? $events[1]['event']->getState() : $events[1]['event']->getTransition()->getFromState())->getStateId(), $this->equalTo(StateMachineInterface::STATE_INITIAL));
328 | $this->assertThat($events[1]['event']->getEvent(), $this->isInstanceOf('Stagehand\FSM\Event\EventInterface'));
329 | $this->assertThat($events[1]['event']->getEvent()->getEventId(), $this->equalTo(StateMachineInterface::EVENT_START));
330 |
331 | $this->assertThat($events[2]['name'], $this->equalTo(StateMachineEvents::EVENT_ENTRY));
332 | $this->assertThat($events[2]['event']->getStateMachine(), $this->identicalTo($stateMachine));
333 | $this->assertThat($events[2]['event']->getState()->getStateId(), $this->equalTo('locked'));
334 | $this->assertThat($events[2]['event']->getEvent(), $this->isInstanceOf('Stagehand\FSM\Event\EventInterface'));
335 | $this->assertThat($events[2]['event']->getEvent()->getEventId(), $this->equalTo(StateActionInterface::EVENT_ENTRY));
336 |
337 | $this->assertThat($events[3]['name'], $this->equalTo(StateMachineEvents::EVENT_DO));
338 | $this->assertThat($events[3]['event']->getStateMachine(), $this->identicalTo($stateMachine));
339 | $this->assertThat($events[3]['event']->getState()->getStateId(), $this->equalTo('locked'));
340 | $this->assertThat($events[3]['event']->getEvent(), $this->isInstanceOf('Stagehand\FSM\Event\EventInterface'));
341 | $this->assertThat($events[3]['event']->getEvent()->getEventId(), $this->equalTo(StateActionInterface::EVENT_DO));
342 |
343 | $this->assertThat($events[4]['name'], $this->equalTo(StateMachineEvents::EVENT_PROCESS));
344 | $this->assertThat($events[4]['event']->getStateMachine(), $this->identicalTo($stateMachine));
345 | $this->assertThat($events[4]['event']->getState()->getStateId(), $this->equalTo('locked'));
346 | $this->assertThat($events[4]['event']->getEvent(), $this->isInstanceOf('Stagehand\FSM\Event\EventInterface'));
347 | $this->assertThat($events[4]['event']->getEvent()->getEventId(), $this->equalTo('insertCoin'));
348 |
349 | $this->assertThat($events[5]['name'], $this->equalTo(StateMachineEvents::EVENT_EXIT));
350 | $this->assertThat($events[5]['event']->getStateMachine(), $this->identicalTo($stateMachine));
351 | $this->assertThat($events[5]['event']->getState()->getStateId(), $this->equalTo('locked'));
352 | $this->assertThat($events[5]['event']->getEvent(), $this->isInstanceOf('Stagehand\FSM\Event\EventInterface'));
353 | $this->assertThat($events[5]['event']->getEvent()->getEventId(), $this->equalTo(StateActionInterface::EVENT_EXIT));
354 |
355 | $this->assertThat($events[6]['name'], $this->equalTo(StateMachineEvents::EVENT_TRANSITION));
356 | $this->assertThat($events[6]['event']->getStateMachine(), $this->identicalTo($stateMachine));
357 | $this->assertThat(($events[6]['event']->getTransition() === null ? $events[6]['event']->getState() : $events[6]['event']->getTransition()->getFromState())->getStateId(), $this->equalTo('locked'));
358 | $this->assertThat($events[6]['event']->getEvent(), $this->isInstanceOf('Stagehand\FSM\Event\EventInterface'));
359 | $this->assertThat($events[6]['event']->getEvent()->getEventId(), $this->equalTo('insertCoin'));
360 |
361 | $this->assertThat($events[7]['name'], $this->equalTo(StateMachineEvents::EVENT_ENTRY));
362 | $this->assertThat($events[7]['event']->getStateMachine(), $this->identicalTo($stateMachine));
363 | $this->assertThat($events[7]['event']->getState()->getStateId(), $this->equalTo('unlocked'));
364 | $this->assertThat($events[7]['event']->getEvent(), $this->isInstanceOf('Stagehand\FSM\Event\EventInterface'));
365 | $this->assertThat($events[7]['event']->getEvent()->getEventId(), $this->equalTo(StateActionInterface::EVENT_ENTRY));
366 |
367 | $this->assertThat($events[8]['name'], $this->equalTo(StateMachineEvents::EVENT_DO));
368 | $this->assertThat($events[8]['event']->getStateMachine(), $this->identicalTo($stateMachine));
369 | $this->assertThat($events[8]['event']->getState()->getStateId(), $this->equalTo('unlocked'));
370 | $this->assertThat($events[8]['event']->getEvent(), $this->isInstanceOf('Stagehand\FSM\Event\EventInterface'));
371 | $this->assertThat($events[8]['event']->getEvent()->getEventId(), $this->equalTo(StateActionInterface::EVENT_DO));
372 | }
373 |
374 | /**
375 | * @test
376 | *
377 | * @since Method available since Release 2.1.0
378 | */
379 | public function returnsNullAsTheCurrentStateBeforeStartingTheStateMachine()
380 | {
381 | $stateMachine = $this->stateMachineBuilder->getStateMachine();
382 |
383 | $this->assertThat($stateMachine->getCurrentState(), $this->isNull());
384 | }
385 |
386 | /**
387 | * @test
388 | *
389 | * @since Method available since Release 2.1.0
390 | */
391 | public function returnsNullAsThePreviousStateBeforeStartingTheStateMachine()
392 | {
393 | $stateMachine = $this->stateMachineBuilder->getStateMachine();
394 |
395 | $this->assertThat($stateMachine->getPreviousState(), $this->isNull());
396 | }
397 |
398 | /**
399 | * @test
400 | *
401 | * @since Method available since Release 2.1.0
402 | */
403 | public function raisesAnExceptionWhenAnEventIsTriggeredBeforeStartingTheStateMachine()
404 | {
405 | $stateMachine = $this->stateMachineBuilder->getStateMachine();
406 |
407 | try {
408 | $stateMachine->triggerEvent('foo');
409 | } catch (StateMachineNotStartedException $e) {
410 | $this->assertTrue(true);
411 |
412 | return;
413 | }
414 |
415 | $this->fail('An expected exception has not been raised.');
416 | }
417 |
418 | /**
419 | * @test
420 | *
421 | * @since Method available since Release 2.1.0
422 | */
423 | public function raisesAnExceptionWhenStartingTheStateMachineIfItIsAlreadyStarted()
424 | {
425 | $stateMachine = $this->stateMachineBuilder->getStateMachine();
426 | $stateMachine->start();
427 |
428 | try {
429 | $stateMachine->start();
430 | } catch (StateMachineAlreadyStartedException $e) {
431 | $this->assertTrue(true);
432 |
433 | return;
434 | }
435 |
436 | $this->fail('An expected exception has not been raised.');
437 | }
438 |
439 | /**
440 | * @test
441 | *
442 | * @since Method available since Release 2.3.0
443 | */
444 | public function logsTransitions()
445 | {
446 | $stateMachine = $this->stateMachineBuilder->getStateMachine();
447 | $stateMachine->start();
448 | $stateMachine->triggerEvent('next');
449 | $stateMachine->triggerEvent('valid');
450 | $stateMachine->triggerEvent('next');
451 | $stateMachine->triggerEvent('next');
452 | $stateMachine->triggerEvent('next');
453 | $transitionLogs = $stateMachine->getTransitionLog();
454 |
455 | $expectedTransitionLogs = [
456 | [StateMachineInterface::STATE_INITIAL, StateMachineInterface::EVENT_START, 'Input'],
457 | ['Input', 'next', 'Validation'],
458 | ['Validation', 'valid', 'Confirmation'],
459 | ['Confirmation', 'next', 'Registration'],
460 | ['Registration', 'next', 'Success'],
461 | ['Success', 'next', StateMachineInterface::STATE_FINAL],
462 | ];
463 |
464 | $this->assertThat(count($transitionLogs), $this->equalTo(count($expectedTransitionLogs)));
465 |
466 | for ($i = 0; $i < count($transitionLogs); ++$i) {
467 | $this->assertThat($transitionLogs[$i]->getFromState()->getStateId(), $this->equalTo($expectedTransitionLogs[$i][0]));
468 | $this->assertThat($transitionLogs[$i]->getEvent()->getEventId(), $this->equalTo($expectedTransitionLogs[$i][1]));
469 | $this->assertThat($transitionLogs[$i]->getToState()->getStateId(), $this->equalTo($expectedTransitionLogs[$i][2]));
470 | $this->assertThat($transitionLogs[$i]->getTransitionDate(), $this->isInstanceOf('DateTime'));
471 | }
472 | }
473 | }
474 |
--------------------------------------------------------------------------------