Skip to content
Snippets Groups Projects
Commit 73cfdd9d authored by Bojan Živanović's avatar Bojan Živanović
Browse files

Improve the state validation to take the previous value into account.

parent 97467afe
No related branches found
No related tags found
1 merge request!26Draft: Automated Project Update Bot fixes
......@@ -7,8 +7,6 @@
namespace Drupal\commerce_workflow\Plugin\Field\FieldType;
use Drupal\Component\Utility\Random;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
......@@ -16,6 +14,7 @@ use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslationWrapper;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\TypedData\OptionsProviderInterface;
use Drupal\Core\Validation\Plugin\Validation\Constraint\AllowedValuesConstraint;
/**
* Plugin implementation of the 'state' field type.
......@@ -37,6 +36,13 @@ class StateItem extends FieldItemBase implements OptionsProviderInterface {
*/
protected static $workflows = [];
/**
* The initial value, used to validate state changes.
*
* @var string
*/
protected $initialValue;
/**
* {@inheritdoc}
*/
......@@ -62,6 +68,23 @@ class StateItem extends FieldItemBase implements OptionsProviderInterface {
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}
*/
......@@ -98,6 +121,8 @@ class StateItem extends FieldItemBase implements OptionsProviderInterface {
* {@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 === '';
}
......@@ -112,6 +137,32 @@ class StateItem extends FieldItemBase implements OptionsProviderInterface {
return $this;
}
/**
* {@inheritdoc}
*/
public function setValue($values, $notify = TRUE) {
if (empty($this->initialValue)) {
// Track the initial field value to allow isValid() to validate changes.
$this->initialValue = $values['value'];
}
parent::setValue($values, $notify);
}
/**
* {@inheritdoc}
*/
public function postSave($update) {
$this->initialValue = $this->value;
}
/**
* {@inheritdoc}
*/
public function isValid() {
$allowed_states = $this->getAllowedStates($this->initialValue);
return isset($allowed_states[$this->value]);
}
/**
* {@inheritdoc}
*/
......@@ -146,36 +197,49 @@ class StateItem extends FieldItemBase implements OptionsProviderInterface {
* {@inheritdoc}
*/
public function getSettableOptions(AccountInterface $account = NULL) {
// $this->value is unpopulated due to https://www.drupal.org/node/2629932
$field_name = $this->getFieldDefinition()->getName();
$value = $this->getEntity()->get($field_name)->value;
$allowed_states = $this->getAllowedStates($value);
$state_labels = array_map(function ($state) {
return $state->getLabel();
}, $allowed_states);
return $state_labels;
}
/**
* Gets the next allowed states for the current field value.
*
* @param string $value
* The field value, representing the state id.
*
* @return \Drupal\commerce_workflow\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 [];
}
$entity = $this->getEntity();
// $this->value is unpopulated due to https://www.drupal.org/node/2629932
$field_name = $this->getFieldDefinition()->getName();
$value = $entity->get($field_name)->value;
$state_labels = [
$allowed_states = [
// The current state is always allowed.
$value => $workflow->getState($value)->getLabel(),
$value => $workflow->getState($value),
];
$transitions = $workflow->getAllowedTransitions($value, $entity);
$transitions = $workflow->getAllowedTransitions($value, $this->getEntity());
foreach ($transitions as $transition) {
$state = $transition->getToState();
$state_labels[$state->getId()] = $state->getLabel();
$allowed_states[$state->getId()] = $state;
}
return $state_labels;
return $allowed_states;
}
/**
* Gets the workflow used by the current field.
*
* @return \Drupal\commerce_workflow\Plugin\Workflow\WorkflowInterface|false
* The workflow, or FALSE if unknown at this time.
* {@inheritdoc}
*/
protected function getWorkflow() {
public function getWorkflow() {
$field_definition = $this->getFieldDefinition();
$definition_id = spl_object_hash($field_definition);
if (!isset(static::$workflows[$definition_id])) {
......
<?php
/**
* @file
* Contains \Drupal\commerce_workflow\Plugin\Field\FieldType\StateItemInterface.
*/
namespace Drupal\commerce_workflow\Plugin\Field\FieldType;
use Drupal\Core\TypedData\OptionsProviderInterface;
/**
* Defines the interface for state item fields.
*/
interface StateItemInterface extends OptionsProviderInterface {
/**
* Gets the workflow used by the current field.
*
* @return \Drupal\commerce_workflow\Plugin\Workflow\WorkflowInterface|false
* The workflow, or FALSE if unknown at this time.
*/
public function getWorkflow();
/**
* 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\commerce_workflow\Plugin\Validation\Constraint\StateConstraintValidator
*
* @return bool
* TRUE if the current state is valid, FALSE otherwise.
*/
public function isValid();
}
<?php
/**
* @file
* Contains \Drupal\commerce_workflow\Plugin\Validation\Constraint\StateConstraint.
*/
namespace Drupal\commerce_workflow\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.";
}
<?php
/**
* @file
* Contains \Drupal\commerce_workflow\Plugin\Validation\Constraint\StateConstraintValidator.
*/
namespace Drupal\commerce_workflow\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the State constraint.
*
* @see \Drupal\commerce_workflow\Plugin\Field\FieldType\StateItemInterface::isValid()
*/
class StateConstraintValidator extends ConstraintValidator {
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint) {
if (!$value->getEntity()->isNew() && !$value->isValid()) {
$this->context->addViolation($constraint->message, ['@state' => $value->value]);
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment