php__phplib/OSInet/Finite_State_Machine/Machine.php
Frederic G. MARAND a28ed41155 FSM: PSR-0 conversion, Grapher added.
- 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.
2012-04-18 00:10:46 +02:00

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']);
}
}
}
}
}