Permalink
Browse files

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
  • Loading branch information...
nicolas-grekas committed Jul 12, 2017
2 parents 3b2f9fb + 07c5aa6 commit ab8ac13dcf12faf839f43c43027a166c6e735582
@@ -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 @@
{
public $message;
public $value;
public $propertyPath;
/**
* {@inheritdoc}
@@ -34,11 +36,18 @@ public function __construct($options = null)
$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 @@
*/
abstract class AbstractComparisonValidator extends ConstraintValidator
{
private $propertyAccessor;
public function __construct(PropertyAccessor $propertyAccessor = null)
{
$this->propertyAccessor = $propertyAccessor;
}
/**
* {@inheritdoc}
*/
@@ -36,7 +47,19 @@ public function validate($value, Constraint $constraint)
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 @@ public function validate($value, Constraint $constraint)
}
}
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 @@
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 @@ public function __toString()
{
return (string) $this->value;
}
public function getValue()
{
return $this->value;
}
}
/**
@@ -76,12 +82,25 @@ public function provideInvalidConstraintOptions()
/**
* @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 @@ public function provideAllValidComparisons()
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 @@ public function provideValidComparisons()
);
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(5),
);
}
/**
* {@inheritdoc}
*/
@@ -54,6 +54,17 @@ public function provideValidComparisons()
);
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(5),
array(6),
);
}
/**
* {@inheritdoc}
*/
@@ -50,6 +50,16 @@ public function provideValidComparisons()
);
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(6),
);
}
/**
* {@inheritdoc}
*/
@@ -69,6 +69,16 @@ public function provideValidComparisons()
return $comparisons;
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(5),
);
}
/**
* {@inheritdoc}
*/
@@ -56,6 +56,17 @@ public function provideValidComparisons()
);
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(4),
array(5),
);
}
/**
* {@inheritdoc}
*/
@@ -50,6 +50,16 @@ public function provideValidComparisons()
);
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(4),
);
}
/**
* {@inheritdoc}
*/
@@ -50,6 +50,16 @@ public function provideValidComparisons()
);
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(0),
);
}
/**
* {@inheritdoc}
*/
@@ -53,6 +53,16 @@ public function provideValidComparisons()
);
}
/**
* {@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": {

0 comments on commit ab8ac13

Please sign in to comment.