From 8715feb585db5adeb898b1f6d40f4decce6b57f7 Mon Sep 17 00:00:00 2001 From: Jibran Ijaz Date: Fri, 1 May 2015 22:23:04 +0500 Subject: [PATCH] 204 --- .../lib/Drupal/Core/TypedData/TypedDataManager.php | 16 +- .../Validation/ConstraintViolationBuilder.php | 220 +++++++++++++ .../Validation/ContextualValidatorInterface.php | 36 +++ .../Core/TypedData/Validation/ExecutionContext.php | 317 +++++++++++++++++++ .../Validation/ExecutionContextFactory.php | 50 +++ .../Drupal/Core/TypedData/Validation/Metadata.php | 115 ------- .../Core/TypedData/Validation/MetadataFactory.php | 62 ---- .../Validation/PropertyContainerMetadata.php | 72 ----- .../Core/TypedData/Validation/PropertyPath.php | 35 +++ .../Validation/RecursiveContextualValidator.php | 222 +++++++++++++ .../TypedData/Validation/RecursiveValidator.php | 119 +++++++ .../Validation/TypedDataAwareValidatorTrait.php | 37 +++ .../TypedData/Validation/TypedDataMetadata.php | 75 +++++ .../Drupal/Core/Validation/ConstraintManager.php | 11 +- .../AllowedValuesConstraintValidator.php | 38 ++- .../Constraint/ComplexDataConstraintValidator.php | 35 +-- .../Validation/Constraint/NotNullConstraint.php | 25 ++ .../Constraint/NotNullConstraintValidator.php | 36 +++ .../Validation/Constraint/NullConstraint.php | 25 ++ .../Constraint/NullConstraintValidator.php | 36 +++ .../PrimitiveTypeConstraintValidator.php | 5 +- .../Constraint/UniqueFieldValueValidator.php | 6 +- .../Constraint/ForumLeafConstraintValidator.php | 6 +- .../quickedit/src/Form/QuickEditFieldForm.php | 8 +- .../Validation/Constraint/UserMailRequired.php | 2 +- .../TypedData/RecursiveContextualValidatorTest.php | 347 +++++++++++++++++++++ .../PrimitiveTypeConstraintValidatorTest.php | 63 ++-- 27 files changed, 1685 insertions(+), 334 deletions(-) create mode 100644 core/lib/Drupal/Core/TypedData/Validation/ConstraintViolationBuilder.php create mode 100644 core/lib/Drupal/Core/TypedData/Validation/ContextualValidatorInterface.php create mode 100644 core/lib/Drupal/Core/TypedData/Validation/ExecutionContext.php create mode 100644 core/lib/Drupal/Core/TypedData/Validation/ExecutionContextFactory.php delete mode 100644 core/lib/Drupal/Core/TypedData/Validation/Metadata.php delete mode 100644 core/lib/Drupal/Core/TypedData/Validation/MetadataFactory.php delete mode 100644 core/lib/Drupal/Core/TypedData/Validation/PropertyContainerMetadata.php create mode 100644 core/lib/Drupal/Core/TypedData/Validation/PropertyPath.php create mode 100644 core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php create mode 100644 core/lib/Drupal/Core/TypedData/Validation/RecursiveValidator.php create mode 100644 core/lib/Drupal/Core/TypedData/Validation/TypedDataAwareValidatorTrait.php create mode 100755 core/lib/Drupal/Core/TypedData/Validation/TypedDataMetadata.php mode change 100644 => 100755 core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraintValidator.php create mode 100644 core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NotNullConstraint.php create mode 100644 core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NotNullConstraintValidator.php create mode 100644 core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NullConstraint.php create mode 100644 core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NullConstraintValidator.php create mode 100644 core/tests/Drupal/Tests/Core/TypedData/RecursiveContextualValidatorTest.php 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..eb6d2ec --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Validation/ConstraintViolationBuilder.php @@ -0,0 +1,220 @@ +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/ContextualValidatorInterface.php b/core/lib/Drupal/Core/TypedData/Validation/ContextualValidatorInterface.php new file mode 100644 index 0000000..c02bc62 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Validation/ContextualValidatorInterface.php @@ -0,0 +1,36 @@ +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($sub_path = '') { + return PropertyPath::append($this->propertyPath, $sub_path); + } + + /** + * {@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($cache_key, $constraint_hash) { + $this->validatedConstraints[$cache_key . ':' . $constraint_hash] = TRUE; + } + + /** + * {@inheritdoc} + */ + public function isConstraintValidated($cache_key, $constraint_hash) { + return isset($this->validatedConstraints[$cache_key . ':' . $constraint_hash]); + } + + /** + * {@inheritdoc} + */ + public function validateValue($value, $constraints, $subPath = '', $groups = NULL) { + throw new \LogicException('Legacy validator API is unsupported.'); + } + + /** + * {@inheritdoc} + */ + public function markGroupAsValidated($cache_key, $group_hash) { + $this->validatedObjects[$cache_key][$group_hash] = TRUE; + } + + /** + * {@inheritdoc} + */ + public function isGroupValidated($cache_key, $group_hash) { + return isset($this->validatedObjects[$cache_key][$group_hash]); + } + + /** + * {@inheritdoc} + */ + public function markObjectAsInitialized($cache_key) { + // Not supported, so nothing todo. + } + + /** + * {@inheritdoc} + */ + public function isObjectInitialized($cache_key) { + // 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..a032c1e --- /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) { + // @todo This value is not used at the moment. + $this->defaultPropertyPath = $this->context->getPropertyPath($path); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function validate($data, $constraints = NULL, $groups = NULL, $is_root_call = TRUE) { + 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, $is_root_call); + 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. + * @param bool $is_root_call + * (optional) Whether its the most upper call in the type data tree. + * + * @return $this + */ + protected function validateNode(TypedDataInterface $data, $constraints = NULL, $is_root_call = FALSE) { + $previous_value = $this->context->getValue(); + $previous_object = $this->context->getObject(); + $previous_metadata = $this->context->getMetadata(); + $previous_path = $this->context->getPropertyPath(); + + $metadata = $this->metadataFactory->getMetadataFor($data); + $cache_key = spl_object_hash($data); + $property_path = $is_root_call ? '' : PropertyPath::append($previous_path, $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, $property_path); + + if (isset($constraints) || !$this->context->isGroupValidated($cache_key, Constraint::DEFAULT_GROUP)) { + if (!isset($constraints)) { + $this->context->markGroupAsValidated($cache_key, Constraint::DEFAULT_GROUP); + $constraints = $metadata->findConstraints(Constraint::DEFAULT_GROUP); + } + $this->validateConstraints($value, $cache_key, $constraints); + } + + // If the data is a list or complex data, validate the contained list items + // or properties. However, do not recurse if the data is empty. + if (($data instanceof ListInterface || $data instanceof ComplexDataInterface) && !$data->isEmpty()) { + foreach ($data as $name => $property) { + $this->validateNode($property); + } + } + + $this->context->setNode($previous_value, $previous_object, $previous_metadata, $previous_path); + + return $this; + } + + /** + * Validates a node's value against all constraints in the given group. + * + * @param mixed $value + * The validated value. + */ + protected function validateConstraints($value, $cache_key, $constraints) { + foreach ($constraints as $constraint) { + // Prevent duplicate validation of constraints, in the case + // that constraints belong to multiple validated groups + if (isset($cache_key)) { + $constraint_hash = spl_object_hash($constraint); + + if ($this->context->isConstraintValidated($cache_key, $constraint_hash)) { + continue; + } + + $this->context->markConstraintAsValidated($cache_key, $constraint_hash); + } + + $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 TypedDataInterface) { + throw new \InvalidArgumentException('The passed in object has to be typed data.'); + } + elseif (!$object instanceof ListInterface && !$object instanceof ComplexDataInterface) { + throw new \InvalidArgumentException('Passed data does not contain properties.'); + } + return $this->validateNode($object->get($propertyName), NULL, TRUE); + } + + /** + * {@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 TypedDataInterface) { + throw new \InvalidArgumentException('The passed in object has to be typed data.'); + } + elseif (!$object instanceof ListInterface && !$object instanceof ComplexDataInterface) { + throw new \InvalidArgumentException('Passed data does not contain properties.'); + } + $data = $object->get($property_name); + $metadata = $this->metadataFactory->getMetadataFor($data); + $constraints = $metadata->findConstraints(Constraint::DEFAULT_GROUP); + return $this->validate($value, $constraints, $groups, TRUE); + } + +} 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..b590f2f --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Validation/TypedDataAwareValidatorTrait.php @@ -0,0 +1,37 @@ +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..94a522e --- 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,28 @@ */ 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) + // Specifically pass along FALSE as $root_call, as we validate the data + // as part of the typed data tree. + ->validate($data->get($name), $constraints, NULL, FALSE); } } + } 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..f463c3a 100644 --- a/core/modules/user/src/Plugin/Validation/Constraint/UserMailRequired.php +++ b/core/modules/user/src/Plugin/Validation/Constraint/UserMailRequired.php @@ -58,7 +58,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/tests/Drupal/Tests/Core/TypedData/RecursiveContextualValidatorTest.php b/core/tests/Drupal/Tests/Core/TypedData/RecursiveContextualValidatorTest.php new file mode 100644 index 0000000..36b0b53 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/TypedData/RecursiveContextualValidatorTest.php @@ -0,0 +1,347 @@ + $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->assertCount(0, $violations); + } + + /** + * @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->assertCount(1, $violations); + // 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->assertCount(2, $violations); + } + + /** + * @covers ::validate + */ + public function testPropertiesValidateWithMultipleLevels() { + + $typed_data = $this->buildExampleTypedDataWithProperties(); + + $violations = $this->recursiveValidator->validate($typed_data); + $this->assertCount(6, $violations); + + $this->assertEquals('violation: 3', $violations->get(0)->getMessage()); + $this->assertEquals('violation: value1', $violations->get(1)->getMessage()); + $this->assertEquals('violation: value2', $violations->get(2)->getMessage()); + $this->assertEquals('violation: 2', $violations->get(3)->getMessage()); + $this->assertEquals('violation: subvalue1', $violations->get(4)->getMessage()); + $this->assertEquals('violation: subvalue2', $violations->get(5)->getMessage()); + + $this->assertEquals('', $violations->get(0)->getPropertyPath()); + $this->assertEquals('key1', $violations->get(1)->getPropertyPath()); + $this->assertEquals('key2', $violations->get(2)->getPropertyPath()); + $this->assertEquals('key_with_properties', $violations->get(3)->getPropertyPath()); + $this->assertEquals('key_with_properties.subkey1', $violations->get(4)->getPropertyPath()); + $this->assertEquals('key_with_properties.subkey2', $violations->get(5)->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 = '') { + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('violation: ' . (is_array($value) ? count($value) : $value)); + }; + + $tree += ['constraints' => []]; + + if (isset($tree['properties'])) { + $map_data_definition = MapDataDefinition::create(); + $map_data_definition->addConstraint('Callback', ['callback' => $callback]); + foreach ($tree['properties'] as $property_name => $property) { + $sub_typed_data = $this->setupTypedData($property, $property_name); + $map_data_definition->setPropertyDefinition($property_name, $sub_typed_data->getDataDefinition()); + } + $typed_data = $this->typedDataManager->create( + $map_data_definition, + $tree['value'], + $name + ); + } + else { + /** @var \Drupal\Core\TypedData\TypedDataInterface $typed_data */ + $typed_data = $this->typedDataManager->create( + DataDefinition::create('string') + ->addConstraint('Callback', ['callback' => $callback]), + $tree['value'], + $name + ); + } + + return $typed_data; + } + + /** + * @covers ::validateProperty + * + * @expectedException \LogicException + */ + public function testValidatePropertyWithCustomGroup() { + $tree = [ + 'value' => [], + 'properties' => [ + 'key1' => ['value' => 'value1'], + ], + ]; + $typed_data = $this->setupTypedData($tree, 'test_name'); + $this->recursiveValidator->validateProperty($typed_data, 'key1', 'test group'); + } + + /** + * @covers ::validateProperty + * + * @dataProvider providerTestValidatePropertyWithInvalidObjects + * + * @expectedException \InvalidArgumentException + */ + public function testValidatePropertyWithInvalidObjects($object) { + $this->recursiveValidator->validateProperty($object, 'key1', NULL); + } + + /** + * Provides data for testValidatePropertyWithInvalidObjects. + * @return array + */ + public function providerTestValidatePropertyWithInvalidObjects() { + $data = []; + $data[] = [new \stdClass()]; + $data[] = [new TestClass()]; + + $data[] = [$this->getMock('Drupal\Core\TypedData\TypedDataInterface')]; + + return $data; + } + + /** + * @covers ::validateProperty + */ + public function testValidateProperty() { + $typed_data = $this->buildExampleTypedDataWithProperties(); + + $violations = $this->recursiveValidator->validateProperty($typed_data, 'key_with_properties'); + $this->assertCount(3, $violations); + + $this->assertEquals('violation: 2', $violations->get(0)->getMessage()); + $this->assertEquals('violation: subvalue1', $violations->get(1)->getMessage()); + $this->assertEquals('violation: subvalue2', $violations->get(2)->getMessage()); + + $this->assertEquals('', $violations->get(0)->getPropertyPath()); + $this->assertEquals('subkey1', $violations->get(1)->getPropertyPath()); + $this->assertEquals('subkey2', $violations->get(2)->getPropertyPath()); + } + + /** + * @covers ::validatePropertyValue + * + * @dataProvider providerTestValidatePropertyWithInvalidObjects + * + * @expectedException \InvalidArgumentException + */ + public function testValidatePropertyValueWithInvalidObjects($object) { + $this->recursiveValidator->validatePropertyValue($object, 'key1', [], NULL); + } + + /** + * @covers ::validatePropertyValue + */ + public function testValidatePropertyValue() { + $typed_data = $this->buildExampleTypedDataWithProperties(['subkey1' => 'subvalue11', 'subkey2' => 'subvalue22']); + + $violations = $this->recursiveValidator->validatePropertyValue($typed_data, 'key_with_properties', $typed_data->get('key_with_properties')); + $this->assertCount(3, $violations); + + $this->assertEquals('violation: 2', $violations->get(0)->getMessage()); + $this->assertEquals('violation: subvalue11', $violations->get(1)->getMessage()); + $this->assertEquals('violation: subvalue22', $violations->get(2)->getMessage()); + + $this->assertEquals('', $violations->get(0)->getPropertyPath()); + $this->assertEquals('subkey1', $violations->get(1)->getPropertyPath()); + $this->assertEquals('subkey2', $violations->get(2)->getPropertyPath()); + } + + /** + * + * Builds some example type data object. + * + * @return \Drupal\Core\TypedData\TypedDataInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected function buildExampleTypedDataWithProperties($subkey_value = NULL) { + $subkey_value = $subkey_value ?: ['subkey1' => 'subvalue1', 'subkey2' => 'subvalue2']; + $tree = [ + 'value' => [ + 'key1' => 'value1', + 'key2' => 'value2', + 'key_with_properties' => $subkey_value + ], + ]; + $tree['properties'] = [ + 'key1' => [ + 'value' => 'value1', + ], + 'key2' => [ + 'value' => 'value2', + ], + 'key_with_properties' => [ + 'value' => $subkey_value ?: ['subkey1' => 'subvalue1', 'subkey2' => 'subvalue2'], + ], + ]; + $tree['properties']['key_with_properties']['properties']['subkey1'] = ['value' => $tree['properties']['key_with_properties']['value']['subkey1']]; + $tree['properties']['key_with_properties']['properties']['subkey2'] = ['value' => $tree['properties']['key_with_properties']['value']['subkey2']]; + + return $this->setupTypedData($tree, 'test_name'); + } + +} + +class TestClass { + +} 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..2bcb318 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 @@ -7,6 +7,12 @@ namespace Drupal\Tests\Core\Validation\Plugin\Validation\Constraint; +use Drupal\Core\TypedData\DataDefinition; +use Drupal\Core\TypedData\Plugin\DataType\BooleanData; +use Drupal\Core\TypedData\Plugin\DataType\FloatData; +use Drupal\Core\TypedData\Plugin\DataType\IntegerData; +use Drupal\Core\TypedData\Plugin\DataType\StringData; +use Drupal\Core\TypedData\Plugin\DataType\Uri; use Drupal\Core\TypedData\PrimitiveInterface; use Drupal\Core\Validation\Plugin\Validation\Constraint\PrimitiveTypeConstraint; use Drupal\Core\Validation\Plugin\Validation\Constraint\PrimitiveTypeConstraintValidator; @@ -24,17 +30,10 @@ class PrimitiveTypeConstraintValidatorTest extends UnitTestCase { * @dataProvider provideTestValidate */ public function testValidate(PrimitiveInterface $typed_data, $value, $valid) { - $metadata = $this->getMockBuilder('Drupal\Core\TypedData\Validation\Metadata') - ->disableOriginalConstructor() - ->getMock(); - $metadata->expects($this->any()) - ->method('getTypedData') - ->willReturn($typed_data); - - $context = $this->getMock('Symfony\Component\Validator\ExecutionContextInterface'); + $context = $this->getMock('\Symfony\Component\Validator\Context\ExecutionContextInterface'); $context->expects($this->any()) - ->method('getMetadata') - ->willReturn($metadata); + ->method('getObject') + ->willReturn($typed_data); if ($valid) { $context->expects($this->never()) @@ -54,31 +53,31 @@ public function testValidate(PrimitiveInterface $typed_data, $value, $valid) { public function provideTestValidate() { $data = []; - $data[] = [$this->getMock('Drupal\Core\TypedData\Type\BooleanInterface'), NULL, TRUE]; + $data[] = [new BooleanData(DataDefinition::create('boolean')), NULL, TRUE]; - $data[] = [$this->getMock('Drupal\Core\TypedData\Type\BooleanInterface'), 1, TRUE]; - $data[] = [$this->getMock('Drupal\Core\TypedData\Type\BooleanInterface'), 'test', FALSE]; - $data[] = [$this->getMock('Drupal\Core\TypedData\Type\FloatInterface'), 1.5, TRUE]; - $data[] = [$this->getMock('Drupal\Core\TypedData\Type\FloatInterface'), 'test', FALSE]; - $data[] = [$this->getMock('Drupal\Core\TypedData\Type\IntegerInterface'), 1, TRUE]; - $data[] = [$this->getMock('Drupal\Core\TypedData\Type\IntegerInterface'), 1.5, FALSE]; - $data[] = [$this->getMock('Drupal\Core\TypedData\Type\IntegerInterface'), 'test', FALSE]; - $data[] = [$this->getMock('Drupal\Core\TypedData\Type\StringInterface'), 'test', TRUE]; + $data[] = [new BooleanData(DataDefinition::create('boolean')), 1, TRUE]; + $data[] = [new BooleanData(DataDefinition::create('boolean')), 'test', FALSE]; + $data[] = [new FloatData(DataDefinition::create('float')), 1.5, TRUE]; + $data[] = [new FloatData(DataDefinition::create('float')), 'test', FALSE]; + $data[] = [new IntegerData(DataDefinition::create('integer')), 1, TRUE]; + $data[] = [new IntegerData(DataDefinition::create('integer')), 1.5, FALSE]; + $data[] = [new IntegerData(DataDefinition::create('integer')), 'test', FALSE]; + $data[] = [new StringData(DataDefinition::create('string')), 'test', TRUE]; // It is odd that 1 is a valid string. // $data[] = [$this->getMock('Drupal\Core\TypedData\Type\StringInterface'), 1, FALSE]; - $data[] = [$this->getMock('Drupal\Core\TypedData\Type\StringInterface'), [], FALSE]; - $data[] = [$this->getMock('Drupal\Core\TypedData\Type\UriInterface'), 'http://www.drupal.org', TRUE]; - $data[] = [$this->getMock('Drupal\Core\TypedData\Type\UriInterface'), 'https://www.drupal.org', TRUE]; - $data[] = [$this->getMock('Drupal\Core\TypedData\Type\UriInterface'), 'Invalid', FALSE]; - $data[] = [$this->getMock('Drupal\Core\TypedData\Type\UriInterface'), 'entity:node/1', TRUE]; - $data[] = [$this->getMock('Drupal\Core\TypedData\Type\UriInterface'), 'base:', TRUE]; - $data[] = [$this->getMock('Drupal\Core\TypedData\Type\UriInterface'), 'base:node', TRUE]; - $data[] = [$this->getMock('Drupal\Core\TypedData\Type\UriInterface'), 'internal:', TRUE]; - $data[] = [$this->getMock('Drupal\Core\TypedData\Type\UriInterface'), 'public://', FALSE]; - $data[] = [$this->getMock('Drupal\Core\TypedData\Type\UriInterface'), 'public://foo.png', TRUE]; - $data[] = [$this->getMock('Drupal\Core\TypedData\Type\UriInterface'), 'private://', FALSE]; - $data[] = [$this->getMock('Drupal\Core\TypedData\Type\UriInterface'), 'private://foo.png', TRUE]; - $data[] = [$this->getMock('Drupal\Core\TypedData\Type\UriInterface'), 'drupal.org', FALSE]; + $data[] = [new StringData(DataDefinition::create('string')), [], FALSE]; + $data[] = [new Uri(DataDefinition::create('uri')), 'http://www.drupal.org', TRUE]; + $data[] = [new Uri(DataDefinition::create('uri')), 'https://www.drupal.org', TRUE]; + $data[] = [new Uri(DataDefinition::create('uri')), 'Invalid', FALSE]; + $data[] = [new Uri(DataDefinition::create('uri')), 'entity:node/1', TRUE]; + $data[] = [new Uri(DataDefinition::create('uri')), 'base:', TRUE]; + $data[] = [new Uri(DataDefinition::create('uri')), 'base:node', TRUE]; + $data[] = [new Uri(DataDefinition::create('uri')), 'internal:', TRUE]; + $data[] = [new Uri(DataDefinition::create('uri')), 'public://', FALSE]; + $data[] = [new Uri(DataDefinition::create('uri')), 'public://foo.png', TRUE]; + $data[] = [new Uri(DataDefinition::create('uri')), 'private://', FALSE]; + $data[] = [new Uri(DataDefinition::create('uri')), 'private://foo.png', TRUE]; + $data[] = [new Uri(DataDefinition::create('uri')), 'drupal.org', FALSE]; return $data; } -- 2.1.4