From 07c5aa6822286ac0d821e7c4d6378378c942483c Mon Sep 17 00:00:00 2001 From: Maxime Steinhausser Date: Tue, 4 Jul 2017 22:50:18 +0200 Subject: [PATCH] [Validator] Allow to use a property path to get value to compare in comparison constraints --- src/Symfony/Component/Validator/CHANGELOG.md | 1 + .../Constraints/AbstractComparison.php | 19 +++-- .../AbstractComparisonValidator.php | 34 +++++++- .../AbstractComparisonValidatorTestCase.php | 85 ++++++++++++++++++- .../Constraints/EqualToValidatorTest.php | 10 +++ .../GreaterThanOrEqualValidatorTest.php | 11 +++ .../Constraints/GreaterThanValidatorTest.php | 10 +++ .../Constraints/IdenticalToValidatorTest.php | 10 +++ .../LessThanOrEqualValidatorTest.php | 11 +++ .../Constraints/LessThanValidatorTest.php | 10 +++ .../Constraints/NotEqualToValidatorTest.php | 10 +++ .../NotIdenticalToValidatorTest.php | 10 +++ src/Symfony/Component/Validator/composer.json | 2 + 13 files changed, 216 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 2c767cd2ca..adffe035fc 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * setting the `checkDNS` option of the `Url` constraint to `true` is deprecated in favor of the `Url::CHECK_DNS_TYPE_*` constants values and will throw an exception in Symfony 4.0 * added min/max amount of pixels check to `Image` constraint via `minPixels` and `maxPixels` + * added a new "propertyPath" option to comparison constraints in order to get the value to compare from an array or object 3.3.0 ----- diff --git a/src/Symfony/Component/Validator/Constraints/AbstractComparison.php b/src/Symfony/Component/Validator/Constraints/AbstractComparison.php index e20a8f3fb7..c41f371e3a 100644 --- a/src/Symfony/Component/Validator/Constraints/AbstractComparison.php +++ b/src/Symfony/Component/Validator/Constraints/AbstractComparison.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; @@ -24,6 +25,7 @@ abstract class AbstractComparison extends Constraint { public $message; public $value; + public $propertyPath; /** * {@inheritdoc} @@ -34,11 +36,18 @@ abstract class AbstractComparison extends Constraint $options = array(); } - if (is_array($options) && !isset($options['value'])) { - throw new ConstraintDefinitionException(sprintf( - 'The %s constraint requires the "value" option to be set.', - get_class($this) - )); + if (is_array($options)) { + if (!isset($options['value']) && !isset($options['propertyPath'])) { + throw new ConstraintDefinitionException(sprintf('The "%s" constraint requires either the "value" or "propertyPath" option to be set.', get_class($this))); + } + + if (isset($options['value']) && isset($options['propertyPath'])) { + throw new ConstraintDefinitionException(sprintf('The "%s" constraint requires only one of the "value" or "propertyPath" options to be set, not both.', get_class($this))); + } + + if (isset($options['propertyPath']) && !class_exists(PropertyAccess::class)) { + throw new ConstraintDefinitionException(sprintf('The "%s" constraint requires the Symfony PropertyAccess component to use the "propertyPath" option.', get_class($this))); + } } parent::__construct($options); diff --git a/src/Symfony/Component/Validator/Constraints/AbstractComparisonValidator.php b/src/Symfony/Component/Validator/Constraints/AbstractComparisonValidator.php index dbaa5cd0b2..95d584bce6 100644 --- a/src/Symfony/Component/Validator/Constraints/AbstractComparisonValidator.php +++ b/src/Symfony/Component/Validator/Constraints/AbstractComparisonValidator.php @@ -11,8 +11,12 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** @@ -23,6 +27,13 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException; */ abstract class AbstractComparisonValidator extends ConstraintValidator { + private $propertyAccessor; + + public function __construct(PropertyAccessor $propertyAccessor = null) + { + $this->propertyAccessor = $propertyAccessor; + } + /** * {@inheritdoc} */ @@ -36,7 +47,19 @@ abstract class AbstractComparisonValidator extends ConstraintValidator return; } - $comparedValue = $constraint->value; + if ($path = $constraint->propertyPath) { + if (null === $object = $this->context->getObject()) { + return; + } + + try { + $comparedValue = $this->getPropertyAccessor()->getValue($object, $path); + } catch (NoSuchPropertyException $e) { + throw new ConstraintDefinitionException(sprintf('Invalid property path "%s" provided to "%s" constraint: %s', $path, get_class($constraint), $e->getMessage()), 0, $e); + } + } else { + $comparedValue = $constraint->value; + } // Convert strings to DateTimes if comparing another DateTime // This allows to compare with any date/time value supported by @@ -63,6 +86,15 @@ abstract class AbstractComparisonValidator extends ConstraintValidator } } + private function getPropertyAccessor() + { + if (null === $this->propertyAccessor) { + $this->propertyAccessor = PropertyAccess::createPropertyAccessor(); + } + + return $this->propertyAccessor; + } + /** * Compares the two given values to find if their relationship is valid. * diff --git a/src/Symfony/Component/Validator/Tests/Constraints/AbstractComparisonValidatorTestCase.php b/src/Symfony/Component/Validator/Tests/Constraints/AbstractComparisonValidatorTestCase.php index a613d18ead..93898bc6ec 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/AbstractComparisonValidatorTestCase.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/AbstractComparisonValidatorTestCase.php @@ -13,6 +13,7 @@ namespace Symfony\Component\Validator\Tests\Constraints; use Symfony\Component\Intl\Util\IntlTestHelper; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; class ComparisonTest_Class @@ -28,6 +29,11 @@ class ComparisonTest_Class { return (string) $this->value; } + + public function getValue() + { + return $this->value; + } } /** @@ -76,12 +82,25 @@ abstract class AbstractComparisonValidatorTestCase extends ConstraintValidatorTe /** * @dataProvider provideInvalidConstraintOptions * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException + * @expectedExceptionMessage requires either the "value" or "propertyPath" option to be set. */ - public function testThrowsConstraintExceptionIfNoValueOrProperty($options) + public function testThrowsConstraintExceptionIfNoValueOrPropertyPath($options) { $this->createConstraint($options); } + /** + * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException + * @expectedExceptionMessage requires only one of the "value" or "propertyPath" options to be set, not both. + */ + public function testThrowsConstraintExceptionIfBothValueAndPropertyPath() + { + $this->createConstraint((array( + 'value' => 'value', + 'propertyPath' => 'propertyPath', + ))); + } + /** * @dataProvider provideAllValidComparisons * @@ -113,11 +132,75 @@ abstract class AbstractComparisonValidatorTestCase extends ConstraintValidatorTe return $comparisons; } + /** + * @dataProvider provideValidComparisonsToPropertyPath + */ + public function testValidComparisonToPropertyPath($comparedValue) + { + $constraint = $this->createConstraint(array('propertyPath' => 'value')); + + $object = new ComparisonTest_Class(5); + + $this->setObject($object); + + $this->validator->validate($comparedValue, $constraint); + + $this->assertNoViolation(); + } + + /** + * @dataProvider provideValidComparisonsToPropertyPath + */ + public function testValidComparisonToPropertyPathOnArray($comparedValue) + { + $constraint = $this->createConstraint(array('propertyPath' => '[root][value]')); + + $this->setObject(array('root' => array('value' => 5))); + + $this->validator->validate($comparedValue, $constraint); + + $this->assertNoViolation(); + } + + public function testNoViolationOnNullObjectWithPropertyPath() + { + $constraint = $this->createConstraint(array('propertyPath' => 'propertyPath')); + + $this->setObject(null); + + $this->validator->validate('some data', $constraint); + + $this->assertNoViolation(); + } + + public function testInvalidValuePath() + { + $constraint = $this->createConstraint(array('propertyPath' => 'foo')); + + if (method_exists($this, 'expectException')) { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage(sprintf('Invalid property path "foo" provided to "%s" constraint', get_class($constraint))); + } else { + $this->setExpectedException(ConstraintDefinitionException::class, sprintf('Invalid property path "foo" provided to "%s" constraint', get_class($constraint))); + } + + $object = new ComparisonTest_Class(5); + + $this->setObject($object); + + $this->validator->validate(5, $constraint); + } + /** * @return array */ abstract public function provideValidComparisons(); + /** + * @return array + */ + abstract public function provideValidComparisonsToPropertyPath(); + /** * @dataProvider provideAllInvalidComparisons * diff --git a/src/Symfony/Component/Validator/Tests/Constraints/EqualToValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/EqualToValidatorTest.php index 5e079ce525..e47de8fce2 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/EqualToValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/EqualToValidatorTest.php @@ -51,6 +51,16 @@ class EqualToValidatorTest extends AbstractComparisonValidatorTestCase ); } + /** + * {@inheritdoc} + */ + public function provideValidComparisonsToPropertyPath() + { + return array( + array(5), + ); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorTest.php index f13cbd9d39..22fb9b662b 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorTest.php @@ -54,6 +54,17 @@ class GreaterThanOrEqualValidatorTest extends AbstractComparisonValidatorTestCas ); } + /** + * {@inheritdoc} + */ + public function provideValidComparisonsToPropertyPath() + { + return array( + array(5), + array(6), + ); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorTest.php index 4347473fe0..08446ec847 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorTest.php @@ -50,6 +50,16 @@ class GreaterThanValidatorTest extends AbstractComparisonValidatorTestCase ); } + /** + * {@inheritdoc} + */ + public function provideValidComparisonsToPropertyPath() + { + return array( + array(6), + ); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Validator/Tests/Constraints/IdenticalToValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/IdenticalToValidatorTest.php index 100600d04b..360eccabc7 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/IdenticalToValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/IdenticalToValidatorTest.php @@ -69,6 +69,16 @@ class IdenticalToValidatorTest extends AbstractComparisonValidatorTestCase return $comparisons; } + /** + * {@inheritdoc} + */ + public function provideValidComparisonsToPropertyPath() + { + return array( + array(5), + ); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorTest.php index bbd0604f84..f31a50e673 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorTest.php @@ -56,6 +56,17 @@ class LessThanOrEqualValidatorTest extends AbstractComparisonValidatorTestCase ); } + /** + * {@inheritdoc} + */ + public function provideValidComparisonsToPropertyPath() + { + return array( + array(4), + array(5), + ); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorTest.php index d5a5c7378d..a5d69355d7 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorTest.php @@ -50,6 +50,16 @@ class LessThanValidatorTest extends AbstractComparisonValidatorTestCase ); } + /** + * {@inheritdoc} + */ + public function provideValidComparisonsToPropertyPath() + { + return array( + array(4), + ); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Validator/Tests/Constraints/NotEqualToValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/NotEqualToValidatorTest.php index fb9aa9f6e8..e7031d4c4d 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/NotEqualToValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/NotEqualToValidatorTest.php @@ -50,6 +50,16 @@ class NotEqualToValidatorTest extends AbstractComparisonValidatorTestCase ); } + /** + * {@inheritdoc} + */ + public function provideValidComparisonsToPropertyPath() + { + return array( + array(0), + ); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Validator/Tests/Constraints/NotIdenticalToValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/NotIdenticalToValidatorTest.php index 7938b0a7e3..907f36c063 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/NotIdenticalToValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/NotIdenticalToValidatorTest.php @@ -53,6 +53,16 @@ class NotIdenticalToValidatorTest extends AbstractComparisonValidatorTestCase ); } + /** + * {@inheritdoc} + */ + public function provideValidComparisonsToPropertyPath() + { + return array( + array(0), + ); + } + public function provideAllInvalidComparisons() { $this->setDefaultTimezone('UTC'); diff --git a/src/Symfony/Component/Validator/composer.json b/src/Symfony/Component/Validator/composer.json index f104f94c5f..69b39c9c82 100644 --- a/src/Symfony/Component/Validator/composer.json +++ b/src/Symfony/Component/Validator/composer.json @@ -30,6 +30,7 @@ "symfony/dependency-injection": "~3.3|~4.0", "symfony/expression-language": "~2.8|~3.0|~4.0", "symfony/cache": "~3.1|~4.0", + "symfony/property-access": "~2.8|~3.0|~4.0", "doctrine/annotations": "~1.0", "doctrine/cache": "~1.0", "egulias/email-validator": "^1.2.8|~2.0" @@ -48,6 +49,7 @@ "symfony/yaml": "", "symfony/config": "", "egulias/email-validator": "Strict (RFC compliant) email validation", + "symfony/property-access": "For accessing properties within comparison constraints", "symfony/expression-language": "For using the Expression validator" }, "autoload": {