clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

View File

@@ -0,0 +1,217 @@
<?php
namespace Drupal\state_machine\Plugin\Field\FieldFormatter;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\state_machine\Form\StateTransitionForm;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Plugin implementation of the 'state_transition_form' formatter.
*
* @FieldFormatter(
* id = "state_transition_form",
* label = @Translation("Transition form"),
* field_types = {
* "state",
* },
* )
*/
class StateTransitionFormFormatter extends FormatterBase implements ContainerFactoryPluginInterface {
/**
* The class resolver.
*
* @var \Drupal\Core\DependencyInjection\ClassResolverInterface
*/
protected $classResolver;
/**
* The form builder.
*
* @var \Drupal\Core\Form\FormBuilderInterface
*/
protected $formBuilder;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new StateTransitionFormFormatter object.
*
* @param string $plugin_id
* The plugin_id for the formatter.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The definition of the field to which the formatter is associated.
* @param array $settings
* The formatter settings.
* @param string $label
* The formatter label display setting.
* @param string $view_mode
* The view mode.
* @param array $third_party_settings
* Any third party settings.
* @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
* The class resolver.
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The form builder.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, ClassResolverInterface $class_resolver, FormBuilderInterface $form_builder, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
$this->classResolver = $class_resolver;
$this->formBuilder = $form_builder;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$plugin_id,
$plugin_definition,
$configuration['field_definition'],
$configuration['settings'],
$configuration['label'],
$configuration['view_mode'],
$configuration['third_party_settings'],
$container->get('class_resolver'),
$container->get('form_builder'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
/** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
$entity = $items->getEntity();
// Do not show the form if the user isn't allowed to modify the entity.
if (!$entity->access('update')) {
return [];
}
/** @var \Drupal\state_machine\Form\StateTransitionFormInterface $form_object */
$form_object = $this->classResolver->getInstanceFromDefinition(StateTransitionForm::class);
$form_object->setEntity($entity);
$form_object->setFieldName($items->getFieldDefinition()->getName());
$form_state_additions = [];
if ($this->supportsConfirmationForm()) {
$form_state_additions += [
// Store in the form state whether a confirmation is required before
// applying the state transition.
'require_confirmation' => (bool) $this->getSetting('require_confirmation'),
'use_modal' => (bool) $this->getSetting('use_modal'),
];
}
$form_state = (new FormState())->setFormState($form_state_additions);
// $elements needs a value for each delta. State fields can't be multivalue,
// so it's safe to hardcode 0.
$elements = [];
$elements[0] = $this->formBuilder->buildForm($form_object, $form_state);
return $elements;
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'require_confirmation' => FALSE,
'use_modal' => FALSE,
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$form = parent::settingsForm($form, $form_state);
$supports_confirmation_form = $this->supportsConfirmationForm();
$form['require_confirmation'] = [
'#title' => $this->t('Require confirmation before applying the state transition'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('require_confirmation'),
// We can't support confirmation forms for state transition forms without
// the "state-transition-form" link template.
'#access' => $supports_confirmation_form,
];
$form['use_modal'] = [
'#title' => $this->t('Display confirmation in a modal dialog'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('use_modal'),
'#states' => [
'visible' => [
':input[name*="require_confirmation"]' => ['checked' => TRUE],
],
],
'#access' => $supports_confirmation_form,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = parent::settingsSummary();
if (!$this->supportsConfirmationForm()) {
return $summary;
}
if ($this->getSetting('require_confirmation')) {
$summary[] = $this->t('Require confirmation before applying the state transition.');
if ($this->getSetting('use_modal')) {
$summary[] = $this->t('Display confirmation in a modal dialog.');
}
}
else {
$summary[] = $this->t('Do not require confirmation before applying the state transition.');
}
return $summary;
}
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
return $field_definition->getType() == 'state';
}
/**
* Gets whether the target entity type supports the confirmation form.
*
* @return bool
* Whether the target entity type supports the confirmation form.
*/
protected function supportsConfirmationForm() {
// If no "state-transition-form" link template is defined, we can't
// support the confirmation form/modal for applying state transitions.
$entity_type = $this->entityTypeManager->getDefinition($this->fieldDefinition->getTargetEntityTypeId());
return $entity_type->hasLinkTemplate('state-transition-form');
}
}

View File

@@ -0,0 +1,485 @@
<?php
namespace Drupal\state_machine\Plugin\Field\FieldType;
use Drupal\Core\Entity\ContentEntityStorageInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\TypedData\OptionsProviderInterface;
use Drupal\Core\Validation\Plugin\Validation\Constraint\AllowedValuesConstraint;
use Drupal\state_machine\Event\WorkflowTransitionEvent;
use Drupal\state_machine\Plugin\Workflow\WorkflowState;
use Drupal\state_machine\Plugin\Workflow\WorkflowTransition;
/**
* Plugin implementation of the 'state' field type.
*
* @FieldType(
* id = "state",
* label = @Translation("State"),
* description = @Translation("Stores the current workflow state."),
* default_widget = "options_select",
* default_formatter = "list_default"
* )
*/
class StateItem extends FieldItemBase implements StateItemInterface, OptionsProviderInterface {
/**
* The original value, used to validate state changes.
*
* @var string
*/
protected $originalValue;
/**
* The transition to apply.
*
* @var \Drupal\state_machine\Plugin\Workflow\WorkflowTransition
*/
protected $transitionToApply;
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
return [
'columns' => [
'value' => [
'type' => 'varchar_ascii',
'length' => 255,
],
],
];
}
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('string')
->setLabel(t('State'))
->setRequired(TRUE);
return $properties;
}
/**
* {@inheritdoc}
*/
public function getConstraints() {
$constraints = parent::getConstraints();
// Replace the 'AllowedValuesConstraint' constraint with the 'State' one.
foreach ($constraints as $key => $constraint) {
if ($constraint instanceof AllowedValuesConstraint) {
unset($constraints[$key]);
}
}
$manager = \Drupal::typedDataManager()->getValidationConstraintManager();
$constraints[] = $manager->create('State', []);
return $constraints;
}
/**
* {@inheritdoc}
*/
public static function defaultFieldSettings() {
return [
'workflow' => '',
'workflow_callback' => '',
] + parent::defaultFieldSettings();
}
/**
* {@inheritdoc}
*/
public function fieldSettingsForm(array $form, FormStateInterface $form_state) {
$element = [];
// Allow the workflow to be changed if it's not determined by a callback.
if (!$this->getSetting('workflow_callback')) {
$workflow_manager = \Drupal::service('plugin.manager.workflow');
$workflows = $workflow_manager->getGroupedLabels($this->getEntity()->getEntityTypeId());
$element['workflow'] = [
'#type' => 'select',
'#title' => $this->t('Workflow'),
'#options' => $workflows,
'#default_value' => $this->getSetting('workflow'),
'#required' => TRUE,
];
}
return $element;
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
// Note that in this field's case the value will never be empty
// because of the default returned in applyDefaultValue().
return $this->value === NULL || $this->value === '';
}
/**
* {@inheritdoc}
*/
public function applyDefaultValue($notify = TRUE) {
if ($workflow = $this->getWorkflow()) {
$states = $workflow->getStates();
$initial_state = reset($states);
$this->setValue(['value' => $initial_state->getId()], $notify);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function setValue($values, $notify = TRUE) {
if (empty($this->originalValue)) {
// If no array is given, then the method received just the state value.
if (isset($values) && !is_array($values)) {
$values = ['value' => $values];
}
// Track the original field value to allow isValid() to validate changes
// and to react to transitions.
$this->originalValue = $values['value'];
}
parent::setValue($values, $notify);
}
/**
* {@inheritdoc}
*/
public function isValid() {
$workflow = $this->getWorkflow();
if (!$workflow) {
return FALSE;
}
// Validate that the state update was allowed.
if ($this->value != $this->originalValue) {
$transition = $workflow->findTransition($this->originalValue, $this->value);
return $transition && $workflow->isTransitionAllowed($transition, $this->getEntity());
}
// Otherwise, if the state didn't change, simply validate that the current
// state belongs to the workflow.
return !empty($workflow->getState($this->value));
}
/**
* {@inheritdoc}
*/
public function getPossibleValues(AccountInterface $account = NULL) {
return array_keys($this->getPossibleOptions($account));
}
/**
* {@inheritdoc}
*/
public function getPossibleOptions(AccountInterface $account = NULL) {
$workflow = $this->getWorkflow();
if (!$workflow) {
// The workflow is not known yet, the field is probably being created.
return [];
}
$state_labels = array_map(function (WorkflowState $state) {
return $state->getLabel();
}, $workflow->getStates());
return $state_labels;
}
/**
* {@inheritdoc}
*/
public function getSettableValues(AccountInterface $account = NULL) {
return array_keys($this->getSettableOptions($account));
}
/**
* {@inheritdoc}
*/
public function getSettableOptions(AccountInterface $account = NULL) {
// $this->value is unpopulated due to https://www.drupal.org/node/2629932
$field_name = $this->getFieldDefinition()->getName();
if (!$this->getEntity()->hasField($field_name)) {
return [];
}
$value = $this->getEntity()->get($field_name)->value;
$allowed_states = $this->getAllowedStates($value);
$state_labels = array_map(function (WorkflowState $state) {
return $state->getLabel();
}, $allowed_states);
return $state_labels;
}
/**
* Gets the next allowed states for the given field value.
*
* @param string $value
* The field value, representing the state ID.
*
* @return \Drupal\state_machine\Plugin\Workflow\WorkflowState[]
* The allowed states.
*/
protected function getAllowedStates($value) {
$workflow = $this->getWorkflow();
if (!$workflow) {
// The workflow is not known yet, the field is probably being created.
return [];
}
$allowed_states = [];
if (!empty($value) && ($current_state = $workflow->getState($value))) {
$allowed_states[$value] = $current_state;
}
$transitions = $workflow->getAllowedTransitions($value, $this->getEntity());
foreach ($transitions as $transition) {
$state = $transition->getToState();
$allowed_states[$state->getId()] = $state;
}
return $allowed_states;
}
/**
* {@inheritdoc}
*/
public function getWorkflow() {
if ($callback = $this->getSetting('workflow_callback')) {
$workflow_id = call_user_func($callback, $this->getEntity());
}
else {
$workflow_id = $this->getSetting('workflow');
}
if (empty($workflow_id)) {
return FALSE;
}
$workflow_manager = \Drupal::service('plugin.manager.workflow');
return $workflow_manager->createInstance($workflow_id);
}
/**
* {@inheritdoc}
*/
public function getOriginalId() {
return $this->originalValue;
}
/**
* {@inheritdoc}
*/
public function getId() {
return $this->value;
}
/**
* {@inheritdoc}
*/
public function getLabel() {
return $this->getStateLabel($this->value);
}
/**
* {@inheritdoc}
*/
public function getOriginalLabel() {
return $this->getStateLabel($this->originalValue);
}
/**
* Gets the state label for the given state ID.
*
* @param string $state_id
* The state ID.
*
* @return string
* The state label.
*/
protected function getStateLabel($state_id) {
$label = $state_id;
if ($workflow = $this->getWorkflow()) {
$state = $workflow->getState($state_id);
if ($state) {
$label = $state->getLabel();
}
}
return $label;
}
/**
* {@inheritdoc}
*/
public function getTransitions() {
$transitions = [];
if ($workflow = $this->getWorkflow()) {
$transitions = $workflow->getAllowedTransitions($this->value, $this->getEntity());
}
return $transitions;
}
/**
* {@inheritdoc}
*/
public function isTransitionAllowed($transition_id) {
$workflow = $this->getWorkflow();
if (!$workflow) {
return FALSE;
}
// We first check that the transition passed is a "possible" transition.
// Note that we don't call the getTransitions() method on purpose since
// it loops over all transitions and invoke the guards on each of them.
$possible_transitions = $workflow->getPossibleTransitions($this->value);
if (!isset($possible_transitions[$transition_id])) {
return FALSE;
}
return $workflow->isTransitionAllowed($possible_transitions[$transition_id], $this->getEntity());
}
/**
* {@inheritdoc}
*/
public function applyTransition(WorkflowTransition $transition) {
if (!$this->isTransitionAllowed($transition->getId())) {
throw new \InvalidArgumentException(sprintf('The transition "%s" is currently not allowed. (Current state: "%s".)', $transition->getId(), $this->getId()));
}
// Store the transition to apply, to ensure we're applying the requested
// transition instead of guessing based on the original state.
$this->transitionToApply = $transition;
$this->setValue(['value' => $transition->getToState()->getId()]);
}
/**
* {@inheritdoc}
*/
public function applyTransitionById($transition_id) {
$transition = NULL;
if ($workflow = $this->getWorkflow()) {
$transition = $workflow->getTransition($transition_id);
}
if (!$transition) {
throw new \InvalidArgumentException(sprintf('Unknown transition ID "%s".', $transition_id));
}
$this->applyTransition($transition);
}
/**
* {@inheritdoc}
*/
public function preSave() {
if ($this->value != $this->originalValue || $this->transitionToApply !== NULL) {
$this->dispatchTransitionEvent('pre_transition');
}
}
/**
* {@inheritdoc}
*/
public function postSave($update) {
if ($this->value != $this->originalValue || $this->transitionToApply !== NULL) {
$this->dispatchTransitionEvent('post_transition');
}
$this->originalValue = $this->value;
// Nullify the transition to apply, to ensure the next entity save
// doesn't trigger the same transition by mistake.
$this->transitionToApply = NULL;
}
/**
* Dispatches a transition event for the given phase.
*
* @param string $phase
* The phase: pre_transition OR post_transition.
*/
protected function dispatchTransitionEvent($phase) {
/** @var \Drupal\state_machine\Plugin\Workflow\WorkflowInterface $workflow */
$workflow = $this->getWorkflow();
$transition = $this->transitionToApply ?? $workflow->findTransition($this->originalValue, $this->value);
if ($transition) {
$field_name = $this->getFieldDefinition()->getName();
$group_id = $workflow->getGroup();
$transition_id = $transition->getId();
$event_dispatcher = \Drupal::getContainer()->get('event_dispatcher');
$event = new WorkflowTransitionEvent($transition, $workflow, $this->getEntity(), $field_name);
$events = [
// For example: 'commerce_order.place.pre_transition'.
$group_id . '.' . $transition_id . '.' . $phase,
// For example: 'commerce_order.pre_transition'.
$group_id . '.' . $phase,
// For example: 'state_machine.pre_transition'.
'state_machine.' . $phase,
];
foreach ($events as $event_id) {
$event_dispatcher->dispatch($event, $event_id);
}
}
}
/**
* {@inheritdoc}
*/
public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
// Attempt to determine the right workflow to use.
if ($callback = $field_definition->getSetting('workflow_callback')) {
$entity_type_id = $field_definition->getTargetEntityTypeId();
$entity_storage = \Drupal::entityTypeManager()->getStorage($entity_type_id);
if (!$entity_storage instanceof ContentEntityStorageInterface) {
return [];
}
$values = [];
// Attempt to create a sample entity with at least the bundle set.
if ($bundle_key = $entity_storage->getEntityType()->getKey('bundle')) {
if ($field_definition->getTargetBundle()) {
$bundle = $field_definition->getTargetBundle();
}
else {
$bundle_ids = \Drupal::service('entity_type.bundle.info')->getBundleInfo($entity_type_id);
$bundle = array_rand($bundle_ids);
}
$values[$bundle_key] = $bundle;
}
$entity = $entity_storage->create($values);
$workflow_id = call_user_func($callback, $entity);
}
else {
$workflow_id = $field_definition->getSetting('workflow');
}
// The workflow could not be determined, cannot generate a sample value.
if (empty($workflow_id)) {
return [];
}
/** @var \Drupal\state_machine\WorkflowManagerInterface $workflow_manager */
$workflow_manager = \Drupal::service('plugin.manager.workflow');
/** @var \Drupal\state_machine\Plugin\Workflow\WorkflowInterface $workflow */
$workflow = $workflow_manager->createInstance($workflow_id);
// Select states that allow at least one transition.
$candidate_states = $states = $workflow->getStates();
foreach ($candidate_states as $key => $candidate) {
if (empty($workflow->getPossibleTransitions($candidate->getId()))) {
unset($states[$key]);
}
}
$random_state = array_rand($states);
$values = ['value' => $states[$random_state]->getId()];
return $values;
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace Drupal\state_machine\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\state_machine\Plugin\Workflow\WorkflowTransition;
/**
* Defines the interface for state field items.
*/
interface StateItemInterface extends FieldItemInterface {
/**
* Gets the workflow used by the field.
*
* @return \Drupal\state_machine\Plugin\Workflow\WorkflowInterface|false
* The workflow, or FALSE if unknown at this time.
*/
public function getWorkflow();
/**
* Gets the original state ID.
*
* If the state ID has been changed after the entity was constructed/loaded,
* the original ID will hold the previous value.
*
* Use this as an alternative to getting the state ID from $entity->original.
*
* @return string
* The original state ID.
*/
public function getOriginalId();
/**
* Gets the current state ID.
*
* @return string
* The current state ID.
*/
public function getId();
/**
* Gets the label of the current state.
*
* @return string
* The label of the current state.
*/
public function getLabel();
/**
* Gets the label of the original state.
*
* @return string
* The label of the original state.
*/
public function getOriginalLabel();
/**
* Gets the allowed transitions for the current state.
*
* @return \Drupal\state_machine\Plugin\Workflow\WorkflowTransition[]
* The allowed transitions, keyed by transition ID.
*/
public function getTransitions();
/**
* Gets whether the given transition is allowed.
*
* @param string $transition_id
* The transition ID.
*
* @return bool
* TRUE if the given transition is allowed, FALSE otherwise.
*/
public function isTransitionAllowed($transition_id);
/**
* Applies the given transition, changing the current state.
*
* @param \Drupal\state_machine\Plugin\Workflow\WorkflowTransition $transition
* The transition to apply.
*
* @throws \InvalidArgumentException
* Thrown when the transition is not allowed.
*/
public function applyTransition(WorkflowTransition $transition);
/**
* Applies a transition with the given ID, changing the current state.
*
* @param string $transition_id
* The transition ID.
*
* @throws \InvalidArgumentException
* Thrown when no matching transition was found.
*/
public function applyTransitionById($transition_id);
/**
* Gets whether the current state is valid.
*
* Drupal separates field validation into a separate step, allowing an
* invalid state to be set before validation is invoked. At that point
* validation has no access to the previous value, so it can't determine
* if the transition is allowed. Thus, the field item must track the state
* changes internally, and answer via this method if the current state is
* valid.
*
* @see \Drupal\state_machine\Plugin\Validation\Constraint\StateConstraintValidator
*
* @return bool
* TRUE if the current state is valid, FALSE otherwise.
*/
public function isValid();
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Drupal\state_machine\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* Ensures the validity of the specified state.
*
* The state must exist on the used workflow, and be in the allowed transitions.
*
* @Constraint(
* id = "State",
* label = @Translation("State", context = "Validation")
* )
*/
class StateConstraint extends Constraint {
/**
* The default violation message.
*
* @var string
*/
public $message = "The state '@state' is invalid.";
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Drupal\state_machine\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the State constraint.
*
* @see \Drupal\state_machine\Plugin\Field\FieldType\StateItemInterface::isValid()
*/
class StateConstraintValidator extends ConstraintValidator {
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint) {
if (!$value->isValid()) {
$this->context->addViolation($constraint->message, ['@state' => $value->value]);
}
}
}

View File

@@ -0,0 +1,186 @@
<?php
namespace Drupal\state_machine\Plugin\Workflow;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\state_machine\Guard\GuardFactoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines the class for workflows.
*/
class Workflow extends PluginBase implements WorkflowInterface, ContainerFactoryPluginInterface {
/**
* The guard factory.
*
* @var \Drupal\state_machine\Guard\GuardFactoryInterface
*/
protected $guardFactory;
/**
* The initialized states.
*
* @var \Drupal\state_machine\Plugin\Workflow\WorkflowState[]
*/
protected $states = [];
/**
* The initialized transitions.
*
* @var \Drupal\state_machine\Plugin\Workflow\WorkflowTransition[]
*/
protected $transitions = [];
/**
* Constructs a new Workflow object.
*
* @param array $configuration
* The plugin configuration.
* @param string $plugin_id
* The workflow plugin_id.
* @param mixed $plugin_definition
* The workflow plugin implementation definition.
* @param \Drupal\state_machine\Guard\GuardFactoryInterface $guard_factory
* The guard factory.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, GuardFactoryInterface $guard_factory) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->guardFactory = $guard_factory;
// Populate value objects for states and transitions.
foreach ($plugin_definition['states'] as $id => $state_definition) {
$this->states[$id] = new WorkflowState($id, $state_definition['label']);
}
foreach ($plugin_definition['transitions'] as $id => $transition_definition) {
$label = $transition_definition['label'];
$from_states = [];
foreach ($transition_definition['from'] as $from_state) {
$from_states[$from_state] = $this->states[$from_state];
}
$to_state = $this->states[$transition_definition['to']];
$this->transitions[$id] = new WorkflowTransition($id, $label, $from_states, $to_state);
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('state_machine.guard_factory')
);
}
/**
* {@inheritdoc}
*/
public function getId() {
return $this->pluginDefinition['id'];
}
/**
* {@inheritdoc}
*/
public function getLabel() {
return $this->pluginDefinition['label'];
}
/**
* {@inheritdoc}
*/
public function getGroup() {
return $this->pluginDefinition['group'];
}
/**
* {@inheritdoc}
*/
public function getStates() {
return $this->states;
}
/**
* {@inheritdoc}
*/
public function getState($id) {
return $this->states[$id] ?? NULL;
}
/**
* {@inheritdoc}
*/
public function getTransitions() {
return $this->transitions;
}
/**
* {@inheritdoc}
*/
public function getTransition($id) {
return $this->transitions[$id] ?? NULL;
}
/**
* {@inheritdoc}
*/
public function getPossibleTransitions($state_id) {
if (empty($state_id)) {
return $this->transitions;
}
$possible_transitions = [];
foreach ($this->transitions as $id => $transition) {
if (array_key_exists($state_id, $transition->getFromStates())) {
$possible_transitions[$id] = $transition;
}
}
return $possible_transitions;
}
/**
* {@inheritdoc}
*/
public function getAllowedTransitions($state_id, EntityInterface $entity) {
$allowed_transitions = [];
foreach ($this->getPossibleTransitions($state_id) as $transition_id => $transition) {
if ($this->isTransitionAllowed($transition, $entity)) {
$allowed_transitions[$transition_id] = $transition;
}
}
return $allowed_transitions;
}
/**
* {@inheritdoc}
*/
public function findTransition($from_state_id, $to_state_id) {
foreach ($this->getPossibleTransitions($from_state_id) as $transition) {
if ($transition->getToState()->getId() == $to_state_id) {
return $transition;
}
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function isTransitionAllowed(WorkflowTransition $transition, EntityInterface $entity) {
foreach ($this->guardFactory->get($this->getGroup()) as $guard) {
if ($guard->allowed($transition, $this, $entity) === FALSE) {
return FALSE;
}
}
return TRUE;
}
}

View File

@@ -0,0 +1,129 @@
<?php
namespace Drupal\state_machine\Plugin\Workflow;
use Drupal\Core\Entity\EntityInterface;
/**
* Defines the interface for workflows.
*/
interface WorkflowInterface {
/**
* Gets the workflow ID.
*
* @return string
* The workflow ID.
*/
public function getId();
/**
* Gets the translated label.
*
* @return string
* The translated label.
*/
public function getLabel();
/**
* Gets the workflow group.
*
* @return string
* The workflow group.
*/
public function getGroup();
/**
* Gets the workflow states.
*
* @return \Drupal\state_machine\Plugin\Workflow\WorkflowState[]
* The states, keyed by state ID.
*/
public function getStates();
/**
* Gets a workflow state with the given ID.
*
* @param string $id
* The state ID.
*
* @return \Drupal\state_machine\Plugin\Workflow\WorkflowState|null
* The requested state, or NULL if not found.
*/
public function getState($id);
/**
* Gets the workflow transitions.
*
* @return \Drupal\state_machine\Plugin\Workflow\WorkflowTransition[]
* The transitions, keyed by transition ID.
*/
public function getTransitions();
/**
* Gets a workflow transition with the given ID.
*
* @param string $id
* The transition ID.
*
* @return \Drupal\state_machine\Plugin\Workflow\WorkflowTransition|null
* The requested transition, or NULL if not found.
*/
public function getTransition($id);
/**
* Gets the possible workflow transitions for the given state ID.
*
* Note that a possible transition might not be allowed (because of a guard
* returning false).
*
* @param string $state_id
* The state ID.
*
* @return \Drupal\state_machine\Plugin\Workflow\WorkflowTransition[]
* The possible transitions, keyed by transition ID.
*/
public function getPossibleTransitions($state_id);
/**
* Gets the allowed workflow transitions for the given state ID.
*
* @param string $state_id
* The state ID.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The parent entity.
*
* @return \Drupal\state_machine\Plugin\Workflow\WorkflowTransition[]
* The allowed transitions, keyed by transition ID.
*/
public function getAllowedTransitions($state_id, EntityInterface $entity);
/**
* Finds the workflow transition for moving between two given states.
*
* @param string $from_state_id
* The ID of the "from" state.
* @param string $to_state_id
* The ID of the "to" state.
*
* @return \Drupal\state_machine\Plugin\Workflow\WorkflowTransition|null
* The transition, or NULL if not found.
*/
public function findTransition($from_state_id, $to_state_id);
/**
* Gets whether the given transition is allowed by the transition guards.
*
* Note that this method assumes the given transition is "possible".
*
* @param \Drupal\state_machine\Plugin\Workflow\WorkflowTransition $transition
* The transition.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The parent entity.
*
* @return bool
* TRUE if the transition is allowed, FALSE otherwise.
*/
public function isTransitionAllowed(WorkflowTransition $transition, EntityInterface $entity);
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Drupal\state_machine\Plugin\Workflow;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Defines the class for workflow states.
*/
class WorkflowState {
use StringTranslationTrait;
/**
* The state ID.
*
* @var string
*/
protected $id;
/**
* The state label.
*
* @var string
*/
protected $label;
/**
* Constructs a new WorkflowState object.
*
* @param string $id
* The state ID.
* @param string $label
* The state label.
*/
public function __construct($id, $label) {
$this->id = $id;
$this->label = $label;
}
/**
* Gets the ID.
*
* @return string
* The ID.
*/
public function getId() {
return $this->id;
}
/**
* Gets the translated label.
*
* @return string
* The translated label.
*/
public function getLabel() {
return (string) $this->t($this->label, [], ['context' => 'workflow state']);
}
/**
* Gets the string representation of the workflow state.
*
* @return string
* The string representation of the workflow state.
*/
public function __toString() {
return $this->getLabel();
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace Drupal\state_machine\Plugin\Workflow;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Defines the class for workflow transitions.
*/
class WorkflowTransition {
use StringTranslationTrait;
/**
* The transition ID.
*
* @var string
*/
protected $id;
/**
* The transition label.
*
* @var string
*/
protected $label;
/**
* The "from" states.
*
* @var \Drupal\state_machine\Plugin\Workflow\WorkflowState[]
*/
protected $fromStates;
/**
* The "to" state.
*
* @var \Drupal\state_machine\Plugin\Workflow\WorkflowState
*/
protected $toState;
/**
* Constructs a new WorkflowTransition object.
*
* @param string $id
* The transition ID.
* @param string $label
* The transition label.
* @param \Drupal\state_machine\Plugin\Workflow\WorkflowState[] $from_states
* The "from" states.
* @param \Drupal\state_machine\Plugin\Workflow\WorkflowState $to_state
* The "to" state.
*/
public function __construct($id, $label, array $from_states, WorkflowState $to_state) {
$this->id = $id;
$this->label = $label;
$this->fromStates = $from_states;
$this->toState = $to_state;
}
/**
* Gets the ID.
*
* @return string
* The ID.
*/
public function getId() {
return $this->id;
}
/**
* Gets the translated label.
*
* @return string
* The translated label.
*/
public function getLabel() {
return (string) $this->t($this->label, [], ['context' => 'workflow transition']);
}
/**
* Gets the "from" states.
*
* @return \Drupal\state_machine\Plugin\Workflow\WorkflowState[]
* The "from" states.
*/
public function getFromStates() {
return $this->fromStates;
}
/**
* Gets the "to" state.
*
* @return \Drupal\state_machine\Plugin\Workflow\WorkflowState
* The "to" state.
*/
public function getToState() {
return $this->toState;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Drupal\state_machine\Plugin\WorkflowGroup;
use Drupal\Core\Plugin\PluginBase;
/**
* Defines the class for workflow groups.
*/
class WorkflowGroup extends PluginBase implements WorkflowGroupInterface {
/**
* {@inheritdoc}
*/
public function getId() {
return $this->pluginDefinition['id'];
}
/**
* {@inheritdoc}
*/
public function getLabel() {
return $this->pluginDefinition['label'];
}
/**
* {@inheritdoc}
*/
public function getEntityTypeId() {
return $this->pluginDefinition['entity_type'];
}
/**
* {@inheritdoc}
*/
public function getWorkflowClass() {
return $this->pluginDefinition['workflow_class'];
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Drupal\state_machine\Plugin\WorkflowGroup;
/**
* Defines the interface for workflow groups.
*/
interface WorkflowGroupInterface {
/**
* Gets the workflow group ID.
*
* @return string
* The workflow group ID.
*/
public function getId();
/**
* Gets the translated label.
*
* @return string
* The translated label.
*/
public function getLabel();
/**
* Gets the entity type ID.
*
* For example, "node" if all workflows in the group are used on content.
*
* @return string
* The entity type ID.
*/
public function getEntityTypeId();
/**
* Gets the workflow class.
*
* By default all workflows use the same class. A group can choose to
* override the class for its workflows, to satisfy advanced use cases.
*
* @return string
* The workflow class.
*/
public function getWorkflowClass();
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Drupal\state_machine\Plugin\diff\Field;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\diff\Plugin\diff\Field\CoreFieldBuilder;
/**
* Plugin to compare state fields.
*
* @FieldDiffBuilder(
* id = "state_diff_builder",
* label = @Translation("State Field Diff"),
* field_types = {
* "state"
* },
* )
*/
class StateFieldBuilder extends CoreFieldBuilder {
/**
* {@inheritdoc}
*/
public function build(FieldItemListInterface $field_items) {
$result = [];
foreach ($field_items as $field_key => $field_item) {
if (!$field_item->isEmpty()) {
$value = $field_item->view(['label' => 'hidden', 'type' => 'default']);
$rendered_value = $this->renderer->renderPlain($value);
$result[$field_key][] = $rendered_value;
}
}
return $result;
}
}

View File

@@ -0,0 +1,208 @@
<?php
namespace Drupal\state_machine\Plugin\views\filter;
use Drupal\Component\Utility\SortArray;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\views\Plugin\views\filter\InOperator;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Filter by workflow state.
*
* @ingroup views_filter_handlers
*
* @ViewsFilter("state_machine_state")
*/
class State extends InOperator {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* Constructs a new State object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
$this->entityFieldManager = $entity_field_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('entity_field.manager')
);
}
/**
* {@inheritdoc}
*/
public function getValueOptions() {
if (!isset($this->valueOptions)) {
$entity_type_id = $this->getEntityType();
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
$field_name = $this->getFieldName();
$workflows = $this->getWorkflows($entity_type, $field_name);
// Merge the states of all workflows into one list, preserving their
// initial positions.
$states = [];
foreach ($workflows as $workflow) {
$weight = 0;
foreach ($workflow->getStates() as $state_id => $state) {
$states[$state_id] = [
'label' => $state->getLabel(),
'weight' => $weight,
];
$weight++;
}
}
uasort($states, [SortArray::class, 'sortByWeightElement']);
$this->valueOptions = array_map(function ($state) {
return $state['label'];
}, $states);
}
return $this->valueOptions;
}
/**
* Gets the name of the entity field on which this filter operates.
*
* @return string
* The field name.
*/
protected function getFieldName() {
if (isset($this->configuration['field_name'])) {
// Configurable field.
$field_name = $this->configuration['field_name'];
}
else {
// Base field.
$field_name = $this->configuration['entity field'];
}
return $field_name;
}
/**
* Gets the workflows used the current entity field.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The current entity type.
* @param string $field_name
* The current field name.
*
* @return \Drupal\state_machine\Plugin\Workflow\WorkflowInterface[]
* The workflows.
*/
protected function getWorkflows(EntityTypeInterface $entity_type, $field_name) {
// Only the StateItem knows which workflow it's using. This requires us
// to create an entity for each bundle in order to get the state field.
$storage = $this->entityTypeManager->getStorage($entity_type->id());
$bundles = $this->getBundles($entity_type, $field_name);
$workflows = [];
foreach ($bundles as $bundle) {
$values = [];
if ($bundle_key = $entity_type->getKey('bundle')) {
$values[$bundle_key] = $bundle;
}
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $storage->create($values);
if ($entity->hasField($field_name)) {
$workflow = $entity->get($field_name)->first()->getWorkflow();
$workflows[$workflow->getId()] = $workflow;
}
}
return $workflows;
}
/**
* Gets the bundles for the current entity field.
*
* If the view has a non-exposed bundle filter, the bundles are taken from
* there. Otherwise, the field's bundles are used.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The current entity type.
* @param string $field_name
* The current field name.
*
* @return string[]
* The bundles.
*/
protected function getBundles(EntityTypeInterface $entity_type, $field_name) {
$bundles = [];
$bundle_key = $entity_type->getKey('bundle');
if ($bundle_key) {
// Get any bundle filters for this entity type and bundle.
// It is unlikely, but there could be multiple.
/** @var \Drupal\views\Plugin\views\filter\FilterPluginBase[] $bundle_filters */
$bundle_filters = array_filter($this->view->filter ?? [], static function ($filter) use ($entity_type, $bundle_key) {
return $filter->getEntityType() === $entity_type->id() && $filter->realField === $bundle_key;
});
foreach ($bundle_filters as $bundle_filter) {
if ($bundle_filter->isExposed()) {
// If any bundle filters are exposed,
// we cannot return a subset of bundles.
$bundles = [];
break;
}
switch ($bundle_filter->operator) {
case 'in':
$bundles = array_merge($bundles, $bundle_filter->value);
break;
case 'not in':
$bundles = array_diff($bundles, $bundle_filter->value);
break;
}
}
// Remove the "all" option, if present.
if (array_key_exists('all', $bundles)) {
unset($bundles['all']);
}
}
// Fallback to the list of bundles the field is attached to.
if (empty($bundles)) {
$map = $this->entityFieldManager->getFieldMap();
$bundles = $map[$entity_type->id()][$field_name]['bundles'];
}
return $bundles;
}
}