WorkflowState.php in Workflow 7.2
Same filename and directory in other branches
Contains workflow\includes\Entity\WorkflowState. Contains workflow\includes\Entity\WorkflowStateController.
File
includes/Entity/WorkflowState.phpView source
<?php
/**
* @file
* Contains workflow\includes\Entity\WorkflowState.
* Contains workflow\includes\Entity\WorkflowStateController.
*/
/**
* Class WorkflowState
*/
class WorkflowState extends Entity {
// Since workflows do not change, it is implemented as a singleton.
protected static $states = array();
public $sid = 0;
public $wid = 0;
public $weight = 0;
public $sysid = 0;
public $state = '';
// @todo D8: remove $state, use $label/$name. (requires conversion of Views displays.)
public $status = 1;
/**
* CRUD functions.
*/
/**
* Constructor.
*
* @param array $values
* @param string $entityType
*/
public function __construct(array $values = array(), $entityType = 'WorkflowState') {
// Please be aware that $entity_type and $entityType are different things!
// Keep official name and external name equal. Both are required.
// @todo: still needed? test import, manual creation, programmatic creation, etc.
if (!isset($values['state']) && isset($values['name'])) {
$values['state'] = $values['name'];
}
// Set default values for '(creation)' state.
if (!empty($values['is_new']) && $values['name'] == WORKFLOW_CREATION_STATE_NAME) {
$values['sysid'] = WORKFLOW_CREATION;
$values['weight'] = WORKFLOW_CREATION_DEFAULT_WEIGHT;
$values['name'] = '(creation)';
// machine_name;
}
parent::__construct($values, $entityType);
if (empty($values)) {
// Automatic constructor when casting an array or object.
// Add pre-existing states to cache (not new/temp ones).
if (!isset(self::$states[$this->sid])) {
self::$states[$this->sid] = $this;
}
}
}
/*
// Implementing clone needs a list of tid-less transitions, and a conversion
// of sids for both States and ConfigTransitions.
// public function __clone() {}
*/
/**
* Alternative constructor, loading objects from table {workflow_states}.
*
* @param int $sid
* The requested State ID
* @param int $wid
* An optional Workflow ID, to check if the requested State is valid for the Workflow.
*
* @return WorkflowState|NULL|FALSE $state
* WorkflowState if state is successfully loaded,
* NULL if not loaded,
* FALSE if state does not belong to requested Workflow.
*/
public static function load($sid, $wid = 0) {
$states = self::getStates();
$state = isset($states[$sid]) ? $states[$sid] : NULL;
if ($wid && $state && $wid != $state->wid) {
return FALSE;
}
return $state;
}
/**
* Get all states in the system, with options to filter, only where a workflow exists.
*
* @param $wid
* The requested Workflow ID.
* @param bool $reset
* An option to refresh all caches.
*
* @return array $states
* An array of cached states.
*
* D7.x-2.x: deprecated workflow_get_workflow_states --> workflow_state_load_multiple
* D7.x-2.x: deprecated workflow_get_workflow_states_all --> workflow_state_load_multiple
* D7.x-2.x: deprecated workflow_get_other_states_by_sid --> workflow_state_load_multiple
*/
public static function getStates($wid = 0, $reset = FALSE) {
if ($reset) {
self::$states = array();
}
if (empty(self::$states)) {
// Build the query, and get ALL states.
// Note: self::states[] is populated in respective constructors.
$query = db_select('workflow_states', 'ws');
$query
->fields('ws');
$query
->orderBy('ws.weight');
$query
->orderBy('ws.wid');
// Just for grins, add a tag that might result in modifications.
$query
->addTag('workflow_states');
// @see #2285983 for using SQLite.
// $query->execute()->fetchAll(PDO::FETCH_CLASS, 'WorkflowState');
/* @var $tmp DatabaseStatementBase */
$statement = $query
->execute();
$statement
->setFetchMode(PDO::FETCH_CLASS, 'WorkflowState');
foreach ($statement
->fetchAll() as $state) {
self::$states[$state->sid] = $state;
}
}
if (!$wid) {
// All states are requested and cached: return them.
return self::$states;
}
else {
// All states of only 1 Workflow is requested: return this one.
$result = array();
foreach (self::$states as $state) {
if ($state->wid == $wid) {
$result[$state->sid] = $state;
}
}
return $result;
}
}
/**
* Get all states in the system, with options to filter, only where a workflow exists.
*
* May return more then one State, since a name is not (yet) an UUID.
*
* @param $name
* @param int $wid
*
* @return WorkflowState
*/
public static function loadByName($name, $wid = 0) {
/* @var $state WorkflowState */
foreach ($states = self::getStates($wid) as $state) {
if ($name == $state->name) {
return $state;
}
}
return NULL;
}
/**
* Deactivate a Workflow State, moving existing nodes to a given State.
*
* @param int $new_sid
* The state ID, to which all affected entities must be moved.
*
* D7.x-2.x: deprecated workflow_delete_workflow_states_by_sid() --> WorkflowState->deactivate() + delete()
*/
public function deactivate($new_sid) {
$current_sid = $this->sid;
$force = TRUE;
// Notify interested modules. We notify first to allow access to data before we zap it.
// E.g., Node API implements this.
// - re-parents any nodes that we don't want to orphan, whilst deactivating a State.
// - delete any lingering node to state values.
module_invoke_all('workflow', 'state delete', $current_sid, $new_sid, NULL, $force);
// Re-parent any nodes that we don't want to orphan, whilst deactivating a State.
if ($new_sid) {
// A candidate for the batch API.
// @TODO: Future updates should seriously consider setting this with batch.
global $user;
// We can use global, since deactivate() is a UI-only function.
$comment = t('Previous state deleted');
// Re-assign workflow_node nodes.
foreach (workflow_get_workflow_node_by_sid($current_sid) as $workflow_node) {
// @todo: add Field support in 'state delete', by using workflow_node_history or reading current field.
$entity_type = 'node';
$entity = entity_load_single('node', $workflow_node->nid);
$field_name = '';
$transition = new WorkflowTransition();
$transition
->setValues($entity_type, $entity, $field_name, $current_sid, $new_sid, $user->uid, REQUEST_TIME, $comment);
$transition
->force($force);
// Execute Transition, invoke 'pre' and 'post' events, save new state in workflow_node, save also in workflow_node_history.
// For Workflow Node, only {workflow_node} and {workflow_node_history} are updated. For Field, also the Entity itself.
$new_sid = workflow_execute_transition($entity_type, $entity, $field_name, $transition, $force);
}
// Re-assign workflow_field_entities.
foreach (_workflow_info_fields() as $field_name => $field_info) {
$query = new EntityFieldQuery();
$query
->fieldCondition($field_name, 'value', $current_sid, '=');
$result = $query
->execute();
foreach ($result as $entity_type => $entities) {
if ($entity_type == 'comment') {
// Do not reset comments.
continue;
}
foreach ($entities as $entity_id => $entity) {
$entity = entity_load_single($entity_type, $entity_id);
/* @var $transition WorkflowTransition */
$transition = new WorkflowTransition();
$transition
->setValues($entity_type, $entity, $field_name, $current_sid, $new_sid, $user->uid, REQUEST_TIME, $comment, TRUE);
$transition
->force($force);
// Execute Transition, invoke 'pre' and 'post' events, save new state in Field-table, save also in workflow_transition_history.
// For Workflow Node, only {workflow_node} and {workflow_transition_history} are updated. For Field, also the Entity itself.
$new_sid = workflow_execute_transition($entity_type, $entity, $field_name, $transition, $force);
}
}
}
}
// Delete any lingering node to state values.
workflow_delete_workflow_node_by_sid($current_sid);
// Delete the config transitions this state is involved in.
$workflow = workflow_load_single($this->wid);
/* @var $transition WorkflowTransition */
foreach ($workflow
->getTransitionsBySid($current_sid, 'ALL') as $transition) {
$transition
->delete();
}
foreach ($workflow
->getTransitionsByTargetSid($current_sid, 'ALL') as $transition) {
$transition
->delete();
}
// Delete the state. -- We don't actually delete, just deactivate.
// This is a matter up for some debate, to delete or not to delete, since this
// causes name conflicts for states. In the meantime, we just stick with what we know.
// If you really want to delete the states, use workflow_cleanup module, or delete().
$this->status = FALSE;
$this
->save();
// Clear the cache.
self::getStates(0, TRUE);
}
/**
* Property functions.
*/
/**
* Returns the Workflow object of this State.
*
* @return Workflow
* Workflow object.
*/
public function getWorkflow() {
if (isset($this->workflow)) {
return $this->workflow;
}
return workflow_load_single($this->wid);
}
public function setWorkflow($workflow) {
$this->wid = $workflow->wid;
$this->workflow = $workflow;
}
/**
* Returns the Workflow object of this State.
*
* @return bool
* TRUE if state is active, else FALSE.
*/
public function isActive() {
return (bool) $this->status;
}
public function isCreationState() {
return $this->sysid == WORKFLOW_CREATION;
}
/**
* Determines if the Workflow Form must be shown.
*
* If not, a formatter must be shown, since there are no valid options.
*
* @param $entity_type
* @param $entity
* @param $field_name
* @param $user
* @param $force
*
* @return bool $show_widget
* TRUE = a form (a.k.a. widget) must be shown; FALSE = no form, a formatter must be shown instead.
*/
public function showWidget($entity_type, $entity, $field_name, $user, $force) {
$options = $this
->getOptions($entity_type, $entity, $field_name, $user, $force);
$count = count($options);
// The easiest case first: more then one option: always show form.
if ($count > 1) {
return TRUE;
}
// #2226451: Even in Creation state, we must have 2 visible states to show the widget.
// // Only when in creation phase, one option is sufficient,
// // since the '(creation)' option is not included in $options.
// // When in creation state,
// if ($this->isCreationState()) {
// return TRUE;
// }
return FALSE;
}
/**
* Returns the allowed transitions for the current state.
*
* @param string $entity_type
* The type of the entity at hand.
* @param object $entity
* The entity at hand. May be NULL (E.g., on a Field settings page).
* @param string $field_name
* @param null $user
* @param bool $force
*
* @return array
* An array of tid=>transition pairs with allowed transitions for State.
*/
public function getTransitions($entity_type = '', $entity = NULL, $field_name = '', $user = NULL, $force = FALSE) {
$transitions = array();
$current_sid = $this->sid;
$current_state = $this;
if (!($workflow = $this
->getWorkflow())) {
// No workflow, no options ;-)
return $transitions;
}
// Get the role IDs of the user, to get the proper permissions.
$roles = $user ? array_keys($user->roles) : array();
// Some entities (e.g., taxonomy_term) do not have a uid.
$entity_uid = isset($entity->uid) ? $entity->uid : 0;
// Fetch entity_id from entity for _newness_ check
$entity_id = $entity ? entity_id($entity_type, $entity) : '';
if ($force || $user && $user->uid == 1) {
// Superuser is special. And $force allows Rules to cause transition.
$roles = 'ALL';
}
elseif ($entity && (!empty($entity->is_new) || empty($entity_id))) {
// Add 'author' role to user, if this is a new entity.
// - $entity can be NULL (E.g., on a Field settings page).
// - on display of new entity, $entity_id and $is_new are not set.
// - on submit of new entity, $entity_id and $is_new are both set.
$roles = array_merge(array(
WORKFLOW_ROLE_AUTHOR_RID,
), $roles);
}
elseif ($entity_uid > 0 && $user->uid > 0 && $entity_uid == $user->uid) {
// Add 'author' role to user, if user is author of this entity.
// - Some entities (e.g, taxonomy_term) do not have a uid.
// - If 'anonymous' is the author, don't allow access to History Tab,
// since anyone can access it, and it will be published in Search engines.
$roles = array_merge(array(
WORKFLOW_ROLE_AUTHOR_RID,
), $roles);
}
// Set up an array with states - they are already properly sorted.
// Unfortunately, the config_transitions are not sorted.
// Also, $transitions does not contain the 'stay on current state' transition.
// The allowed objects will be replaced with names.
$transitions = $workflow
->getTransitionsBySid($current_sid, $roles);
// Let custom code add/remove/alter the available transitions.
// Using the new drupal_alter.
// Modules may veto a choice by removing a transition from the list.
$context = array(
'entity_type' => $entity_type,
'entity' => $entity,
'field_name' => $field_name,
'force' => $force,
'workflow' => $workflow,
'state' => $current_state,
'user' => $user,
'user_roles' => $roles,
);
// @todo D8: rename to 'workflow_permitted_transitions'.
drupal_alter('workflow_permitted_state_transitions', $transitions, $context);
// Let custom code change the options, using old_style hook.
// @todo D8: delete below foreach/hook for better performance and flexibility.
// Above drupal_alter() calls hook_workflow_permitted_state_transitions_alter() only once.
foreach ($transitions as $transition) {
$new_sid = $transition->target_sid;
$permitted = array();
// We now have a list of config_transitions. Check each against the Entity.
// Invoke a callback indicating that we are collecting state choices.
// Modules may veto a choice by returning FALSE.
// In this case, the choice is never presented to the user.
if ($roles != 'ALL') {
$permitted = module_invoke_all('workflow', 'transition permitted', $current_sid, $new_sid, $entity, $force, $entity_type, $field_name, $transition, $user);
}
// If vetoed by a module, remove from list.
if (in_array(FALSE, $permitted, TRUE)) {
unset($transitions[$transition->tid]);
}
}
return $transitions;
}
/**
* Returns the allowed values for the current state.
*
* @param string $entity_type
* The type of the entity at hand.
* @param object $entity
* The entity at hand. May be NULL (E.g., on a Field settings page).
* @param $field_name
* @param $user
* @param bool $force
*
* @return array
* An array of sid=>label pairs.
* If $this->sid is set, returns the allowed transitions from this state.
* If $this->sid is 0 or FALSE, then labels of ALL states of the State's
* Workflow are returned.
*
* D7.x-2.x: deprecated workflow_field_choices() --> WorkflowState->getOptions()
*/
public function getOptions($entity_type, $entity, $field_name, $user, $force = FALSE) {
// Define an Entity-specific cache per page load.
static $cache = array();
$options = array();
$entity_id = $entity ? entity_id($entity_type, $entity) : '';
$current_sid = $this->sid;
// Get options from page cache, using a non-empty index (just to be sure).
$entity_index = !$entity ? 'x' : $entity_id;
if (isset($cache[$entity_type][$entity_index][$force][$current_sid])) {
$options = $cache[$entity_type][$entity_index][$force][$current_sid];
return $options;
}
$workflow = $this
->getWorkflow();
if (!$workflow) {
// No workflow, no options ;-)
}
elseif (!$current_sid) {
// If no State ID is given, we return all states.
// We cannot use getTransitions, since there are no ConfigTransitions
// from State with ID 0, and we do not want to repeat States.
foreach ($workflow
->getStates() as $state) {
$options[$state
->value()] = $state
->label();
// Translation is done as part of defaultLabel().
}
}
else {
/* @var $transition WorkflowTransition */
$transitions = $this
->getTransitions($entity_type, $entity, $field_name, $user, $force);
foreach ($transitions as $transition) {
// Get the label of the transition, and if empty of the target state.
// Beware: the target state may not exist, since it can be invented
// by custom code in the above drupal_alter() hook.
if (!($label = $transition
->label())) {
$target_state = $transition
->getNewState();
$label = $target_state ? $target_state
->label() : '';
}
$new_sid = $transition->target_sid;
$options[$new_sid] = $label;
// Translation is done as part of defaultLabel().
}
// Include current state for same-state transitions, except when $sid = 0.
// Caveat: this unnecessary since 7.x-2.3 (where stay-on-state transitions are saved, too.)
// but only if the transitions have been saved at least one time.
if ($current_sid && $current_sid != $workflow
->getCreationSid()) {
if (!isset($options[$current_sid])) {
$options[$current_sid] = $this
->label();
// Translation is done as part of defaultLabel().
}
}
// Save to entity-specific cache.
$cache[$entity_type][$entity_index][$force][$current_sid] = $options;
}
return $options;
}
/**
* Returns the number of entities with this state.
*
* @return int
* Counted number.
*
* @todo: add $options to select on entity type, etc.
*/
public function count() {
$sid = $this->sid;
// Get the numbers for Workflow Node.
$result = db_select('workflow_node', 'wn')
->fields('wn')
->condition('sid', $sid, '=')
->execute();
$count = count($result
->fetchAll());
// @see #2285983 for using SQLite.
// Get the numbers for Workflow Field.
$fields = _workflow_info_fields();
foreach ($fields as $field_name => $field_map) {
if ($field_map['type'] == 'workflow') {
$query = new EntityFieldQuery();
$query
->fieldCondition($field_name, 'value', $sid, '=')
->count();
// We only need the count.
$result = $query
->execute();
$count += $result;
}
}
return $count;
}
/**
* Mimics Entity API functions.
*/
protected function defaultLabel() {
return isset($this->state) ? t('@state', array(
'@state' => $this->state,
)) : '';
}
public function getName() {
return isset($this->name) ? $this->name : '';
}
public function setName($name) {
return $this->name = $name;
}
public function value() {
return $this->sid;
}
public function save() {
parent::save();
// Ensure Workflow is marked overridden.
$workflow = $this
->getWorkflow();
if ($workflow->status == ENTITY_IN_CODE) {
$workflow->status = ENTITY_OVERRIDDEN;
$workflow
->save();
}
}
}
class WorkflowStateController extends EntityAPIController {
public function save($entity, DatabaseTransaction $transaction = NULL) {
// Create the machine_name.
if (empty($entity->name)) {
if ($label = $entity->state) {
$entity->name = str_replace(' ', '_', strtolower($label));
}
else {
$entity->name = 'state_' . $entity->sid;
}
}
$return = parent::save($entity, $transaction);
if ($return) {
$workflow = $entity
->getWorkflow();
// Maintain the new object in the workflow.
$workflow->states[$entity->sid] = $entity;
}
// Reset the cache for the affected workflow.
workflow_reset_cache($entity->wid);
return $return;
}
public function delete($ids, DatabaseTransaction $transaction = NULL) {
// @todo: replace with parent.
foreach ($ids as $id) {
if ($state = workflow_state_load($id)) {
$wid = $state->wid;
db_delete('workflow_states')
->condition('sid', $state->sid)
->execute();
// Reset the cache for the affected workflow.
workflow_reset_cache($wid);
}
}
}
}
Classes
Name | Description |
---|---|
WorkflowState | Class WorkflowState |
WorkflowStateController |