Skip to content

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

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 8, 2019
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
6 changes: 6 additions & 0 deletions src/Symfony/Component/Validator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----
Expand Down
23 changes: 21 additions & 2 deletions src/Symfony/Component/Validator/Constraints/Range.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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']);
}
}
}
75 changes: 65 additions & 10 deletions src/Symfony/Component/Validator/Constraints/RangeValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,26 @@

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;

/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class RangeValidator extends ConstraintValidator
{
private $propertyAccessor;

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

/**
* {@inheritdoc}
*/
Expand All @@ -42,8 +53,8 @@ public function validate($value, Constraint $constraint)
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
Expand All @@ -59,22 +70,66 @@ public function validate($value, Constraint $constraint)
}
}

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;
}
}
51 changes: 51 additions & 0 deletions src/Symfony/Component/Validator/Tests/Constraints/RangeTest.php
Original file line number Diff line number Diff line change
@@ -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');
}
}
Loading