From ef6f5f50c5c4c77662d05d1429311c2b17a43855 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 28 Jul 2014 16:24:30 +0200 Subject: [PATCH 1/2] [Validator] Fixed: Made it possible (again) to pass a class name to Validator::validatePropertyValue() --- .../Tests/Validator/AbstractValidatorTest.php | 51 +++++++++++++++++++ src/Symfony/Component/Validator/Validator.php | 8 ++- .../ContextualValidatorInterface.php | 14 ++--- .../RecursiveContextualValidator.php | 21 ++++++-- .../Validator/RecursiveValidator.php | 7 +-- .../Validator/ValidatorInterface.php | 14 ++--- .../Validator/ValidatorInterface.php | 2 +- 7 files changed, 92 insertions(+), 25 deletions(-) diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index 2808d38137..952f5111a7 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -904,6 +904,57 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase $this->assertNull($violations[0]->getCode()); } + public function testValidatePropertyValueWithClassName() + { + $test = $this; + + $callback1 = function ($value, ExecutionContextInterface $context) use ($test) { + $propertyMetadatas = $test->metadata->getPropertyMetadata('firstName'); + + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertSame('firstName', $context->getPropertyName()); + $test->assertSame('', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($propertyMetadatas[0], $context->getMetadata()); + $test->assertSame('Bernhard', $context->getRoot()); + $test->assertSame('Bernhard', $context->getValue()); + $test->assertSame('Bernhard', $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Other violation'); + }; + + $this->metadata->addPropertyConstraint('firstName', new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->metadata->addPropertyConstraint('lastName', new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validatePropertyValue( + self::ENTITY_CLASS, + 'firstName', + 'Bernhard', + 'Group' + ); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('', $violations[0]->getPropertyPath()); + $this->assertSame('Bernhard', $violations[0]->getRoot()); + $this->assertSame('Bernhard', $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + /** * Cannot be UnsupportedMetadataException for BC with Symfony < 2.5. * diff --git a/src/Symfony/Component/Validator/Validator.php b/src/Symfony/Component/Validator/Validator.php index f907287d3d..87b1b394a2 100644 --- a/src/Symfony/Component/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator.php @@ -142,7 +142,7 @@ class Validator implements ValidatorInterface, Mapping\Factory\MetadataFactoryIn */ public function validatePropertyValue($containingValue, $property, $value, $groups = null) { - $visitor = $this->createVisitor($containingValue); + $visitor = $this->createVisitor(is_object($containingValue) ? $containingValue : $value); $metadata = $this->metadataFactory->getMetadataFor($containingValue); if (!$metadata instanceof PropertyMetadataContainerInterface) { @@ -153,13 +153,17 @@ class Validator implements ValidatorInterface, Mapping\Factory\MetadataFactoryIn throw new ValidatorException(sprintf('The metadata for '.$valueAsString.' does not support properties.')); } + // If $containingValue is passed as class name, take $value as root + // and start the traversal with an empty property path + $propertyPath = is_object($containingValue) ? $property : ''; + foreach ($this->resolveGroups($groups) as $group) { if (!$metadata->hasPropertyMetadata($property)) { continue; } foreach ($metadata->getPropertyMetadata($property) as $propMeta) { - $propMeta->accept($visitor, $value, $group, $property); + $propMeta->accept($visitor, $value, $group, $propertyPath); } } diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php index 83b5d07120..767f89585f 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php @@ -68,16 +68,16 @@ interface ContextualValidatorInterface * Validates a value against the constraints specified for an object's * property. * - * @param object $object The object - * @param string $propertyName The name of the property - * @param mixed $value The value to validate against the - * property's constraints - * @param array|null $groups The validation groups to validate. If - * none is given, "Default" is assumed + * @param object|string $objectOrClass The object or its class name + * @param string $propertyName The name of the property + * @param mixed $value The value to validate against the + * property's constraints + * @param array|null $groups The validation groups to validate. If + * none is given, "Default" is assumed * * @return ContextualValidatorInterface This validator */ - public function validatePropertyValue($object, $propertyName, $value, $groups = null); + public function validatePropertyValue($objectOrClass, $propertyName, $value, $groups = null); /** * Returns the violations that have been generated so far in the context diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index 581b2f17f5..29d0c6c00c 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -194,6 +194,7 @@ class RecursiveContextualValidator implements ContextualValidatorInterface $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; $cacheKey = spl_object_hash($object); + $propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName); $previousValue = $this->context->getValue(); $previousObject = $this->context->getObject(); @@ -209,7 +210,7 @@ class RecursiveContextualValidator implements ContextualValidatorInterface $object, $cacheKey.':'.$propertyName, $propertyMetadata, - PropertyPath::append($this->defaultPropertyPath, $propertyName), + $propertyPath, $groups, null, TraversalStrategy::IMPLICIT, @@ -226,9 +227,9 @@ class RecursiveContextualValidator implements ContextualValidatorInterface /** * {@inheritdoc} */ - public function validatePropertyValue($object, $propertyName, $value, $groups = null) + public function validatePropertyValue($objectOrClass, $propertyName, $value, $groups = null) { - $classMetadata = $this->metadataFactory->getMetadataFor($object); + $classMetadata = $this->metadataFactory->getMetadataFor($objectOrClass); if (!$classMetadata instanceof ClassMetadataInterface) { // Cannot be UnsupportedMetadataException because of BC with @@ -243,7 +244,17 @@ class RecursiveContextualValidator implements ContextualValidatorInterface $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - $cacheKey = spl_object_hash($object); + + if (is_object($objectOrClass)) { + $object = $objectOrClass; + $cacheKey = spl_object_hash($objectOrClass); + $propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName); + } else { + // $objectOrClass contains a class name + $object = null; + $cacheKey = null; + $propertyPath = $this->defaultPropertyPath; + } $previousValue = $this->context->getValue(); $previousObject = $this->context->getObject(); @@ -257,7 +268,7 @@ class RecursiveContextualValidator implements ContextualValidatorInterface $object, $cacheKey.':'.$propertyName, $propertyMetadata, - PropertyPath::append($this->defaultPropertyPath, $propertyName), + $propertyPath, $groups, null, TraversalStrategy::IMPLICIT, diff --git a/src/Symfony/Component/Validator/Validator/RecursiveValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveValidator.php index c7ec2793ec..ddf0850c1c 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveValidator.php @@ -130,10 +130,11 @@ class RecursiveValidator implements ValidatorInterface /** * {@inheritdoc} */ - public function validatePropertyValue($object, $propertyName, $value, $groups = null) + public function validatePropertyValue($objectOrClass, $propertyName, $value, $groups = null) { - return $this->startContext($object) - ->validatePropertyValue($object, $propertyName, $value, $groups) + // If a class name is passed, take $value as root + return $this->startContext(is_object($objectOrClass) ? $objectOrClass : $value) + ->validatePropertyValue($objectOrClass, $propertyName, $value, $groups) ->getViolations(); } } diff --git a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php index acbb28f03a..2582bf65c8 100644 --- a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php @@ -62,18 +62,18 @@ interface ValidatorInterface extends MetadataFactoryInterface * Validates a value against the constraints specified for an object's * property. * - * @param object $object The object - * @param string $propertyName The name of the property - * @param mixed $value The value to validate against the - * property's constraints - * @param array|null $groups The validation groups to validate. If - * none is given, "Default" is assumed + * @param object|string $objectOrClass The object or its class name + * @param string $propertyName The name of the property + * @param mixed $value The value to validate against the + * property's constraints + * @param array|null $groups The validation groups to validate. If + * none is given, "Default" is assumed * * @return ConstraintViolationListInterface A list of constraint violations. * If the list is empty, validation * succeeded */ - public function validatePropertyValue($object, $propertyName, $value, $groups = null); + public function validatePropertyValue($objectOrClass, $propertyName, $value, $groups = null); /** * Starts a new validation context and returns a validator for that context. diff --git a/src/Symfony/Component/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/ValidatorInterface.php index fd4e3b9e0b..03c8921bb2 100644 --- a/src/Symfony/Component/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/ValidatorInterface.php @@ -68,7 +68,7 @@ interface ValidatorInterface * The accepted values depend on the {@link MetadataFactoryInterface} * implementation. * - * @param string $containingValue The value containing the property. + * @param mixed $containingValue The value containing the property. * @param string $property The name of the property to validate * @param string $value The value to validate against the * constraints of the property. From 2bf1b375c16a38a193a3aa55d1e2506aafef86f0 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 28 Jul 2014 16:32:19 +0200 Subject: [PATCH 2/2] [Validator] Fixed ExpressionValidator when the validation root is not an object --- .../Constraints/ExpressionValidator.php | 16 ++++-- .../Constraints/ExpressionValidatorTest.php | 56 +++++++++++++++++++ 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php b/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php index 243604d066..3df23d4342 100644 --- a/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php +++ b/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php @@ -74,14 +74,18 @@ class ExpressionValidator extends ConstraintValidator $variables['value'] = $value; $variables['this'] = $value; } else { - // Extract the object that the property belongs to from the object - // graph - $path = new PropertyPath($this->context->getPropertyPath()); - $parentPath = $path->getParent(); $root = $this->context->getRoot(); - $variables['value'] = $value; - $variables['this'] = $parentPath ? $this->getPropertyAccessor()->getValue($root, $parentPath) : $root; + + if (is_object($root)) { + // Extract the object that the property belongs to from the object + // graph + $path = new PropertyPath($this->context->getPropertyPath()); + $parentPath = $path->getParent(); + $variables['this'] = $parentPath ? $this->getPropertyAccessor()->getValue($root, $parentPath) : $root; + } else { + $variables['this'] = null; + } } if (!$this->getExpressionLanguage()->evaluate($constraint->expression, $variables)) { diff --git a/src/Symfony/Component/Validator/Tests/Constraints/ExpressionValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/ExpressionValidatorTest.php index b71138e5f6..ac62ce2420 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/ExpressionValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/ExpressionValidatorTest.php @@ -194,4 +194,60 @@ class ExpressionValidatorTest extends \PHPUnit_Framework_TestCase $this->validator->validate('2', $constraint); } + + /** + * When validatePropertyValue() is called with a class name + * https://github.com/symfony/symfony/pull/11498 + */ + public function testSucceedingExpressionAtPropertyLevelWithoutRoot() + { + $constraint = new Expression('value == "1"'); + + $this->context->expects($this->any()) + ->method('getPropertyName') + ->will($this->returnValue('property')); + + $this->context->expects($this->any()) + ->method('getPropertyPath') + ->will($this->returnValue('')); + + $this->context->expects($this->any()) + ->method('getRoot') + ->will($this->returnValue('1')); + + $this->context->expects($this->never()) + ->method('addViolation'); + + $this->validator->validate('1', $constraint); + } + + /** + * When validatePropertyValue() is called with a class name + * https://github.com/symfony/symfony/pull/11498 + */ + public function testFailingExpressionAtPropertyLevelWithoutRoot() + { + $constraint = new Expression(array( + 'expression' => 'value == "1"', + 'message' => 'myMessage', + )); + + $this->context->expects($this->any()) + ->method('getPropertyName') + ->will($this->returnValue('property')); + + $this->context->expects($this->any()) + ->method('getPropertyPath') + ->will($this->returnValue('')); + + $this->context->expects($this->any()) + ->method('getRoot') + ->will($this->returnValue('2')); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('myMessage'); + + $this->validator->validate('2', $constraint); + } }