chippyash / finite-state-machine
A Finite State Machine Implementation
Requires
- php: >=7.2
- chippyash/assembly-builder: ^2.1
- chippyash/monad: ~2.1
- graphp/graphviz: ~0.2
- myclabs/php-enum: ^1.7
- psr/event-dispatcher: ^1.0
Requires (Dev)
- ext-dom: *
- chippyash/testdox-converter: ^1.0
- mikey179/vfsstream: ~1.6
- phpunit/phpunit: ~6.5
Suggests
- ext-dom: To build StateGraphs from an XML Definition file
This package is auto-updated.
Last update: 2024-12-08 10:06:10 UTC
README
chippyash/finite-state-machine
Quality Assurance
The above badges represent the current development branch. As a rule, I don't push to GitHub unless tests, coverage and usability are acceptable. This may not be true for short periods of time; on holiday, need code for some other downstream project etc. If you need stable code, use a tagged version.
What
Provides an Event aware Finite State Machine (FSM) implementation
Why
FSMs allow us to separate out the logic of how objects change state from the object themselves. It also means that we can configure state in the DIC or some other external storage, and therefore change it easily to fine tune for particular circumstances without breaking the object itself.
FSMs are the perfect tool for implementing workflow solutions.
How
Conceptually, a state machine is a form of directed graph: A number of nodes (states), connected by edges (transitions) that can only go in one direction. In graph theory, a directed graph doesn’t necessarily have a start and end point.
We can implement this theoretical graph using a Finite State Machine (FSM).
Another term for this is ‘Deterministic Finite Automaton’. An FSM always
has a start point or starting state
, but may not have an end state, i.e.
the state can be constantly in flux. An object
transitions
from one state to another depending on the transitions
available to it and any conditions attached to the transition.
A transition is initiated by an event
.
A thing or object that is being controlled by an FSM can only be in one state at a time. This is very different from a Multiple State Machine or Nondeterministic Finite Automaton, which whilst they have their use in computing patterns, are not used for typical human oriented workflows (people being really bad at doing two things at the same time!). Note however that it is entirely possible for an object to be involved in two or more parallel activities, e.g. if an object has a state of ‘communicate’, then it is feasible that as parallel activities it can communicate via email and communicate via sms. When both activities are complete it can be moved from the ‘communicate’ state to some other state.
I use the term ‘object that is being controlled by an FSM' in a precise sense. A common mistake when designing workflow systems is to tie the state change to the object in question. This invariably leads to polluting the object itself with stuff about how to change state. That is wrong, and leads to convoluted code that has nothing to do with the object’s principle responsibility, e.g. representing an Issue in an issue tracking system; representing a car on an assembly line etc.
Rather it is better that the object implements an interface that allows it to converse with an external FSM. This abstracts the logic of state change away from the object, leaving the object to do its job and the FSM responsible for transitioning state for the object. In a light touch sense, the object only wants to know what state it is in now. It shouldn’t really care where it is next until it gets there. Conversely, the FSM doesn’t care what the object is, only that it has somehow been asked to change its state. If the object really wants to know what choices it has to change state, it asks the FSM what its options are. This conforms to the ‘Single Responsibility Principle’ of SOLID.
This implementation provides:
- A StateGraph. A directed graph of states and the possible transitions between those states. This utilises the graphp/graphviz library.
- An EventableStateGraph. This supports PSR-14 Event Dispatcher interface. This allows Transitions from one state to another to be a result of an external force. It also allows you to create event listeners that can block a Transition and carry out actions after a Transition has been made.
- An object that can move through states, the client object. This is via the StateAware interface. You need to implement this interface in objects that you want the StateGraph to manage
For development
See the Test Contract
Installation
composer require chippyash/finite-state-machine
Creating state graphs
use Chippyash\StateMachine\StateGraph; //add states and transitions $graph = new StateGraph('nameOfGraph'); $graph->addState(new State('state1')) ->addState(new State('state2')) ->addState(new State('state3')) ->addTransition(new State('state1'), new State('state2'), new Transition('pending')) ->addTransition(new State('state2'), new State('state3'), new Transition('complete'));
Trying to add duplicate state or transition names will throw an exception.
NB - you must have at least one initial state, i.e. one that has no incoming transitions. You do not need a final state, but in most applications there will be one. A final state is a state with no outgoing transitions.
You can retrieve the initial states for a graph with getInitialStates(): array
. It is up to your client to determine
which initial state to use. For most applications there will only be one initial state.
You can validate the graph with isValid(): bool
. This checks that you do not have states that are not part of
a transition and that there is at least one initial state.
You can retrieve a clone of the composited Graph object with getGraph(): Graph
. This allows you to use the underlaying
graph for other operations outside the remit of this library. Once such operation may be to create a visual representation
of the Graph. See graphp/graphviz. This library is built on top of Graphiz, so
you already have access to its functionality. In addition, you can proxy a method
call to the underlying Graph object via the __call() method
Creating objects that can move through state
Any class can be used in a State Machine. Simply implement the StateAware interface in your class. You can use the HasState trait to do this simply.
The State Machine is NOT responsible for saving that state to storage.
That is the concern of the object.
Transitions
Call the $graph->transition()
method.
$object = new MyStateAwareObject(); $transition = new Transition('t1'); try { $graph->transition($object, $transition); } catch (StateMachineException $e) { //process the error - see `Exceptions` directory }
transition()
will throw exceptions if the Transition doesn't exist in the
graph or the object's current state doesn't exist etc.
Event driven state transitions
The StateMachine\Events\EventableStateGraph
class extends StateGraph
to provide methods for latching a Stategraph into a PSR-14 compliant
Event Manager.
use Chippyash\StateMachine\Events\EventableStateGraph; $graph = new EventableStateGraph('stategraphname'); $graph->setEventDispatcher($EventDispatcherInterfaceCompatible);
Register the EventableStateGraph::eventListener(StateGraphEventable $event) method as a listener for
StateGraphEventable` compatible events with a
StateGraphEventType::DO_TRANSITION() event type.
The eventListener()
method will trigger StateGraphEvent
type events
for StateGraphEventType::START_TRANSITION()
and StateGraphEventType::END_TRANSITION()
event types. You can register listeners for those events. For START_TRANSITION
event types, set the received event::stopPropagation attribute to true if you want
want to stop the transition from occurring. This allows you to set up
pre-conditions etc.
The END_TRANSITION event is useful for storing the new state of the object or logging changes etc.
All StateGraphEventable
compatible events get the Transition and the object
being acted upon in the event message class.
You can find example code for event driven State machines at https://github.com/the-matrix/the-coffee-machine
Building State Graphs
XML
use Chippyash\StateMachine\Builder\XmlBuilder; use Chippyash\StateMachine\Exceptions\InvalidStateMachineFileException; try { $stateGraph = (new XmlBuilder())->build('path/to/graph.xml'); } catch (InvalidStateMachineFileException $e) { //process errors }
The build method takes a second parameter. Set it true to validate the source XML input.
XSD for validation is at src/StateMachine/Builder/statemachine.xsd
.
Example XML file is at docs/stategraph-example.xml
.
Changing the library
- fork it
- write the test
- amend it
- do a pull request
Found a bug you can't figure out?
- fork it
- write the test
- do a pull request
NB. Make sure you rebase to HEAD before your pull request
Or - raise an issue ticket.
For production
Any specific notes required to get this library into production
Roadmap
- StateGraph persistence
- XML - DONE
- Json
- Yaml
License
This software library is released under the BSD 3 Clause license
This software library is Copyright (c) 2018-2019, Ashley Kitson, UK
History
V0... pre release
V1.0.0 Initial release