diff --git a/core/lib/Drupal/Core/TypedData/Validation/ConstraintViolationBuilder.php b/core/lib/Drupal/Core/TypedData/Validation/ConstraintViolationBuilder.php index 9fb6d22..bba76bd 100644 --- a/core/lib/Drupal/Core/TypedData/Validation/ConstraintViolationBuilder.php +++ b/core/lib/Drupal/Core/TypedData/Validation/ConstraintViolationBuilder.php @@ -7,6 +7,7 @@ use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Util\PropertyPath; +use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; /** * Implements the ConstraintViolationBuilderInterface. diff --git a/core/lib/Drupal/Core/TypedData/Validation/ExecutionContext.php b/core/lib/Drupal/Core/TypedData/Validation/ExecutionContext.php index ddbe8fe..c55f495 100644 --- a/core/lib/Drupal/Core/TypedData/Validation/ExecutionContext.php +++ b/core/lib/Drupal/Core/TypedData/Validation/ExecutionContext.php @@ -7,10 +7,14 @@ namespace Drupal\Core\TypedData\Validation; -use Symfony\Component\CssSelector\XPath\TranslatorInterface; +use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintViolation; +use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Mapping\MetadataInterface; +use Symfony\Component\Validator\Util\PropertyPath; +use Symfony\Component\Validator\Validator\ValidatorInterface; /** * diff --git a/core/lib/Drupal/Core/TypedData/Validation/ExecutionContextFactory.php b/core/lib/Drupal/Core/TypedData/Validation/ExecutionContextFactory.php index 46a5697..1c9313f 100644 --- a/core/lib/Drupal/Core/TypedData/Validation/ExecutionContextFactory.php +++ b/core/lib/Drupal/Core/TypedData/Validation/ExecutionContextFactory.php @@ -8,6 +8,7 @@ namespace Drupal\Core\TypedData\Validation; use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Validator\Context\ExecutionContextFactoryInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; /** diff --git a/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php b/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php index ab5ec73..5bc31be 100644 --- a/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php +++ b/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php @@ -8,10 +8,11 @@ namespace Drupal\Core\TypedData\Validation; use Drupal\Core\TypedData\TraversableTypedDataInterface; +use Drupal\Core\TypedData\TypedDataInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface;; -use Symfony\Component\Validator\Mapping\MetadataFactoryInterface; +use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface; use Symfony\Component\Validator\Util\PropertyPath; use Symfony\Component\Validator\Validator\ContextualValidatorInterface; @@ -65,6 +66,11 @@ public function validate($value, $constraints = NULL, $groups = NULL) { if (isset($groups)) { throw new \LogicException('Passing custom groups is not supported.'); } + + if (!$value 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)) { @@ -72,6 +78,7 @@ public function validate($value, $constraints = NULL, $groups = NULL) { } $this->validateNode($value, $constraints); + return $this; } /** @@ -85,7 +92,7 @@ protected function validateNode(TypedDataInterface $data, $constraints = NULL) { $metadata = $this->metadataFactory->getMetadataFor($data); $cacheKey = spl_object_hash($data); - $propertyPath = PropertyPath::append($this->defaultPropertyPath, $data->getName()); + $propertyPath = PropertyPath::append($previousPath, $data->getName()); $this->context->setNode($data, $data, $metadata, $propertyPath); if (!$this->context->isGroupValidated($cacheKey, Constraint::DEFAULT_GROUP)) { diff --git a/core/lib/Drupal/Core/TypedData/Validation/RecursiveValidator.php b/core/lib/Drupal/Core/TypedData/Validation/RecursiveValidator.php index 7f8075a..e0e8ded 100644 --- a/core/lib/Drupal/Core/TypedData/Validation/RecursiveValidator.php +++ b/core/lib/Drupal/Core/TypedData/Validation/RecursiveValidator.php @@ -7,6 +7,7 @@ namespace Drupal\Core\TypedData\Validation; +use Drupal\Core\TypedData\TypedDataInterface; use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; use Symfony\Component\Validator\Context\ExecutionContextFactoryInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; @@ -34,9 +35,8 @@ class RecursiveValidator implements ValidatorInterface { * creating new contexts * @param \Symfony\Component\Validator\ConstraintValidatorFactoryInterface $validator_factory */ - public function __construct(ExecutionContextInterface $context, ConstraintValidatorFactoryInterface $validator_factory) { - $this->context = $context; - $this->defaultPropertyPath = $context->getPropertyPath(); + public function __construct(ExecutionContextFactoryInterface $context_factory, ConstraintValidatorFactoryInterface $validator_factory) { + $this->contextFactory = $context_factory; $this->constraintValidatorFactory = $validator_factory; } diff --git a/core/lib/Drupal/Core/TypedData/Validation/TypedDataMetadata.php b/core/lib/Drupal/Core/TypedData/Validation/TypedDataMetadata.php index 36eae94..14f6b21 100755 --- a/core/lib/Drupal/Core/TypedData/Validation/TypedDataMetadata.php +++ b/core/lib/Drupal/Core/TypedData/Validation/TypedDataMetadata.php @@ -10,6 +10,7 @@ use Drupal\Core\TypedData\TypedDataInterface; use Drupal\Core\TypedData\TypedDataManager; use Symfony\Component\Validator\Exception\BadMethodCallException; +use Symfony\Component\Validator\Mapping\CascadingStrategy; use Symfony\Component\Validator\Mapping\TraversalStrategy; use Symfony\Component\Validator\ValidationVisitorInterface; 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..fcd14b7 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/TypedData/RecursiveContextualValidatorTest.php @@ -0,0 +1,221 @@ +contextFactory = new ExecutionContextFactory($translator); + $this->validatorFactory = new ConstraintValidatorFactory(); + $this->recursiveValidator = new RecursiveValidator($this->contextFactory, $this->validatorFactory); + } + + /** + * @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->getMock('\Drupal\Core\TypedData\TypedDataInterface'); + $typed_data->expects($this->any()) + ->method('getConstraints') + ->willReturn([]); + $violations = $this->recursiveValidator->validate($typed_data); + $this->assertEquals(0, $violations->count()); + } + + /** + * @covers ::validate + */ + public function testBasicValidateWithConstraint() { + $callback_constraint = new Callback([ + 'callback' => function ($value, ExecutionContextInterface $context) { + $context->addViolation('test violation: ' . spl_object_hash($value)); + } + ]); + + $typed_data = $this->getMock('\Drupal\Core\TypedData\TypedDataInterface'); + $typed_data->expects($this->any()) + ->method('getConstraints') + ->willReturn([$callback_constraint]); + + $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: ' . spl_object_hash($typed_data), $violations->get(0)->getMessage()); + } + + /** + * @covers ::validate + */ + public function testBasicValidateWithMultipleConstraints() { + $callback_constraint = new Callback([ + 'callback' => function ($value, ExecutionContextInterface $context) { + $context->addViolation('test violation'); + } + ]); + + $typed_data = $this->getMock('\Drupal\Core\TypedData\TypedDataInterface'); + $typed_data->expects($this->any()) + ->method('getConstraints') + ->willReturn([$callback_constraint, clone $callback_constraint]); + + $violations = $this->recursiveValidator->validate($typed_data); + $this->assertEquals(2, $violations->count()); + } + + /** + * @covers ::validate + */ + public function testPropertiesValidateWithMultipleLevels() { + $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 { + +} +