Commit 73cfdd9d authored by bojanz's avatar bojanz

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

parent 97467afe
......@@ -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]);
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment