WorkflowState.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. <?php
  2. /**
  3. * @file
  4. * Contains workflow\includes\Entity\WorkflowState.
  5. * Contains workflow\includes\Entity\WorkflowStateController.
  6. */
  7. /**
  8. * Class WorkflowState
  9. */
  10. class WorkflowState extends Entity {
  11. // Since workflows do not change, it is implemented as a singleton.
  12. protected static $states = array();
  13. public $sid = 0;
  14. public $wid = 0;
  15. public $weight = 0;
  16. public $sysid = 0;
  17. public $state = ''; // @todo D8: remove $state, use $label/$name. (requires conversion of Views displays.)
  18. public $status = 1;
  19. /**
  20. * CRUD functions.
  21. */
  22. /**
  23. * Constructor.
  24. *
  25. * @param array $values
  26. * @param string $entityType
  27. */
  28. public function __construct(array $values = array(), $entityType = 'WorkflowState') {
  29. // Please be aware that $entity_type and $entityType are different things!
  30. // Keep official name and external name equal. Both are required.
  31. // @todo: still needed? test import, manual creation, programmatic creation, etc.
  32. if (!isset($values['state']) && isset($values['name'])) {
  33. $values['state'] = $values['name'];
  34. }
  35. // Set default values for '(creation)' state.
  36. if (!empty($values['is_new']) && $values['name'] == WORKFLOW_CREATION_STATE_NAME) {
  37. $values['sysid'] = WORKFLOW_CREATION;
  38. $values['weight'] = WORKFLOW_CREATION_DEFAULT_WEIGHT;
  39. $values['name'] = '(creation)'; // machine_name;
  40. }
  41. parent::__construct($values, $entityType);
  42. if (empty($values)) {
  43. // Automatic constructor when casting an array or object.
  44. // Add pre-existing states to cache (not new/temp ones).
  45. if (!isset(self::$states[$this->sid])) {
  46. self::$states[$this->sid] = $this;
  47. }
  48. }
  49. }
  50. /*
  51. // Implementing clone needs a list of tid-less transitions, and a conversion
  52. // of sids for both States and ConfigTransitions.
  53. // public function __clone() {}
  54. */
  55. /**
  56. * Alternative constructor, loading objects from table {workflow_states}.
  57. *
  58. * @param int $sid
  59. * The requested State ID
  60. * @param int $wid
  61. * An optional Workflow ID, to check if the requested State is valid for the Workflow.
  62. *
  63. * @return WorkflowState|NULL|FALSE $state
  64. * WorkflowState if state is successfully loaded,
  65. * NULL if not loaded,
  66. * FALSE if state does not belong to requested Workflow.
  67. */
  68. public static function load($sid, $wid = 0) {
  69. $states = self::getStates();
  70. $state = isset($states[$sid]) ? $states[$sid] : NULL;
  71. if ($wid && $state && ($wid != $state->wid)) {
  72. return FALSE;
  73. }
  74. return $state;
  75. }
  76. /**
  77. * Get all states in the system, with options to filter, only where a workflow exists.
  78. *
  79. * @param $wid
  80. * The requested Workflow ID.
  81. * @param bool $reset
  82. * An option to refresh all caches.
  83. *
  84. * @return array $states
  85. * An array of cached states.
  86. *
  87. * D7.x-2.x: deprecated workflow_get_workflow_states --> workflow_state_load_multiple
  88. * D7.x-2.x: deprecated workflow_get_workflow_states_all --> workflow_state_load_multiple
  89. * D7.x-2.x: deprecated workflow_get_other_states_by_sid --> workflow_state_load_multiple
  90. */
  91. public static function getStates($wid = 0, $reset = FALSE) {
  92. if ($reset) {
  93. self::$states = array();
  94. }
  95. if (empty(self::$states)) {
  96. // Build the query, and get ALL states.
  97. // Note: self::states[] is populated in respective constructors.
  98. $query = db_select('workflow_states', 'ws');
  99. $query->fields('ws');
  100. $query->orderBy('ws.weight');
  101. $query->orderBy('ws.wid');
  102. // Just for grins, add a tag that might result in modifications.
  103. $query->addTag('workflow_states');
  104. // @see #2285983 for using SQLite.
  105. // $query->execute()->fetchAll(PDO::FETCH_CLASS, 'WorkflowState');
  106. /* @var $tmp DatabaseStatementBase */
  107. $statement = $query->execute();
  108. $statement->setFetchMode(PDO::FETCH_CLASS,'WorkflowState');
  109. foreach ($statement->fetchAll() as $state) {
  110. self::$states[$state->sid] = $state;
  111. }
  112. }
  113. if (!$wid) {
  114. // All states are requested and cached: return them.
  115. return self::$states;
  116. }
  117. else {
  118. // All states of only 1 Workflow is requested: return this one.
  119. $result = array();
  120. foreach (self::$states as $state) {
  121. if ($state->wid == $wid) {
  122. $result[$state->sid] = $state;
  123. }
  124. }
  125. return $result;
  126. }
  127. }
  128. /**
  129. * Get all states in the system, with options to filter, only where a workflow exists.
  130. *
  131. * May return more then one State, since a name is not (yet) an UUID.
  132. *
  133. * @param $name
  134. * @param int $wid
  135. *
  136. * @return WorkflowState
  137. */
  138. public static function loadByName($name, $wid = 0) {
  139. /* @var $state WorkflowState */
  140. foreach ($states = self::getStates($wid) as $state) {
  141. if ($name == $state->name) {
  142. return $state;
  143. }
  144. }
  145. return NULL;
  146. }
  147. /**
  148. * Deactivate a Workflow State, moving existing nodes to a given State.
  149. *
  150. * @param int $new_sid
  151. * The state ID, to which all affected entities must be moved.
  152. *
  153. * D7.x-2.x: deprecated workflow_delete_workflow_states_by_sid() --> WorkflowState->deactivate() + delete()
  154. */
  155. public function deactivate($new_sid) {
  156. $current_sid = $this->sid;
  157. $force = TRUE;
  158. // Notify interested modules. We notify first to allow access to data before we zap it.
  159. // E.g., Node API implements this.
  160. // - re-parents any nodes that we don't want to orphan, whilst deactivating a State.
  161. // - delete any lingering node to state values.
  162. module_invoke_all('workflow', 'state delete', $current_sid, $new_sid, NULL, $force);
  163. // Re-parent any nodes that we don't want to orphan, whilst deactivating a State.
  164. if ($new_sid) {
  165. // A candidate for the batch API.
  166. // @TODO: Future updates should seriously consider setting this with batch.
  167. global $user; // We can use global, since deactivate() is a UI-only function.
  168. $comment = t('Previous state deleted');
  169. // Re-assign workflow_node nodes.
  170. foreach (workflow_get_workflow_node_by_sid($current_sid) as $workflow_node) {
  171. // @todo: add Field support in 'state delete', by using workflow_node_history or reading current field.
  172. $entity_type = 'node';
  173. $entity = entity_load_single('node', $workflow_node->nid);
  174. $field_name = '';
  175. $transition = new WorkflowTransition();
  176. $transition->setValues($entity_type, $entity, $field_name, $current_sid, $new_sid, $user->uid, REQUEST_TIME, $comment);
  177. $transition->force($force);
  178. // Execute Transition, invoke 'pre' and 'post' events, save new state in workflow_node, save also in workflow_node_history.
  179. // For Workflow Node, only {workflow_node} and {workflow_node_history} are updated. For Field, also the Entity itself.
  180. $new_sid = workflow_execute_transition($entity_type, $entity, $field_name, $transition, $force);
  181. }
  182. // Re-assign workflow_field_entities.
  183. foreach(_workflow_info_fields() as $field_name => $field_info) {
  184. $query = new EntityFieldQuery();
  185. $query->fieldCondition($field_name, 'value', $current_sid, '=');
  186. $result = $query->execute();
  187. foreach ($result as $entity_type => $entities) {
  188. if ($entity_type == 'comment') {
  189. // Do not reset comments.
  190. continue;
  191. }
  192. foreach ($entities as $entity_id => $entity) {
  193. $entity = entity_load_single($entity_type, $entity_id);
  194. /* @var $transition WorkflowTransition */
  195. $transition = new WorkflowTransition();
  196. $transition->setValues($entity_type, $entity, $field_name, $current_sid, $new_sid, $user->uid, REQUEST_TIME, $comment, TRUE);
  197. $transition->force($force);
  198. // Execute Transition, invoke 'pre' and 'post' events, save new state in Field-table, save also in workflow_transition_history.
  199. // For Workflow Node, only {workflow_node} and {workflow_transition_history} are updated. For Field, also the Entity itself.
  200. $new_sid = workflow_execute_transition($entity_type, $entity, $field_name, $transition, $force);
  201. }
  202. }
  203. }
  204. }
  205. // Delete any lingering node to state values.
  206. workflow_delete_workflow_node_by_sid($current_sid);
  207. // Delete the config transitions this state is involved in.
  208. $workflow = workflow_load_single($this->wid);
  209. /* @var $transition WorkflowTransition */
  210. foreach ($workflow->getTransitionsBySid($current_sid, 'ALL') as $transition) {
  211. $transition->delete();
  212. }
  213. foreach ($workflow->getTransitionsByTargetSid($current_sid, 'ALL') as $transition) {
  214. $transition->delete();
  215. }
  216. // Delete the state. -- We don't actually delete, just deactivate.
  217. // This is a matter up for some debate, to delete or not to delete, since this
  218. // causes name conflicts for states. In the meantime, we just stick with what we know.
  219. // If you really want to delete the states, use workflow_cleanup module, or delete().
  220. $this->status = FALSE;
  221. $this->save();
  222. // Clear the cache.
  223. self::getStates(0, TRUE);
  224. }
  225. /**
  226. * Property functions.
  227. */
  228. /**
  229. * Returns the Workflow object of this State.
  230. *
  231. * @return Workflow
  232. * Workflow object.
  233. */
  234. public function getWorkflow() {
  235. if (isset($this->workflow)) {
  236. return $this->workflow;
  237. }
  238. return workflow_load_single($this->wid);
  239. }
  240. public function setWorkflow($workflow) {
  241. $this->wid = $workflow->wid;
  242. $this->workflow = $workflow;
  243. }
  244. /**
  245. * Returns the Workflow object of this State.
  246. *
  247. * @return bool
  248. * TRUE if state is active, else FALSE.
  249. */
  250. public function isActive() {
  251. return (bool) $this->status;
  252. }
  253. public function isCreationState() {
  254. return $this->sysid == WORKFLOW_CREATION;
  255. }
  256. /**
  257. * Determines if the Workflow Form must be shown.
  258. *
  259. * If not, a formatter must be shown, since there are no valid options.
  260. *
  261. * @param $entity_type
  262. * @param $entity
  263. * @param $field_name
  264. * @param $user
  265. * @param $force
  266. *
  267. * @return bool $show_widget
  268. * TRUE = a form (a.k.a. widget) must be shown; FALSE = no form, a formatter must be shown instead.
  269. */
  270. public function showWidget($entity_type, $entity, $field_name, $user, $force) {
  271. $options = $this->getOptions($entity_type, $entity, $field_name, $user, $force);
  272. $count = count($options);
  273. // The easiest case first: more then one option: always show form.
  274. if ($count > 1) {
  275. return TRUE;
  276. }
  277. // #2226451: Even in Creation state, we must have 2 visible states to show the widget.
  278. // // Only when in creation phase, one option is sufficient,
  279. // // since the '(creation)' option is not included in $options.
  280. // // When in creation state,
  281. // if ($this->isCreationState()) {
  282. // return TRUE;
  283. // }
  284. return FALSE;
  285. }
  286. /**
  287. * Returns the allowed transitions for the current state.
  288. *
  289. * @param string $entity_type
  290. * The type of the entity at hand.
  291. * @param object $entity
  292. * The entity at hand. May be NULL (E.g., on a Field settings page).
  293. * @param string $field_name
  294. * @param null $user
  295. * @param bool $force
  296. *
  297. * @return array
  298. * An array of tid=>transition pairs with allowed transitions for State.
  299. */
  300. public function getTransitions($entity_type = '', $entity = NULL, $field_name = '', $user = NULL, $force = FALSE) {
  301. $transitions = array();
  302. $current_sid = $this->sid;
  303. $current_state = $this;
  304. if (!$workflow = $this->getWorkflow()) {
  305. // No workflow, no options ;-)
  306. return $transitions;
  307. }
  308. // Get the role IDs of the user, to get the proper permissions.
  309. $roles = $user ? array_keys($user->roles) : array();
  310. // Some entities (e.g., taxonomy_term) do not have a uid.
  311. $entity_uid = isset($entity->uid) ? $entity->uid : 0;
  312. // Fetch entity_id from entity for _newness_ check
  313. $entity_id = ($entity) ? entity_id($entity_type, $entity) : '';
  314. if ($force || ($user && $user->uid == 1)) {
  315. // Superuser is special. And $force allows Rules to cause transition.
  316. $roles = 'ALL';
  317. }
  318. elseif ($entity && (!empty($entity->is_new) || empty($entity_id))) {
  319. // Add 'author' role to user, if this is a new entity.
  320. // - $entity can be NULL (E.g., on a Field settings page).
  321. // - on display of new entity, $entity_id and $is_new are not set.
  322. // - on submit of new entity, $entity_id and $is_new are both set.
  323. $roles = array_merge(array(WORKFLOW_ROLE_AUTHOR_RID), $roles);
  324. }
  325. elseif (($entity_uid > 0) && ($user->uid > 0) && ($entity_uid == $user->uid)) {
  326. // Add 'author' role to user, if user is author of this entity.
  327. // - Some entities (e.g, taxonomy_term) do not have a uid.
  328. // - If 'anonymous' is the author, don't allow access to History Tab,
  329. // since anyone can access it, and it will be published in Search engines.
  330. $roles = array_merge(array(WORKFLOW_ROLE_AUTHOR_RID), $roles);
  331. }
  332. // Set up an array with states - they are already properly sorted.
  333. // Unfortunately, the config_transitions are not sorted.
  334. // Also, $transitions does not contain the 'stay on current state' transition.
  335. // The allowed objects will be replaced with names.
  336. $transitions = $workflow->getTransitionsBySid($current_sid, $roles);
  337. // Let custom code add/remove/alter the available transitions.
  338. // Using the new drupal_alter.
  339. // Modules may veto a choice by removing a transition from the list.
  340. $context = array(
  341. 'entity_type' => $entity_type,
  342. 'entity' => $entity,
  343. 'field_name' => $field_name,
  344. 'force' => $force,
  345. 'workflow' => $workflow,
  346. 'state' => $current_state,
  347. 'user' => $user,
  348. 'user_roles' => $roles, // @todo: can be removed in D8, since $user is in.
  349. );
  350. // @todo D8: rename to 'workflow_permitted_transitions'.
  351. drupal_alter('workflow_permitted_state_transitions', $transitions, $context);
  352. // Let custom code change the options, using old_style hook.
  353. // @todo D8: delete below foreach/hook for better performance and flexibility.
  354. // Above drupal_alter() calls hook_workflow_permitted_state_transitions_alter() only once.
  355. foreach ($transitions as $transition) {
  356. $new_sid = $transition->target_sid;
  357. $permitted = array();
  358. // We now have a list of config_transitions. Check each against the Entity.
  359. // Invoke a callback indicating that we are collecting state choices.
  360. // Modules may veto a choice by returning FALSE.
  361. // In this case, the choice is never presented to the user.
  362. if ($roles != 'ALL') {
  363. $permitted = module_invoke_all('workflow', 'transition permitted', $current_sid, $new_sid, $entity, $force, $entity_type, $field_name, $transition, $user);
  364. }
  365. // If vetoed by a module, remove from list.
  366. if (in_array(FALSE, $permitted, TRUE)) {
  367. unset($transitions[$transition->tid]);
  368. }
  369. }
  370. return $transitions;
  371. }
  372. /**
  373. * Returns the allowed values for the current state.
  374. *
  375. * @param string $entity_type
  376. * The type of the entity at hand.
  377. * @param object $entity
  378. * The entity at hand. May be NULL (E.g., on a Field settings page).
  379. * @param $field_name
  380. * @param $user
  381. * @param bool $force
  382. *
  383. * @return array
  384. * An array of sid=>label pairs.
  385. * If $this->sid is set, returns the allowed transitions from this state.
  386. * If $this->sid is 0 or FALSE, then labels of ALL states of the State's
  387. * Workflow are returned.
  388. *
  389. * D7.x-2.x: deprecated workflow_field_choices() --> WorkflowState->getOptions()
  390. */
  391. public function getOptions($entity_type, $entity, $field_name, $user, $force = FALSE) {
  392. // Define an Entity-specific cache per page load.
  393. static $cache = array();
  394. $options = array();
  395. $entity_id = ($entity) ? entity_id($entity_type, $entity) : '';
  396. $current_sid = $this->sid;
  397. // Get options from page cache, using a non-empty index (just to be sure).
  398. $entity_index = (!$entity) ? 'x' : $entity_id;
  399. if (isset($cache[$entity_type][$entity_index][$force][$current_sid])) {
  400. $options = $cache[$entity_type][$entity_index][$force][$current_sid];
  401. return $options;
  402. }
  403. $workflow = $this->getWorkflow();
  404. if (!$workflow) {
  405. // No workflow, no options ;-)
  406. }
  407. elseif (!$current_sid) {
  408. // If no State ID is given, we return all states.
  409. // We cannot use getTransitions, since there are no ConfigTransitions
  410. // from State with ID 0, and we do not want to repeat States.
  411. foreach ($workflow->getStates() as $state) {
  412. $options[$state->value()] = $state->label(); // Translation is done as part of defaultLabel().
  413. }
  414. }
  415. else {
  416. /* @var $transition WorkflowTransition */
  417. $transitions = $this->getTransitions($entity_type, $entity, $field_name, $user, $force);
  418. foreach ($transitions as $transition) {
  419. // Get the label of the transition, and if empty of the target state.
  420. // Beware: the target state may not exist, since it can be invented
  421. // by custom code in the above drupal_alter() hook.
  422. if (!$label = $transition->label()) {
  423. $target_state = $transition->getNewState();
  424. $label = $target_state ? $target_state->label() : '';
  425. }
  426. $new_sid = $transition->target_sid;
  427. $options[$new_sid] = $label; // Translation is done as part of defaultLabel().
  428. }
  429. // Include current state for same-state transitions, except when $sid = 0.
  430. // Caveat: this unnecessary since 7.x-2.3 (where stay-on-state transitions are saved, too.)
  431. // but only if the transitions have been saved at least one time.
  432. if ($current_sid && ($current_sid != $workflow->getCreationSid())) {
  433. if (!isset($options[$current_sid])) {
  434. $options[$current_sid] = $this->label(); // Translation is done as part of defaultLabel().
  435. }
  436. }
  437. // Save to entity-specific cache.
  438. $cache[$entity_type][$entity_index][$force][$current_sid] = $options;
  439. }
  440. return $options;
  441. }
  442. /**
  443. * Returns the number of entities with this state.
  444. *
  445. * @return int
  446. * Counted number.
  447. *
  448. * @todo: add $options to select on entity type, etc.
  449. */
  450. public function count() {
  451. $sid = $this->sid;
  452. // Get the numbers for Workflow Node.
  453. $result = db_select('workflow_node', 'wn')
  454. ->fields('wn')
  455. ->condition('sid', $sid, '=')
  456. ->execute();
  457. $count = count($result->fetchAll()); // @see #2285983 for using SQLite.
  458. // Get the numbers for Workflow Field.
  459. $fields = _workflow_info_fields();
  460. foreach ($fields as $field_name => $field_map) {
  461. if ($field_map['type'] == 'workflow') {
  462. $query = new EntityFieldQuery();
  463. $query
  464. ->fieldCondition($field_name, 'value', $sid, '=')
  465. // ->entityCondition('bundle', 'article')
  466. // ->addMetaData('account', user_load(1)) // Run the query as user 1.
  467. ->count(); // We only need the count.
  468. $result = $query->execute();
  469. $count += $result;
  470. }
  471. }
  472. return $count;
  473. }
  474. /**
  475. * Mimics Entity API functions.
  476. */
  477. protected function defaultLabel() {
  478. return isset($this->state) ? t('@state', array('@state' => $this->state)) : '';
  479. }
  480. public function getName() {
  481. return isset($this->name) ? $this->name : '';
  482. }
  483. public function setName($name) {
  484. return $this->name = $name;
  485. }
  486. public function value() {
  487. return $this->sid;
  488. }
  489. public function save() {
  490. parent::save();
  491. // Ensure Workflow is marked overridden.
  492. $workflow = $this->getWorkflow();
  493. if ($workflow->status == ENTITY_IN_CODE) {
  494. $workflow->status = ENTITY_OVERRIDDEN;
  495. $workflow->save();
  496. }
  497. }
  498. }
  499. class WorkflowStateController extends EntityAPIController {
  500. public function save($entity, DatabaseTransaction $transaction = NULL) {
  501. // Create the machine_name.
  502. if (empty($entity->name)) {
  503. if ($label = $entity->state) {
  504. $entity->name = str_replace(' ', '_', strtolower($label));
  505. }
  506. else {
  507. $entity->name = 'state_' . $entity->sid;
  508. }
  509. }
  510. $return = parent::save($entity, $transaction);
  511. if ($return) {
  512. $workflow = $entity->getWorkflow();
  513. // Maintain the new object in the workflow.
  514. $workflow->states[$entity->sid] = $entity;
  515. }
  516. // Reset the cache for the affected workflow.
  517. workflow_reset_cache($entity->wid);
  518. return $return;
  519. }
  520. public function delete($ids, DatabaseTransaction $transaction = NULL) {
  521. // @todo: replace with parent.
  522. foreach ($ids as $id) {
  523. if ($state = workflow_state_load($id)) {
  524. $wid = $state->wid;
  525. db_delete('workflow_states')
  526. ->condition('sid', $state->sid)
  527. ->execute();
  528. // Reset the cache for the affected workflow.
  529. workflow_reset_cache($wid);
  530. }
  531. }
  532. }
  533. }