tmilos / light-fsm
Light and simple finite state machine with nice API for automatic workflow and external state, with export to dot feature
Requires (Dev)
- phpunit/phpunit: ~4.8
- satooshi/php-coveralls: ~0.6
This package is not auto-updated.
Last update: 2024-10-26 19:32:24 UTC
README
Finite-state machine FSM PHP library. Create state machines and lightweight state machine-based workflows directly in PHP code.
$phoneCall = new StateMachine(State::OFF_HOOK); $phoneCall->configure(State::OFF_HOOK) ->permit(Event::CALL_DIALED, State::RINGING); $phoneCall->configure(State::RINGING) ->permit(Event::HUNG_UP, State::OFF_HOOK) ->permit(Event::CALL_CONNECTED, State::CONNECTED); $phoneCall->configure(State::CONNECTED) ->onEntry([$this, 'startTimer']) ->onExit([$this, 'stopTimer']) ->permit(Event::HUNG_UP, State::OFF_HOOK) ->permit(Event::PLACE_ON_HOLD, State::ON_HOLD); $phoneCall->fire(Event::CALL_DIALED); $this->assertEquals(State::RINGING, $phoneCall->getCurrentState());
This project, as well as the example above, was inspired by stateless.
Features
- State and trigger events of type string or int
- Firing trigger events with additional data
- Hierarchical states
- Entry/exit events for states
- Introspection
- Guard callbacks to support conditional transitions
- Ability to store state externally (for example, in a property tracked by an ORM)
- Export to DOT graph
Firing trigger events with additional data
Event can be fired with additional data StateMachine::fire($event, $data)
that will be passed and available to entry/exit and guard
listeners, so they can base their logic based on it.
Hierarchical States
In the example below, the ON_HOLD
state is a substate of the CONNECTED
state. This means that an ON_HOLD
call is still connected.
$phoneCall->configure(State::ON_HOLD) ->subStateOf(State::CONNECTED) ->permit(Event::CALL_CONNECTED, State::CONNECTED);
In addition to the StateMachine::getCurrentState()
method, which will report the precise current state, an isInState($state)
method is also provided. isInState($state)
will take substates into account, so that if the example above was in the
ON_HOLD
state, isInState(State::CONNECTED)
would also evaluate to true
.
Entry/Exit Events
In the example, the startTimer()
method will be executed when a call is connected. The stopTimer()
will be executed when
call completes.
When call moves between the CONNECTED
and ON_HOLD
states, since the ON_HOLD
state is a substate of the CONNECTED
state,
these listeners can distinguish substates and note that call is still connected based on the first $isSubState
argument.
External State Storage
In order to listen for state changes for persistence purposes, for example with some ORM tool, pass the listener callback
to the StateMachine
constructor.
$stateObject = $orm-find(); $stateMachine = new StateMachine( function () use ($stateObject) { return $stateObject->getValue(); }, function ($state) use ($stateObject) { $stateObject->setValue($state); $orm->persist($stateObject); } );
In this case, when StateMachine
is constructed with two callbacks, the state is held totaly external, and each time
StateMachine
needs current state, the first callback will be called, and each time the state changes, the second callback
will be called.
Introspection
The state machine can provide a list of the trigger events than can be successfully fired within the current state by
the StateMachine::getPermittedTriggers()
method.
Guard Clauses
The state machine will choose between multiple transitions based on guard clauses, e.g.:
$phoneCall->configure(State::OFF_HOOK) .permit(Trigger::CALL_DIALLED, State::RINGING, function ($data) { return IsValidNumber($data); }) .permit(Trigger::CALL_DIALLED, State::BEEPING, function ($data) { return !IsValidNumber($data); });
Export to DOT graph
It can be useful to visualize state machines on runtime. With this approach the code is the authoritative source and state diagrams are by-products which are always up to date.
$phoneCall->configure(State::OFF_HOOK) .permit(Trigger::CALL_DIALED, State::RINGING, 'IsValidNumber'); $graph = phoneCall->toDotGraph();
The StateMachine::toDotGraph()
method returns a string representation of the state machine in the
DOT graph language, e.g.:
digraph {
"off-hook" -> "ringing" [label="call-dialed [IsValidNumber]"];
}
This can then be rendered by tools that support the DOT graph language, such as the dot command line tool from graphviz.org or viz.js. See (http://www.webgraphviz.com) for instant gratification. Command line example to generate a PDF file:
> dot -T pdf -o phoneCall.pdf phoneCall.dot