Updated Drupal to 8.6. This goes with the following updates because it's possible...
[yaffs-website] / web / core / modules / workflows / src / Plugin / WorkflowTypeBase.php
1 <?php
2
3 namespace Drupal\workflows\Plugin;
4
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;
13
14 /**
15  * A base class for Workflow type plugins.
16  *
17  * @see \Drupal\workflows\Annotation\WorkflowType
18  */
19 abstract class WorkflowTypeBase extends PluginBase implements WorkflowTypeInterface {
20
21   use PluginWithFormsTrait;
22
23   /**
24    * A regex for matching a valid state/transition machine name.
25    */
26   const VALID_ID_REGEX = '/[^a-z0-9_]+/';
27
28   /**
29    * {@inheritdoc}
30    */
31   public function __construct(array $configuration, $plugin_id, $plugin_definition) {
32     parent::__construct($configuration, $plugin_id, $plugin_definition);
33     $this->setConfiguration($configuration);
34   }
35
36   /**
37    * {@inheritdoc}
38    */
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'];
44   }
45
46   /**
47    * {@inheritdoc}
48    */
49   public function workflowHasData(WorkflowInterface $workflow) {
50     return FALSE;
51   }
52
53   /**
54    * {@inheritdoc}
55    */
56   public function workflowStateHasData(WorkflowInterface $workflow, StateInterface $state) {
57     return FALSE;
58   }
59
60   /**
61    * {@inheritdoc}
62    */
63   public function getConfiguration() {
64     return $this->configuration;
65   }
66
67   /**
68    * {@inheritdoc}
69    */
70   public function setConfiguration(array $configuration) {
71     $this->configuration = $configuration + $this->defaultConfiguration();
72   }
73
74   /**
75    * {@inheritdoc}
76    */
77   public function getRequiredStates() {
78     return $this->getPluginDefinition()['required_states'];
79   }
80
81   /**
82    * {@inheritdoc}
83    */
84   public function defaultConfiguration() {
85     return [
86       'states' => [],
87       'transitions' => [],
88     ];
89   }
90
91   /**
92    * {@inheritdoc}
93    */
94   public function calculateDependencies() {
95     return [];
96   }
97
98   /**
99    * {@inheritdoc}
100    */
101   public function onDependencyRemoval(array $dependencies) {
102     return FALSE;
103   }
104
105   /**
106    * {@inheritdoc}
107    */
108   public function getInitialState() {
109     $ordered_states = $this->getStates();
110     return reset($ordered_states);
111   }
112
113   /**
114    * {@inheritdoc}
115    */
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.");
119     }
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");
122     }
123     $this->configuration['states'][$state_id] = [
124       'label' => $label,
125       'weight' => $this->getNextWeight($this->configuration['states']),
126     ];
127     ksort($this->configuration['states']);
128     return $this;
129   }
130
131   /**
132    * {@inheritdoc}
133    */
134   public function hasState($state_id) {
135     return isset($this->configuration['states'][$state_id]);
136   }
137
138   /**
139    * {@inheritdoc}
140    */
141   public function getStates($state_ids = NULL) {
142     if ($state_ids === NULL) {
143       $state_ids = array_keys($this->configuration['states']);
144     }
145     /** @var \Drupal\workflows\StateInterface[] $states */
146     $states = array_combine($state_ids, array_map([$this, 'getState'], $state_ids));
147     return static::labelWeightMultisort($states);
148   }
149
150   /**
151    * {@inheritdoc}
152    */
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.");
156     }
157     return new State(
158       $this,
159       $state_id,
160       $this->configuration['states'][$state_id]['label'],
161       $this->configuration['states'][$state_id]['weight']
162     );
163   }
164
165   /**
166    * {@inheritdoc}
167    */
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.");
171     }
172     $this->configuration['states'][$state_id]['label'] = $label;
173     return $this;
174   }
175
176   /**
177    * {@inheritdoc}
178    */
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.");
182     }
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'.");
186     }
187     $this->configuration['states'][$state_id]['weight'] = $weight;
188     return $this;
189   }
190
191   /**
192    * {@inheritdoc}
193    */
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.");
197     }
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.");
200     }
201
202     foreach ($this->configuration['transitions'] as $transition_id => $transition) {
203       if ($transition['to'] === $state_id) {
204         $this->deleteTransition($transition_id);
205         continue;
206       }
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);
214           continue;
215         }
216         // We changed the from state, update the transition.
217         $this->setTransitionFromStates($transition_id, $transition['from']);
218       }
219     }
220     unset($this->configuration['states'][$state_id]);
221     return $this;
222   }
223
224   /**
225    * {@inheritdoc}
226    */
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.");
230     }
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.");
233     }
234
235     if (!$this->hasState($to_state_id)) {
236       throw new \InvalidArgumentException("The state '$to_state_id' does not exist in workflow.");
237     }
238     $this->configuration['transitions'][$transition_id] = [
239       'label' => $label,
240       'from' => [],
241       'to' => $to_state_id,
242       // Always add to the end.
243       'weight' => $this->getNextWeight($this->configuration['transitions']),
244     ];
245
246     try {
247       $this->setTransitionFromStates($transition_id, $from_state_ids);
248     }
249     catch (\InvalidArgumentException $e) {
250       unset($this->configuration['transitions'][$transition_id]);
251       throw $e;
252     }
253
254     ksort($this->configuration['transitions']);
255     return $this;
256   }
257
258   /**
259    * {@inheritdoc}
260    */
261   public function getTransitions(array $transition_ids = NULL) {
262     if ($transition_ids === NULL) {
263       $transition_ids = array_keys($this->configuration['transitions']);
264     }
265     /** @var \Drupal\workflows\TransitionInterface[] $transitions */
266     $transitions = array_combine($transition_ids, array_map([$this, 'getTransition'], $transition_ids));
267     return static::labelWeightMultisort($transitions);
268   }
269
270   /**
271    * Sort states or transitions by weight, label, and key.
272    *
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.
276    *
277    * @return \Drupal\workflows\StateInterface[]|\Drupal\workflows\TransitionInterface[]
278    *   An array of sorted transitions or states, keyed by the state or
279    *   transition ID.
280    */
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();
289       }
290       // Sort weights, labels, and keys in the same order as each other.
291       array_multisort(
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
297         // the weights.
298         $keys
299       );
300       // Combine keys and weights to make sure the weights are keyed with the
301       // correct keys.
302       $weights = array_combine($keys, $weights);
303       // Return the objects sorted by weight.
304       return array_replace($weights, $objects);
305     }
306     return $objects;
307   }
308
309   /**
310    * {@inheritdoc}
311    */
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.");
315     }
316     return new Transition(
317       $this,
318       $transition_id,
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']
323     );
324   }
325
326   /**
327    * {@inheritdoc}
328    */
329   public function hasTransition($transition_id) {
330     return isset($this->configuration['transitions'][$transition_id]);
331   }
332
333   /**
334    * {@inheritdoc}
335    */
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);
339     }));
340     return $this->getTransitions($transition_ids);
341   }
342
343   /**
344    * {@inheritdoc}
345    */
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.");
350     }
351     return $this->getTransition($transition_id);
352   }
353
354   /**
355    * {@inheritdoc}
356    */
357   public function hasTransitionFromStateToState($from_state_id, $to_state_id) {
358     return $this->getTransitionIdFromStateToState($from_state_id, $to_state_id) !== NULL;
359   }
360
361   /**
362    * Gets the transition ID from state to state.
363    *
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.
368    *
369    * @return string|null
370    *   The transition ID, or NULL if no transition exists.
371    */
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;
376       }
377     }
378     return NULL;
379   }
380
381   /**
382    * {@inheritdoc}
383    */
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.");
387     }
388     $this->configuration['transitions'][$transition_id]['label'] = $label;
389     return $this;
390   }
391
392   /**
393    * {@inheritdoc}
394    */
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.");
398     }
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'.");
402     }
403     $this->configuration['transitions'][$transition_id]['weight'] = $weight;
404     return $this;
405   }
406
407   /**
408    * {@inheritdoc}
409    */
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.");
413     }
414
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.");
419       }
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.");
424         }
425       }
426     }
427
428     // Preserve the order of the state IDs in the from value and don't save any
429     // keys.
430     $from_state_ids = array_values($from_state_ids);
431     sort($from_state_ids);
432     $this->configuration['transitions'][$transition_id]['from'] = $from_state_ids;
433
434     return $this;
435   }
436
437   /**
438    * {@inheritdoc}
439    */
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.");
443     }
444     unset($this->configuration['transitions'][$transition_id]);
445     return $this;
446   }
447
448   /**
449    * Gets the weight for a new state or transition.
450    *
451    * @param array $items
452    *   An array of states or transitions information where each item has a
453    *   'weight' key with a numeric value.
454    *
455    * @return int
456    *   The weight for a new item in the array so that it has the highest weight.
457    */
458   protected function getNextWeight(array $items) {
459     return array_reduce($items, function ($carry, $item) {
460       return max($carry, $item['weight'] + 1);
461     }, 0);
462   }
463
464 }