tmilos/light-fsm

Light and simple finite state machine with nice API for automatic workflow and external state, with export to dot feature

1.0.1 2016-12-21 11:49 UTC

This package is not auto-updated.

Last update: 2024-12-21 20:30:22 UTC


README

License Build Status Coverage Status HHVM Status Scrutinizer Code Quality SensioLabsInsight

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