3 namespace Drupal\workflows\Entity;
5 use Drupal\Core\Config\Entity\ConfigEntityBase;
6 use Drupal\Core\Entity\EntityStorageInterface;
7 use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
8 use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
9 use Drupal\workflows\Exception\RequiredStateMissingException;
10 use Drupal\workflows\State;
11 use Drupal\workflows\Transition;
12 use Drupal\workflows\WorkflowInterface;
15 * Defines the workflow entity.
19 * label = @Translation("Workflow"),
20 * label_collection = @Translation("Workflows"),
22 * "access" = "Drupal\workflows\WorkflowAccessControlHandler",
23 * "list_builder" = "Drupal\workflows\WorkflowListBuilder",
25 * "add" = "Drupal\workflows\Form\WorkflowAddForm",
26 * "edit" = "Drupal\workflows\Form\WorkflowEditForm",
27 * "delete" = "Drupal\workflows\Form\WorkflowDeleteForm",
28 * "add-state" = "Drupal\workflows\Form\WorkflowStateAddForm",
29 * "edit-state" = "Drupal\workflows\Form\WorkflowStateEditForm",
30 * "delete-state" = "Drupal\workflows\Form\WorkflowStateDeleteForm",
31 * "add-transition" = "Drupal\workflows\Form\WorkflowTransitionAddForm",
32 * "edit-transition" = "Drupal\workflows\Form\WorkflowTransitionEditForm",
33 * "delete-transition" = "Drupal\workflows\Form\WorkflowTransitionDeleteForm",
35 * "route_provider" = {
36 * "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
39 * config_prefix = "workflow",
40 * admin_permission = "administer workflows",
47 * "add-form" = "/admin/config/workflow/workflows/add",
48 * "edit-form" = "/admin/config/workflow/workflows/manage/{workflow}",
49 * "delete-form" = "/admin/config/workflow/workflows/manage/{workflow}/delete",
50 * "add-state-form" = "/admin/config/workflow/workflows/manage/{workflow}/add_state",
51 * "add-transition-form" = "/admin/config/workflow/workflows/manage/{workflow}/add_transition",
52 * "collection" = "/admin/config/workflow/workflows"
65 * The workflow system is currently experimental and should only be leveraged
66 * by experimental modules and development releases of contributed modules.
68 class Workflow extends ConfigEntityBase implements WorkflowInterface, EntityWithPluginCollectionInterface {
78 * The Moderation state label.
85 * The states of the workflow.
87 * The array key is the machine name for the state. The structure of each
90 * label: {translatable label}
91 * weight: {integer value}
96 protected $states = [];
99 * The permitted transitions of the workflow.
101 * The array key is the machine name for the transition. The machine name is
102 * generated from the machine names of the states. The structure of each array
106 * - {state machine name}
107 * - {state machine name}
108 * to: {state machine name}
109 * label: {translatable label}
114 protected $transitions = [];
117 * The workflow type plugin ID.
119 * @see \Drupal\workflows\WorkflowTypeManager
126 * The configuration for the workflow type plugin.
129 protected $type_settings = [];
132 * The workflow type plugin collection.
134 * @var \Drupal\Component\Plugin\LazyPluginCollection
136 protected $pluginCollection;
141 public function preSave(EntityStorageInterface $storage) {
142 $workflow_type = $this->getTypePlugin();
143 $missing_states = array_diff($workflow_type->getRequiredStates(), array_keys($this->getStates()));
144 if (!empty($missing_states)) {
145 throw new RequiredStateMissingException(sprintf("Workflow type '{$workflow_type->label()}' requires states with the ID '%s' in workflow '{$this->id()}'", implode("', '", $missing_states)));
147 parent::preSave($storage);
153 public function addState($state_id, $label) {
154 if (isset($this->states[$state_id])) {
155 throw new \InvalidArgumentException("The state '$state_id' already exists in workflow '{$this->id()}'");
157 if (preg_match('/[^a-z0-9_]+/', $state_id)) {
158 throw new \InvalidArgumentException("The state ID '$state_id' must contain only lowercase letters, numbers, and underscores");
160 $this->states[$state_id] = [
162 'weight' => $this->getNextWeight($this->states),
164 ksort($this->states);
171 public function hasState($state_id) {
172 return isset($this->states[$state_id]);
178 public function getStates($state_ids = NULL) {
179 if ($state_ids === NULL) {
180 $state_ids = array_keys($this->states);
182 /** @var \Drupal\workflows\StateInterface[] $states */
183 $states = array_combine($state_ids, array_map([$this, 'getState'], $state_ids));
184 if (count($states) > 1) {
185 // Sort states by weight and then label.
186 $weights = $labels = [];
187 foreach ($states as $id => $state) {
188 $weights[$id] = $state->weight();
189 $labels[$id] = $state->label();
192 $weights, SORT_NUMERIC, SORT_ASC,
193 $labels, SORT_NATURAL, SORT_ASC
195 $states = array_replace($weights, $states);
203 public function getState($state_id) {
204 if (!isset($this->states[$state_id])) {
205 throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow '{$this->id()}'");
210 $this->states[$state_id]['label'],
211 $this->states[$state_id]['weight']
213 return $this->getTypePlugin()->decorateState($state);
219 public function setStateLabel($state_id, $label) {
220 if (!isset($this->states[$state_id])) {
221 throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow '{$this->id()}'");
223 $this->states[$state_id]['label'] = $label;
230 public function setStateWeight($state_id, $weight) {
231 if (!isset($this->states[$state_id])) {
232 throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow '{$this->id()}'");
234 $this->states[$state_id]['weight'] = $weight;
241 public function deleteState($state_id) {
242 if (!isset($this->states[$state_id])) {
243 throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow '{$this->id()}'");
245 if (count($this->states) === 1) {
246 throw new \InvalidArgumentException("The state '$state_id' can not be deleted from workflow '{$this->id()}' as it is the only state");
249 foreach ($this->transitions as $transition_id => $transition) {
250 $from_key = array_search($state_id, $transition['from'], TRUE);
251 if ($from_key !== FALSE) {
252 // Remove state from the from array.
253 unset($transition['from'][$from_key]);
255 if (empty($transition['from']) || $transition['to'] === $state_id) {
256 $this->deleteTransition($transition_id);
258 elseif ($from_key !== FALSE) {
259 $this->setTransitionFromStates($transition_id, $transition['from']);
262 unset($this->states[$state_id]);
263 $this->getTypePlugin()->deleteState($state_id);
270 public function addTransition($transition_id, $label, array $from_state_ids, $to_state_id) {
271 if (isset($this->transitions[$transition_id])) {
272 throw new \InvalidArgumentException("The transition '$transition_id' already exists in workflow '{$this->id()}'");
274 if (preg_match('/[^a-z0-9_]+/', $transition_id)) {
275 throw new \InvalidArgumentException("The transition ID '$transition_id' must contain only lowercase letters, numbers, and underscores");
278 if (!$this->hasState($to_state_id)) {
279 throw new \InvalidArgumentException("The state '$to_state_id' does not exist in workflow '{$this->id()}'");
281 $this->transitions[$transition_id] = [
284 'to' => $to_state_id,
285 // Always add to the end.
286 'weight' => $this->getNextWeight($this->transitions),
290 $this->setTransitionFromStates($transition_id, $from_state_ids);
292 catch (\InvalidArgumentException $e) {
293 unset($this->transitions[$transition_id]);
303 public function getTransitions(array $transition_ids = NULL) {
304 if ($transition_ids === NULL) {
305 $transition_ids = array_keys($this->transitions);
307 /** @var \Drupal\workflows\TransitionInterface[] $transitions */
308 $transitions = array_combine($transition_ids, array_map([$this, 'getTransition'], $transition_ids));
309 if (count($transitions) > 1) {
310 // Sort transitions by weights and then labels.
311 $weights = $labels = [];
312 foreach ($transitions as $id => $transition) {
313 $weights[$id] = $transition->weight();
314 $labels[$id] = $transition->label();
317 $weights, SORT_NUMERIC, SORT_ASC,
318 $labels, SORT_NATURAL, SORT_ASC
320 $transitions = array_replace($weights, $transitions);
328 public function getTransition($transition_id) {
329 if (!isset($this->transitions[$transition_id])) {
330 throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow '{$this->id()}'");
332 $transition = new Transition(
335 $this->transitions[$transition_id]['label'],
336 $this->transitions[$transition_id]['from'],
337 $this->transitions[$transition_id]['to'],
338 $this->transitions[$transition_id]['weight']
340 return $this->getTypePlugin()->decorateTransition($transition);
346 public function hasTransition($transition_id) {
347 return isset($this->transitions[$transition_id]);
353 public function getTransitionsForState($state_id, $direction = 'from') {
354 $transition_ids = array_keys(array_filter($this->transitions, function ($transition) use ($state_id, $direction) {
355 return in_array($state_id, (array) $transition[$direction], TRUE);
357 return $this->getTransitions($transition_ids);
363 public function getTransitionFromStateToState($from_state_id, $to_state_id) {
364 $transition_id = $this->getTransitionIdFromStateToState($from_state_id, $to_state_id);
365 if (empty($transition_id)) {
366 throw new \InvalidArgumentException("The transition from '$from_state_id' to '$to_state_id' does not exist in workflow '{$this->id()}'");
368 return $this->getTransition($transition_id);
374 public function hasTransitionFromStateToState($from_state_id, $to_state_id) {
375 return !empty($this->getTransitionIdFromStateToState($from_state_id, $to_state_id));
379 * Gets the transition ID from state to state.
381 * @param string $from_state_id
382 * The state ID to transition from.
383 * @param string $to_state_id
384 * The state ID to transition to.
386 * @return string|null
387 * The transition ID, or NULL if no transition exists.
389 protected function getTransitionIdFromStateToState($from_state_id, $to_state_id) {
390 foreach ($this->transitions as $transition_id => $transition) {
391 if (in_array($from_state_id, $transition['from'], TRUE) && $transition['to'] === $to_state_id) {
392 return $transition_id;
401 public function setTransitionLabel($transition_id, $label) {
402 if (isset($this->transitions[$transition_id])) {
403 $this->transitions[$transition_id]['label'] = $label;
406 throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow '{$this->id()}'");
414 public function setTransitionWeight($transition_id, $weight) {
415 if (isset($this->transitions[$transition_id])) {
416 $this->transitions[$transition_id]['weight'] = $weight;
419 throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow '{$this->id()}'");
427 public function setTransitionFromStates($transition_id, array $from_state_ids) {
428 if (!isset($this->transitions[$transition_id])) {
429 throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow '{$this->id()}'");
432 // Ensure that the states exist.
433 foreach ($from_state_ids as $from_state_id) {
434 if (!$this->hasState($from_state_id)) {
435 throw new \InvalidArgumentException("The state '$from_state_id' does not exist in workflow '{$this->id()}'");
437 if ($this->hasTransitionFromStateToState($from_state_id, $this->transitions[$transition_id]['to'])) {
438 $transition = $this->getTransitionFromStateToState($from_state_id, $this->transitions[$transition_id]['to']);
439 if ($transition_id !== $transition->id()) {
440 throw new \InvalidArgumentException("The '{$transition->id()}' transition already allows '$from_state_id' to '{$this->transitions[$transition_id]['to']}' transitions in workflow '{$this->id()}'");
445 // Preserve the order of the state IDs in the from value and don't save any
447 $from_state_ids = array_values($from_state_ids);
448 sort($from_state_ids);
449 $this->transitions[$transition_id]['from'] = $from_state_ids;
450 ksort($this->transitions);
458 public function deleteTransition($transition_id) {
459 if (isset($this->transitions[$transition_id])) {
460 unset($this->transitions[$transition_id]);
461 $this->getTypePlugin()->deleteTransition($transition_id);
464 throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow '{$this->id()}'");
472 public function getTypePlugin() {
473 return $this->getPluginCollection()->get($this->type);
479 public function getPluginCollections() {
480 return ['type_settings' => $this->getPluginCollection()];
484 * Encapsulates the creation of the workflow's plugin collection.
486 * @return \Drupal\Core\Plugin\DefaultSingleLazyPluginCollection
487 * The workflow's plugin collection.
489 protected function getPluginCollection() {
490 if (!$this->pluginCollection && $this->type) {
491 $this->pluginCollection = new DefaultSingleLazyPluginCollection(\Drupal::service('plugin.manager.workflows.type'), $this->type, $this->type_settings);
493 return $this->pluginCollection;
497 * Loads all workflows of the provided type.
499 * @param string $type
500 * The workflow type to load all workflows for.
503 * An array of workflow objects of the provided workflow type, indexed by
506 * @see \Drupal\workflows\Annotation\WorkflowType
508 public static function loadMultipleByType($type) {
509 return self::loadMultiple(\Drupal::entityQuery('workflow')->condition('type', $type)->execute());
513 * Gets the weight for a new state or transition.
515 * @param array $items
516 * An array of states or transitions information where each item has a
517 * 'weight' key with a numeric value.
520 * The weight for a new item in the array so that it has the highest weight.
522 protected function getNextWeight(array $items) {
523 return array_reduce($items, function ($carry, $item) {
524 return max($carry, $item['weight'] + 1);
531 public function status() {
532 // In order for a workflow to be usable it must have at least one state.
533 return !empty($this->status) && !empty($this->states);
539 public function onDependencyRemoval(array $dependencies) {
540 $changed = $this->getTypePlugin()->onDependencyRemoval($dependencies);
541 // Ensure the parent method is called in order to process dependencies that
542 // affect third party settings.
543 return parent::onDependencyRemoval($dependencies) || $changed;