You are here

state_flow_entity.module in State Machine 7.3

An implementation of entity workflow for Drupal based on the State Machine system.

File

modules/state_flow_entity/state_flow_entity.module
View source
<?php

/**
 * @file
 * An implementation of entity workflow for Drupal based on the
 * State Machine system.
 */

/**
 * Implements hook_views_api().
 */
function state_flow_entity_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'state_flow_entity') . '/includes/views',
  );
}

/**
 * Checks if an entity type has state flow entity handling enabled.
 *
 * @param string $entity_type
 *   The entity type to check for.
 *
 * @return bool
 *   TRUE if the entity type has state flow handling enabled, FALSE otherwise.
 */
function state_flow_entity_entity_type_is_enabled($entity_type) {
  $entity_info = entity_get_info($entity_type);
  return !empty($entity_info['state_flow_entity']);
}

/**
 * Implements hook_entity_load().
 */
function state_flow_entity_entity_load(&$entities, $entity_type) {

  // Fetch entity state information.
  $entity_info = entity_get_info($entity_type);
  $revision_key = $entity_info['entity keys']['revision'];
  if (state_flow_entity_entity_type_is_enabled($entity_type)) {

    // Get the revision id that is available to be published.
    foreach ($entities as $entity_id => $entity) {
      if ($revision_key && ($machine = state_flow_entity_load_state_machine($entity, $entity_type))) {

        // @todo, this is already getting set in
        // set_available_publish_revision()
        // So it should not need to be reset on the same object.
        if (!empty($machine->object->published_revision_id)) {
          $entity->published_revision_id = $machine->object->published_revision_id;
        }

        // Store the revision id which was used to load the entity. This is
        // needed in case this entity is saved as forward revision to clone the
        // original state.
        $entity->state_machine_loaded_revision = $entity->{$revision_key};
        $entity->state_flow = $machine;
      }
    }
  }
}

/**
 * Implements hook_entity_presave().
 */
function state_flow_entity_entity_presave($entity, $entity_type) {

  // If this is invoked by drafty skip handling as we did this already.
  // This is also necessary to avoid that the machine is updated with this
  // temporary entity.
  if (!empty($entity->is_draft_revision) || !empty($entity->default_revision)) {
    return TRUE;
  }
  if (state_flow_entity_entity_type_is_enabled($entity_type)) {

    // Write history and update active revision for this entity. This call is
    // also necessary since entity_load_unchanged() probably was called and thus
    // the entity in the cached machine isn't the same as we're storing anymore.
    // This call ensures that the entity is the proper one.
    $machine = state_flow_entity_load_state_machine($entity, $entity_type);

    // @todo 7.x-2.x introduces a new method in the base StateMachine class
    // called ignore().
    // This needs to be reviewed and tested.
    if (!method_exists($machine, 'ignore') || !$machine
      ->ignore()) {
      $machine
        ->entityPresave();
    }
  }
}

/**
 * Implements hook_entity_insert().
 */
function state_flow_entity_entity_insert($entity, $entity_type) {
  _state_flow_entity_entity_saved($entity, $entity_type);
}

/**
 * Implements hook_entity_update().
 */
function state_flow_entity_entity_update($entity, $entity_type) {
  _state_flow_entity_entity_saved($entity, $entity_type);
}

/**
 * Helper function for hook_entity_insert() and hook_entity_update().
 *
 * @see StateFlowEntiy::entity_saved()
 */
function _state_flow_entity_entity_saved($entity, $entity_type) {
  if (state_flow_entity_entity_type_is_enabled($entity_type)) {
    global $user;
    $event_uid = isset($entity->event_uid) ? $entity->event_uid : $user->uid;

    // Write history and update active revision for this entity.
    $machine = state_flow_entity_load_state_machine($entity, $entity_type);

    // @todo 7.x-2.x introduces a new method in the base StateMachine class
    // called ignore().
    // This needs to be reviewed and tested.
    if (!method_exists($machine, 'ignore') || !$machine
      ->ignore()) {
      $event_comment = isset($entity->event_comment) ? $entity->event_comment : '';
      $event = !empty($entity->event) ? $entity->event : FALSE;
      $machine
        ->entity_saved($event, $event_uid, $event_comment);
    }
  }
}

/**
 * Implements hook_entity_delete().
 */
function state_flow_entity_entity_delete($entity, $entity_type) {
  $entity_info = entity_get_info($entity_type);
  $id_key = $entity_info['entity keys']['id'];
  if (state_flow_entity_entity_type_is_enabled($entity_type)) {

    // Delete history and active revision records for this entity.
    $machine = state_flow_entity_load_state_machine($entity, $entity_type);

    // @todo deleting field data needs testing
    field_attach_delete('state_flow_history_entity', $machine
      ->get_history_entity());

    // Delete all state flow revision records for this entity.
    db_delete('state_flow_history')
      ->condition('entity_id', $entity->{$id_key})
      ->condition('entity_type', $entity_type)
      ->execute();
    db_delete('state_flow_states')
      ->condition('entity_id', $entity->{$id_key})
      ->condition('entity_type', $entity_type)
      ->execute();
  }
}

/**
 * Load the state_flow state_machine for the given node.
 *
 * @param object $entity
 *   The entity to handle.
 * @param string $entity_type
 *   The machine name of the entity_type.
 * @param bool $reset
 *   (legacy) Reset
 *
 * @return StateFlowEntity
 *   The state flow entity or FALSE if the entity type isn't enabled.
 */
function state_flow_entity_load_state_machine($entity, $entity_type = 'node', $reset = FALSE) {

  // If this entity already has a state machine attached re-use it.
  if (!empty($entity->state_flow) && $reset === FALSE) {

    // @todo, review how this is set in the first place and if it is a good idea
    // for entities to carry around their machine objects.
    // Something like this is necessary to update history records twice during
    // the node save process.
    $entity->state_flow
      ->set_object($entity);
    return $entity->state_flow;
  }
  if (!state_flow_entity_entity_type_is_enabled($entity_type)) {
    return FALSE;
  }
  $entity_info = entity_get_info($entity_type);
  $revision_key = $entity_info['entity keys']['revision'];

  // @todo, what's the point of using this objects array if it is not statically
  // cached?
  // And this function would break if there were more than one entity type in
  // play because it is just building up an array keyed by revision_id so node
  // vids and user vids could collide.
  $objects =& drupal_static(__FUNCTION__, array());
  $entity_ids = entity_extract_ids($entity_type, $entity);
  $cid = $entity_type . ':' . implode(':', $entity_ids);

  // New objects have no ids and can't be cached yet.
  $is_new = empty($entity_ids[0]);

  // Check if this object has a cached state machine related to the ids.
  if (!$is_new && isset($objects[$cid])) {
    $objects[$cid]
      ->set_object($entity);
    return $objects[$cid];
  }
  ctools_include('plugins');
  $machine_type = 'state_flow_entity';

  // Allow other modules to invoke other machine types.
  drupal_alter('state_flow_entity_machine_type', $machine_type, $entity, $entity_type);

  // @todo, We should return FALSE if this entity is not state-able.
  // This will likely require cleanup around functions that don't expect this.
  // if ($machine_type === FALSE) {
  //   return FALSE;
  // }
  $plugin = ctools_get_plugins('state_flow_entity', 'plugins', $machine_type);
  if (!empty($plugin) && $revision_key) {
    $class = ctools_plugin_get_class($plugin, 'handler');
    $plugin['object'] = $entity;
    $state_flow_object = new $class($plugin);
    $objects[$cid] = $state_flow_object;
    $entity->state_flow = $objects[$cid];
  }
  return $objects[$cid];
}

/**
 * Inform external systems about a workflow transition.
 */
function state_flow_entity_invoke_event_handlers($object, $state, $history_entity) {

  // Load related objects.
  $author = !empty($object->uid) ? user_load($object->uid) : drupal_anonymous_user();

  // Invoke the Rules state_flow_event_fired event.
  if ($object && module_exists('rules')) {
    $wrapped_entity = entity_metadata_wrapper($history_entity->entity_type, $object);
    $wrapped_history_entity = entity_metadata_wrapper('state_flow_history_entity', $history_entity->hid);
    rules_invoke_event('state_flow_entity_event_fired', $wrapped_entity, $author, $state, $history_entity->event, $wrapped_history_entity);
  }

  // These are not in the same order as the rules call above for BC reasons.
  module_invoke_all('state_flow_event', $state, $object, $history_entity->event, $history_entity);
}

/**
 * Returns a rendered state_flow_history_entity.
 *
 * This function is patterned on node_view().
 *
 * @param object $state_flow_history_entity
 *   An entity object.
 * @param string $view_mode
 *   The machine name of the view mode to be rendered.
 *
 * @return string
 *   The rendered entity.
 */
function state_flow_entity_state_flow_history_entity_view($state_flow_history_entity, $view_mode = 'full') {

  // Allow modules to change the view mode.
  $context = array(
    'entity_type' => 'state_flow_history_entity',
    'entity' => $state_flow_history_entity,
  );
  drupal_alter('entity_view_mode', $view_mode, $context);

  // Add fields to the state_flow_history_entity.
  $state_flow_history_entity->content = array();
  field_attach_prepare_view('state_flow_history_entity', array(
    $state_flow_history_entity->hid => $state_flow_history_entity,
  ), $view_mode);
  entity_prepare_view('state_flow_history_entity', array(
    $state_flow_history_entity->hid => $state_flow_history_entity,
  ));
  $state_flow_history_entity->content += field_attach_view('state_flow_history_entity', $state_flow_history_entity, $view_mode);
  $build = $state_flow_history_entity->content;

  // We don't need duplicate rendering info in
  // state_flow_history_entity->content.
  unset($state_flow_history_entity->content);
  $build += array(
    '#theme' => 'state_flow_history_entity',
    '#state_flow_history_entity' => $state_flow_history_entity,
    '#view_mode' => $view_mode,
  );

  // Allow modules to modify the structured state_flow_history_entity.
  $type = 'state_flow_history_entity';
  drupal_alter(array(
    'entity_view',
  ), $build, $type);
  $return = drupal_render($build);
  return $return;
}

/**
 * Implements hook_theme().
 */
function state_flow_entity_theme() {
  return array(
    'state_flow_history_entity' => array(
      'render element' => 'elements',
      'template' => 'state-flow-history-entity',
    ),
  );
}

/**
 * Implements hook_ctools_plugin_directory().
 *
 * Let the system know we implement task and task_handler plugins.
 */
function state_flow_entity_ctools_plugin_directory($module, $plugin) {

  // Most of this module is implemented as an export ui plugin, and the
  // rest is in ctools/includes/workbench_states.inc
  $ctools_plugin_types = array(
    'relationships',
    'content_types',
  );
  if ($module == 'ctools' && in_array($plugin, $ctools_plugin_types)) {
    return 'plugins/' . $plugin;
  }
}

/**
 * Implements hook_entity_info().
 */
function state_flow_entity_entity_info() {
  $return = array(
    'state_flow_history_entity' => array(
      'label' => 'State Flow History Record',
      'base table' => 'state_flow_history',
      'static cache' => TRUE,
      'fieldable' => TRUE,
      'field cache' => TRUE,
      'controller class' => 'DrupalDefaultEntityController',
      'entity keys' => array(
        'id' => 'hid',
        'entity_id' => 'entity_id',
        'revision_id' => 'revision_id',
      ),
      'view modes' => array(
        'full' => array(
          'label' => t('Full'),
          'custom settings' => FALSE,
        ),
      ),
      'bundles' => array(
        'state_flow_history_entity' => array(
          'label' => t('State Flow'),
          'admin' => array(
            'path' => 'admin/config/workflow/state-flow-history-entity',
            'access arguments' => array(
              'manage content workflow',
            ),
          ),
        ),
      ),
    ),
  );
  return $return;
}

/**
 * Prepare variables for the state_flow_history_entity template.
 */
function template_preprocess_state_flow_history_entity(&$variables) {
  $variables['view_mode'] = $variables['elements']['#view_mode'];
  $state_flow_history_entity = $variables['elements']['#state_flow_history_entity'];

  // These variables should be plain text already. Double check. There should
  // not be a risk of double-escaping as these should all be plain alphanumeric.
  $variables['hid'] = check_plain($state_flow_history_entity->hid);
  $variables['state'] = check_plain($state_flow_history_entity->state);
  $variables['from_state'] = check_plain($state_flow_history_entity->from_state);
  $variables['formatted_timestamp'] = format_date($state_flow_history_entity->timestamp, variable_get('state_flow_etity_date_format', 'medium'));
  $account = user_load($state_flow_history_entity->uid);
  $variables['formatted_username'] = format_username($account);
  $variables['log'] = check_plain($state_flow_history_entity->log);

  // Helpful $content variable for templates.
  $variables += array(
    'content' => array(),
  );
  foreach (element_children($variables['elements']) as $key) {
    $variables['content'][$key] = $variables['elements'][$key];
  }

  // Make the field variables available with the appropriate language.
  field_attach_preprocess('state_flow_history_entity', $state_flow_history_entity, $variables['content'], $variables);
}

/**
 * Implements hook_menu().
 */
function state_flow_entity_menu() {
  $items = array();
  $items['admin/config/workflow/state-flow-history-entity'] = array(
    'title' => 'State Flow History Entities',
    // @todo make more descriptive once this path does something.
    'description' => 'Add fields to State Flow History Entities.',
    'page callback' => 'state_flow_entity_history_entity_admin',
    'access arguments' => array(
      'manage content workflow',
    ),
  );

  // @todo, this path might list bundles for state-flow-history-entities.
  // If it does not not, use an argument other than /list.
  $items['admin/config/workflow/state-flow-history-entity/list'] = array(
    'title' => 'List',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  return $items;
}

/**
 * Returns the configuration page for state flow history entities.
 *
 * @todo, this path is still a placeholder. It may eventually list different
 * bundles for the state_flow_history_entity entity_type.
 */
function state_flow_entity_history_entity_admin() {
  return t('This is a placeholder for bundle administration. This is discussed at http://drupal.org/node/1412728.');
}

/**
 * An Entity API Getter callback for the state_flow_history_entity state label.
 *
 * This is the value used be the entity
 */
function state_flow_entity_history_entity_label_get($entity, array $options, $name, $entity_type, $info) {
  $return = '';
  if (!empty($entity->state)) {
    $state_labels = state_flow_entity_state_labels();
    if (!empty($state_labels[$entity->state])) {

      // @todo, check_plain might not be necessary as escaping could happen
      // later. Until I know otherwise, I'll stay on the safe side.
      $return = check_plain($state_labels[$entity->state]);
    }
  }
  return $return;
}

/**
 * Returns an array of all state labels keyed by state machine name.
 *
 * @todo, should this argument take an optional entity_type argument?
 */
function state_flow_entity_state_labels() {

  // @todo, this function should have static caching.
  $all_plugins = ctools_get_plugins('state_flow_entity', 'plugins');
  $state_labels = array();
  foreach ($all_plugins as $plugin) {
    if (!empty($plugin['handler']['workflow_options']['states'])) {
      foreach ($plugin['handler']['workflow_options']['states'] as $state_key => $state_array) {
        if (!empty($state_array['label'])) {
          $state_labels[$state_key] = $state_array['label'];
        }
      }
    }
  }
  return $state_labels;
}

/**
 * Implements hook_entity_property_info().
 *
 * @see hook_entity_property_info()
 */
function state_flow_entity_entity_property_info() {
  $info = array();
  $info['state_flow_history_entity']['properties'] = array(
    'hid' => array(
      'type' => 'integer',
      'label' => 'History ID',
      'getter callback' => 'entity_property_verbatim_get',
      'sanitize' => 'filter_xss',
    ),
    // @todo, review what this state array should contain.
    'state' => array(
      'label' => 'State (machine name)',
      'getter callback' => 'entity_property_verbatim_get',
      'sanitize' => 'filter_xss',
    ),
    'from_state' => array(
      'label' => 'From State (machine name)',
      'getter callback' => 'entity_property_verbatim_get',
      'sanitize' => 'filter_xss',
    ),
    'state_label' => array(
      'label' => 'State Label',
      'getter callback' => 'state_flow_entity_history_entity_label_get',
      'sanitize' => 'filter_xss',
      'schema field' => 'uid',
    ),
    // @todo, all db column should be available as properties.
    'revision_id' => array(
      'type' => 'integer',
      'label' => 'Parent entity revision_id',
      'getter callback' => 'entity_property_verbatim_get',
      'sanitize' => 'filter_xss',
    ),
    'entity_type' => array(
      'type' => 'text',
      'label' => 'Parent entity_type',
      'getter callback' => 'entity_property_verbatim_get',
      'sanitize' => 'filter_xss',
    ),
    'timestamp' => array(
      'label' => t("Timestamp"),
      'type' => 'date',
      'description' => t("The date and time the history record was created"),
    ),
    'entity_id' => array(
      'type' => 'integer',
      'label' => 'Parent entity entity_id',
      'getter callback' => 'entity_property_verbatim_get',
      'sanitize' => 'filter_xss',
    ),
    'log' => array(
      'type' => 'text',
      'label' => 'State Flow log message',
      'getter callback' => 'entity_property_verbatim_get',
      'sanitize' => 'filter_xss',
    ),
    'user' => array(
      'label' => t("User"),
      'type' => 'user',
      'description' => t("The user who performed the state change"),
      'setter callback' => 'entity_property_verbatim_set',
      'schema field' => 'uid',
    ),
    'event' => array(
      'label' => t("Event"),
      'type' => 'text',
      'description' => t("The event that prompted this state change."),
      'sanitize' => 'filter_xss',
    ),
    'entity_revision' => array(
      'label' => t('Entity revision'),
      'type' => 'entity',
      'description' => t('The entity revision object.'),
    ),
  );
  return $info;
}

/**
 * Implements hook_ctools_plugin_type().
 */
function state_flow_entity_ctools_plugin_type() {
  $plugins = array(
    'plugins' => array(
      'cache' => TRUE,
      'use hooks' => TRUE,
      'info file' => TRUE,
      'alterable' => TRUE,
      'classes' => array(
        'handler',
      ),
    ),
  );
  return $plugins;
}

/**
 * Implements hook_state_flow_entity_plugins().
 */
function state_flow_entity_state_flow_entity_plugins() {
  $info = array();
  $path = drupal_get_path('module', 'state_flow_entity') . '/plugins';
  $workflow_options = array(
    'states' => array(
      'draft' => array(
        'label' => t('Draft'),
      ),
      'published' => array(
        'label' => t('Published'),
        'on_enter' => 'on_enter_published',
        'on_exit' => 'on_exit_published',
      ),
      'unpublished' => array(
        'label' => t('Unpublished'),
      ),
    ),
    'events' => array(
      'publish' => array(
        'label' => t('Publish'),
        'origin' => 'draft',
        'target' => 'published',
      ),
      'unpublish' => array(
        'label' => t('Unpublish'),
        'origin' => 'published',
        'target' => 'unpublished',
        'permission' => 'publish and unpublish content',
      ),
      'to draft' => array(
        'label' => t('To Draft'),
        'origin' => 'unpublished',
        'target' => 'draft',
      ),
    ),
  );
  $info['state_flow_entity'] = array(
    'handler' => array(
      'class' => 'StateFlowEntity',
      'file' => 'state_flow_entity.inc',
      'path' => $path,
      'parent' => 'state_flow_entity',
      'workflow_options' => $workflow_options,
      'entity_type' => 'node',
    ),
  );
  return $info;
}

/**
 * Get the active revision id for an entity.
 *
 * @param object $entity
 *   An entity object such as a node object.
 * @param string $entity_type
 *   A text string for the machine name of an entity such as 'node'.
 *
 * @return int
 *   A revision id for the 'active' revision, which, in the normal behavior of
 *   this module, is the revision saved most recently.
 */
function state_flow_entity_get_active_revision_id($entity, $entity_type) {

  // Fetch machine. Returns FLASE if the entity type isn't enabled.
  if ($machine = state_flow_entity_load_state_machine($entity, $entity_type)) {
    $active_id = $machine
      ->get_active_revision();
  }
  else {

    // If this entity isn't enabled we just return the current revision id.
    $entity_ids = entity_extract_ids($entity_type, $entity);
    $active_id = $entity_ids[0];
    if (isset($entity_ids[1])) {
      $active_id = $entity_ids[1];
    }
  }
  return $active_id;
}

/**
 * Get the active revision for an entity.
 *
 * @param object $entity
 *   An entity object such as a node object.
 * @param string $entity_type
 *   A text string for the machine name of an entity such as 'node'.
 *
 * @return object
 *   An entity object for the 'active' revision, which, in the normal behavior
 *   of this module, is the revision saved most recently.
 */
function state_flow_entity_get_active_revision($entity, $entity_type) {

  // Get the active revision id for this entity.
  $active_id = state_flow_entity_get_active_revision_id($entity, $entity_type);

  // Get the necessary variables.
  $entity_info = entity_get_info($entity_type);
  $revision_key = $entity_info['entity keys']['revision'];
  list($entity_id, $revision_id, $bundle) = entity_extract_ids($entity_type, $entity);
  $conditions = array(
    $revision_key => $active_id,
  );

  // Entity load returns an array of objects and this function only needs
  // to return one object.
  $active_revisions = entity_load($entity_type, array(
    $entity_id,
  ), $conditions);
  $active_revision = $active_revisions[$entity_id];
  if (empty($active_revision) && !empty($entity)) {
    watchdog('state_flow_entity', 'Entity @type @id has corrupted {state_flow_states} rows.', array(
      '@type' => $entity_type,
      '@id' => $entity_id,
    ), WATCHDOG_ERROR);
    drupal_set_message(t('Workflow data is corrupted, report this incident to the site administrators.'), 'error');
    $active_revision = $entity;
  }
  return $active_revision;
}

Functions

Namesort descending Description
state_flow_entity_ctools_plugin_directory Implements hook_ctools_plugin_directory().
state_flow_entity_ctools_plugin_type Implements hook_ctools_plugin_type().
state_flow_entity_entity_delete Implements hook_entity_delete().
state_flow_entity_entity_info Implements hook_entity_info().
state_flow_entity_entity_insert Implements hook_entity_insert().
state_flow_entity_entity_load Implements hook_entity_load().
state_flow_entity_entity_presave Implements hook_entity_presave().
state_flow_entity_entity_property_info Implements hook_entity_property_info().
state_flow_entity_entity_type_is_enabled Checks if an entity type has state flow entity handling enabled.
state_flow_entity_entity_update Implements hook_entity_update().
state_flow_entity_get_active_revision Get the active revision for an entity.
state_flow_entity_get_active_revision_id Get the active revision id for an entity.
state_flow_entity_history_entity_admin Returns the configuration page for state flow history entities.
state_flow_entity_history_entity_label_get An Entity API Getter callback for the state_flow_history_entity state label.
state_flow_entity_invoke_event_handlers Inform external systems about a workflow transition.
state_flow_entity_load_state_machine Load the state_flow state_machine for the given node.
state_flow_entity_menu Implements hook_menu().
state_flow_entity_state_flow_entity_plugins Implements hook_state_flow_entity_plugins().
state_flow_entity_state_flow_history_entity_view Returns a rendered state_flow_history_entity.
state_flow_entity_state_labels Returns an array of all state labels keyed by state machine name.
state_flow_entity_theme Implements hook_theme().
state_flow_entity_views_api Implements hook_views_api().
template_preprocess_state_flow_history_entity Prepare variables for the state_flow_history_entity template.
_state_flow_entity_entity_saved Helper function for hook_entity_insert() and hook_entity_update().