diff --git a/core/lib/Drupal/Core/TypedData/TypedDataManager.php b/core/lib/Drupal/Core/TypedData/TypedDataManager.php index cd7ad17..9e53110 100644 --- a/core/lib/Drupal/Core/TypedData/TypedDataManager.php +++ b/core/lib/Drupal/Core/TypedData/TypedDataManager.php @@ -13,11 +13,12 @@ use Drupal\Core\DependencyInjection\ClassResolverInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Plugin\DefaultPluginManager; +use Drupal\Core\TypedData\Validation\ExecutionContextFactory; use Drupal\Core\TypedData\Validation\MetadataFactory; +use Drupal\Core\TypedData\Validation\RecursiveValidator; use Drupal\Core\Validation\ConstraintManager; use Drupal\Core\Validation\ConstraintValidatorFactory; use Drupal\Core\Validation\DrupalTranslator; -use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Validator\ValidatorInterface; /** @@ -28,7 +29,7 @@ class TypedDataManager extends DefaultPluginManager { /** * The validator used for validating typed data. * - * @var \Symfony\Component\Validator\ValidatorInterface + * @var \Symfony\Component\Validator\Validator\ValidatorInterface */ protected $validator; @@ -331,12 +332,11 @@ public function setValidator(ValidatorInterface $validator) { */ public function getValidator() { if (!isset($this->validator)) { - $this->validator = Validation::createValidatorBuilder() - ->setMetadataFactory(new MetadataFactory($this)) - ->setTranslator(new DrupalTranslator()) - ->setConstraintValidatorFactory(new ConstraintValidatorFactory($this->classResolver)) - ->setApiVersion(Validation::API_VERSION_2_4) - ->getValidator(); + $this->validator = new RecursiveValidator( + new ExecutionContextFactory(new DrupalTranslator()), + new ConstraintValidatorFactory($this->classResolver), + $this + ); } return $this->validator; } diff --git a/core/lib/Drupal/Core/TypedData/Validation/ConstraintViolationBuilder.php b/core/lib/Drupal/Core/TypedData/Validation/ConstraintViolationBuilder.php new file mode 100644 index 0000000..4e31a3e --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Validation/ConstraintViolationBuilder.php @@ -0,0 +1,182 @@ +violations = $violations; + $this->message = $message; + $this->parameters = $parameters; + $this->root = $root; + $this->propertyPath = $propertyPath; + $this->invalidValue = $invalidValue; + $this->translator = $translator; + $this->translationDomain = $translationDomain; + $this->constraint = $constraint; + } + + /** + * {@inheritdoc} + */ + public function atPath($path) { + $this->propertyPath = PropertyPath::append($this->propertyPath, $path); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setParameter($key, $value) { + $this->parameters[$key] = $value; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setParameters(array $parameters) { + $this->parameters = $parameters; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setTranslationDomain($translationDomain) { + $this->translationDomain = $translationDomain; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setInvalidValue($invalidValue) { + $this->invalidValue = $invalidValue; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setPlural($number) { + $this->plural = $number; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setCode($code) { + $this->code = $code; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setCause($cause) { + $this->cause = $cause; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function addViolation() { + if (NULL === $this->plural) { + $translatedMessage = $this->translator->trans($this->message, $this->parameters, $this->translationDomain); + } + else { + try { + $translatedMessage = $this->translator->transChoice($this->message, $this->plural, $this->parameters, $this->translationDomain# + ); + } + catch (\InvalidArgumentException $e) { + $translatedMessage = $this->translator->trans($this->message, $this->parameters, $this->translationDomain); + } + } + + $this->violations->add(new ConstraintViolation($translatedMessage, $this->message, $this->parameters, $this->root, $this->propertyPath, $this->invalidValue, $this->plural, $this->code, $this->constraint, $this->cause)); + } +} diff --git a/core/lib/Drupal/Core/TypedData/Validation/ExecutionContext.php b/core/lib/Drupal/Core/TypedData/Validation/ExecutionContext.php new file mode 100644 index 0000000..896ccdc --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Validation/ExecutionContext.php @@ -0,0 +1,318 @@ +validator = $validator; + $this->root = $root; + $this->translator = $translator; + $this->translationDomain = $translationDomain; + $this->violations = new ConstraintViolationList(); + } + + /** + * {@inheritdoc} + */ + public function setNode($value, $object, MetadataInterface $metadata = NULL, $propertyPath) { + $this->value = $value; + $this->data = $object; + $this->metadata = $metadata; + $this->propertyPath = (string) $propertyPath; + } + + /** + * {@inheritdoc} + */ + public function setGroup($group) { + $this->group = $group; + } + + /** + * {@inheritdoc} + */ + public function setConstraint(Constraint $constraint) { + $this->constraint = $constraint; + } + + /** + * {@inheritdoc} + */ + public function addViolation($message, array $parameters = array(), $invalidValue = NULL, $plural = NULL, $code = NULL) { + // The parameters $invalidValue and following are ignored by the new + // API, as they are not present in the new interface anymore. + // You should use buildViolation() instead. + if (func_num_args() > 2) { + throw new \LogicException('Legacy validator API is unsupported.'); + } + + $this->violations->add(new ConstraintViolation($this->translator->trans($message, $parameters, $this->translationDomain), $message, $parameters, $this->root, $this->propertyPath, $this->value, NULL, NULL, $this->constraint)); + } + + /** + * {@inheritdoc} + */ + public function buildViolation($message, array $parameters = array()) { + return new ConstraintViolationBuilder($this->violations, $this->constraint, $message, $parameters, $this->root, $this->propertyPath, $this->value, $this->translator, $this->translationDomain); + } + + /** + * {@inheritdoc} + */ + public function getViolations() { + return $this->violations; + } + + /** + * {@inheritdoc} + */ + public function getValidator() { + return $this->validator; + } + + /** + * {@inheritdoc} + */ + public function getRoot() { + return $this->root; + } + + /** + * {@inheritdoc} + */ + public function getValue() { + return $this->value; + } + + /** + * {@inheritdoc} + */ + public function getObject() { + return $this->data; + } + + /** + * {@inheritdoc} + */ + public function getMetadata() { + return $this->metadata; + } + + /** + * {@inheritdoc} + */ + public function getGroup() { + return Constraint::DEFAULT_GROUP; + } + + /** + * {@inheritdoc} + */ + public function getClassName() { + return get_class($this->data); + } + + /** + * {@inheritdoc} + */ + public function getPropertyName() { + return $this->data->getName(); + } + + /** + * {@inheritdoc} + */ + public function getPropertyPath($subPath = '') { + return PropertyPath::append($this->propertyPath, $subPath); + } + + /** + * {@inheritdoc} + */ + public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = NULL, $plural = NULL, $code = NULL) { + throw new \LogicException('Legacy validator API is unsupported.'); + } + + /** + * {@inheritdoc} + */ + public function validate($value, $subPath = '', $groups = NULL, $traverse = FALSE, $deep = FALSE) { + throw new \LogicException('Legacy validator API is unsupported.'); + } + + /** + * {@inheritdoc} + */ + public function markConstraintAsValidated($cacheKey, $constraintHash) { + $this->validatedConstraints[$cacheKey . ':' . $constraintHash] = TRUE; + } + + /** + * {@inheritdoc} + */ + public function isConstraintValidated($cacheKey, $constraintHash) { + return isset($this->validatedConstraints[$cacheKey . ':' . $constraintHash]); + } + + /** + * {@inheritdoc} + */ + public function validateValue($value, $constraints, $subPath = '', $groups = NULL) { + throw new \LogicException('Legacy validator API is unsupported.'); + } + + /** + * {@inheritdoc} + */ + public function markGroupAsValidated($cacheKey, $groupHash) { + $this->validatedObjects[$cacheKey][$groupHash] = TRUE; + } + + /** + * {@inheritdoc} + */ + public function isGroupValidated($cacheKey, $groupHash) { + return isset($this->validatedObjects[$cacheKey][$groupHash]); + } + + /** + * {@inheritdoc} + */ + public function markObjectAsInitialized($cacheKey) { + // Not supported, so nothing todo. + } + + /** + * {@inheritdoc} + */ + public function isObjectInitialized($cacheKey) { + // Not supported, so nothing todo. + } + + /** + * {@inheritdoc} + */ + public function getMetadataFactory() { + throw new \LogicException('Legacy validator API is unsupported.'); + } +} diff --git a/core/lib/Drupal/Core/TypedData/Validation/ExecutionContextFactory.php b/core/lib/Drupal/Core/TypedData/Validation/ExecutionContextFactory.php new file mode 100644 index 0000000..2eda012 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Validation/ExecutionContextFactory.php @@ -0,0 +1,50 @@ +translator = $translator; + $this->translationDomain = $translationDomain; + } + + /** + * {@inheritdoc} + */ + public function createContext(ValidatorInterface $validator, $root) { + return new ExecutionContext($validator, $root, $this->translator, $this->translationDomain); + } +} diff --git a/core/lib/Drupal/Core/TypedData/Validation/Metadata.php b/core/lib/Drupal/Core/TypedData/Validation/Metadata.php deleted file mode 100644 index 6fe5255..0000000 --- a/core/lib/Drupal/Core/TypedData/Validation/Metadata.php +++ /dev/null @@ -1,115 +0,0 @@ -typedData = $typed_data; - $this->name = $name; - $this->factory = $factory; - $this->typedDataManager = $typed_data_manager; - } - - /** - * Implements MetadataInterface::accept(). - */ - public function accept(ValidationVisitorInterface $visitor, $typed_data, $group, $propertyPath) { - - // @todo: Do we have to care about groups? Symfony class metadata has - // $propagatedGroup. - - $visitor->visit($this, $this->typedDataManager->getCanonicalRepresentation($typed_data), $group, $propertyPath); - } - - /** - * Implements MetadataInterface::findConstraints(). - */ - public function findConstraints($group) { - return $this->typedData->getConstraints(); - } - - /** - * Returns the name of the property. - * - * @return string The property name. - */ - public function getPropertyName() { - return $this->name; - } - - /** - * Extracts the value of the property from the given container. - * - * @param mixed $container The container to extract the property value from. - * - * @return mixed The value of the property. - */ - public function getPropertyValue($container) { - return $this->typedDataManager->getCanonicalRepresentation($this->typedData); - } - - /** - * Returns the typed data object. - * - * @return \Drupal\Core\TypedData\TypedDataInterface - * The typed data object. - */ - public function getTypedData() { - return $this->typedData; - } -} diff --git a/core/lib/Drupal/Core/TypedData/Validation/MetadataFactory.php b/core/lib/Drupal/Core/TypedData/Validation/MetadataFactory.php deleted file mode 100644 index fcd0557..0000000 --- a/core/lib/Drupal/Core/TypedData/Validation/MetadataFactory.php +++ /dev/null @@ -1,62 +0,0 @@ -typedDataManager = $typed_data_manager; - } - - /** - * {@inheritdoc} - * - * @param \Drupal\Core\TypedData\TypedDataInterface $typed_data - * Some typed data object containing the value to validate. - * @param $name - * (optional) The name of the property to get metadata for. Leave empty, if - * the data is the root of the typed data tree. - */ - public function getMetadataFor($typed_data, $name = '') { - if (!$typed_data instanceof TypedDataInterface) { - throw new \InvalidArgumentException('The passed value must be a typed data object.'); - } - $is_container = $typed_data instanceof ComplexDataInterface || $typed_data instanceof ListInterface; - $class = '\Drupal\Core\TypedData\Validation\\' . ($is_container ? 'PropertyContainerMetadata' : 'Metadata'); - return new $class($typed_data, $name, $this, $this->typedDataManager); - } - - /** - * Implements MetadataFactoryInterface::hasMetadataFor(). - */ - public function hasMetadataFor($value) { - return $value instanceof TypedDataInterface; - } -} diff --git a/core/lib/Drupal/Core/TypedData/Validation/PropertyContainerMetadata.php b/core/lib/Drupal/Core/TypedData/Validation/PropertyContainerMetadata.php deleted file mode 100644 index 80d3320..0000000 --- a/core/lib/Drupal/Core/TypedData/Validation/PropertyContainerMetadata.php +++ /dev/null @@ -1,72 +0,0 @@ -isEmpty()) { - $data = NULL; - } - else { - $data = $this->typedDataManager->getCanonicalRepresentation($typed_data); - } - $visitor->visit($this, $data, $group, $propertyPath); - $pathPrefix = isset($propertyPath) && $propertyPath !== '' ? $propertyPath . '.' : ''; - - // Only continue validating if the data is not empty. - if ($data) { - foreach ($typed_data as $name => $data) { - $metadata = $this->factory->getMetadataFor($data, $name); - $metadata->accept($visitor, $data, $group, $pathPrefix . $name); - } - } - } - - /** - * Implements PropertyMetadataContainerInterface::hasPropertyMetadata(). - */ - public function hasPropertyMetadata($property_name) { - try { - $exists = (bool)$this->getPropertyMetadata($property_name); - } - catch (\LogicException $e) { - $exists = FALSE; - } - return $exists; - } - - /** - * Implements PropertyMetadataContainerInterface::getPropertyMetadata(). - */ - public function getPropertyMetadata($property_name) { - if ($this->typedData instanceof ListInterface) { - return array(new Metadata($this->typedData[$property_name], $property_name, $this->factory, $this->typedDataManager)); - } - elseif ($this->typedData instanceof ComplexDataInterface) { - return array(new Metadata($this->typedData->get($property_name), $property_name, $this->factory, $this->typedDataManager)); - } - else { - throw new \LogicException("There are no known properties."); - } - } -} diff --git a/core/lib/Drupal/Core/TypedData/Validation/PropertyPath.php b/core/lib/Drupal/Core/TypedData/Validation/PropertyPath.php new file mode 100644 index 0000000..e1cdb2c --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Validation/PropertyPath.php @@ -0,0 +1,35 @@ +context = $context; + $this->metadataFactory = $metadata_factory; + $this->constraintValidatorFactory = $validator_factory; + $this->typedDataManager = $typed_data_manager; + } + + /** + * {@inheritdoc} + */ + public function atPath($path) { + $this->defaultPropertyPath = $this->context->getPropertyPath($path); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function validate($data, $constraints = NULL, $groups = NULL) { + if (isset($groups)) { + throw new \LogicException('Passing custom groups is not supported.'); + } + + if (!$data instanceof TypedDataInterface) { + 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 + if (isset($constraints) && !is_array($constraints)) { + $constraints = array($constraints); + } + + $this->validateNode($data, $constraints); + return $this; + } + + /** + * Validates a Typed Data node in the validation tree. + * + * If no constraints are passed, the data is validated against the + * constraints specified in its data definition. If the data is complex or a + * list and no constraints are passed, the contained properties or list items + * are validated recursively. + * + * @param \Drupal\Core\TypedData\TypedDataInterface $data + * The data to validated. + * @param \Symfony\Component\Validator\Constraint[]|null $constraints + * (optional) If set, an array of constraints to validate. + * + * @return $this + */ + protected function validateNode(TypedDataInterface $data, $constraints = NULL) { + $previousValue = $this->context->getValue(); + $previousObject = $this->context->getObject(); + $previousMetadata = $this->context->getMetadata(); + $previousPath = $this->context->getPropertyPath(); + + $metadata = $this->metadataFactory->getMetadataFor($data); + $cacheKey = spl_object_hash($data); + $propertyPath = PropertyPath::append($previousPath, $data->getName()); + // Pass the canonical representation of the data as validated value to + // constraint validators, such that they do not have to care about Typed + // Data. + $value = $this->typedDataManager->getCanonicalRepresentation($data); + $this->context->setNode($value, $data, $metadata, $propertyPath); + + if (!$this->context->isGroupValidated($cacheKey, Constraint::DEFAULT_GROUP)) { + $this->context->markGroupAsValidated($cacheKey, Constraint::DEFAULT_GROUP); + + $constraints = isset($constraints) ? $constraints : $metadata->findConstraints(Constraint::DEFAULT_GROUP); + $this->validateConstraints($value, $cacheKey, $constraints); + } + + // If the data is a list or complex data, validate the contained list items + // or properties. + if ($data instanceof TraversableTypedDataInterface) { + foreach ($data as $name => $property) { + $this->validateNode($property); + } + } + + $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath); + + return $this; + } + + /** + * Validates a node's value against all constraints in the given group. + * + * @param mixed $value + * The validated value. + */ + protected function validateConstraints($value, $cacheKey, $constraints) { + foreach ($constraints as $constraint) { + // Prevent duplicate validation of constraints, in the case + // that constraints belong to multiple validated groups + if (null !== $cacheKey) { + $constraintHash = spl_object_hash($constraint); + + if ($this->context->isConstraintValidated($cacheKey, $constraintHash)) { + continue; + } + + $this->context->markConstraintAsValidated($cacheKey, $constraintHash); + } + + $this->context->setConstraint($constraint); + + $validator = $this->constraintValidatorFactory->getInstance($constraint); + $validator->initialize($this->context); + $validator->validate($value, $constraint); + } + } + + /** + * {@inheritdoc} + */ + public function getViolations() { + return $this->context->getViolations(); + } + + /** + * {@inheritdoc} + */ + public function validateProperty($object, $propertyName, $groups = NULL) { + if (isset($groups)) { + throw new \LogicException('Passing custom groups is not supported.'); + } + if (!is_object($object)) { + throw new \InvalidArgumentException('Passing class name is not supported.'); + } + elseif (!$object instanceof ListInterface && !$object instanceof ComplexDataInterface) { + throw new \InvalidArgumentException('Passed data does not contain properties.'); + } + return $this->validateNode($object->get($propertyName)); + } + + /** + * {@inheritdoc} + */ + public function validatePropertyValue($object, $property_name, $value, $groups = NULL) { + if (!is_object($object)) { + throw new \InvalidArgumentException('Passing class name is not supported.'); + } + elseif (!$object instanceof ListInterface && !$object instanceof ComplexDataInterface) { + throw new \InvalidArgumentException('Passed data does not contain properties.'); + } + $data = $object->get($property_name); + $constraints = $data->getDataDefinition()->getConstraints(); + return $this->validate($value, $constraints, $groups); + } + +} diff --git a/core/lib/Drupal/Core/TypedData/Validation/RecursiveValidator.php b/core/lib/Drupal/Core/TypedData/Validation/RecursiveValidator.php new file mode 100644 index 0000000..723fa02 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Validation/RecursiveValidator.php @@ -0,0 +1,119 @@ +contextFactory = $context_factory; + $this->constraintValidatorFactory = $validator_factory; + $this->typedDataManager = $typed_data_manager; + } + + /** + * {@inheritdoc} + */ + public function startContext($root = NULL) { + return new RecursiveContextualValidator($this->contextFactory->createContext($this, $root), $this, $this->constraintValidatorFactory, $this->typedDataManager); + } + + /** + * {@inheritdoc} + */ + public function inContext(ExecutionContextInterface $context) { + return new RecursiveContextualValidator($context, $this, $this->constraintValidatorFactory, $this->typedDataManager); + } + + /** + * {@inheritdoc} + * + * @param \Drupal\Core\TypedData\TypedDataInterface $typed_data + * A typed data object containing the value to validate. + */ + public function getMetadataFor($typed_data) { + if (!$typed_data instanceof TypedDataInterface) { + throw new \InvalidArgumentException('The passed value must be a typed data object.'); + } + $class = '\Drupal\Core\TypedData\Validation\TypedDataMetadata'; + return new $class($typed_data); + } + + /** + * {@inheritdoc} + */ + public function hasMetadataFor($value) { + return $value instanceof TypedDataInterface; + } + + /** + * {@inheritdoc} + */ + public function validate($value, $constraints = null, $groups = null) { + return $this->startContext($value) + ->validate($value, $constraints, $groups) + ->getViolations(); + } + + /** + * {@inheritdoc} + */ + public function validateProperty($object, $propertyName, $groups = NULL) { + return $this->startContext($object) + ->validateProperty($object, $propertyName, $groups) + ->getViolations(); + } + + /** + * {@inheritdoc} + */ + public function validatePropertyValue($objectOrClass, $propertyName, $value, $groups = NULL) { + // Just passing a class name is not supported. + if (!is_object($objectOrClass)) { + throw new \LogicException('Typed data validation does not support passing the class name only.'); + } + return $this->startContext($objectOrClass) + ->validatePropertyValue($objectOrClass, $propertyName, $value, $groups) + ->getViolations(); + } + +} diff --git a/core/lib/Drupal/Core/TypedData/Validation/TypedDataAwareValidatorTrait.php b/core/lib/Drupal/Core/TypedData/Validation/TypedDataAwareValidatorTrait.php new file mode 100644 index 0000000..7c0509f --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Validation/TypedDataAwareValidatorTrait.php @@ -0,0 +1,36 @@ +context; + /** @var \Symfony\Component\Validator\Context\ExecutionContextInterface $context */ + $data = $context->getObject(); + if (!$data instanceof TypedDataInterface) { + throw new \LogicException("There is no Typed Data object available."); + } + return $data; + } + +} diff --git a/core/lib/Drupal/Core/TypedData/Validation/TypedDataMetadata.php b/core/lib/Drupal/Core/TypedData/Validation/TypedDataMetadata.php new file mode 100755 index 0000000..2df09d6 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Validation/TypedDataMetadata.php @@ -0,0 +1,75 @@ +typedData = $typed_data; + } + + /** + * {@inheritdoc} + */ + public function accept(ValidationVisitorInterface $visitor, $typed_data, $group, $propertyPath) { + throw new BadMethodCallException('Not supported.'); + } + + /** + * {@inheritdoc} + */ + public function findConstraints($group) { + return $this->getConstraints(); + } + + /** + * {@inheritdoc} + */ + public function getConstraints() { + return $this->typedData->getConstraints(); + } + + /** + * {@inheritdoc} + */ + public function getTraversalStrategy() { + return TraversalStrategy::NONE; + } + + /** + * {@inheritdoc} + */ + public function getCascadingStrategy() { + // By default, never cascade into validating referenced data structures. + return CascadingStrategy::NONE; + } + +} diff --git a/core/lib/Drupal/Core/Validation/ConstraintManager.php b/core/lib/Drupal/Core/Validation/ConstraintManager.php index c760d1e..0ed8d97 100644 --- a/core/lib/Drupal/Core/Validation/ConstraintManager.php +++ b/core/lib/Drupal/Core/Validation/ConstraintManager.php @@ -79,14 +79,9 @@ public function create($name, $options) { * @see ConstraintManager::__construct() */ public function registerDefinitions() { - $this->discovery->setDefinition('Null', array( - 'label' => new TranslationWrapper('Null'), - 'class' => '\Symfony\Component\Validator\Constraints\Null', - 'type' => FALSE, - )); - $this->discovery->setDefinition('NotNull', array( - 'label' => new TranslationWrapper('Not null'), - 'class' => '\Symfony\Component\Validator\Constraints\NotNull', + $this->discovery->setDefinition('Callback', array( + 'label' => new TranslationWrapper('Callback'), + 'class' => '\Symfony\Component\Validator\Constraints\Callback', 'type' => FALSE, )); $this->discovery->setDefinition('Blank', array( diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/AllowedValuesConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/AllowedValuesConstraintValidator.php index efc6906..a85fbf2 100644 --- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/AllowedValuesConstraintValidator.php +++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/AllowedValuesConstraintValidator.php @@ -7,25 +7,53 @@ namespace Drupal\Core\Validation\Plugin\Validation\Constraint; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Session\AccountInterface; use Drupal\Core\TypedData\OptionsProviderInterface; use Drupal\Core\TypedData\ComplexDataInterface; +use Drupal\Core\TypedData\Validation\TypedDataAwareValidatorTrait; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\ChoiceValidator; /** * Validates the AllowedValues constraint. */ -class AllowedValuesConstraintValidator extends ChoiceValidator { +class AllowedValuesConstraintValidator extends ChoiceValidator implements ContainerInjectionInterface { + + use TypedDataAwareValidatorTrait; + + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $currentUser; /** * {@inheritdoc} */ - public function validate($value, Constraint $constraint) { - $typed_data = $this->context->getMetadata()->getTypedData(); + public static function create(ContainerInterface $container) { + return new static($container->get('current_user')); + } + + /** + * Constructs a new AllowedValuesConstraintValidator. + * + * @param \Drupal\Core\Session\AccountInterface $current_user + * The current user. + */ + public function __construct(AccountInterface $current_user) { + $this->currentUser = $current_user; + } + /** + * {@inheritdoc} + */ + public function validate($value, Constraint $constraint) { + $typed_data = $this->getTypedData(); if ($typed_data instanceof OptionsProviderInterface) { - $account = \Drupal::currentUser(); - $allowed_values = $typed_data->getSettableValues($account); + $allowed_values = $typed_data->getSettableValues($this->currentUser); $constraint->choices = $allowed_values; // If the data is complex, we have to validate its main property. diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraintValidator.php old mode 100644 new mode 100755 index add46eb..b29a231 --- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraintValidator.php +++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraintValidator.php @@ -9,6 +9,8 @@ use Drupal\Core\TypedData\ComplexDataInterface; use Drupal\Core\TypedData\TypedDataInterface; +use Drupal\Core\TypedData\Validation\TypedDataAwareValidatorTrait; +use Drupal\Core\TypedData\Validation\TypedDataPropertyValidationEnvelope; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; @@ -18,35 +20,26 @@ */ class ComplexDataConstraintValidator extends ConstraintValidator { + use TypedDataAwareValidatorTrait; + /** * {@inheritdoc} */ - public function validate($value, Constraint $constraint) { - if (!isset($value)) { - return; - } + public function validate($data, Constraint $constraint) { // If un-wrapped data has been passed, fetch the typed data object first. - if (!$value instanceof TypedDataInterface) { - $value = $this->context->getMetadata()->getTypedData(); + if (!$data instanceof TypedDataInterface) { + $data = $this->getTypedData(); } - if (!$value instanceof ComplexDataInterface) { - throw new UnexpectedTypeException($value, 'ComplexData'); + if (!$data instanceof ComplexDataInterface) { + throw new UnexpectedTypeException($data, 'ComplexData'); } - $group = $this->context->getGroup(); - foreach ($constraint->properties as $name => $constraints) { - $property = $value->get($name); - $is_container = $property instanceof ComplexDataInterface || $property instanceof ListInterface; - if (!$is_container) { - $property = $property->getValue(); - } - elseif ($property->isEmpty()) { - // @see \Drupal\Core\TypedData\Validation\PropertyContainerMetadata::accept(); - $property = NULL; - } - $this->context->validateValue($property, $constraints, $name, $group); + $this->context->getValidator() + ->inContext($this->context) + ->validate($data->get($name), $constraints); } } + } diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NotNullConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NotNullConstraint.php new file mode 100644 index 0000000..cdf1b63 --- /dev/null +++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NotNullConstraint.php @@ -0,0 +1,25 @@ +getTypedData(); + if (($typed_data instanceof ListInterface || $typed_data instanceof ComplexDataInterface) && $typed_data->isEmpty()) { + $value = NULL; + } + parent::validate($value, $constraint); + } + +} diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NullConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NullConstraint.php new file mode 100644 index 0000000..3d87128 --- /dev/null +++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NullConstraint.php @@ -0,0 +1,25 @@ +getTypedData(); + if (($typed_data instanceof ListInterface || $typed_data instanceof ComplexDataInterface) && $typed_data->isEmpty()) { + $value = NULL; + } + parent::validate($value, $constraint); + } + +} diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php index d1bdc0f..d873820 100644 --- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php +++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php @@ -15,6 +15,7 @@ use Drupal\Core\TypedData\Type\IntegerInterface; use Drupal\Core\TypedData\Type\StringInterface; use Drupal\Core\TypedData\Type\UriInterface; +use Drupal\Core\TypedData\Validation\TypedDataAwareValidatorTrait; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -23,6 +24,8 @@ */ class PrimitiveTypeConstraintValidator extends ConstraintValidator { + use TypedDataAwareValidatorTrait; + /** * Implements \Symfony\Component\Validator\ConstraintValidatorInterface::validate(). */ @@ -32,7 +35,7 @@ public function validate($value, Constraint $constraint) { return; } - $typed_data = $this->context->getMetadata()->getTypedData(); + $typed_data = $this->getTypedData(); $valid = TRUE; if ($typed_data instanceof BinaryInterface && !is_resource($value)) { $valid = FALSE; diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php index a5dde71..d6c3910 100644 --- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php +++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php @@ -19,7 +19,7 @@ class UniqueFieldValueValidator extends ConstraintValidator { * {@inheritdoc} */ public function validate($items, Constraint $constraint) { - if (!isset($items)) { + if (!$item = $items->first()) { return; } $field_name = $items->getFieldDefinition()->getName(); @@ -31,13 +31,13 @@ public function validate($items, Constraint $constraint) { $value_taken = (bool) \Drupal::entityQuery($entity_type_id) // The id could be NULL, so we cast it to 0 in that case. ->condition($id_key, (int) $items->getEntity()->id(), '<>') - ->condition($field_name, $items->first()->value) + ->condition($field_name, $item->value) ->range(0, 1) ->count() ->execute(); if ($value_taken) { - $this->context->addViolation($constraint->message, array("%value" => $items->value)); + $this->context->addViolation($constraint->message, array("%value" => $item->value)); } } } diff --git a/core/modules/forum/src/Plugin/Validation/Constraint/ForumLeafConstraintValidator.php b/core/modules/forum/src/Plugin/Validation/Constraint/ForumLeafConstraintValidator.php index e4e9be2..f2d2400 100644 --- a/core/modules/forum/src/Plugin/Validation/Constraint/ForumLeafConstraintValidator.php +++ b/core/modules/forum/src/Plugin/Validation/Constraint/ForumLeafConstraintValidator.php @@ -20,10 +20,10 @@ class ForumLeafConstraintValidator extends ConstraintValidator { * {@inheritdoc} */ public function validate($items, Constraint $constraint) { - if (!isset($items)) { - return; - } $item = $items->first(); + if (!isset($item)) { + return NULL; + } // Verify that a term has been selected. if (!$item->entity) { diff --git a/core/modules/quickedit/src/Form/QuickEditFieldForm.php b/core/modules/quickedit/src/Form/QuickEditFieldForm.php index 2db50c4..de8dc5b 100644 --- a/core/modules/quickedit/src/Form/QuickEditFieldForm.php +++ b/core/modules/quickedit/src/Form/QuickEditFieldForm.php @@ -17,7 +17,7 @@ use Drupal\Core\Entity\Entity\EntityFormDisplay; use Drupal\user\PrivateTempStoreFactory; use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\Validator\ValidatorInterface; +use Symfony\Component\Validator\Validator\ValidatorInterface; /** * Builds and process a form for editing a single entity field. @@ -48,7 +48,7 @@ class QuickEditFieldForm extends FormBase { /** * The typed data validator. * - * @var \Symfony\Component\Validator\ValidatorInterface + * @var \Symfony\Component\Validator\Validator\ValidatorInterface */ protected $validator; @@ -61,7 +61,7 @@ class QuickEditFieldForm extends FormBase { * The module handler. * @param \Drupal\Core\Entity\EntityStorageInterface $node_type_storage * The node type storage. - * @param \Symfony\Component\Validator\ValidatorInterface $validator + * @param \Symfony\Component\Validator\Validator\ValidatorInterface $validator * The typed data validator service. */ public function __construct(PrivateTempStoreFactory $temp_store_factory, ModuleHandlerInterface $module_handler, EntityStorageInterface $node_type_storage, ValidatorInterface $validator) { @@ -165,7 +165,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) { // @todo: Improve this in https://www.drupal.org/node/2395831. $typed_entity = $entity->getTypedData(); $violations = $this->validator - ->validateValue($entity, $typed_entity->getConstraints()); + ->validate($typed_entity, $typed_entity->getConstraints()); foreach ($violations as $violation) { $form_state->setErrorByName($violation->getPropertyPath(), $violation->getMessage()); diff --git a/core/modules/user/src/Plugin/Validation/Constraint/UserMailRequired.php b/core/modules/user/src/Plugin/Validation/Constraint/UserMailRequired.php index 76bc17d..6e9fa3b 100644 --- a/core/modules/user/src/Plugin/Validation/Constraint/UserMailRequired.php +++ b/core/modules/user/src/Plugin/Validation/Constraint/UserMailRequired.php @@ -8,6 +8,7 @@ namespace Drupal\user\Plugin\Validation\Constraint; use Drupal\Component\Utility\SafeMarkup; +use Drupal\Core\TypedData\Validation\TypedDataAwareValidatorTrait; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidatorInterface; use Symfony\Component\Validator\ExecutionContextInterface; @@ -58,7 +59,7 @@ public function validatedBy() { public function validate($items, Constraint $constraint) { /** @var \Drupal\Core\Field\FieldItemListInterface $items */ /** @var \Drupal\user\UserInterface $account */ - $account = $this->context->getMetadata()->getTypedData()->getEntity(); + $account = $items->getEntity(); $existing_value = NULL; if ($account->id()) { $account_unchanged = \Drupal::entityManager() diff --git a/core/modules/user/src/Tests/UserValidationTest.php b/core/modules/user/src/Tests/UserValidationTest.php index ec44a79..172b79d 100644 --- a/core/modules/user/src/Tests/UserValidationTest.php +++ b/core/modules/user/src/Tests/UserValidationTest.php @@ -122,9 +122,9 @@ function testValidation() { // https://drupal.org/node/2023465. $this->assertEqual(count($violations), 2, 'Violations found when email is too long'); $this->assertEqual($violations[0]->getPropertyPath(), 'mail.0.value'); - $this->assertEqual($violations[0]->getMessage(), t('%name: the email address can not be longer than @max characters.', array('%name' => $user->get('mail')->getFieldDefinition()->getLabel(), '@max' => Email::EMAIL_MAX_LENGTH))); + $this->assertEqual($violations[0]->getMessage(), t('This value is not a valid email address.')); $this->assertEqual($violations[1]->getPropertyPath(), 'mail.0.value'); - $this->assertEqual($violations[1]->getMessage(), t('This value is not a valid email address.')); + $this->assertEqual($violations[1]->getMessage(), t('%name: the email address can not be longer than @max characters.', array('%name' => $user->get('mail')->getFieldDefinition()->getLabel(), '@max' => Email::EMAIL_MAX_LENGTH))); // Provoke an email collision with an existing user. $user->set('mail', 'existing@example.com'); diff --git a/core/tests/Drupal/Tests/Core/TypedData/RecursiveContextualValidatorTest.php b/core/tests/Drupal/Tests/Core/TypedData/RecursiveContextualValidatorTest.php new file mode 100644 index 0000000..4d75cdc --- /dev/null +++ b/core/tests/Drupal/Tests/Core/TypedData/RecursiveContextualValidatorTest.php @@ -0,0 +1,249 @@ + $this->root . '/core/lib/Drupal/Core/TypedData', + 'Drupal\\Core\\Validation' => $this->root . '/core/lib/Drupal/Core/Validation', + ]); + $module_handler = $this->getMockBuilder('Drupal\Core\Extension\ModuleHandlerInterface') + ->disableOriginalConstructor() + ->getMock(); + $class_resolver = $this->getMockBuilder('Drupal\Core\DependencyInjection\ClassResolverInterface') + ->disableOriginalConstructor() + ->getMock(); + + $this->typedDataManager = new TypedDataManager($namespaces, $cache_backend, $module_handler, $class_resolver); + $this->typedDataManager->setValidationConstraintManager( + new ConstraintManager($namespaces, $cache_backend, $module_handler) + ); + // Typed data definitions access the manager in the container. + $container = new ContainerBuilder(); + $container->set('typed_data_manager', $this->typedDataManager); + \Drupal::setContainer($container); + + $translator = new DefaultTranslator(); + $this->contextFactory = new ExecutionContextFactory($translator); + $this->validatorFactory = new ConstraintValidatorFactory(); + $this->recursiveValidator = new RecursiveValidator($this->contextFactory, $this->validatorFactory, $this->typedDataManager); + } + + /** + * @covers ::validate + * + * @expectedException \Exception + */ + public function testValidateWithGroups() { + $this->recursiveValidator->validate('test', NULL, 'test group'); + } + + /** + * @covers ::validate + * + * @expectedException \Exception + */ + public function testValidateWithoutTypedData() { + $this->recursiveValidator->validate('test'); + } + + /** + * @covers ::validate + */ + public function testBasicValidateWithoutConstraints() { + $typed_data = $this->typedDataManager->create(DataDefinition::create('string')); + $violations = $this->recursiveValidator->validate($typed_data); + $this->assertEquals(0, $violations->count()); + } + + /** + * @covers ::validate + */ + public function testBasicValidateWithConstraint() { + $typed_data = $this->typedDataManager->create( + DataDefinition::create('string') + ->addConstraint('Callback', [ + 'callback' => function ($value, ExecutionContextInterface $context) { + $context->addViolation('test violation: ' . $value); + } + ]) + ); + $typed_data->setValue('foo'); + + $violations = $this->recursiveValidator->validate($typed_data); + $this->assertEquals(1, $violations->count()); + // Ensure that the right value is passed into the validator. + $this->assertEquals('test violation: foo', $violations->get(0)->getMessage()); + } + + /** + * @covers ::validate + */ + public function testBasicValidateWithMultipleConstraints() { + $options = [ + 'callback' => function ($value, ExecutionContextInterface $context) { + $context->addViolation('test violation'); + } + ]; + $typed_data = $this->typedDataManager->create( + DataDefinition::create('string') + ->addConstraint('Callback', $options) + ->addConstraint('NotNull') + ); + $violations = $this->recursiveValidator->validate($typed_data); + $this->assertEquals(2, $violations->count()); + } + + /** + * @covers ::validate + * @todo: Fix. + */ + public function SKIPtestPropertiesValidateWithMultipleLevels() { + $callback_constraint = new Callback([ + 'callback' => function (TypedDataInterface $value, ExecutionContextInterface $context) { + $context->addViolation('violation: ' . $value->getValue()); + } + ]); + + $tree = ['value' => []]; + $tree['properties'] = [ + 'key1' => [ + 'value' => 'value1', + 'constraints' => [clone $callback_constraint], + ], + 'key2' => [ + 'value' => 'value2', + 'constraints' => [clone $callback_constraint] + ], + 'key_with_properties' => [ + 'value' => 'value_with_properties', + 'constraints' => [clone $callback_constraint], + 'properties' => [ + 'subkey1' => [ + 'value' => 'subvalue1', + 'constraints' => [clone $callback_constraint] + ], + 'subkey2' => [ + 'value' => 'subvalue2', + 'constraints' => [clone $callback_constraint] + ], + ] + ], + ]; + + $typed_data = $this->setupTypedData($tree); + $violations = $this->recursiveValidator->validate($typed_data); + $this->assertEquals(5, $violations->count()); + + $this->assertEquals('violation: value1', $violations->get(0)->getMessage()); + $this->assertEquals('violation: value2', $violations->get(1)->getMessage()); + $this->assertEquals('violation: value_with_properties', $violations->get(2)->getMessage()); + $this->assertEquals('violation: subvalue1', $violations->get(3)->getMessage()); + $this->assertEquals('violation: subvalue2', $violations->get(4)->getMessage()); + + $this->assertEquals('key1', $violations->get(0)->getPropertyPath()); + $this->assertEquals('key2', $violations->get(1)->getPropertyPath()); + $this->assertEquals('key_with_properties', $violations->get(2)->getPropertyPath()); + $this->assertEquals('key_with_properties.subkey1', $violations->get(3)->getPropertyPath()); + $this->assertEquals('key_with_properties.subkey2', $violations->get(4)->getPropertyPath()); + } + + /** + * Setups a typed data object used for test purposes. + * + * @param array $tree + * An array of value, constraints and properties. + * + * @return \Drupal\Core\TypedData\TypedDataInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected function setupTypedData(array $tree, $name = '') { + $tree += ['constraints' => []]; + + if (isset($tree['properties'])) { + $properties = []; + foreach ($tree['properties'] as $property_name => $property) { + $sub_typed_data = $this->setupTypedData($property, $property_name); + $properties[$property_name] = $sub_typed_data; + } + $typed_data = $this->getMock('\Drupal\Tests\Core\TypedData\TraversableTypedDataInterfaceWithIterator'); + $typed_data->expects($this->any()) + ->method('getIterator') + ->willReturn(new \ArrayIterator($properties)); + } + else { + $typed_data = $this->getMock('\Drupal\Core\TypedData\TypedDataInterface'); + } + + $typed_data->expects($this->any()) + ->method('getValue') + ->willReturn($tree['value']); + $typed_data->expects($this->any()) + ->method('getConstraints') + ->willReturn($tree['constraints']); + $typed_data->expects($this->any()) + ->method('getName') + ->willReturn($name); + + return $typed_data; + } + +} + +interface TraversableTypedDataInterfaceWithIterator extends TraversableTypedDataInterface, \IteratorAggregate { + +} + diff --git a/core/tests/Drupal/Tests/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidatorTest.php b/core/tests/Drupal/Tests/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidatorTest.php index 0e90672..3a73b60 100644 --- a/core/tests/Drupal/Tests/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidatorTest.php +++ b/core/tests/Drupal/Tests/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidatorTest.php @@ -24,7 +24,7 @@ class PrimitiveTypeConstraintValidatorTest extends UnitTestCase { * @dataProvider provideTestValidate */ public function testValidate(PrimitiveInterface $typed_data, $value, $valid) { - $metadata = $this->getMockBuilder('Drupal\Core\TypedData\Validation\Metadata') + $metadata = $this->getMockBuilder('Drupal\Core\TypedData\Validation\GenericMetadata') ->disableOriginalConstructor() ->getMock(); $metadata->expects($this->any()) diff --git a/core/vendor/symfony/validator/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/core/vendor/symfony/validator/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index 191decd..6f82d6d 100644 --- a/core/vendor/symfony/validator/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/core/vendor/symfony/validator/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -92,7 +92,7 @@ public function atPath($path) /** * {@inheritdoc} */ - public function validate($value, $constraints = null, $groups = null) + public function validate($data, $constraints = null, $groups = null) { $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; @@ -115,9 +115,9 @@ public function validate($value, $constraints = null, $groups = null) $metadata->addConstraints($constraints); $this->validateGenericNode( - $value, + $data, null, - is_object($value) ? spl_object_hash($value) : null, + is_object($data) ? spl_object_hash($data) : null, $metadata, $this->defaultPropertyPath, $groups, @@ -134,9 +134,9 @@ public function validate($value, $constraints = null, $groups = null) // If an object is passed without explicit constraints, validate that // object against the constraints defined for the object's class - if (is_object($value)) { + if (is_object($data)) { $this->validateObject( - $value, + $data, $this->defaultPropertyPath, $groups, TraversalStrategy::IMPLICIT, @@ -151,9 +151,9 @@ public function validate($value, $constraints = null, $groups = null) // If an array is passed without explicit constraints, validate each // object in the array - if (is_array($value)) { + if (is_array($data)) { $this->validateEachObjectIn( - $value, + $data, $this->defaultPropertyPath, $groups, true, @@ -169,7 +169,7 @@ public function validate($value, $constraints = null, $groups = null) throw new RuntimeException(sprintf( 'Cannot validate values of type "%s" automatically. Please '. 'provide a constraint.', - gettype($value) + gettype($data) )); }