Skip to content

Commit 0154b2c

Browse files
committed
[Validator] Allow to use a property path to get value to compare in comparison constraints
1 parent 374e454 commit 0154b2c

13 files changed

+216
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ CHANGELOG
99
* setting the `checkDNS` option of the `Url` constraint to `true` is deprecated in favor of
1010
the `Url::CHECK_DNS_TYPE_*` constants values and will throw an exception in Symfony 4.0
1111
* added min/max amount of pixels check to `Image` constraint via `minPixels` and `maxPixels`
12+
* added a new "propertyPath" option to comparison constraints in order to get the value to compare from an array or object
1213

1314
3.3.0
1415
-----

Constraints/AbstractComparison.php

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Validator\Constraints;
1313

14+
use Symfony\Component\PropertyAccess\PropertyAccess;
1415
use Symfony\Component\Validator\Constraint;
1516
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
1617

@@ -24,6 +25,7 @@ abstract class AbstractComparison extends Constraint
2425
{
2526
public $message;
2627
public $value;
28+
public $propertyPath;
2729

2830
/**
2931
* {@inheritdoc}
@@ -34,11 +36,18 @@ public function __construct($options = null)
3436
$options = array();
3537
}
3638

37-
if (is_array($options) && !isset($options['value'])) {
38-
throw new ConstraintDefinitionException(sprintf(
39-
'The %s constraint requires the "value" option to be set.',
40-
get_class($this)
41-
));
39+
if (is_array($options)) {
40+
if (!isset($options['value']) && !isset($options['propertyPath'])) {
41+
throw new ConstraintDefinitionException(sprintf('The "%s" constraint requires either the "value" or "propertyPath" option to be set.', get_class($this)));
42+
}
43+
44+
if (isset($options['value']) && isset($options['propertyPath'])) {
45+
throw new ConstraintDefinitionException(sprintf('The "%s" constraint requires only one of the "value" or "propertyPath" options to be set, not both.', get_class($this)));
46+
}
47+
48+
if (isset($options['propertyPath']) && !class_exists(PropertyAccess::class)) {
49+
throw new ConstraintDefinitionException(sprintf('The "%s" constraint requires the Symfony PropertyAccess component to use the "propertyPath" option.', get_class($this)));
50+
}
4251
}
4352

4453
parent::__construct($options);

Constraints/AbstractComparisonValidator.php

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@
1111

1212
namespace Symfony\Component\Validator\Constraints;
1313

14+
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
15+
use Symfony\Component\PropertyAccess\PropertyAccess;
16+
use Symfony\Component\PropertyAccess\PropertyAccessor;
1417
use Symfony\Component\Validator\Constraint;
1518
use Symfony\Component\Validator\ConstraintValidator;
19+
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
1620
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
1721

1822
/**
@@ -23,6 +27,13 @@
2327
*/
2428
abstract class AbstractComparisonValidator extends ConstraintValidator
2529
{
30+
private $propertyAccessor;
31+
32+
public function __construct(PropertyAccessor $propertyAccessor = null)
33+
{
34+
$this->propertyAccessor = $propertyAccessor;
35+
}
36+
2637
/**
2738
* {@inheritdoc}
2839
*/
@@ -36,7 +47,19 @@ public function validate($value, Constraint $constraint)
3647
return;
3748
}
3849

39-
$comparedValue = $constraint->value;
50+
if ($path = $constraint->propertyPath) {
51+
if (null === $object = $this->context->getObject()) {
52+
return;
53+
}
54+
55+
try {
56+
$comparedValue = $this->getPropertyAccessor()->getValue($object, $path);
57+
} catch (NoSuchPropertyException $e) {
58+
throw new ConstraintDefinitionException(sprintf('Invalid property path "%s" provided to "%s" constraint: %s', $path, get_class($constraint), $e->getMessage()), 0, $e);
59+
}
60+
} else {
61+
$comparedValue = $constraint->value;
62+
}
4063

4164
// Convert strings to DateTimes if comparing another DateTime
4265
// This allows to compare with any date/time value supported by
@@ -63,6 +86,15 @@ public function validate($value, Constraint $constraint)
6386
}
6487
}
6588

89+
private function getPropertyAccessor()
90+
{
91+
if (null === $this->propertyAccessor) {
92+
$this->propertyAccessor = PropertyAccess::createPropertyAccessor();
93+
}
94+
95+
return $this->propertyAccessor;
96+
}
97+
6698
/**
6799
* Compares the two given values to find if their relationship is valid.
68100
*

Tests/Constraints/AbstractComparisonValidatorTestCase.php

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\Intl\Util\IntlTestHelper;
1515
use Symfony\Component\Validator\Constraint;
16+
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
1617
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
1718

1819
class ComparisonTest_Class
@@ -28,6 +29,11 @@ public function __toString()
2829
{
2930
return (string) $this->value;
3031
}
32+
33+
public function getValue()
34+
{
35+
return $this->value;
36+
}
3137
}
3238

3339
/**
@@ -76,12 +82,25 @@ public function provideInvalidConstraintOptions()
7682
/**
7783
* @dataProvider provideInvalidConstraintOptions
7884
* @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException
85+
* @expectedExceptionMessage requires either the "value" or "propertyPath" option to be set.
7986
*/
80-
public function testThrowsConstraintExceptionIfNoValueOrProperty($options)
87+
public function testThrowsConstraintExceptionIfNoValueOrPropertyPath($options)
8188
{
8289
$this->createConstraint($options);
8390
}
8491

92+
/**
93+
* @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException
94+
* @expectedExceptionMessage requires only one of the "value" or "propertyPath" options to be set, not both.
95+
*/
96+
public function testThrowsConstraintExceptionIfBothValueAndPropertyPath()
97+
{
98+
$this->createConstraint((array(
99+
'value' => 'value',
100+
'propertyPath' => 'propertyPath',
101+
)));
102+
}
103+
85104
/**
86105
* @dataProvider provideAllValidComparisons
87106
*
@@ -113,11 +132,75 @@ public function provideAllValidComparisons()
113132
return $comparisons;
114133
}
115134

135+
/**
136+
* @dataProvider provideValidComparisonsToPropertyPath
137+
*/
138+
public function testValidComparisonToPropertyPath($comparedValue)
139+
{
140+
$constraint = $this->createConstraint(array('propertyPath' => 'value'));
141+
142+
$object = new ComparisonTest_Class(5);
143+
144+
$this->setObject($object);
145+
146+
$this->validator->validate($comparedValue, $constraint);
147+
148+
$this->assertNoViolation();
149+
}
150+
151+
/**
152+
* @dataProvider provideValidComparisonsToPropertyPath
153+
*/
154+
public function testValidComparisonToPropertyPathOnArray($comparedValue)
155+
{
156+
$constraint = $this->createConstraint(array('propertyPath' => '[root][value]'));
157+
158+
$this->setObject(array('root' => array('value' => 5)));
159+
160+
$this->validator->validate($comparedValue, $constraint);
161+
162+
$this->assertNoViolation();
163+
}
164+
165+
public function testNoViolationOnNullObjectWithPropertyPath()
166+
{
167+
$constraint = $this->createConstraint(array('propertyPath' => 'propertyPath'));
168+
169+
$this->setObject(null);
170+
171+
$this->validator->validate('some data', $constraint);
172+
173+
$this->assertNoViolation();
174+
}
175+
176+
public function testInvalidValuePath()
177+
{
178+
$constraint = $this->createConstraint(array('propertyPath' => 'foo'));
179+
180+
if (method_exists($this, 'expectException')) {
181+
$this->expectException(ConstraintDefinitionException::class);
182+
$this->expectExceptionMessage(sprintf('Invalid property path "foo" provided to "%s" constraint', get_class($constraint)));
183+
} else {
184+
$this->setExpectedException(ConstraintDefinitionException::class, sprintf('Invalid property path "foo" provided to "%s" constraint', get_class($constraint)));
185+
}
186+
187+
$object = new ComparisonTest_Class(5);
188+
189+
$this->setObject($object);
190+
191+
$this->validator->validate(5, $constraint);
192+
}
193+
116194
/**
117195
* @return array
118196
*/
119197
abstract public function provideValidComparisons();
120198

199+
/**
200+
* @return array
201+
*/
202+
abstract public function provideValidComparisonsToPropertyPath();
203+
121204
/**
122205
* @dataProvider provideAllInvalidComparisons
123206
*

Tests/Constraints/EqualToValidatorTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ public function provideValidComparisons()
5151
);
5252
}
5353

54+
/**
55+
* {@inheritdoc}
56+
*/
57+
public function provideValidComparisonsToPropertyPath()
58+
{
59+
return array(
60+
array(5),
61+
);
62+
}
63+
5464
/**
5565
* {@inheritdoc}
5666
*/

Tests/Constraints/GreaterThanOrEqualValidatorTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@ public function provideValidComparisons()
5454
);
5555
}
5656

57+
/**
58+
* {@inheritdoc}
59+
*/
60+
public function provideValidComparisonsToPropertyPath()
61+
{
62+
return array(
63+
array(5),
64+
array(6),
65+
);
66+
}
67+
5768
/**
5869
* {@inheritdoc}
5970
*/

Tests/Constraints/GreaterThanValidatorTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,16 @@ public function provideValidComparisons()
5050
);
5151
}
5252

53+
/**
54+
* {@inheritdoc}
55+
*/
56+
public function provideValidComparisonsToPropertyPath()
57+
{
58+
return array(
59+
array(6),
60+
);
61+
}
62+
5363
/**
5464
* {@inheritdoc}
5565
*/

Tests/Constraints/IdenticalToValidatorTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,16 @@ public function provideValidComparisons()
6969
return $comparisons;
7070
}
7171

72+
/**
73+
* {@inheritdoc}
74+
*/
75+
public function provideValidComparisonsToPropertyPath()
76+
{
77+
return array(
78+
array(5),
79+
);
80+
}
81+
7282
/**
7383
* {@inheritdoc}
7484
*/

Tests/Constraints/LessThanOrEqualValidatorTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,17 @@ public function provideValidComparisons()
5656
);
5757
}
5858

59+
/**
60+
* {@inheritdoc}
61+
*/
62+
public function provideValidComparisonsToPropertyPath()
63+
{
64+
return array(
65+
array(4),
66+
array(5),
67+
);
68+
}
69+
5970
/**
6071
* {@inheritdoc}
6172
*/

Tests/Constraints/LessThanValidatorTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,16 @@ public function provideValidComparisons()
5050
);
5151
}
5252

53+
/**
54+
* {@inheritdoc}
55+
*/
56+
public function provideValidComparisonsToPropertyPath()
57+
{
58+
return array(
59+
array(4),
60+
);
61+
}
62+
5363
/**
5464
* {@inheritdoc}
5565
*/

Tests/Constraints/NotEqualToValidatorTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,16 @@ public function provideValidComparisons()
5050
);
5151
}
5252

53+
/**
54+
* {@inheritdoc}
55+
*/
56+
public function provideValidComparisonsToPropertyPath()
57+
{
58+
return array(
59+
array(0),
60+
);
61+
}
62+
5363
/**
5464
* {@inheritdoc}
5565
*/

Tests/Constraints/NotIdenticalToValidatorTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ public function provideValidComparisons()
5353
);
5454
}
5555

56+
/**
57+
* {@inheritdoc}
58+
*/
59+
public function provideValidComparisonsToPropertyPath()
60+
{
61+
return array(
62+
array(0),
63+
);
64+
}
65+
5666
public function provideAllInvalidComparisons()
5767
{
5868
$this->setDefaultTimezone('UTC');

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"symfony/dependency-injection": "~3.3|~4.0",
3131
"symfony/expression-language": "~2.8|~3.0|~4.0",
3232
"symfony/cache": "~3.1|~4.0",
33+
"symfony/property-access": "~2.8|~3.0|~4.0",
3334
"doctrine/annotations": "~1.0",
3435
"doctrine/cache": "~1.0",
3536
"egulias/email-validator": "^1.2.8|~2.0"
@@ -48,6 +49,7 @@
4849
"symfony/yaml": "",
4950
"symfony/config": "",
5051
"egulias/email-validator": "Strict (RFC compliant) email validation",
52+
"symfony/property-access": "For accessing properties within comparison constraints",
5153
"symfony/expression-language": "For using the Expression validator"
5254
},
5355
"autoload": {

0 commit comments

Comments
 (0)