You are here

base.inc in State Machine 7.3

Same filename and directory in other branches
  1. 6 inc/base.inc
  2. 7 inc/base.inc
  3. 7.2 inc/base.inc

Defines the base classes of the state machine.

File

inc/base.inc
View source
<?php

/**
 * @file
 * Defines the base classes of the state machine.
 */

/**
 * The base class.
 */
class StateMachine {
  protected $states = array();
  protected $events = array();
  protected $initial;
  protected $current;
  protected $object;

  /**
   * Create instance of StateMachine.
   *
   * @param object $object
   *   The object to set.
   */
  public function __construct($object = NULL) {
    $this->object = $object;
    if (!$this
      ->ignore()) {
      $this
        ->init();
      $this
        ->set_current_state((string) $this
        ->load());
    }
  }

  /**
   * Initialize the state machine.
   *
   * Uses the provided create_state and create_event methods.
   */
  protected function init() {
  }

  /**
   * Persist the current state to the object storage.
   */
  protected function persist() {
  }

  /**
   * Load the current state from the given state storage.
   */
  protected function load() {
  }

  /**
   * Create a new state.
   *
   * This method does not actually create a state instance, it only stores the
   * options array until an instance is requested from get_state().
   *
   * @param string $key
   *   The string identifier for this state.  Must be unique within the scope
   *   of the State Machine.
   * @param array $options
   *   An array of options that will be passed into the State constructor.
   *   - title: The human-readable title of the state.
   *   - on_enter: An array of callbacks to be fired when entering this state.
   *   - on_exit: An array of callbacks to be fired when exiting this state.
   */
  protected function create_state($key, $options = array()) {
    $this->states[$key] = $options;
  }

  /**
   * Return a state instance by key, lazy-loading the instance if necessary.
   *
   * @param string $key
   *   The string identifier of the state to be returned.
   *
   * @return StateMachine_State
   *   An instance of StateMachine_State, or FALSE if the key specified does
   *   not represent a valid state.
   */
  public function get_state($key) {
    if (!array_key_exists($key, $this->states)) {
      return FALSE;
    }
    if (is_array($this->states[$key])) {
      $options = $this->states[$key];
      $this->states[$key] = new StateMachine_State($key, $this, $options);
    }
    return $this->states[$key];
  }

  /**
   * Set the current state to the state identified by the specified key.
   */
  protected function set_current_state($key) {
    if (array_key_exists($key, $this->states)) {
      $this->current = $key;
      return TRUE;
    }
    return FALSE;
  }

  /**
   * Returns the current state.
   */
  public function get_current_state() {
    if (!$this->current) {
      if (!empty($this->default_state)) {
        $this->current = $this->default_state;
      }
      else {
        $this->current = key($this->states);
      }
    }
    return $this->current;
  }

  /**
   * Set the initial state for this machine.
   *
   * By default, the initial state is set the the first created state.
   */
  protected function set_initial_state($key) {
    if (array_key_exists($key, $this->states)) {
      $this->initial_state = $key;
      return TRUE;
    }
    return FALSE;
  }

  /**
   * Create a new event.
   *
   * This method does not actually create an event instance, it only stores the
   * options array until an instance is requested from get_event().
   *
   * @param string $key
   *   The string identifier for this event.  Must be unique within the scope
   *   of the State Machine.
   * @param array $options
   *   An array of options that will be passed into the Event constructor.
   *   - title: The human-readable title of the event.
   *   - origin: A key or array of keys representing the valid origin state(s)
   *     for this event.
   *   - target: The key of the state which this event will transition to.
   *   - guard: A callback to be fired before the event. If this function
   *     returns FALSE, the event will be cancelled.
   *   - before_transition: A callback to be fired after the guard condition,
   *     but before the transition.
   *   - after_transition: A callback to be fired after the completion of the
   *     transition.
   */
  protected function create_event($key, $options = array()) {
    $this->events[$key] = $options;
  }

  /**
   * Return an event instance by key, lazy-loading the instance if necessary.
   *
   * @param string $key
   *   The string identifier of the event to be returned.
   *
   * @return StateMachine_Event|FALSE
   *   An instance of StateMachine_Event, or FALSE if the key specified does
   *   not represent a valid event.
   */
  public function get_event($key) {
    if (!array_key_exists($key, $this->events)) {
      return FALSE;
    }
    if (is_array($this->events[$key])) {
      $options = $this->events[$key];
      $this->events[$key] = new StateMachine_Event($key, $this, $options);
    }
    return $this->events[$key];
  }

  /**
   * Trigger an event to process a transition.
   *
   * Callbacks and guard conditions will be processed in the following order:
   * - event:before_transition
   * - event:guard, exits if guard condition return FALSE
   * - oldstate:on_exit
   * - update the current state
   * - newstate:on_enter
   * - event:after_transition
   */
  public function fire_event($key) {
    $event = $this
      ->get_event($key);
    if ($event && ($new_state = $event
      ->execute())) {

      // Allow the previous state to run its 'on_exit' callbacks.
      $this
        ->get_state($this
        ->get_current_state())
        ->on_exit();

      // Set and save the new state.
      $this
        ->set_current_state($new_state);
      $this
        ->persist();

      // Allow the new state to run its 'on_enter' callbacks.
      $this
        ->get_state($this
        ->get_current_state())
        ->on_enter();

      // Allow the event to "finish"
      $event
        ->finish();
    }
    else {
      $this
        ->on_event_fail($event);
      return FALSE;
    }
  }

  /**
   * Method to be called when firing an event fails for any reason.
   */
  protected function on_event_fail($event) {
  }

  /**
   * Returns an array of events that are valid for the current state.
   */
  public function get_available_events() {
    $events = array();
    foreach ($this->events as $key => $event) {
      $event_object = $this
        ->get_event($key);
      if ($this
        ->get_event($key)
        ->can_transition_from($this
        ->get_current_state())) {
        $events[$key] = $event_object;
      }
    }
    return $events;
  }

  /**
   * Get all of the events.
   */
  public function get_all_events() {
    return $this->events;
  }

  /**
   * Whether State Machine to be ignored.
   *
   * @return bool
   *   Whether State Machine to be ignored.
   */
  public function ignore() {
    return FALSE;
  }

}

/**
 * Base class for states.
 */
class StateMachine_State {
  public $key;
  protected $machine;
  protected $options;

  /**
   * Instantiate state.
   *
   * @param string $name
   *   The machine readable name of the state.
   * @param StateMachine $machine
   *   The related machine.
   * @param array $options
   *   The options array.
   */
  public function __construct($name, $machine, $options = array()) {
    $this->name = $name;
    $this->machine = $machine;
    $this->options = $options;
    $this->title = isset($this->options['title']) ? $this->options['title'] : $name;
  }

  /**
   * Return the $options array.
   *
   * @return array
   *   The options array.
   */
  public function get_options() {
    return $this->options;
  }

  /**
   * Return a specific key value from the $options array.
   *
   * @param string $key
   *   The options key.
   *
   * @return mixed
   *   The value of the option or FALSE if the option wasn't found.
   */
  public function get_option($key) {
    return array_key_exists($key, $this->options) ? $this->options[$key] : FALSE;
  }

  /**
   * Called when entering into this state.
   */
  public function on_enter() {
    $args = func_get_args();
    if (!empty($this->options['on_enter'])) {
      call_user_func_array($this->options['on_enter'], $args);
    }
  }

  /**
   * Called when exiting out of this state.
   */
  public function on_exit() {
    $args = func_get_args();
    if (!empty($this->options['on_exit'])) {
      call_user_func_array($this->options['on_exit'], $args);
    }
  }

  /**
   * Check if this state is published.
   */
  public function is_published() {
    if (!empty($this->options['on_enter'])) {
      return in_array('on_enter_published', $this->options['on_enter']);
    }
    return FALSE;
  }

  /**
   * Check if this state is unpublished.
   */
  public function is_unpublished() {
    if (!empty($this->options['on_enter'])) {
      return in_array('on_enter_unpublished', $this->options['on_enter']);
    }
    return FALSE;
  }

}

/**
 * Base class for events.
 */
class StateMachine_Event {
  public $name;
  protected $machine;
  protected $options;

  /**
   * Instantiate event.
   *
   * @param string $name
   *   The machine readable name of the state.
   * @param StateMachine $machine
   *   The related machine.
   * @param array $options
   *   The options array.
   */
  public function __construct($name, $machine, $options = array()) {
    $this->name = $name;
    $this->machine = $machine;

    // Normalize the origin option.
    $origin = empty($options['origin']) ? array() : $options['origin'];
    $options['origin'] = is_array($origin) ? $origin : array(
      $origin,
    );
    $this->options = $options;
    $this->title = isset($this->options['title']) ? $this->options['title'] : $name;
  }

  /**
   * Return the $options array.
   *
   * @return array
   *   The options array.
   */
  public function get_options() {
    return $this->options;
  }

  /**
   * Return a specific key value from the $options array.
   *
   * @param string $key
   *   The options key.
   *
   * @return mixed
   *   The value of the option or FALSE if the option wasn't found.
   */
  public function get_option($key) {
    return array_key_exists($key, $this->options) ? $this->options[$key] : FALSE;
  }

  /**
   * Validate that the given event can take place.
   */
  public function validate() {

    // Check that the current state is a valid origin for the given transition.
    if (!in_array($this->machine
      ->get_current_state(), $this->options['origin'])) {
      return FALSE;
    }

    // Execute guard condition if it exists.
    if (!empty($this->options['guard'])) {
      if (call_user_func($this->options['guard'], $this) === FALSE) {
        return FALSE;
      }
    }
    return TRUE;
  }

  /**
   * Execute the event.
   */
  public function execute() {
    if (!$this
      ->validate()) {
      return FALSE;
    }
    if (!empty($this->options['before_transition'])) {
      call_user_func($this->options['before_transition']);
    }
    return $this->options['target'];
  }

  /**
   * Allow the event to finish after the machine has changed state.
   */
  public function finish() {
    if (!empty($this->options['after_transition'])) {
      call_user_func($this->options['after_transition']);
    }
  }

  /**
   * Evaluates if this event can be used to transition from the specified state.
   */
  public function can_transition_from($state) {
    return in_array($state, $this->options['origin']);
  }

  /**
   * Returns the target state of this event.
   *
   * @return \StateMachine_State
   *   The target state of this event.
   */
  public function get_target_state() {
    return $this->machine
      ->get_state($this
      ->get_option('target'));
  }

}

Classes

Namesort descending Description
StateMachine The base class.
StateMachine_Event Base class for events.
StateMachine_State Base class for states.