Loading core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php +24 −3 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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. Loading Loading @@ -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, Loading Loading @@ -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; } Loading core/lib/Drupal/Core/Validation/CompositeConstraintInterface.php 0 → 100644 +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; } core/lib/Drupal/Core/Validation/ConstraintFactory.php +11 −0 Original line number Diff line number Diff line Loading @@ -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); Loading core/lib/Drupal/Core/Validation/ExecutionContext.php +14 −0 Original line number Diff line number Diff line Loading @@ -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; } } core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/AtLeastOneOfConstraint.php +11 −14 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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
core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php +24 −3 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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. Loading Loading @@ -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, Loading Loading @@ -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; } Loading
core/lib/Drupal/Core/Validation/CompositeConstraintInterface.php 0 → 100644 +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; }
core/lib/Drupal/Core/Validation/ConstraintFactory.php +11 −0 Original line number Diff line number Diff line Loading @@ -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); Loading
core/lib/Drupal/Core/Validation/ExecutionContext.php +14 −0 Original line number Diff line number Diff line Loading @@ -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; } }
core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/AtLeastOneOfConstraint.php +11 −14 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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; } }