[Validator] Allow to use property paths to get limits in range constraint

This commit is contained in:
Lctrs 2019-05-16 14:24:54 +02:00 committed by Fabien Potencier
parent dca9325e61
commit 2b509904c8
5 changed files with 547 additions and 12 deletions

View File

@ -8,6 +8,12 @@ CHANGELOG
* added the `compared_value_path` parameter in violations when using any
comparison constraint with the `propertyPath` option.
* added support for checking an array of types in `TypeValidator`
* Added new `minPropertyPath` and `maxPropertyPath` options
to `Range` constraint in order to get the value to compare
from an array or object
* added the `limit_path` parameter in violations when using
`Range` constraint with the `minPropertyPath` or
`maxPropertyPath` options.
4.3.0
-----

View File

@ -11,7 +11,10 @@
namespace Symfony\Component\Validator\Constraints;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Exception\LogicException;
use Symfony\Component\Validator\Exception\MissingOptionsException;
/**
@ -36,14 +39,30 @@ class Range extends Constraint
public $maxMessage = 'This value should be {{ limit }} or less.';
public $invalidMessage = 'This value should be a valid number.';
public $min;
public $minPropertyPath;
public $max;
public $maxPropertyPath;
public function __construct($options = null)
{
if (\is_array($options)) {
if (isset($options['min']) && isset($options['minPropertyPath'])) {
throw new ConstraintDefinitionException(sprintf('The "%s" constraint requires only one of the "min" or "minPropertyPath" options to be set, not both.', \get_class($this)));
}
if (isset($options['max']) && isset($options['maxPropertyPath'])) {
throw new ConstraintDefinitionException(sprintf('The "%s" constraint requires only one of the "max" or "maxPropertyPath" options to be set, not both.', \get_class($this)));
}
if ((isset($options['minPropertyPath']) || isset($options['maxPropertyPath'])) && !class_exists(PropertyAccess::class)) {
throw new LogicException(sprintf('The "%s" constraint requires the Symfony PropertyAccess component to use the "minPropertyPath" or "maxPropertyPath" option.', \get_class($this)));
}
}
parent::__construct($options);
if (null === $this->min && null === $this->max) {
throw new MissingOptionsException(sprintf('Either option "min" or "max" must be given for constraint %s', __CLASS__), ['min', 'max']);
if (null === $this->min && null === $this->minPropertyPath && null === $this->max && null === $this->maxPropertyPath) {
throw new MissingOptionsException(sprintf('Either option "min", "minPropertyPath", "max" or "maxPropertyPath" must be given for constraint %s', __CLASS__), ['min', 'max']);
}
}
}

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\PropertyAccessorInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
/**
@ -20,6 +24,13 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
*/
class RangeValidator extends ConstraintValidator
{
private $propertyAccessor;
public function __construct(PropertyAccessorInterface $propertyAccessor = null)
{
$this->propertyAccessor = $propertyAccessor;
}
/**
* {@inheritdoc}
*/
@ -42,8 +53,8 @@ class RangeValidator extends ConstraintValidator
return;
}
$min = $constraint->min;
$max = $constraint->max;
$min = $this->getLimit($constraint->minPropertyPath, $constraint->min, $constraint);
$max = $this->getLimit($constraint->maxPropertyPath, $constraint->max, $constraint);
// Convert strings to DateTimes if comparing another DateTime
// This allows to compare with any date/time value supported by
@ -59,22 +70,66 @@ class RangeValidator extends ConstraintValidator
}
}
if (null !== $constraint->max && $value > $max) {
$this->context->buildViolation($constraint->maxMessage)
if (null !== $max && $value > $max) {
$violationBuilder = $this->context->buildViolation($constraint->maxMessage)
->setParameter('{{ value }}', $this->formatValue($value, self::PRETTY_DATE))
->setParameter('{{ limit }}', $this->formatValue($max, self::PRETTY_DATE))
->setCode(Range::TOO_HIGH_ERROR)
->addViolation();
->setCode(Range::TOO_HIGH_ERROR);
if (null !== $constraint->maxPropertyPath) {
$violationBuilder->setParameter('{{ max_limit_path }}', $constraint->maxPropertyPath);
}
if (null !== $constraint->minPropertyPath) {
$violationBuilder->setParameter('{{ min_limit_path }}', $constraint->minPropertyPath);
}
$violationBuilder->addViolation();
return;
}
if (null !== $constraint->min && $value < $min) {
$this->context->buildViolation($constraint->minMessage)
if (null !== $min && $value < $min) {
$violationBuilder = $this->context->buildViolation($constraint->minMessage)
->setParameter('{{ value }}', $this->formatValue($value, self::PRETTY_DATE))
->setParameter('{{ limit }}', $this->formatValue($min, self::PRETTY_DATE))
->setCode(Range::TOO_LOW_ERROR)
->addViolation();
->setCode(Range::TOO_LOW_ERROR);
if (null !== $constraint->maxPropertyPath) {
$violationBuilder->setParameter('{{ max_limit_path }}', $constraint->maxPropertyPath);
}
if (null !== $constraint->minPropertyPath) {
$violationBuilder->setParameter('{{ min_limit_path }}', $constraint->minPropertyPath);
}
$violationBuilder->addViolation();
}
}
private function getLimit($propertyPath, $default, Constraint $constraint)
{
if (null === $propertyPath) {
return $default;
}
if (null === $object = $this->context->getObject()) {
return $default;
}
try {
return $this->getPropertyAccessor()->getValue($object, $propertyPath);
} catch (NoSuchPropertyException $e) {
throw new ConstraintDefinitionException(sprintf('Invalid property path "%s" provided to "%s" constraint: %s', $propertyPath, \get_class($constraint), $e->getMessage()), 0, $e);
}
}
private function getPropertyAccessor(): PropertyAccessorInterface
{
if (null === $this->propertyAccessor) {
$this->propertyAccessor = PropertyAccess::createPropertyAccessor();
}
return $this->propertyAccessor;
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace Symfony\Component\Validator\Tests\Constraints;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Constraints\Range;
class RangeTest extends TestCase
{
/**
* @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException
* @expectedExceptionMessage requires only one of the "min" or "minPropertyPath" options to be set, not both.
*/
public function testThrowsConstraintExceptionIfBothMinLimitAndPropertyPath()
{
new Range([
'min' => 'min',
'minPropertyPath' => 'minPropertyPath',
]);
}
/**
* @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException
* @expectedExceptionMessage requires only one of the "max" or "maxPropertyPath" options to be set, not both.
*/
public function testThrowsConstraintExceptionIfBothMaxLimitAndPropertyPath()
{
new Range([
'max' => 'min',
'maxPropertyPath' => 'maxPropertyPath',
]);
}
/**
* @expectedException \Symfony\Component\Validator\Exception\MissingOptionsException
* @expectedExceptionMessage Either option "min", "minPropertyPath", "max" or "maxPropertyPath" must be given
*/
public function testThrowsConstraintExceptionIfNoLimitNorPropertyPath()
{
new Range([]);
}
/**
* @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException
* @expectedExceptionMessage No default option is configured
*/
public function testThrowsNoDefaultOptionConfiguredException()
{
new Range('value');
}
}

View File

@ -389,4 +389,408 @@ class RangeValidatorTest extends ConstraintValidatorTestCase
->setCode(Range::INVALID_CHARACTERS_ERROR)
->assertRaised();
}
public function testNoViolationOnNullObjectWithPropertyPaths()
{
$this->setObject(null);
$this->validator->validate(1, new Range([
'minPropertyPath' => 'minPropertyPath',
'maxPropertyPath' => 'maxPropertyPath',
]));
$this->assertNoViolation();
}
/**
* @dataProvider getTenToTwenty
*/
public function testValidValuesMinPropertyPath($value)
{
$this->setObject(new Limit(10));
$this->validator->validate($value, new Range([
'minPropertyPath' => 'value',
]));
$this->assertNoViolation();
}
/**
* @dataProvider getTenToTwenty
*/
public function testValidValuesMinPropertyPathOnArray($value)
{
$this->setObject(['root' => ['value' => 10]]);
$this->validator->validate($value, new Range([
'minPropertyPath' => '[root][value]',
]));
$this->assertNoViolation();
}
/**
* @dataProvider getTenToTwenty
*/
public function testValidValuesMaxPropertyPath($value)
{
$this->setObject(new Limit(20));
$this->validator->validate($value, new Range([
'maxPropertyPath' => 'value',
]));
$this->assertNoViolation();
}
/**
* @dataProvider getTenToTwenty
*/
public function testValidValuesMaxPropertyPathOnArray($value)
{
$this->setObject(['root' => ['value' => 20]]);
$this->validator->validate($value, new Range([
'maxPropertyPath' => '[root][value]',
]));
$this->assertNoViolation();
}
/**
* @dataProvider getTenToTwenty
*/
public function testValidValuesMinMaxPropertyPath($value)
{
$this->setObject(new MinMax(10, 20));
$this->validator->validate($value, new Range([
'minPropertyPath' => 'min',
'maxPropertyPath' => 'max',
]));
$this->assertNoViolation();
}
/**
* @dataProvider getLessThanTen
*/
public function testInvalidValuesMinPropertyPath($value, $formattedValue)
{
$this->setObject(new Limit(10));
$constraint = new Range([
'minPropertyPath' => 'value',
'minMessage' => 'myMessage',
]);
$this->validator->validate($value, $constraint);
$this->buildViolation('myMessage')
->setParameter('{{ value }}', $formattedValue)
->setParameter('{{ limit }}', 10)
->setParameter('{{ min_limit_path }}', 'value')
->setCode(Range::TOO_LOW_ERROR)
->assertRaised();
}
/**
* @dataProvider getMoreThanTwenty
*/
public function testInvalidValuesMaxPropertyPath($value, $formattedValue)
{
$this->setObject(new Limit(20));
$constraint = new Range([
'maxPropertyPath' => 'value',
'maxMessage' => 'myMessage',
]);
$this->validator->validate($value, $constraint);
$this->buildViolation('myMessage')
->setParameter('{{ value }}', $formattedValue)
->setParameter('{{ limit }}', 20)
->setParameter('{{ max_limit_path }}', 'value')
->setCode(Range::TOO_HIGH_ERROR)
->assertRaised();
}
/**
* @dataProvider getMoreThanTwenty
*/
public function testInvalidValuesCombinedMaxPropertyPath($value, $formattedValue)
{
$this->setObject(new MinMax(10, 20));
$constraint = new Range([
'minPropertyPath' => 'min',
'maxPropertyPath' => 'max',
'minMessage' => 'myMinMessage',
'maxMessage' => 'myMaxMessage',
]);
$this->validator->validate($value, $constraint);
$this->buildViolation('myMaxMessage')
->setParameter('{{ value }}', $formattedValue)
->setParameter('{{ limit }}', 20)
->setParameter('{{ max_limit_path }}', 'max')
->setParameter('{{ min_limit_path }}', 'min')
->setCode(Range::TOO_HIGH_ERROR)
->assertRaised();
}
/**
* @dataProvider getLessThanTen
*/
public function testInvalidValuesCombinedMinPropertyPath($value, $formattedValue)
{
$this->setObject(new MinMax(10, 20));
$constraint = new Range([
'minPropertyPath' => 'min',
'maxPropertyPath' => 'max',
'minMessage' => 'myMinMessage',
'maxMessage' => 'myMaxMessage',
]);
$this->validator->validate($value, $constraint);
$this->buildViolation('myMinMessage')
->setParameter('{{ value }}', $formattedValue)
->setParameter('{{ limit }}', 10)
->setParameter('{{ max_limit_path }}', 'max')
->setParameter('{{ min_limit_path }}', 'min')
->setCode(Range::TOO_LOW_ERROR)
->assertRaised();
}
/**
* @dataProvider getLessThanTen
*/
public function testViolationOnNullObjectWithDefinedMin($value, $formattedValue)
{
$this->setObject(null);
$this->validator->validate($value, new Range([
'min' => 10,
'maxPropertyPath' => 'max',
'minMessage' => 'myMessage',
]));
$this->buildViolation('myMessage')
->setParameter('{{ value }}', $formattedValue)
->setParameter('{{ limit }}', 10)
->setParameter('{{ max_limit_path }}', 'max')
->setCode(Range::TOO_LOW_ERROR)
->assertRaised();
}
/**
* @dataProvider getMoreThanTwenty
*/
public function testViolationOnNullObjectWithDefinedMax($value, $formattedValue)
{
$this->setObject(null);
$this->validator->validate($value, new Range([
'minPropertyPath' => 'min',
'max' => 20,
'maxMessage' => 'myMessage',
]));
$this->buildViolation('myMessage')
->setParameter('{{ value }}', $formattedValue)
->setParameter('{{ limit }}', 20)
->setParameter('{{ min_limit_path }}', 'min')
->setCode(Range::TOO_HIGH_ERROR)
->assertRaised();
}
/**
* @dataProvider getTenthToTwentiethMarch2014
*/
public function testValidDatesMinPropertyPath($value)
{
$this->setObject(new Limit('March 10, 2014'));
$this->validator->validate($value, new Range(['minPropertyPath' => 'value']));
$this->assertNoViolation();
}
/**
* @dataProvider getTenthToTwentiethMarch2014
*/
public function testValidDatesMaxPropertyPath($value)
{
$this->setObject(new Limit('March 20, 2014'));
$constraint = new Range(['maxPropertyPath' => 'value']);
$this->validator->validate($value, $constraint);
$this->assertNoViolation();
}
/**
* @dataProvider getTenthToTwentiethMarch2014
*/
public function testValidDatesMinMaxPropertyPath($value)
{
$this->setObject(new MinMax('March 10, 2014', 'March 20, 2014'));
$constraint = new Range(['minPropertyPath' => 'min', 'maxPropertyPath' => 'max']);
$this->validator->validate($value, $constraint);
$this->assertNoViolation();
}
/**
* @dataProvider getSoonerThanTenthMarch2014
*/
public function testInvalidDatesMinPropertyPath($value, $dateTimeAsString)
{
// Conversion of dates to string differs between ICU versions
// Make sure we have the correct version loaded
IntlTestHelper::requireIntl($this, '57.1');
$this->setObject(new Limit('March 10, 2014'));
$constraint = new Range([
'minPropertyPath' => 'value',
'minMessage' => 'myMessage',
]);
$this->validator->validate($value, $constraint);
$this->buildViolation('myMessage')
->setParameter('{{ value }}', $dateTimeAsString)
->setParameter('{{ limit }}', 'Mar 10, 2014, 12:00 AM')
->setParameter('{{ min_limit_path }}', 'value')
->setCode(Range::TOO_LOW_ERROR)
->assertRaised();
}
/**
* @dataProvider getLaterThanTwentiethMarch2014
*/
public function testInvalidDatesMaxPropertyPath($value, $dateTimeAsString)
{
// Conversion of dates to string differs between ICU versions
// Make sure we have the correct version loaded
IntlTestHelper::requireIntl($this, '57.1');
$this->setObject(new Limit('March 20, 2014'));
$constraint = new Range([
'maxPropertyPath' => 'value',
'maxMessage' => 'myMessage',
]);
$this->validator->validate($value, $constraint);
$this->buildViolation('myMessage')
->setParameter('{{ value }}', $dateTimeAsString)
->setParameter('{{ limit }}', 'Mar 20, 2014, 12:00 AM')
->setParameter('{{ max_limit_path }}', 'value')
->setCode(Range::TOO_HIGH_ERROR)
->assertRaised();
}
/**
* @dataProvider getLaterThanTwentiethMarch2014
*/
public function testInvalidDatesCombinedMaxPropertyPath($value, $dateTimeAsString)
{
// Conversion of dates to string differs between ICU versions
// Make sure we have the correct version loaded
IntlTestHelper::requireIntl($this, '57.1');
$this->setObject(new MinMax('March 10, 2014', 'March 20, 2014'));
$constraint = new Range([
'minPropertyPath' => 'min',
'maxPropertyPath' => 'max',
'minMessage' => 'myMinMessage',
'maxMessage' => 'myMaxMessage',
]);
$this->validator->validate($value, $constraint);
$this->buildViolation('myMaxMessage')
->setParameter('{{ value }}', $dateTimeAsString)
->setParameter('{{ limit }}', 'Mar 20, 2014, 12:00 AM')
->setParameter('{{ max_limit_path }}', 'max')
->setParameter('{{ min_limit_path }}', 'min')
->setCode(Range::TOO_HIGH_ERROR)
->assertRaised();
}
/**
* @dataProvider getSoonerThanTenthMarch2014
*/
public function testInvalidDatesCombinedMinPropertyPath($value, $dateTimeAsString)
{
// Conversion of dates to string differs between ICU versions
// Make sure we have the correct version loaded
IntlTestHelper::requireIntl($this, '57.1');
$this->setObject(new MinMax('March 10, 2014', 'March 20, 2014'));
$constraint = new Range([
'minPropertyPath' => 'min',
'maxPropertyPath' => 'max',
'minMessage' => 'myMinMessage',
'maxMessage' => 'myMaxMessage',
]);
$this->validator->validate($value, $constraint);
$this->buildViolation('myMinMessage')
->setParameter('{{ value }}', $dateTimeAsString)
->setParameter('{{ limit }}', 'Mar 10, 2014, 12:00 AM')
->setParameter('{{ max_limit_path }}', 'max')
->setParameter('{{ min_limit_path }}', 'min')
->setCode(Range::TOO_LOW_ERROR)
->assertRaised();
}
}
final class Limit
{
private $value;
public function __construct($value)
{
$this->value = $value;
}
public function getValue()
{
return $this->value;
}
}
final class MinMax
{
private $min;
private $max;
public function __construct($min, $max)
{
$this->min = $min;
$this->max = $max;
}
public function getMin()
{
return $this->min;
}
public function getMax()
{
return $this->max;
}
}