You are here

class SelectOtherAllowedValuesConstraintValidator in CCK Select Other 8

Bypass AllowedValuesConstraintValidator by rewriting it.

This is not the "right way", but there is no other method to allow a widget to modify the allowed values of a list field in Drupal 8 thanks to "decoupling".Instead this class re-couples the dependency even when the widget is not in-use for a field instance. DrupalWTF.

Instead Drupal core fields should provide a means to override validation or provide non-widget based validation for web services.

Hierarchy

Expanded class hierarchy of SelectOtherAllowedValuesConstraintValidator

File

src/Validation/Plugin/Validation/Constraint/SelectOtherAllowedValuesConstraintValidator.php, line 31

Namespace

Drupal\cck_select_other\Validation\Plugin\Validation\Constraint
View source
class SelectOtherAllowedValuesConstraintValidator extends ChoiceValidator implements ContainerInjectionInterface {
  use TypedDataAwareValidatorTrait;
  use EntityDisplayTrait;

  /**
   * The current user account session.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static($container
      ->get('current_user'), $container
      ->get('entity_type.manager'));
  }

  /**
   * Constructs a new SelectOtherAllowedValuesConstraintValidator.
   *
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user. Used for fallback mode.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity_type.manager service.
   */
  public function __construct(AccountInterface $current_user, EntityTypeManagerInterface $entityTypeManager) {
    $this->currentUser = $current_user;
    $this
      ->setEntityTypeManager($entityTypeManager);
  }

  /**
   * {@inheritdoc}
   */
  public function validate($value, Constraint $constraint) {
    $typed_data = $this
      ->getTypedData();

    // Only bypass validation for ListItemBase.
    if ($typed_data instanceof ListItemBase) {

      // Get the field instance definition.
      $constraint->choices = [];

      /** @var \Drupal\Core\Field\FieldDefinitionInterface $instance */
      $instance = $typed_data
        ->getFieldDefinition();
      $value = $typed_data
        ->getValue();
      if ($this
        ->hasSelectOtherWidget($instance) && !in_array($value, $constraint->choices)) {

        // Add the other value to the constraint choices.
        $constraint->choices[] = $value;
      }
    }
    elseif ($typed_data instanceof EntityReferenceItem) {

      // Entity reference fields remove their allowed values constraint in the
      // getConstraint method, but those fields will have their constraints
      // altered already, and so those fields must be ignored.
      return;
    }
    if (empty($constraint->choices)) {
      $this
        ->validateFallback($value, $constraint);
      return;
    }

    // The parent implementation ignores values that are not set, but makes
    // sure some choices are available firstly. However, we want to support
    // empty choices for undefined values, e.g. if a term reference field
    // points to an empty vocabulary.
    if (!isset($value)) {
      return;
    }
    parent::validate($value, $constraint);
  }

  /**
   * Fallback to what core does.
   *
   * @param mixed $value
   *   The value to check.
   * @param \Symfony\Component\Validator\Constraint $constraint
   *   Constraint object.
   *
   * @throws \Drupal\Core\TypedData\Exception\MissingDataException
   * @throws \LogicException
   */
  public function validateFallback($value, Constraint $constraint) {
    $typed_data = $this
      ->getTypedData();
    if ($typed_data instanceof OptionsProviderInterface) {
      $allowed_values = $typed_data
        ->getSettableValues($this->currentUser);
      $constraint->choices = $allowed_values;

      // If the data is complex, we have to validate its main property.
      if ($typed_data instanceof ComplexDataInterface) {
        $name = $typed_data
          ->getDataDefinition()
          ->getMainPropertyName();
        if (!isset($name)) {
          throw new \LogicException('Cannot validate allowed values for complex data without a main property.');
        }
        $typed_data = $typed_data
          ->get($name);
        $value = $typed_data
          ->getValue();
      }
    }

    // The parent implementation ignores values that are not set, but makes
    // sure some choices are available firstly. However, we want to support
    // empty choices for undefined values; for instance, if a term reference
    // field points to an empty vocabulary.
    if (!isset($value)) {
      return;
    }

    // Get the value with the proper datatype in order to make strict
    // comparisons using in_array().
    if (!$typed_data instanceof PrimitiveInterface) {
      throw new \LogicException('The data type must be a PrimitiveInterface at this point.');
    }
    $value = $typed_data
      ->getCastedValue();

    // In a better world where typed data just returns typed values, we could
    // set a constraint callback to use the OptionsProviderInterface.
    // This is not possible right now though because we do the typecasting
    // further down.
    if ($constraint->callback) {
      if (!\is_callable($choices = [
        $this->context
          ->getObject(),
        $constraint->callback,
      ]) && !\is_callable($choices = [
        $this->context
          ->getClassName(),
        $constraint->callback,
      ]) && !\is_callable($choices = $constraint->callback)) {
        throw new ConstraintDefinitionException('The AllowedValuesConstraint constraint expects a valid callback');
      }
      $allowed_values = \call_user_func($choices);
      $constraint->choices = $allowed_values;

      // parent::validate() does not need to invoke the callback again.
      $constraint->callback = NULL;
    }

    // Force the choices to be the same type as the value.
    $type = gettype($value);
    foreach ($constraint->choices as &$choice) {
      settype($choice, $type);
    }
    parent::validate($value, $constraint);
  }

}

Members

Namesort descending Modifiers Type Description Overrides
EntityDisplayTrait::$entityTypeManager protected property The entity type manager.
EntityDisplayTrait::getEntityTypeManager public function Gets the entity type manager.
EntityDisplayTrait::getFormDisplays protected function Get the entity form displays for a field definition.
EntityDisplayTrait::getWidgetSettings public function Get the select other widget settings from the form display.
EntityDisplayTrait::hasSelectOtherWidget public function Determine if a field has the select other widget configured.
EntityDisplayTrait::setEntityTypeManager public function Sets the entity type manager.
SelectOtherAllowedValuesConstraintValidator::$currentUser protected property The current user account session.
SelectOtherAllowedValuesConstraintValidator::create public static function Instantiates a new instance of this class. Overrides ContainerInjectionInterface::create
SelectOtherAllowedValuesConstraintValidator::validate public function Checks if the passed value is valid.
SelectOtherAllowedValuesConstraintValidator::validateFallback public function Fallback to what core does.
SelectOtherAllowedValuesConstraintValidator::__construct public function Constructs a new SelectOtherAllowedValuesConstraintValidator.
TypedDataAwareValidatorTrait::getTypedData public function Gets the typed data object for the validated value.