Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Component/Validator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----
Expand Down
19 changes: 14 additions & 5 deletions src/Symfony/Component/Validator/Constraints/AbstractComparison.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -24,6 +25,7 @@ abstract class AbstractComparison extends Constraint
{
public $message;
public $value;
public $propertyPath;

/**
* {@inheritdoc}
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -23,6 +27,13 @@
*/
abstract class AbstractComparisonValidator extends ConstraintValidator
{
private $propertyAccessor;

public function __construct(PropertyAccessor $propertyAccessor = null)
{
$this->propertyAccessor = $propertyAccessor;
}

/**
* {@inheritdoc}
*/
Expand All @@ -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
Expand All @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,6 +29,11 @@ public function __toString()
{
return (string) $this->value;
}

public function getValue()
{
return $this->value;
}
}

/**
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ public function provideValidComparisons()
);
}

/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(5),
);
}

/**
* {@inheritdoc}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ public function provideValidComparisons()
);
}

/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(5),
array(6),
);
}

/**
* {@inheritdoc}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ public function provideValidComparisons()
);
}

/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(6),
);
}

/**
* {@inheritdoc}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ public function provideValidComparisons()
return $comparisons;
}

/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(5),
);
}

/**
* {@inheritdoc}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ public function provideValidComparisons()
);
}

/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(4),
array(5),
);
}

/**
* {@inheritdoc}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ public function provideValidComparisons()
);
}

/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(4),
);
}

/**
* {@inheritdoc}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ public function provideValidComparisons()
);
}

/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(0),
);
}

/**
* {@inheritdoc}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ public function provideValidComparisons()
);
}

/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(0),
);
}

public function provideAllInvalidComparisons()
{
$this->setDefaultTimezone('UTC');
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Component/Validator/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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": {
Expand Down