- converted class names and directories for PSR0 - replaced the basic __autoload() by the canonical PSR0 loader example. - added a GraphViz-based Grapher class to plot Machine structures.
413 lines
10 KiB
PHP
413 lines
10 KiB
PHP
<?php
|
|
|
|
namespace OSInet\Finite_State_Machine;
|
|
|
|
/**
|
|
* Abstract state machine.
|
|
*
|
|
* @copyright (c) 2007-2012 Ouest Systèmes Informatiques
|
|
* @author Frederic G. MARAND
|
|
* @license CeCILL 2.0
|
|
* @link http://wiki.audean.com/fsm/fsm
|
|
* @since FSM 1.6
|
|
*
|
|
* This class must be inherited by code implementing actual FSMs.
|
|
*
|
|
* Applications must create a Machine descendant in which they will:
|
|
* - define the fTransitions table, usually in their constructor
|
|
* - invoke the parent constructor to set up the FSM from the transitions table
|
|
* - create "f_foo" functions for each "foo" event, except "idle", which is builtin.
|
|
*
|
|
* Applications may:
|
|
* - disable event processing by setting $this->event to one of the fsm::EVENT_* constants
|
|
* - disable post-event actions by setting $this->allowsActions to false
|
|
* - disable the builtin idle event by setting $this->idleWork to false
|
|
* - query the current state by using $this->getState()
|
|
* - send an idle event by using $this->idle()
|
|
* - submit any event (including idle) by using $this->applyEvent($eventName)
|
|
*/
|
|
abstract class Machine {
|
|
const VERSION = '12D22';
|
|
|
|
const IDLE_EVENT = 'idle'; // must not change name: method fIdle depends on it
|
|
|
|
const INIT_STATE = 'init';
|
|
const FINAL_STATE = 'final';
|
|
|
|
const EVENT_NORMAL = 1; // processes events
|
|
const EVENT_QUEUE = 2; // queue events for later use
|
|
const EVENT_SINK = 3; // throw away events
|
|
|
|
public $idleWork = TRUE;
|
|
public $allowActions = TRUE;
|
|
|
|
protected $fEventMode = self::EVENT_NORMAL;
|
|
protected $fQueue = array(); // event queue for EVENT_QUEUE mode
|
|
|
|
/**
|
|
* the current state of the object
|
|
* @var string
|
|
*/
|
|
protected $fState;
|
|
|
|
/**
|
|
* Transitions holds the transitions table
|
|
* state1
|
|
* event1
|
|
* result1
|
|
* state_name|Result
|
|
* event2
|
|
* ...
|
|
* ..
|
|
* ..
|
|
* @var array
|
|
*/
|
|
public $fTransitions = null;
|
|
|
|
/**
|
|
* constructor initializes the FSM to the first
|
|
* state in the transitions table
|
|
* @return void
|
|
*/
|
|
public function __construct() {
|
|
$this->checkTransitions();
|
|
|
|
reset($this->fTransitions);
|
|
$x = each($this->fTransitions);
|
|
$x = $x[0];
|
|
$this->fState = $x;
|
|
}
|
|
|
|
/**
|
|
* Make sure a transitions graph has been defined
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function checkTransitions() {
|
|
if (!isset($this->fTransitions))
|
|
throw new Exception('No FSM processing is allowed without a transitions table\n');
|
|
}
|
|
|
|
/**
|
|
* Getter for fState
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getState() {
|
|
return $this->fState;
|
|
}
|
|
|
|
/**
|
|
* return the list of events accepted in the current state
|
|
* @return array
|
|
*/
|
|
public function getAcceptedEvents() {
|
|
$this->checkTransitions();
|
|
|
|
try {
|
|
$events = array_keys($this->fTransitions[$this->fState]);
|
|
// echo func_name() . ": state $this->fState, accepted events are:\n" . print_r($events, true). "\n";
|
|
}
|
|
catch (Exception $e) {
|
|
echo "Exception in getAcceptedEvents" . print_r($e);
|
|
print_r(debug_backtrace());
|
|
$events = array();
|
|
}
|
|
|
|
return $events;
|
|
}
|
|
|
|
/**
|
|
* return the list of outcome accepted in the current state for the give event
|
|
* @param string $eventName
|
|
* @param mixed $outcome
|
|
* @return array
|
|
*/
|
|
public function getAcceptedOutcomes($eventName) {
|
|
// echo func_name() . "\n";
|
|
$this->checkTransitions();
|
|
|
|
/**
|
|
* Spare some paranioa
|
|
*
|
|
if (!$this->isEventAllowed($eventName))
|
|
throw new Exception(func_name() . ": event \"$eventName\" not allowed in state \"$this->fState\"\n");
|
|
*/
|
|
|
|
$outcomes = array_keys($this->fTransitions[$this->fState][$eventName]);
|
|
// print_r($this->fTransitions[$this->fState][$eventName]);
|
|
// echo "outcomes for event $eventName: " . var_dump($outcomes) . "\n";
|
|
return $outcomes;
|
|
}
|
|
|
|
/**
|
|
* is this event accepted in the current state
|
|
* the FSM is in ?
|
|
*
|
|
* @param string $eventName
|
|
* @return boolean
|
|
*/
|
|
public function isEventAllowed($eventName) {
|
|
// echo func_name() . "($eventName)";
|
|
$this->checkTransitions();
|
|
|
|
$ret = in_array($eventName, $this->getAcceptedEvents());
|
|
// echo " in state $this->fState, result = <$ret>\n";
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* is a given outcome available for a given event,
|
|
* considering the current context ?
|
|
*
|
|
* @param string $eventName
|
|
* @param mixed $outcome
|
|
* @return boolean
|
|
*/
|
|
public function isOutcomeAllowed($eventName, $outcome) {
|
|
$this->checkTransitions();
|
|
|
|
if (!$this->isEventAllowed($eventName)) {
|
|
return false;
|
|
}
|
|
|
|
$ret = array_key_exists($outcome, $this->getAcceptedOutcomes($eventName));
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* apply an event, and the resulting event chain if needed
|
|
*
|
|
* @param string $eventName
|
|
* @param array $params the
|
|
* @return Result resulting state
|
|
*/
|
|
public function applyEvent($eventName) {
|
|
// echo "Start of " . func_name() . "\n";
|
|
|
|
do {
|
|
$result = $this->applySimpleEvent($eventName);
|
|
if ($this->allowActions) {
|
|
$eventName = $result->fsmAction; // can be NULL
|
|
} else {
|
|
$eventName = NULL;
|
|
}
|
|
} while ($eventName);
|
|
// echo "End of " . func_name() . "\n";
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Helper for applyEvent that does not implement the post-transition action
|
|
*
|
|
* @see applyEvent()
|
|
*
|
|
* @param string $eventName
|
|
*
|
|
* @return Result
|
|
*/
|
|
private function applySimpleEvent($eventName) {
|
|
// echo "Start of " . func_name() . ", event = $eventName\n";
|
|
$currentState = $this->fState;
|
|
if (($eventName == self::IDLE_EVENT) && !$this->idleWork) {
|
|
return new Result();
|
|
}
|
|
|
|
if (!$this->isEventAllowed($eventName)) {
|
|
die("Event $eventName not allowed in current state $currentState.\n");
|
|
/* throw new Exception(func_name()
|
|
. ": Event \"$eventName\" not accepted in current state \"$currentState\"");
|
|
*/
|
|
}
|
|
|
|
$outcomes = $this->getAcceptedOutcomes($eventName);
|
|
|
|
$methodName = "event$eventName";
|
|
if (!method_exists($this, $methodName)) {
|
|
die (func_name() . ": missing method "
|
|
. get_class($this) . "::$methodName. Aborting.\n");
|
|
}
|
|
$outcome = $this->$methodName();
|
|
|
|
if (!in_array($outcome, $outcomes)) {
|
|
throw new \Exception(func_name()
|
|
. ": event guard. Transition on \"$eventName\" return invalid outcome: "
|
|
. print_r($outcome, true)
|
|
. " for state $this->fState\n");
|
|
}
|
|
|
|
$transition = &$this->fTransitions[$currentState][$eventName][$outcome];
|
|
$result = new Result (
|
|
$outcome,
|
|
$transition[0],
|
|
$transition[1]
|
|
);
|
|
if (isset($result->fsmState)) {
|
|
$this->fState = $result->fsmState;
|
|
}
|
|
/* print_r($this->fTransitions[$currentState][$eventName]);
|
|
var_dump($result); */
|
|
if (!isset($outcome)) {
|
|
$outcome = 'NULL';
|
|
}
|
|
|
|
/*
|
|
echo func_name()
|
|
. ": $currentState: " . $eventName . '[' . $outcome . ']'
|
|
. " -> $this->fState / " . $result->fsmAction . PHP_EOL;
|
|
*/
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Default event.
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function fIdle() {
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* Apply an fsm::IDLE_EVENT event. Do not confuse with fIdle !
|
|
*
|
|
* @return Result
|
|
*/
|
|
public function idle() {
|
|
return $this->applyEvent(self::IDLE_EVENT);
|
|
}
|
|
|
|
/**
|
|
* return the current operating mode of the FSM
|
|
* @return int
|
|
*/
|
|
public function getEventMode() {
|
|
return $this->fEventMode;
|
|
}
|
|
|
|
/**
|
|
* set the current operating mode for the FSM
|
|
*
|
|
* @param int $mode fsm::EVENT_* constants
|
|
* @return int $mode fsm::EVENT_* constants
|
|
*/
|
|
public function setEventMode($mode) {
|
|
switch ($mode) {
|
|
case self::EVENT_NORMAL :
|
|
if (count($this->fQueue) > 0) {
|
|
while (($event = array_shift($this->fQueue)) !== NULL) {
|
|
$this->applyEvent($event);
|
|
}
|
|
}
|
|
break;
|
|
|
|
// Nothing special to do
|
|
case self::EVENT_QUEUE:
|
|
break;
|
|
|
|
// Empty queue if needed
|
|
case self::EVENT_SINK :
|
|
if (count($this->fQueue) > 0) {
|
|
$this->fQueue = array();
|
|
}
|
|
break;
|
|
|
|
default:
|
|
throw new Exception("Trying to set unknown FSM mode $mode");
|
|
}
|
|
|
|
$this->fEventMode = $mode;
|
|
return $this->fEventMode;
|
|
}
|
|
|
|
/**
|
|
* Load the fTransitions table from an external resource.
|
|
*
|
|
* @param string $url
|
|
* Optional: defaults to the name of the class, . ".xml"
|
|
*/
|
|
public function loadFsm($url = NULL) {
|
|
$osd = FALSE; // on screen display (debug)
|
|
if ($osd) {
|
|
echo "Loading FSM from $url\n";
|
|
}
|
|
|
|
if (!isset($url)) {
|
|
$url = get_class($this) . ".xml";
|
|
}
|
|
|
|
$fsm = simplexml_load_file($url);
|
|
$fsmVersion = (string) $fsm['fsm_version'];
|
|
if ($fsmVersion !== '1.3') {
|
|
die("Revision $fsmVersion of schema is not supported.\n");
|
|
}
|
|
|
|
$this->idleWork = ($fsm['idle_work'] == 1);
|
|
$this->allowActions = ($fsm['allow_actions'] == 1);
|
|
|
|
$this->fTransitions = array();
|
|
$t = &$this->fTransitions;
|
|
|
|
// (string) casts in this loop are required: RHS is a SimpleXMLElement
|
|
foreach ($fsm->state as $state) {
|
|
$id = (string) $state['id'];
|
|
if ($osd) {
|
|
echo "State $id :\n";
|
|
}
|
|
$t[$id] = array();
|
|
switch ($id) {
|
|
case self::INIT_STATE :
|
|
if ($osd) {
|
|
echo " Initial state\n";
|
|
}
|
|
break;
|
|
|
|
case self::FINAL_STATE :
|
|
if ($osd) {
|
|
echo " Final state\n";
|
|
}
|
|
break;
|
|
}
|
|
|
|
foreach ($state->event as $event) {
|
|
$name = (string) $event['name'];
|
|
if ($osd) {
|
|
echo " Event $name";
|
|
}
|
|
|
|
if (!isset($event['type'])) {
|
|
$event['type'] = 'void';
|
|
}
|
|
|
|
$eventType = (string) $event['type'];
|
|
if ($osd) {
|
|
echo ", type $eventType\n";
|
|
}
|
|
|
|
foreach ($event as $next) {
|
|
if ($event['type'] == 'void') {
|
|
$next['result'] = 'always';
|
|
$result = null;
|
|
}
|
|
else {
|
|
$result = (string) $next['result'];
|
|
}
|
|
|
|
if (!isset($next['state'])) {
|
|
$next['state'] = (string) $state['id'];
|
|
}
|
|
if ($osd) {
|
|
echo " Next(" . $next['result'] . ') = ' . $next['state'];
|
|
if (isset($next['action'])) {
|
|
echo " + event " . $next['action'];
|
|
}
|
|
echo PHP_EOL;
|
|
}
|
|
$t[$id][$name][$result] = array(
|
|
(string) $next['state'],
|
|
(string) $next['action']);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|