diff --git a/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php b/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php index 29efd63f7e477ca7beec8b096551e10c5112c89b..b0ac7dbd3be20c73eef091d5e1e243911286a606 100644 --- a/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php +++ b/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php @@ -93,8 +93,16 @@ public function validate($data, $constraints = NULL, $groups = NULL, $is_root_ca throw new \LogicException('Passing custom groups is not supported.'); } + // Convert to TypedDataInterface if it matches the context object's value if (!$data instanceof TypedDataInterface) { - throw new \InvalidArgumentException('The passed value must be a typed data object.'); + $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. diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/SequentiallyConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/SequentiallyConstraint.php new file mode 100644 index 0000000000000000000000000000000000000000..aefd2354fb0261b3c41be48ccd4311c1524b394a --- /dev/null +++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/SequentiallyConstraint.php @@ -0,0 +1,37 @@ +<?php + +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 Symfony\Component\Validator\Constraints\Sequentially; + +/** + * Checks constraints sequentially and shows the error from the first. + */ +#[Constraint( + id: 'Sequentially', + label: new TranslatableMarkup('Sequentially validate', [], ['context' => 'Validation']) +)] +class SequentiallyConstraint extends Sequentially implements ContainerFactoryPluginInterface { + + /** + * {@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); + } + } + + return new static($constraint_instances, [SymfonyConstraint::DEFAULT_GROUP]); + } + +} diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/SequentiallyConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/SequentiallyConstraintValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..b3b113a6f73f43b915a19d400a5c49664b8f3c1f --- /dev/null +++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/SequentiallyConstraintValidator.php @@ -0,0 +1,30 @@ +<?php + +namespace Drupal\Core\Validation\Plugin\Validation\Constraint; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\SequentiallyValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; + +/** + * Validates the Sequentially constraint. + */ +class SequentiallyConstraintValidator extends SequentiallyValidator { + + /** + * Validate a set of constraints against a value sequentially. + * + * @param mixed $value + * The value to validate. + * @param \Symfony\Component\Validator\Constraint $constraint + * The constraint to validate against. + */ + public function validate(mixed $value, Constraint $constraint): void { + if (!$constraint instanceof SequentiallyConstraint) { + throw new UnexpectedTypeException($constraint, SequentiallyConstraint::class); + } + + parent::validate($value, $constraint); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Validation/SequentiallyConstraintValidatorTest.php b/core/tests/Drupal/KernelTests/Core/Validation/SequentiallyConstraintValidatorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..f6c1a1d8760bdd8a0001b7b25716f668b738d906 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Validation/SequentiallyConstraintValidatorTest.php @@ -0,0 +1,105 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\KernelTests\Core\Validation; + +use Drupal\Core\TypedData\DataDefinition; +use Drupal\Core\TypedData\TypedDataManagerInterface; +use Drupal\KernelTests\KernelTestBase; + +/** + * Tests Sequentially validation constraint with both valid and invalid values. + * + * @covers \Drupal\Core\Validation\Plugin\Validation\Constraint\SequentiallyConstraint + * @covers \Drupal\Core\Validation\Plugin\Validation\Constraint\SequentiallyConstraintValidator + * + * @group Validation + */ +class SequentiallyConstraintValidatorTest extends KernelTestBase { + + /** + * The typed data manager to use. + */ + protected TypedDataManagerInterface $typedData; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->typedData = $this->container->get('typed_data_manager'); + } + + /** + * Tests the Sequentially validation constraint validator. + * + * @dataProvider dataProvider + */ + public function testValidation(string $type, mixed $value, array $constraints, array $expectedViolations, array $extra_constraints = []): void { + // Create a definition that specifies some AllowedValues. + $definition = DataDefinition::create($type); + + if (count($extra_constraints) > 0) { + foreach ($extra_constraints as $name => $settings) { + $definition->addConstraint($name, $settings); + } + } + + $definition->addConstraint('Sequentially', [ + 'constraints' => $constraints, + ]); + + // Test the validation. + $typed_data = $this->typedData->create($definition, $value); + $violations = $typed_data->validate(); + + $violationMessages = []; + foreach ($violations as $violation) { + $violationMessages[] = (string) $violation->getMessage(); + } + + $this->assertEquals($expectedViolations, $violationMessages, 'Validation passed for correct value.'); + } + + /** + * Data provider for testValidation(). + */ + public static function dataProvider(): array { + return [ + 'It should fail on a failing sibling validator' => [ + 'integer', + 150, + [ + ['Range' => ['min' => 100]], + ['NotNull' => []], + ], + ['This value should be blank.'], + ['Blank' => []], + ], + 'it should fail if second validator fails' => [ + 'integer', + 250, + [ + ['Range' => ['min' => 100]], + ['AllowedValues' => [500]], + ], + [ + 'The value you selected is not a valid choice.', + ], + ], + 'it should show first validation error only even when multiple would fail' => [ + 'string', + 'Green', + [ + ['AllowedValues' => ['test']], + ['Blank' => []], + ], + [ + 'The value you selected is not a valid choice.', + ], + ], + ]; + } + +}