| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 | /*Package fsm defines an embeddable finite state machine, as a straight port of the 2012 PHP[OSInet\Finite_State_Machine\Machine](https://code.osinet.fr/fgm/php__phplib.git]See http://wiki.audean.com/fsm/fsm for details.Copyright  (c) 2023 Ouest Systèmes InformatiquesLicense GPL-3.0*/package legacyimport (	"errors"	"fmt"	"log"	"golang.org/x/exp/maps"	"golang.org/x/exp/slices")type (	Event      any	EventMode  int	State      string	Transition map[Event][2]Result)const (	Version   string = "12D22"	IdleEvent        = "idle" // must not change name: method fIdle depends on it	InitState  State = "init"	FinalState State = "final")const (	EventInvalid EventMode = iota // Should not be seen	EventNormal                   // Processes events	EventQueue                    // Queue events for later use	EventSink                     // Throw away events)// Machine is a composable state machine.//// Applications must create an actual FSM composing such a Machine, 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)type Machine struct {	AllowActions bool	EventMode    EventMode	IdleWork     bool	Queue        []any	State        State	Transitions  map[State]Transition}// checkTransitions verifies than a transitions graph has been defined.//// Since applications are not expected to modify the machine once initialized,// it is not meant to be used beyond New.func (m Machine) checkTransitions() error {	if len(m.Transitions) == 0 {		return errors.New("no FSM processing is allowed without a transitions table")	}	if _, ok := m.Transitions[InitState]; ok {		return errors.New("transitions map does not have any initial state")	}	return nil}// New initializes the FSM to the first state in the transitions tablefunc New(transitions map[State]Transition) (*Machine, error) {	m := Machine{		AllowActions: true,		IdleWork:     true,		EventMode:    EventNormal,		Queue:        []any{},     // Queue is the event queue for EVENT_QUEUE mode		State:        InitState,   // State holds the current state of the machine		Transitions:  transitions, // Transitions holds the transitions table	}	if err := m.checkTransitions(); err != nil {		return nil, err	}	return &m, nil}// AcceptedEvents returns the list of events accepted in the current state.func (m Machine) AcceptedEvents() []Event {	t, ok := m.Transitions[m.State]	if !ok {		return nil	}	events := maps.Keys(t)	return events}// AcceptedOutcomes returns the list of outcomes accepted in the current state for the given eventfunc (m Machine) AcceptedOutcomes(e Event) ([2]Result, error) {	res, ok := m.Transitions[m.State][e]	if !ok {		return [2]Result{}, fmt.Errorf("event %q not accepted in state %q", e, m.State)	}	return res, nil}// IsEventAllowed verifies whether the given event is accepted in the current statefunc (m Machine) IsEventAllowed(e Event) bool {	t := m.Transitions[m.State]	isIt := slices.Contains(maps.Keys(t), e)	return isIt}// IsOutcomeAllowed checks whether a given outcome available for a given event, considering the current state.func (m Machine) IsOutcomeAllowed(e Event, outcome Result) bool {	if !m.IsEventAllowed(e) {		return false	}	res, err := m.AcceptedOutcomes(e)	if err != nil {		return false	}	isIt := outcome == res[0] || outcome == res[1]	return isIt}// ApplyEvent applies both the given event and the resulting event chain if applicable.func (m *Machine) ApplyEvent(e Event) Result {	var res Result	for {		var next Event		res = m.ApplySimpleEvent(e)		if m.AllowActions {			next = res.Action		} else {			next = nil		}		if next != nil {			break		}	}	return res}// ApplySimpleEvent is a helper for ApplyEvent that does not implement the post-transition action//// See ApplyEventfunc (m *Machine) ApplySimpleEvent(e Event) Result {	currentState := m.State	if e == IdleEvent && !m.IdleWork {		return Result{}	}	if !m.IsEventAllowed(e) {	   log.Fatalf("Event %q not allowed in current state %q.\n", e, currentState)	}return Result{}}	$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 docase self::EVENT_QUEUE:break// Empty queue if neededcase self::EVENT_SINK:if (count($this->fQueue) > 0) {$this->fQueue = array()}breakdefault:throw new Exception("Trying to set unknown FSM mode $mode")}$this->fEventMode = $modereturn $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 SimpleXMLElementforeach ($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"}breakcase 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'])}}}}}
 |