feature #22576 [Validator] Allow to use a property path to get value to compare in comparison constraints (ogizanagi)
This PR was merged into the 3.4 branch.
Discussion
----------
[Validator] Allow to use a property path to get value to compare in comparison constraints
| Q | A
| ------------- | ---
| Branch? | 3.4
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | N/A
| License | MIT
| Doc PR | todo
So we can simply declare something like:
```php
class Activity
{
/**
* @var \DateTime
*
* @Assert\DateTime()
*/
private $startDate;
/**
* @var \DateTime
*
* @Assert\DateTime()
* @Assert\GreaterThan(propertyPath="startDate")
*/
private $endDate;
// [...]
public function getStartDate(): \DateTime
{
return $this->startDate;
}
public function getEndDate(): \DateTime
{
return $this->startDate;
}
}
```
Of course, this is actually already possible by using an `Expression` constraint (or a callable), but it feels more natural to me to use proper comparison constraints for this.
Commits
-------
07c5aa6
[Validator] Allow to use a property path to get value to compare in comparison constraints
This commit is contained in:
commit
ab8ac13dcf
@ -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
|
||||
-----
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -51,6 +51,16 @@ class EqualToValidatorTest extends AbstractComparisonValidatorTestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function provideValidComparisonsToPropertyPath()
|
||||
{
|
||||
return array(
|
||||
array(5),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
@ -54,6 +54,17 @@ class GreaterThanOrEqualValidatorTest extends AbstractComparisonValidatorTestCas
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function provideValidComparisonsToPropertyPath()
|
||||
{
|
||||
return array(
|
||||
array(5),
|
||||
array(6),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
@ -50,6 +50,16 @@ class GreaterThanValidatorTest extends AbstractComparisonValidatorTestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function provideValidComparisonsToPropertyPath()
|
||||
{
|
||||
return array(
|
||||
array(6),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
@ -69,6 +69,16 @@ class IdenticalToValidatorTest extends AbstractComparisonValidatorTestCase
|
||||
return $comparisons;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function provideValidComparisonsToPropertyPath()
|
||||
{
|
||||
return array(
|
||||
array(5),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
@ -56,6 +56,17 @@ class LessThanOrEqualValidatorTest extends AbstractComparisonValidatorTestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function provideValidComparisonsToPropertyPath()
|
||||
{
|
||||
return array(
|
||||
array(4),
|
||||
array(5),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
@ -50,6 +50,16 @@ class LessThanValidatorTest extends AbstractComparisonValidatorTestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function provideValidComparisonsToPropertyPath()
|
||||
{
|
||||
return array(
|
||||
array(4),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
@ -50,6 +50,16 @@ class NotEqualToValidatorTest extends AbstractComparisonValidatorTestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function provideValidComparisonsToPropertyPath()
|
||||
{
|
||||
return array(
|
||||
array(0),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
@ -53,6 +53,16 @@ class NotIdenticalToValidatorTest extends AbstractComparisonValidatorTestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function provideValidComparisonsToPropertyPath()
|
||||
{
|
||||
return array(
|
||||
array(0),
|
||||
);
|
||||
}
|
||||
|
||||
public function provideAllInvalidComparisons()
|
||||
{
|
||||
$this->setDefaultTimezone('UTC');
|
||||
|
@ -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": {
|
||||
|
Reference in New Issue
Block a user