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\WorkflowInterface;
11 use Drupal\workflows\WorkflowTypeInterface;
14 * A base class for Workflow type plugins.
16 * @see \Drupal\workflows\Annotation\WorkflowType
18 abstract class WorkflowTypeBase extends PluginBase implements WorkflowTypeInterface {
20 use PluginWithFormsTrait;
23 * A regex for matching a valid state/transition machine name.
25 const VALID_ID_REGEX = '/[^a-z0-9_]+/';
30 public function __construct(array $configuration, $plugin_id, $plugin_definition) {
31 parent::__construct($configuration, $plugin_id, $plugin_definition);
32 $this->setConfiguration($configuration);
38 public function label() {
39 $definition = $this->getPluginDefinition();
40 // The label can be an object.
41 // @see \Drupal\Core\StringTranslation\TranslatableMarkup
42 return $definition['label'];
48 public function workflowHasData(WorkflowInterface $workflow) {
55 public function workflowStateHasData(WorkflowInterface $workflow, StateInterface $state) {
62 public function getConfiguration() {
63 return $this->configuration;
69 public function setConfiguration(array $configuration) {
70 $this->configuration = $configuration + $this->defaultConfiguration();
76 public function getRequiredStates() {
77 return $this->getPluginDefinition()['required_states'];
83 public function defaultConfiguration() {
93 public function calculateDependencies() {
100 public function onDependencyRemoval(array $dependencies) {
107 public function getInitialState() {
108 $ordered_states = $this->getStates();
109 return reset($ordered_states);
115 public function addState($state_id, $label) {
116 if ($this->hasState($state_id)) {
117 throw new \InvalidArgumentException("The state '$state_id' already exists in workflow.");
119 if (preg_match(static::VALID_ID_REGEX, $state_id)) {
120 throw new \InvalidArgumentException("The state ID '$state_id' must contain only lowercase letters, numbers, and underscores");
122 $this->configuration['states'][$state_id] = [
124 'weight' => $this->getNextWeight($this->configuration['states']),
126 ksort($this->configuration['states']);
133 public function hasState($state_id) {
134 return isset($this->configuration['states'][$state_id]);
140 public function getStates($state_ids = NULL) {
141 if ($state_ids === NULL) {
142 $state_ids = array_keys($this->configuration['states']);
144 /** @var \Drupal\workflows\StateInterface[] $states */
145 $states = array_combine($state_ids, array_map([$this, 'getState'], $state_ids));
146 return static::labelWeightMultisort($states);
152 public function getState($state_id) {
153 if (!isset($this->configuration['states'][$state_id])) {
154 throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow.'");
159 $this->configuration['states'][$state_id]['label'],
160 $this->configuration['states'][$state_id]['weight']
167 public function setStateLabel($state_id, $label) {
168 if (!$this->hasState($state_id)) {
169 throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow.'");
171 $this->configuration['states'][$state_id]['label'] = $label;
178 public function setStateWeight($state_id, $weight) {
179 if (!$this->hasState($state_id)) {
180 throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow.'");
182 $this->configuration['states'][$state_id]['weight'] = $weight;
189 public function deleteState($state_id) {
190 if (!$this->hasState($state_id)) {
191 throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow.'");
193 if (count($this->configuration['states']) === 1) {
194 throw new \InvalidArgumentException("The state '$state_id' can not be deleted from workflow as it is the only state.");
197 foreach ($this->configuration['transitions'] as $transition_id => $transition) {
198 if ($transition['to'] === $state_id) {
199 $this->deleteTransition($transition_id);
202 $from_key = array_search($state_id, $transition['from'], TRUE);
203 if ($from_key !== FALSE) {
204 // Remove state from the from array.
205 unset($transition['from'][$from_key]);
206 if (empty($transition['from'])) {
207 // There are no more 'from' entries, remove the transition.
208 $this->deleteTransition($transition_id);
211 // We changed the from state, update the transition.
212 $this->setTransitionFromStates($transition_id, $transition['from']);
215 unset($this->configuration['states'][$state_id]);
222 public function addTransition($transition_id, $label, array $from_state_ids, $to_state_id) {
223 if ($this->hasTransition($transition_id)) {
224 throw new \InvalidArgumentException("The transition '$transition_id' already exists in workflow.'");
226 if (preg_match(static::VALID_ID_REGEX, $transition_id)) {
227 throw new \InvalidArgumentException("The transition ID '$transition_id' must contain only lowercase letters, numbers, and underscores.");
230 if (!$this->hasState($to_state_id)) {
231 throw new \InvalidArgumentException("The state '$to_state_id' does not exist in workflow.'");
233 $this->configuration['transitions'][$transition_id] = [
236 'to' => $to_state_id,
237 // Always add to the end.
238 'weight' => $this->getNextWeight($this->configuration['transitions']),
242 $this->setTransitionFromStates($transition_id, $from_state_ids);
244 catch (\InvalidArgumentException $e) {
245 unset($this->configuration['transitions'][$transition_id]);
249 ksort($this->configuration['transitions']);
256 public function getTransitions(array $transition_ids = NULL) {
257 if ($transition_ids === NULL) {
258 $transition_ids = array_keys($this->configuration['transitions']);
260 /** @var \Drupal\workflows\TransitionInterface[] $transitions */
261 $transitions = array_combine($transition_ids, array_map([$this, 'getTransition'], $transition_ids));
262 return static::labelWeightMultisort($transitions);
266 * Sort states or transitions by weight, label, and key.
268 * @param \Drupal\workflows\StateInterface[]|\Drupal\workflows\TransitionInterface[] $objects
269 * An array of state or transition objects to multi-sort, keyed by the
270 * state or transition ID.
272 * @return \Drupal\workflows\StateInterface[]|\Drupal\workflows\TransitionInterface[]
273 * An array of sorted transitions or states, keyed by the state or
276 protected static function labelWeightMultisort($objects) {
277 if (count($objects) > 1) {
278 // Separate weights, labels, and keys into arrays.
279 $weights = $labels = [];
280 $keys = array_keys($objects);
281 foreach ($objects as $id => $object) {
282 $weights[$id] = $object->weight();
283 $labels[$id] = $object->label();
285 // Sort weights, labels, and keys in the same order as each other.
287 // Use the numerical weight as the primary sort.
288 $weights, SORT_NUMERIC, SORT_ASC,
289 // When objects have the same weight, sort them alphabetically by label.
290 $labels, SORT_NATURAL, SORT_ASC,
291 // Ensure that the keys (the object IDs) are sorted in the same order as
295 // Combine keys and weights to make sure the weights are keyed with the
297 $weights = array_combine($keys, $weights);
298 // Return the objects sorted by weight.
299 return array_replace($weights, $objects);
307 public function getTransition($transition_id) {
308 if (!$this->hasTransition($transition_id)) {
309 throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow.'");
311 return new Transition(
314 $this->configuration['transitions'][$transition_id]['label'],
315 $this->configuration['transitions'][$transition_id]['from'],
316 $this->configuration['transitions'][$transition_id]['to'],
317 $this->configuration['transitions'][$transition_id]['weight']
324 public function hasTransition($transition_id) {
325 return isset($this->configuration['transitions'][$transition_id]);
331 public function getTransitionsForState($state_id, $direction = 'from') {
332 $transition_ids = array_keys(array_filter($this->configuration['transitions'], function ($transition) use ($state_id, $direction) {
333 return in_array($state_id, (array) $transition[$direction], TRUE);
335 return $this->getTransitions($transition_ids);
341 public function getTransitionFromStateToState($from_state_id, $to_state_id) {
342 $transition_id = $this->getTransitionIdFromStateToState($from_state_id, $to_state_id);
343 if (empty($transition_id)) {
344 throw new \InvalidArgumentException("The transition from '$from_state_id' to '$to_state_id' does not exist in workflow.'");
346 return $this->getTransition($transition_id);
352 public function hasTransitionFromStateToState($from_state_id, $to_state_id) {
353 return $this->getTransitionIdFromStateToState($from_state_id, $to_state_id) !== NULL;
357 * Gets the transition ID from state to state.
359 * @param string $from_state_id
360 * The state ID to transition from.
361 * @param string $to_state_id
362 * The state ID to transition to.
364 * @return string|null
365 * The transition ID, or NULL if no transition exists.
367 protected function getTransitionIdFromStateToState($from_state_id, $to_state_id) {
368 foreach ($this->configuration['transitions'] as $transition_id => $transition) {
369 if (in_array($from_state_id, $transition['from'], TRUE) && $transition['to'] === $to_state_id) {
370 return $transition_id;
379 public function setTransitionLabel($transition_id, $label) {
380 if (!$this->hasTransition($transition_id)) {
381 throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow.");
383 $this->configuration['transitions'][$transition_id]['label'] = $label;
390 public function setTransitionWeight($transition_id, $weight) {
391 if (!$this->hasTransition($transition_id)) {
392 throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow.'");
394 $this->configuration['transitions'][$transition_id]['weight'] = $weight;
401 public function setTransitionFromStates($transition_id, array $from_state_ids) {
402 if (!$this->hasTransition($transition_id)) {
403 throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow.");
406 // Ensure that the states exist.
407 foreach ($from_state_ids as $from_state_id) {
408 if (!$this->hasState($from_state_id)) {
409 throw new \InvalidArgumentException("The state '$from_state_id' does not exist in workflow.");
411 if ($this->hasTransitionFromStateToState($from_state_id, $this->configuration['transitions'][$transition_id]['to'])) {
412 $existing_transition_id = $this->getTransitionIdFromStateToState($from_state_id, $this->configuration['transitions'][$transition_id]['to']);
413 if ($transition_id !== $existing_transition_id) {
414 throw new \InvalidArgumentException("The '$existing_transition_id' transition already allows '$from_state_id' to '{$this->configuration['transitions'][$transition_id]['to']}' transitions in workflow.");
419 // Preserve the order of the state IDs in the from value and don't save any
421 $from_state_ids = array_values($from_state_ids);
422 sort($from_state_ids);
423 $this->configuration['transitions'][$transition_id]['from'] = $from_state_ids;
431 public function deleteTransition($transition_id) {
432 if (!$this->hasTransition($transition_id)) {
433 throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow.");
435 unset($this->configuration['transitions'][$transition_id]);
440 * Gets the weight for a new state or transition.
442 * @param array $items
443 * An array of states or transitions information where each item has a
444 * 'weight' key with a numeric value.
447 * The weight for a new item in the array so that it has the highest weight.
449 protected function getNextWeight(array $items) {
450 return array_reduce($items, function ($carry, $item) {
451 return max($carry, $item['weight'] + 1);