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.
- 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
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.
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
.
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.
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.
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.
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); });
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