├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .php_cs ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── docker-compose.yml.dist ├── docker ├── Dockerfile └── php.ini.dist ├── phpunit.xml ├── src ├── Event │ ├── Event.php │ └── EventInterface.php ├── Resources │ └── config │ │ └── serialization.xml ├── State │ ├── AutomaticTransitionInterface.php │ ├── AutomaticTransitionTrait.php │ ├── FinalState.php │ ├── ForkState.php │ ├── InitialState.php │ ├── JoinState.php │ ├── ParentStateInterface.php │ ├── State.php │ ├── StateActionInterface.php │ ├── StateActionTrait.php │ ├── StateCollection.php │ ├── StateInterface.php │ ├── TransitionalStateInterface.php │ └── TransitionalStateTrait.php ├── StateMachine │ ├── ConstraintException.php │ ├── StateMachine.php │ ├── StateMachineAlreadyShutdownException.php │ ├── StateMachineAlreadyStartedException.php │ ├── StateMachineBuilder.php │ ├── StateMachineEvent.php │ ├── StateMachineEvents.php │ ├── StateMachineInterface.php │ ├── StateMachineNotStartedException.php │ ├── StateNotFoundException.php │ └── TransitionLog.php └── Transition │ ├── ActionRunnerInterface.php │ ├── GuardEvaluatorInterface.php │ ├── Transition.php │ └── TransitionInterface.php └── tests ├── StateMachine ├── ActionLogger.php ├── ForkAndJoinTest.php ├── StateMachineBuilderTest.php └── StateMachineTest.php └── bootstrap.php /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stagehand_FSM 2 | 3 | A finite state machine 4 | 5 | [![Total Downloads](https://poser.pugx.org/piece/stagehand-fsm/downloads.png)](https://packagist.org/packages/piece/stagehand-fsm) 6 | [![Latest Stable Version](https://poser.pugx.org/piece/stagehand-fsm/v/stable.png)](https://packagist.org/packages/piece/stagehand-fsm) 7 | [![Latest Unstable Version](https://poser.pugx.org/phpmentors/stagehand-fsm/v/unstable.png)](https://packagist.org/packages/phpmentors/stagehand-fsm) 8 | [![Build Status](https://travis-ci.org/phpmentors-jp/stagehand-fsm.svg?branch=master)](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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | ./tests 11 | 12 | 13 | 14 | 15 | 16 | ./src 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------