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,63 @@
<?php
namespace Drupal\state_machine\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines an access checker for the state transition confirmation form.
*/
class StateTransitionAccessCheck implements AccessInterface {
/**
* Checks access to the state transition confirmation form.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(RouteMatchInterface $route_match, AccountInterface $account) {
// Get the entity type from the route name.
// The entity route name is 'entity.{entity_type}.state_transition_form'.
$parts = explode('.', $route_match->getRouteName());
$entity_type = $parts[1];
$parameters = $route_match->getParameters();
// Check if one of the required parameter is missing.
foreach ([$entity_type, 'field_name', 'transition_id'] as $required_parameter) {
if (!$parameters->has($required_parameter)) {
return AccessResult::neutral();
}
}
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $route_match->getParameter($entity_type);
$field_name = $route_match->getParameter('field_name');
// Ensures the passed entity has a state field matching the field name
// passed in the url.
if (!$entity || !$entity->hasField($field_name)) {
return AccessResult::forbidden();
}
/** @var \Drupal\state_machine\Plugin\Field\FieldType\StateItemInterface $state_item */
$state_item = $entity->get($field_name)->first();
$allowed_transitions = array_keys($state_item->getTransitions());
// Now check if the requested transition is allowed.
$requested_transition = $route_match->getParameter('transition_id');
if (!in_array($requested_transition, $allowed_transitions, TRUE)) {
return AccessResult::forbidden()->addCacheableDependency($entity);
}
// Now finally check that the current user can update the given entity.
return $entity->access('update', $account, TRUE);
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Drupal\state_machine\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Adds the context provider service IDs to the context manager.
*/
class GuardsPass implements CompilerPassInterface {
/**
* {@inheritdoc}
*
* Passes the grouped service IDs of guards to the guard factory.
*/
public function process(ContainerBuilder $container) {
$guards = [];
$priorities = [];
foreach ($container->findTaggedServiceIds('state_machine.guard') as $id => $attributes) {
if (empty($attributes[0]['group'])) {
// Guards without a specified group should be invoked for all of them.
$attributes[0]['group'] = '_generic';
}
$group_id = $attributes[0]['group'];
$guards[$group_id][$id] = $id;
$priorities[$group_id][$id] = $attributes[0]['priority'] ?? 0;
}
// Sort the guards by priority.
foreach ($priorities as $group_id => $services) {
array_multisort($priorities[$group_id], SORT_DESC, $guards[$group_id]);
}
$definition = $container->getDefinition('state_machine.guard_factory');
$definition->addArgument($guards);
}
}

View File

@@ -0,0 +1,143 @@
<?php
namespace Drupal\state_machine\Event;
use Drupal\Component\EventDispatcher\Event;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\state_machine\Plugin\Workflow\WorkflowInterface;
use Drupal\state_machine\Plugin\Workflow\WorkflowTransition;
/**
* Defines the workflow transition event.
*/
class WorkflowTransitionEvent extends Event {
/**
* The transition.
*
* @var \Drupal\state_machine\Plugin\Workflow\WorkflowTransition
*/
protected $transition;
/**
* The workflow.
*
* @var \Drupal\state_machine\Plugin\Workflow\WorkflowInterface
*/
protected $workflow;
/**
* The entity.
*
* @var \Drupal\Core\Entity\ContentEntityInterface
*/
protected $entity;
/**
* The state field name.
*
* @var string
*/
protected $fieldName;
/**
* Constructs a new WorkflowTransitionEvent object.
*
* @param \Drupal\state_machine\Plugin\Workflow\WorkflowTransition $transition
* The transition.
* @param \Drupal\state_machine\Plugin\Workflow\WorkflowInterface $workflow
* The workflow.
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity.
* @param string $field_name
* The state field name.
*/
public function __construct(WorkflowTransition $transition, WorkflowInterface $workflow, ContentEntityInterface $entity, $field_name) {
$this->transition = $transition;
$this->workflow = $workflow;
$this->entity = $entity;
$this->fieldName = $field_name;
}
/**
* Gets the transition.
*
* @return \Drupal\state_machine\Plugin\Workflow\WorkflowTransition
* The transition.
*/
public function getTransition() {
return $this->transition;
}
/**
* Gets the workflow.
*
* @return \Drupal\state_machine\Plugin\Workflow\WorkflowInterface
* The workflow.
*/
public function getWorkflow() {
return $this->workflow;
}
/**
* Gets the entity.
*
* @return \Drupal\Core\Entity\ContentEntityInterface
* The entity.
*/
public function getEntity() {
return $this->entity;
}
/**
* Gets the state field name.
*
* @return string
* The state field name.
*/
public function getFieldName() {
return $this->fieldName;
}
/**
* Gets the state field.
*
* @return \Drupal\state_machine\Plugin\Field\FieldType\StateItemInterface
* The state field.
*/
public function getField() {
/** @var \Drupal\state_machine\Plugin\Field\FieldType\StateItemInterface $field */
$field = $this->entity->get($this->fieldName)->first();
return $field;
}
/**
* Gets the "from" state.
*
* @deprecated in state_machine:8.x-1.0-rc1 and is removed from state_machine:8.x-2.0.
* Use $this->getField()->getOriginalId() instead.
* @see https://www.drupal.org/node/2982709
*
* @return \Drupal\state_machine\Plugin\Workflow\WorkflowState
* The "from" state.
*/
public function getFromState() {
$original_id = $this->getField()->getOriginalId();
return $this->workflow->getState($original_id);
}
/**
* Gets the "to" state.
*
* @deprecated in state_machine:8.x-1.0-rc1 and is removed from state_machine:8.x-2.0.
* Use $this->getTransition->getToState() instead.
* @see https://www.drupal.org/node/2982709
*
* @return \Drupal\state_machine\Plugin\Workflow\WorkflowState
* The "to" state.
*/
public function getToState() {
return $this->transition->getToState();
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace Drupal\state_machine\Form;
use Drupal\Core\Entity\ContentEntityConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a confirmation form for performing an entity state transition.
*/
class StateTransitionConfirmForm extends ContentEntityConfirmFormBase {
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The field name.
*
* @var string
*/
protected $fieldName;
/**
* The transition.
*
* @var \Drupal\state_machine\Plugin\Workflow\WorkflowTransition
*/
protected $transition;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$instance = parent::create($container);
$instance->renderer = $container->get('renderer');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getBaseFormId() {
return 'state_machine_transition_confirm_form';
}
/**
* Returns the transition object.
*
* @return \Drupal\state_machine\Plugin\Workflow\WorkflowTransition
* The transition object.
*/
public function getTransition() {
return $this->transition;
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $field_name = '', $transition_id = '') {
/** @var \Drupal\state_machine\Plugin\Field\FieldType\StateItemInterface $state_item */
$state_item = $this->entity->get($field_name)->first();
$transition = $state_item->getWorkflow()->getTransition($transition_id);
$this->fieldName = $field_name;
$this->transition = $transition;
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function getDescription() {
/** @var \Drupal\state_machine\Plugin\Field\FieldType\StateItemInterface $state_item */
$state_item = $this->entity->get($this->fieldName)->first();
$items = [
$this->t('<b>Transition</b>: @transition_label', ['@transition_label' => $this->transition->getLabel()]),
$this->t('<b>@entity_type</b>: @entity_label', ['@entity_type' => $this->entity->getEntityType()->getLabel(), '@entity_label' => $this->entity->label()]),
$this->t('<b>From</b>: @from_state', ['@from_state' => $state_item->getOriginalLabel()]),
$this->t('<b>To</b>: @to_state', ['@to_state' => $this->transition->getToState()]),
];
$description = [
'items' => [
'#type' => 'html_tag',
'#value' => implode('<br/>', $items),
'#tag' => 'p',
],
'warning' => [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => parent::getDescription(),
],
];
return $this->renderer->renderPlain($description);
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to apply this transition?');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->entity->toUrl('canonical');
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
/** @var \Drupal\state_machine\Plugin\Field\FieldType\StateItemInterface $state_item */
$state_item = $this->entity->get($this->fieldName)->first();
if ($state_item->isTransitionAllowed($this->transition->getId())) {
$state_item->applyTransition($this->transition);
$this->entity->save();
}
$form_state->setRedirectUrl($this->getCancelUrl());
}
}

View File

@@ -0,0 +1,182 @@
<?php
namespace Drupal\state_machine\Form;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
class StateTransitionForm extends FormBase implements StateTransitionFormInterface {
/**
* The redirect destination.
*
* @var \Drupal\Core\Routing\RedirectDestinationInterface
*/
protected $redirectDestination;
/**
* The entity repository.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/**
* The entity.
*
* @var \Drupal\Core\Entity\ContentEntityInterface
*/
protected $entity;
/**
* The state field name.
*
* @var string
*/
protected $fieldName;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$instance = parent::create($container);
$instance->redirectDestination = $container->get('redirect.destination');
$instance->entityRepository = $container->get('entity.repository');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getEntity() {
return $this->entity;
}
/**
* {@inheritdoc}
*/
public function setEntity(ContentEntityInterface $entity) {
$this->entity = $this->entityRepository->getTranslationFromContext($entity);
return $this;
}
/**
* {@inheritdoc}
*/
public function getFieldName() {
return $this->fieldName;
}
/**
* {@inheritdoc}
*/
public function setFieldName($field_name) {
$this->fieldName = $field_name;
return $this;
}
/**
* {@inheritdoc}
*/
public function getBaseFormId() {
return 'state_machine_transition_form';
}
/**
* {@inheritdoc}
*/
public function getFormId() {
$entity = $this->getEntity();
if (!$entity) {
throw new \RuntimeException('No entity provided to StateTransitionForm.');
}
// Example ID: "state_machine_transition_form_commerce_order_state_1".
$form_id = $this->getBaseFormId();
$form_id .= '_' . $entity->getEntityTypeId() . '_' . $this->fieldName;
$form_id .= '_' . $entity->id();
return $form_id;
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
/** @var \Drupal\state_machine\Plugin\Field\FieldType\StateItemInterface $state_item */
$state_item = $this->entity->get($this->fieldName)->first();
if (!isset($state_item)) {
return;
}
$form['actions'] = [
'#type' => 'container',
];
// Determine whether we should output links to the confirmation form,
// or submit buttons.
$require_confirmation = $form_state->get('require_confirmation');
foreach ($state_item->getTransitions() as $transition_id => $transition) {
if (!$require_confirmation) {
$form['actions'][$transition_id] = [
'#type' => 'submit',
'#value' => $transition->getLabel(),
'#submit' => ['::submitForm'],
'#transition' => $transition,
];
continue;
}
$url = $this->entity->toUrl('state-transition-form');
$route_parameters = $url->getRouteParameters() + [
$this->entity->getEntityTypeId() => $this->entity->id(),
'field_name' => $this->fieldName,
'transition_id' => $transition_id,
];
$form['actions'][$transition_id] = [
'#type' => 'link',
'#title' => $transition->getLabel(),
'#url' => Url::fromRoute("entity.{$this->entity->getEntityTypeId()}.state_transition_form", $route_parameters, [
'query' => $this->redirectDestination->getAsArray(),
]),
'#attributes' => [
'class' => [
'button',
],
],
];
if ($form_state->get('use_modal')) {
$form['actions'][$transition_id]['#attributes']['class'][] = 'use-ajax';
$form['actions'][$transition_id]['#attributes']['data-dialog-type'] = 'modal';
$form['actions'][$transition_id]['#attributes']['data-dialog-options'] = Json::encode([
'width' => 'auto',
]);
$form['#attached']['library'][] = 'core/drupal.dialog.ajax';
}
}
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
/** @var \Drupal\state_machine\Plugin\Workflow\WorkflowTransition $transition */
$transition = $triggering_element['#transition'];
/** @var \Drupal\state_machine\Plugin\Field\FieldType\StateItemInterface $state_item */
$state_item = $this->entity->get($this->fieldName)->first();
// Ensure the transition is still allowed before applying it.
if ($state_item->isTransitionAllowed($transition->getId())) {
$state_item->applyTransition($triggering_element['#transition']);
$this->entity->save();
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Drupal\state_machine\Form;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Form\BaseFormIdInterface;
/**
* Defines the interface for state transition forms.
*
* Used for applying a transition to the form entity's state field.
*/
interface StateTransitionFormInterface extends BaseFormIdInterface {
/**
* Gets the form entity.
*
* @return \Drupal\Core\Entity\EntityInterface
* The form entity.
*/
public function getEntity();
/**
* Sets the form entity.
*
* When the form is submitted, a transition will be applied to the entity,
* and the entity will be saved.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The form entity.
*
* @return $this
*/
public function setEntity(ContentEntityInterface $entity);
/**
* Gets the state field name.
*
* @return string
* The state field name.
*/
public function getFieldName();
/**
* Sets the state field name.
*
* @param string $field_name
* The state field name.
*
* @return $this
*/
public function setFieldName($field_name);
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Drupal\state_machine\Guard;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Default implementation of the guard factory.
*/
class GuardFactory implements GuardFactoryInterface {
/**
* The service container.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
/**
* The guard service ids, grouped by workflow group ID.
*
* @var string[]
*/
protected $guardServiceIds;
/**
* Constructs a new GuardFactory object.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The service container.
* @param string[] $guard_service_ids
* The guard service IDs, grouped by workflow group ID.
*/
public function __construct(ContainerInterface $container, array $guard_service_ids) {
$this->container = $container;
$this->guardServiceIds = $guard_service_ids;
}
/**
* {@inheritdoc}
*/
public function get($group_id) {
$service_ids = [];
if (isset($this->guardServiceIds[$group_id])) {
$service_ids = array_merge($service_ids, $this->guardServiceIds[$group_id]);
}
if (isset($this->guardServiceIds['_generic'])) {
$service_ids = array_merge($service_ids, $this->guardServiceIds['_generic']);
}
$guards = [];
foreach ($service_ids as $service_id) {
$guards[] = $this->container->get($service_id);
}
return $guards;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Drupal\state_machine\Guard;
/**
* Defines the interface for guard factories.
*/
interface GuardFactoryInterface {
/**
* Gets the instantiated guards for the given group ID.
*
* @param string $group_id
* The group ID.
*
* @return \Drupal\state_machine\Guard\GuardInterface[]
* The instantiated guards.
*/
public function get($group_id);
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Drupal\state_machine\Guard;
use Drupal\Core\Entity\EntityInterface;
use Drupal\state_machine\Plugin\Workflow\WorkflowInterface;
use Drupal\state_machine\Plugin\Workflow\WorkflowTransition;
/**
* Defines the interface for guards.
*
* Allows for custom logic controlling the availability of specific transitions.
* Transitions could be restricted based on the current user's permissions, a
* parent entity field, etc.
*
* By default, a transition is allowed unless at least one guard returns FALSE.
*/
interface GuardInterface {
/**
* Checks whether the given transition is allowed.
*
* @param \Drupal\state_machine\Plugin\Workflow\WorkflowTransition $transition
* The transition.
* @param \Drupal\state_machine\Plugin\Workflow\WorkflowInterface $workflow
* The workflow.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The parent entity.
*
* @return bool
* TRUE if the transition is allowed, FALSE otherwise.
*/
public function allowed(WorkflowTransition $transition, WorkflowInterface $workflow, EntityInterface $entity);
}

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;
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Drupal\state_machine\Routing;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Subscriber for state machine transition routes on entities.
*/
class RouteSubscriber extends RouteSubscriberBase {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new RouteSubscriber object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_manager) {
$this->entityTypeManager = $entity_manager;
}
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
if (!$entity_type->hasLinkTemplate('state-transition-form')) {
continue;
}
$route = new Route($entity_type->getLinkTemplate('state-transition-form'));
$route
->setDefaults([
'_entity_form' => "$entity_type_id.state-transition-confirm",
])
->setRequirement('_state_transition_access', "TRUE")
->setRequirement($entity_type_id, '\d+')
->setRequirement('transition_id', '[a-z0-9_]+')
->setRequirement('field_name', '[a-z0-9_]+')
->setOption('parameters', [
$entity_type_id => ['type' => 'entity:' . $entity_type_id],
]);
$collection->add("entity.$entity_type_id.state_transition_form", $route);
}
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Drupal\state_machine;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
use Drupal\state_machine\DependencyInjection\Compiler\GuardsPass;
/**
* Registers the guard compiler pass.
*/
class StateMachineServiceProvider implements ServiceProviderInterface {
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container) {
$container->addCompilerPass(new GuardsPass());
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Drupal\state_machine;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
use Drupal\Core\Plugin\Discovery\YamlDiscovery;
use Drupal\state_machine\Plugin\Workflow\Workflow;
use Drupal\state_machine\Plugin\WorkflowGroup\WorkflowGroup;
/**
* Manages discovery and instantiation of workflow_group plugins.
*
* @see \Drupal\state_machine\Plugin\WorkflowGroup\WorkflowGroupInterface
* @see plugin_api
*/
class WorkflowGroupManager extends DefaultPluginManager implements WorkflowGroupManagerInterface {
/**
* Default values for each workflow_group plugin.
*
* @var array
*/
protected $defaults = [
'id' => '',
'label' => '',
'entity_type' => '',
'class' => WorkflowGroup::class,
// Groups can override the default workflow class for advanced use cases.
'workflow_class' => Workflow::class,
];
/**
* Constructs a new WorkflowGroupManager object.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* The cache backend.
*/
public function __construct(ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend) {
$this->moduleHandler = $module_handler;
$this->setCacheBackend($cache_backend, 'workflow_group', ['workflow_group']);
$this->alterInfo('workflow_groups');
}
/**
* {@inheritdoc}
*/
protected function getDiscovery() {
if (!isset($this->discovery)) {
$this->discovery = new YamlDiscovery('workflow_groups', $this->moduleHandler->getModuleDirectories());
$this->discovery->addTranslatableProperty('label', 'label_context');
$this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery);
}
return $this->discovery;
}
/**
* {@inheritdoc}
*/
public function processDefinition(&$definition, $plugin_id) {
parent::processDefinition($definition, $plugin_id);
if ($plugin_id == 'state_machine') {
throw new PluginException('The "state_machine" workflow_group ID is reserved and must not be used.');
}
$definition['id'] = $plugin_id;
foreach (['label', 'entity_type'] as $required_property) {
if (empty($definition[$required_property])) {
throw new PluginException(sprintf('The workflow_group %s must define the %s property.', $plugin_id, $required_property));
}
}
}
/**
* {@inheritdoc}
*/
public function getDefinitionsByEntityType($entity_type_id = NULL) {
$definitions = $this->getDefinitions();
if ($entity_type_id) {
$definitions = array_filter($definitions, function ($definition) use ($entity_type_id) {
return $definition['entity_type'] == $entity_type_id;
});
}
return $definitions;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Drupal\state_machine;
use Drupal\Component\Plugin\PluginManagerInterface;
/**
* Defines the interface for workflow_group plugin managers.
*/
interface WorkflowGroupManagerInterface extends PluginManagerInterface {
/**
* Gets the definitions filtered by entity type.
*
* @param string $entity_type_id
* The entity type ID.
*
* @return array
* The definitions.
*/
public function getDefinitionsByEntityType($entity_type_id = NULL);
}

View File

@@ -0,0 +1,193 @@
<?php
namespace Drupal\state_machine;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
use Drupal\Core\Plugin\Discovery\YamlDiscovery;
/**
* Manages discovery and instantiation of workflow plugins.
*
* @see \Drupal\state_machine\Plugin\Workflow\WorkflowInterface
* @see plugin_api
*/
class WorkflowManager extends DefaultPluginManager implements WorkflowManagerInterface {
/**
* The workflow group manager.
*
* @var \Drupal\state_machine\WorkflowGroupManagerInterface
*/
protected $groupManager;
/**
* A cache of loaded workflows, keyed by workflow ID.
*
* @var \Drupal\state_machine\Plugin\Workflow\WorkflowInterface[]
*/
protected $plugins;
/**
* Default values for each workflow plugin.
*
* @var array
*/
protected $defaults = [
'id' => '',
'label' => '',
'group' => '',
'states' => [],
'transitions' => [],
];
/**
* Constructs a new WorkflowManager object.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* The cache backend.
* @param \Drupal\state_machine\WorkflowGroupManagerInterface $group_manager
* The workflow group manager.
*/
public function __construct(ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, WorkflowGroupManagerInterface $group_manager) {
$this->moduleHandler = $module_handler;
$this->setCacheBackend($cache_backend, 'workflow', ['workflow']);
$this->groupManager = $group_manager;
$this->alterInfo('workflows');
}
/**
* {@inheritdoc}
*/
protected function getDiscovery() {
if (!isset($this->discovery)) {
$this->discovery = new YamlDiscovery('workflows', $this->moduleHandler->getModuleDirectories());
$this->discovery->addTranslatableProperty('label', 'label_context');
$this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery);
}
return $this->discovery;
}
/**
* {@inheritdoc}
*/
public function createInstance($plugin_id, array $configuration = []) {
if (empty($this->plugins[$plugin_id])) {
$plugin_definition = $this->getDefinition($plugin_id);
if (empty($plugin_definition['group'])) {
throw new PluginException(sprintf('The workflow %s must define the group property.', $plugin_id));
}
$group_definition = $this->groupManager->getDefinition($plugin_definition['group']);
$plugin_class = $group_definition['workflow_class'];
if (is_subclass_of($plugin_class, ContainerFactoryPluginInterface::class)) {
$this->plugins[$plugin_id] = $plugin_class::create(\Drupal::getContainer(), $configuration, $plugin_id, $plugin_definition);
}
else {
$this->plugins[$plugin_id] = new $plugin_class($configuration, $plugin_id, $plugin_definition);
}
}
return $this->plugins[$plugin_id];
}
/**
* {@inheritdoc}
*/
public function processDefinition(&$definition, $plugin_id) {
parent::processDefinition($definition, $plugin_id);
$definition['id'] = $plugin_id;
foreach (['label', 'group', 'states', 'transitions'] as $required_property) {
if (empty($definition[$required_property])) {
throw new PluginException(sprintf('The workflow %s must define the %s property.', $plugin_id, $required_property));
}
}
foreach ($definition['states'] as $state_id => $state_definition) {
if (empty($state_definition['label'])) {
throw new PluginException(sprintf('The workflow state %s must define the label property.', $state_id));
}
}
foreach ($definition['transitions'] as $transition_id => $transition_definition) {
foreach (['label', 'from', 'to'] as $required_property) {
if (empty($transition_definition[$required_property])) {
throw new PluginException(sprintf('The workflow transition %s must define the %s property.', $transition_id, $required_property));
}
}
// Validate the referenced "from" and "to" states.
foreach ($transition_definition['from'] as $from_state) {
if (!isset($definition['states'][$from_state])) {
throw new PluginException(sprintf('The workflow transition %s specified an invalid "from" property: %s.', $transition_id, $from_state));
}
}
$to_state = $transition_definition['to'];
if (!isset($definition['states'][$to_state])) {
throw new PluginException(sprintf('The workflow transition %s specified an invalid "to" property.', $transition_id));
}
}
}
/**
* {@inheritdoc}
*/
public function getGroupedLabels($entity_type_id = NULL) {
$definitions = $this->getSortedDefinitions();
$group_labels = $this->getGroupLabels($entity_type_id);
$grouped_definitions = [];
foreach ($definitions as $id => $definition) {
$group_id = $definition['group'];
if (!isset($group_labels[$group_id])) {
// Don't return workflows for groups ignored due to their entity type.
continue;
}
$group_label = $group_labels[$group_id];
$grouped_definitions[$group_label][$id] = $definition['label'];
}
return $grouped_definitions;
}
/**
* Gets the sorted workflow plugin definitions.
*
* @return array
* The workflow plugin definitions, sorted by group and label.
*/
protected function getSortedDefinitions() {
// Sort the plugins first by group, then by label.
$definitions = $this->getDefinitions();
uasort($definitions, function ($a, $b) {
if ($a['group'] != $b['group']) {
return strnatcasecmp($a['group'], $b['group']);
}
return strnatcasecmp($a['label'], $b['label']);
});
return $definitions;
}
/**
* Gets a list of group labels for the given entity type ID.
*
* @param string $entity_type_id
* The entity type ID.
*
* @return array
* A list of groups labels keyed by ID.
*/
protected function getGroupLabels($entity_type_id = NULL) {
$group_definitions = $this->groupManager->getDefinitionsByEntityType($entity_type_id);
$group_labels = array_map(function ($group_definition) {
return (string) $group_definition['label'];
}, $group_definitions);
natcasesort($group_labels);
return $group_labels;
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Drupal\state_machine;
use Drupal\Component\Plugin\PluginManagerInterface;
/**
* Defines the interface for workflow plugin managers.
*/
interface WorkflowManagerInterface extends PluginManagerInterface {
/**
* Gets the grouped workflow labels.
*
* @param string $entity_type_id
* (optional) The entity type id to filter by. If provided, only workflows
* that belong to groups with the specified entity type will be returned.
*
* @return array
* Keys are group labels, and values are arrays of which the keys are
* workflow IDs and the values are workflow labels.
*/
public function getGroupedLabels($entity_type_id = NULL);
}