Verified Commit 177fc79d authored by catch's avatar catch Committed by Dave Long
Browse files

feat: #3521131 Add support for Sequentially constraint

By: @bbrala
By: @smustgrave
By: @quietone
By: @borisson_
By: @dcam
By: @alexpott
By: @godotislate
By: @longwave
By: @benjifisher
(cherry picked from commit d4441514)
parent 3acf8ea0
Loading
Loading
Loading
Loading
Loading
+24 −3
Original line number Diff line number Diff line
@@ -7,7 +7,9 @@
use Drupal\Core\TypedData\ListInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\Core\TypedData\TypedDataManagerInterface;
use Drupal\Core\Validation\ExecutionContext;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Composite;
use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
@@ -89,13 +91,24 @@ public function atPath($path): static {
   * {@inheritdoc}
   */
  public function validate($data, $constraints = NULL, $groups = NULL, $is_root_call = TRUE): static {
    if (isset($groups)) {
    if (isset($groups) && $groups !== Constraint::DEFAULT_GROUP) {
      throw new \LogicException('Passing custom groups is not supported.');
    }

    if ($this->context instanceof ExecutionContext && $this->context->getConstraint() instanceof Composite) {
      $is_root_call = FALSE;
    }
    // Convert to TypedDataInterface if it matches the context object's value
    if (!$data instanceof TypedDataInterface) {
      $context_object = $this->context->getObject();

      if ($context_object instanceof TypedDataInterface && $data === $context_object->getValue()) {
        $data = $context_object;
      }
      else {
        throw new \InvalidArgumentException('The passed value must be a typed data object.');
      }
    }

    // You can pass a single constraint or an array of constraints.
    // Make sure to deal with an array in the rest of the code.
@@ -129,10 +142,15 @@ protected function validateNode(TypedDataInterface $data, $constraints = NULL, $
    $previous_object = $this->context->getObject();
    $previous_metadata = $this->context->getMetadata();
    $previous_path = $this->context->getPropertyPath();
    $previous_constraint = $this->context instanceof ExecutionContext ? $this->context->getConstraint() : NULL;

    $metadata = $this->metadataFactory->getMetadataFor($data);
    $cache_key = spl_object_hash($data);
    $property_path = $is_root_call ? '' : PropertyPath::append($previous_path, $data->getName());
    $property_path = match(TRUE) {
      $is_root_call => '',
      $previous_constraint instanceof Composite => $previous_path,
      default => PropertyPath::append($previous_path, $data->getName())
    };

    // Prefer a specific instance of the typed data manager stored by the data
    // if it is available. This is necessary for specialized typed data objects,
@@ -166,6 +184,9 @@ protected function validateNode(TypedDataInterface $data, $constraints = NULL, $
    }

    $this->context->setNode($previous_value, $previous_object, $previous_metadata, $previous_path);
    if ($previous_constraint) {
      $this->context->setConstraint($previous_constraint);
    }

    return $this;
  }
+23 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Core\Validation;

/**
 * An interface to provide a bridge to Symfony composite constraints.
 */
interface CompositeConstraintInterface {

  /**
   * Returns the name of the property or properties that contain constraints.
   *
   * This method should be a static implementation of
   * Composite::getCompositeOption().
   *
   * @return array|string
   *   The name of the property or properties that contain constraints.
   *
   * @see \Symfony\Component\Validator\Constraints\Composite::getCompositeOption()
   */
  public static function getCompositeOptionStatic(): array|string;

}
+11 −0
Original line number Diff line number Diff line
@@ -22,6 +22,17 @@ public function createInstance($plugin_id, array $configuration = []) {
    $plugin_definition = $this->discovery->getDefinition($plugin_id);
    $plugin_class = static::getPluginClass($plugin_id, $plugin_definition, $this->interface);

    if (is_subclass_of($plugin_class, CompositeConstraintInterface::class)) {
      $composite_constraint_options = (array) $plugin_class::getCompositeOptionStatic();
      foreach ($composite_constraint_options as $option) {
        foreach ($configuration[$option] as $key => $value) {
          foreach ($value as $nested_constraint_id => $nested_constraint_configuration) {
            $configuration[$option][$key] = $this->createInstance($nested_constraint_id, $nested_constraint_configuration);
          }
        }
      }
    }

    // If the plugin provides a factory method, pass the container to it.
    if (is_subclass_of($plugin_class, ContainerFactoryPluginInterface::class)) {
      return $plugin_class::create(\Drupal::getContainer(), $configuration, $plugin_id, $plugin_definition);
+14 −0
Original line number Diff line number Diff line
@@ -248,4 +248,18 @@ public function __clone(): void {
    $this->violations = clone $this->violations;
  }

  /**
   * Gets the current constraint.
   *
   * @return \Symfony\Component\Validator\Constraint|null
   *   The constraint.
   *
   * @internal
   *   This method is not part of the public API. It is only used to make
   *   recursive calls work.
   */
  public function getConstraint(): ?Constraint {
    return $this->constraint ?? NULL;
  }

}
+11 −14
Original line number Diff line number Diff line
@@ -2,12 +2,11 @@

namespace Drupal\Core\Validation\Plugin\Validation\Constraint;

use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Validation\Attribute\Constraint;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint as SymfonyConstraint;
use Drupal\Core\Validation\CompositeConstraintInterface;
use Symfony\Component\Validator\Constraints\AtLeastOneOf;
use Symfony\Component\Validator\Constraints\AtLeastOneOfValidator;

/**
 * Checks that at least one of the given constraint is satisfied.
@@ -19,22 +18,20 @@
  id: 'AtLeastOneOf',
  label: new TranslatableMarkup('At least one of', [], ['context' => 'Validation'])
)]
class AtLeastOneOfConstraint extends AtLeastOneOf implements ContainerFactoryPluginInterface {
class AtLeastOneOfConstraint extends AtLeastOneOf implements CompositeConstraintInterface {

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    $constraint_manager = $container->get('validation.constraint');
    $constraints = $configuration['constraints'];
    $constraint_instances = [];
    foreach ($constraints as $constraint_id => $constraint) {
      foreach ($constraint as $constraint_name => $constraint_options) {
        $constraint_instances[$constraint_id] = $constraint_manager->create($constraint_name, $constraint_options);
      }
  public static function getCompositeOptionStatic(): string {
    return 'constraints';
  }

    return new static($constraint_instances, [SymfonyConstraint::DEFAULT_GROUP]);
  /**
   * {@inheritdoc}
   */
  public function validatedBy(): string {
    return AtLeastOneOfValidator::class;
  }

}
Loading