[Validator] Allow to use a property path to get value to compare in comparison constraints

This commit is contained in:
Maxime Steinhausser 2017-07-04 22:50:18 +02:00
parent 0bcc5afbda
commit 07c5aa6822
13 changed files with 216 additions and 7 deletions

View File

@ -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
-----

View File

@ -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);

View File

@ -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.
*

View File

@ -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
*

View File

@ -51,6 +51,16 @@ class EqualToValidatorTest extends AbstractComparisonValidatorTestCase
);
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(5),
);
}
/**
* {@inheritdoc}
*/

View File

@ -54,6 +54,17 @@ class GreaterThanOrEqualValidatorTest extends AbstractComparisonValidatorTestCas
);
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(5),
array(6),
);
}
/**
* {@inheritdoc}
*/

View File

@ -50,6 +50,16 @@ class GreaterThanValidatorTest extends AbstractComparisonValidatorTestCase
);
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(6),
);
}
/**
* {@inheritdoc}
*/

View File

@ -69,6 +69,16 @@ class IdenticalToValidatorTest extends AbstractComparisonValidatorTestCase
return $comparisons;
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(5),
);
}
/**
* {@inheritdoc}
*/

View File

@ -56,6 +56,17 @@ class LessThanOrEqualValidatorTest extends AbstractComparisonValidatorTestCase
);
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(4),
array(5),
);
}
/**
* {@inheritdoc}
*/

View File

@ -50,6 +50,16 @@ class LessThanValidatorTest extends AbstractComparisonValidatorTestCase
);
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(4),
);
}
/**
* {@inheritdoc}
*/

View File

@ -50,6 +50,16 @@ class NotEqualToValidatorTest extends AbstractComparisonValidatorTestCase
);
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(0),
);
}
/**
* {@inheritdoc}
*/

View File

@ -53,6 +53,16 @@ class NotIdenticalToValidatorTest extends AbstractComparisonValidatorTestCase
);
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(0),
);
}
public function provideAllInvalidComparisons()
{
$this->setDefaultTimezone('UTC');

View File

@ -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": {