3 namespace Drupal\workflows\Plugin;
5 use Drupal\Component\Plugin\PluginBase;
6 use Drupal\Core\Plugin\PluginWithFormsTrait;
7 use Drupal\workflows\State;
8 use Drupal\workflows\StateInterface;
9 use Drupal\workflows\Transition;
10 use Drupal\workflows\TransitionInterface;
11 use Drupal\workflows\WorkflowInterface;
12 use Drupal\workflows\WorkflowTypeInterface;
15 * A base class for Workflow type plugins.
17 * @see \Drupal\workflows\Annotation\WorkflowType
19 abstract class WorkflowTypeBase extends PluginBase implements WorkflowTypeInterface {
21 use PluginWithFormsTrait;
24 * A regex for matching a valid state/transition machine name.
26 const VALID_ID_REGEX = '/[^a-z0-9_]+/';
31 public function __construct(array $configuration, $plugin_id, $plugin_definition) {
32 parent::__construct($configuration, $plugin_id, $plugin_definition);
33 $this->setConfiguration($configuration);
39 public function label() {
40 $definition = $this->getPluginDefinition();
41 // The label can be an object.
42 // @see \Drupal\Core\StringTranslation\TranslatableMarkup
43 return $definition['label'];
49 public function workflowHasData(WorkflowInterface $workflow) {
56 public function workflowStateHasData(WorkflowInterface $workflow, StateInterface $state) {
63 public function getConfiguration() {
64 return $this->configuration;
70 public function setConfiguration(array $configuration) {
71 $this->configuration = $configuration + $this->defaultConfiguration();
77 public function getRequiredStates() {
78 return $this->getPluginDefinition()['required_states'];
84 public function defaultConfiguration() {
94 public function calculateDependencies() {
101 public function onDependencyRemoval(array $dependencies) {
108 public function getInitialState() {
109 $ordered_states = $this->getStates();
110 return reset($ordered_states);
116 public function addState($state_id, $label) {
117 if ($this->hasState($state_id)) {
118 throw new \InvalidArgumentException("The state '$state_id' already exists in workflow.");
120 if (preg_match(static::VALID_ID_REGEX, $state_id)) {
121 throw new \InvalidArgumentException("The state ID '$state_id' must contain only lowercase letters, numbers, and underscores");
123 $this->configuration['states'][$state_id] = [
125 'weight' => $this->getNextWeight($this->configuration['states']),
127 ksort($this->configuration['states']);
134 public function hasState($state_id) {
135 return isset($this->configuration['states'][$state_id]);
141 public function getStates($state_ids = NULL) {
142 if ($state_ids === NULL) {
143 $state_ids = array_keys($this->configuration['states']);
145 /** @var \Drupal\workflows\StateInterface[] $states */
146 $states = array_combine($state_ids, array_map([$this, 'getState'], $state_ids));
147 return static::labelWeightMultisort($states);
153 public function getState($state_id) {
154 if (!isset($this->configuration['states'][$state_id])) {
155 throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow.");
160 $this->configuration['states'][$state_id]['label'],
161 $this->configuration['states'][$state_id]['weight']
168 public function setStateLabel($state_id, $label) {
169 if (!$this->hasState($state_id)) {
170 throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow.");
172 $this->configuration['states'][$state_id]['label'] = $label;
179 public function setStateWeight($state_id, $weight) {
180 if (!$this->hasState($state_id)) {
181 throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow.");
183 if (!is_numeric($weight)) {
184 $label = $this->getState($state_id)->label();
185 throw new \InvalidArgumentException("The weight '$weight' must be numeric for state '$label'.");
187 $this->configuration['states'][$state_id]['weight'] = $weight;
194 public function deleteState($state_id) {
195 if (!$this->hasState($state_id)) {
196 throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow.");
198 if (count($this->configuration['states']) === 1) {
199 throw new \InvalidArgumentException("The state '$state_id' can not be deleted from workflow as it is the only state.");
202 foreach ($this->configuration['transitions'] as $transition_id => $transition) {
203 if ($transition['to'] === $state_id) {
204 $this->deleteTransition($transition_id);
207 $from_key = array_search($state_id, $transition['from'], TRUE);
208 if ($from_key !== FALSE) {
209 // Remove state from the from array.
210 unset($transition['from'][$from_key]);
211 if (empty($transition['from'])) {
212 // There are no more 'from' entries, remove the transition.
213 $this->deleteTransition($transition_id);
216 // We changed the from state, update the transition.
217 $this->setTransitionFromStates($transition_id, $transition['from']);
220 unset($this->configuration['states'][$state_id]);
227 public function addTransition($transition_id, $label, array $from_state_ids, $to_state_id) {
228 if ($this->hasTransition($transition_id)) {
229 throw new \InvalidArgumentException("The transition '$transition_id' already exists in workflow.");
231 if (preg_match(static::VALID_ID_REGEX, $transition_id)) {
232 throw new \InvalidArgumentException("The transition ID '$transition_id' must contain only lowercase letters, numbers, and underscores.");
235 if (!$this->hasState($to_state_id)) {
236 throw new \InvalidArgumentException("The state '$to_state_id' does not exist in workflow.");
238 $this->configuration['transitions'][$transition_id] = [
241 'to' => $to_state_id,
242 // Always add to the end.
243 'weight' => $this->getNextWeight($this->configuration['transitions']),
247 $this->setTransitionFromStates($transition_id, $from_state_ids);
249 catch (\InvalidArgumentException $e) {
250 unset($this->configuration['transitions'][$transition_id]);
254 ksort($this->configuration['transitions']);
261 public function getTransitions(array $transition_ids = NULL) {
262 if ($transition_ids === NULL) {
263 $transition_ids = array_keys($this->configuration['transitions']);
265 /** @var \Drupal\workflows\TransitionInterface[] $transitions */
266 $transitions = array_combine($transition_ids, array_map([$this, 'getTransition'], $transition_ids));
267 return static::labelWeightMultisort($transitions);
271 * Sort states or transitions by weight, label, and key.
273 * @param \Drupal\workflows\StateInterface[]|\Drupal\workflows\TransitionInterface[] $objects
274 * An array of state or transition objects to multi-sort, keyed by the
275 * state or transition ID.
277 * @return \Drupal\workflows\StateInterface[]|\Drupal\workflows\TransitionInterface[]
278 * An array of sorted transitions or states, keyed by the state or
281 protected static function labelWeightMultisort($objects) {
282 if (count($objects) > 1) {
283 // Separate weights, labels, and keys into arrays.
284 $weights = $labels = [];
285 $keys = array_keys($objects);
286 foreach ($objects as $id => $object) {
287 $weights[$id] = $object->weight();
288 $labels[$id] = $object->label();
290 // Sort weights, labels, and keys in the same order as each other.
292 // Use the numerical weight as the primary sort.
293 $weights, SORT_NUMERIC, SORT_ASC,
294 // When objects have the same weight, sort them alphabetically by label.
295 $labels, SORT_NATURAL, SORT_ASC,
296 // Ensure that the keys (the object IDs) are sorted in the same order as
300 // Combine keys and weights to make sure the weights are keyed with the
302 $weights = array_combine($keys, $weights);
303 // Return the objects sorted by weight.
304 return array_replace($weights, $objects);
312 public function getTransition($transition_id) {
313 if (!$this->hasTransition($transition_id)) {
314 throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow.");
316 return new Transition(
319 $this->configuration['transitions'][$transition_id]['label'],
320 $this->configuration['transitions'][$transition_id]['from'],
321 $this->configuration['transitions'][$transition_id]['to'],
322 $this->configuration['transitions'][$transition_id]['weight']
329 public function hasTransition($transition_id) {
330 return isset($this->configuration['transitions'][$transition_id]);
336 public function getTransitionsForState($state_id, $direction = TransitionInterface::DIRECTION_FROM) {
337 $transition_ids = array_keys(array_filter($this->configuration['transitions'], function ($transition) use ($state_id, $direction) {
338 return in_array($state_id, (array) $transition[$direction], TRUE);
340 return $this->getTransitions($transition_ids);
346 public function getTransitionFromStateToState($from_state_id, $to_state_id) {
347 $transition_id = $this->getTransitionIdFromStateToState($from_state_id, $to_state_id);
348 if (empty($transition_id)) {
349 throw new \InvalidArgumentException("The transition from '$from_state_id' to '$to_state_id' does not exist in workflow.");
351 return $this->getTransition($transition_id);
357 public function hasTransitionFromStateToState($from_state_id, $to_state_id) {
358 return $this->getTransitionIdFromStateToState($from_state_id, $to_state_id) !== NULL;
362 * Gets the transition ID from state to state.
364 * @param string $from_state_id
365 * The state ID to transition from.
366 * @param string $to_state_id
367 * The state ID to transition to.
369 * @return string|null
370 * The transition ID, or NULL if no transition exists.
372 protected function getTransitionIdFromStateToState($from_state_id, $to_state_id) {
373 foreach ($this->configuration['transitions'] as $transition_id => $transition) {
374 if (in_array($from_state_id, $transition['from'], TRUE) && $transition['to'] === $to_state_id) {
375 return $transition_id;
384 public function setTransitionLabel($transition_id, $label) {
385 if (!$this->hasTransition($transition_id)) {
386 throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow.");
388 $this->configuration['transitions'][$transition_id]['label'] = $label;
395 public function setTransitionWeight($transition_id, $weight) {
396 if (!$this->hasTransition($transition_id)) {
397 throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow.");
399 if (!is_numeric($weight)) {
400 $label = $this->getTransition($transition_id)->label();
401 throw new \InvalidArgumentException("The weight '$weight' must be numeric for transition '$label'.");
403 $this->configuration['transitions'][$transition_id]['weight'] = $weight;
410 public function setTransitionFromStates($transition_id, array $from_state_ids) {
411 if (!$this->hasTransition($transition_id)) {
412 throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow.");
415 // Ensure that the states exist.
416 foreach ($from_state_ids as $from_state_id) {
417 if (!$this->hasState($from_state_id)) {
418 throw new \InvalidArgumentException("The state '$from_state_id' does not exist in workflow.");
420 if ($this->hasTransitionFromStateToState($from_state_id, $this->configuration['transitions'][$transition_id]['to'])) {
421 $existing_transition_id = $this->getTransitionIdFromStateToState($from_state_id, $this->configuration['transitions'][$transition_id]['to']);
422 if ($transition_id !== $existing_transition_id) {
423 throw new \InvalidArgumentException("The '$existing_transition_id' transition already allows '$from_state_id' to '{$this->configuration['transitions'][$transition_id]['to']}' transitions in workflow.");
428 // Preserve the order of the state IDs in the from value and don't save any
430 $from_state_ids = array_values($from_state_ids);
431 sort($from_state_ids);
432 $this->configuration['transitions'][$transition_id]['from'] = $from_state_ids;
440 public function deleteTransition($transition_id) {
441 if (!$this->hasTransition($transition_id)) {
442 throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow.");
444 unset($this->configuration['transitions'][$transition_id]);
449 * Gets the weight for a new state or transition.
451 * @param array $items
452 * An array of states or transitions information where each item has a
453 * 'weight' key with a numeric value.
456 * The weight for a new item in the array so that it has the highest weight.
458 protected function getNextWeight(array $items) {
459 return array_reduce($items, function ($carry, $item) {
460 return max($carry, $item['weight'] + 1);