Skip to content

Commit

Permalink
feature #11673 [Validator] Added date support to comparison constrain…
Browse files Browse the repository at this point in the history
…ts and Range (webmozart)

This PR was merged into the 2.6-dev branch.

Discussion
----------

[Validator] Added date support to comparison constraints and Range

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #3640, #7766, #9164, #9390, #8300
| License       | MIT
| Doc PR        | symfony/symfony-docs#4143

This commit adds frequently requested functionality to compare dates. Since the `DateTime` constructor is very flexible, you can do many fancy things now such as:

```php
/**
 * Only accept requests that start in at least an hour.
 * @Assert\GreaterThanOrEqual("+1 hours")
 */
private $date;

/**
 * Same as before.
 * @Assert\Range(min = "+1 hours")
 */
private $date;

/**
 * Only accept dates in the current year.
 * @Assert\Range(min = "first day of January", max = "first day of January next year")
 */
private $date;

/**
 * Timezones are supported.
 * @Assert\Range(min = "first day of January UTC", max = "first day of January next year UTC")
 */
private $date;
```

Commits
-------

60a5863 [Validator] Added date support to comparison constraints and Range
  • Loading branch information
nicolas-grekas committed Aug 24, 2014
2 parents cf3f35b + 154cd12 commit d44e0f3
Show file tree
Hide file tree
Showing 14 changed files with 419 additions and 14 deletions.
13 changes: 12 additions & 1 deletion ConstraintValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,22 @@ protected function formatTypeOf($value)
*/
protected function formatValue($value, $format = 0)
{
if (($format & self::PRETTY_DATE) && $value instanceof \DateTime) {
$isDateTime = $value instanceof \DateTime || $value instanceof \DateTimeInterface;

if (($format & self::PRETTY_DATE) && $isDateTime) {
if (class_exists('IntlDateFormatter')) {
$locale = \Locale::getDefault();
$formatter = new \IntlDateFormatter($locale, \IntlDateFormatter::MEDIUM, \IntlDateFormatter::SHORT);

// neither the native nor the stub IntlDateFormatter support
// DateTimeImmutable as of yet
if (!$value instanceof \DateTime) {
$value = new \DateTime(
$value->format('Y-m-d H:i:s.u e'),
$value->getTimezone()
);
}

return $formatter->format($value);
}

Expand Down
23 changes: 20 additions & 3 deletions Constraints/AbstractComparisonValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,28 @@ public function validate($value, Constraint $constraint)
return;
}

if (!$this->compareValues($value, $constraint->value)) {
$comparedValue = $constraint->value;

// Convert strings to DateTimes if comparing another DateTime
// This allows to compare with any date/time value supported by
// the DateTime constructor:
// http://php.net/manual/en/datetime.formats.php
if (is_string($comparedValue)) {
if ($value instanceof \DatetimeImmutable) {
// If $value is immutable, convert the compared value to a
// DateTimeImmutable too
$comparedValue = new \DatetimeImmutable($comparedValue);
} elseif ($value instanceof \DateTime || $value instanceof \DateTimeInterface) {
// Otherwise use DateTime
$comparedValue = new \DateTime($comparedValue);
}
}

if (!$this->compareValues($value, $comparedValue)) {
$this->context->addViolation($constraint->message, array(
'{{ value }}' => $this->formatValue($value, self::OBJECT_TO_STRING | self::PRETTY_DATE),
'{{ compared_value }}' => $this->formatValue($constraint->value, self::OBJECT_TO_STRING | self::PRETTY_DATE),
'{{ compared_value_type }}' => $this->formatTypeOf($constraint->value)
'{{ compared_value }}' => $this->formatValue($comparedValue, self::OBJECT_TO_STRING | self::PRETTY_DATE),
'{{ compared_value_type }}' => $this->formatTypeOf($comparedValue)
));
}
}
Expand Down
27 changes: 22 additions & 5 deletions Constraints/RangeValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,27 +33,44 @@ public function validate($value, Constraint $constraint)
return;
}

if (!is_numeric($value)) {
if (!is_numeric($value) && !$value instanceof \DateTime && !$value instanceof \DateTimeInterface) {
$this->context->addViolation($constraint->invalidMessage, array(
'{{ value }}' => $this->formatValue($value),
));

return;
}

if (null !== $constraint->max && $value > $constraint->max) {
$min = $constraint->min;
$max = $constraint->max;

// Convert strings to DateTimes if comparing another DateTime
// This allows to compare with any date/time value supported by
// the DateTime constructor:
// http://php.net/manual/en/datetime.formats.php
if ($value instanceof \DateTime || $value instanceof \DateTimeInterface) {
if (is_string($min)) {
$min = new \DateTime($min);
}

if (is_string($max)) {
$max = new \DateTime($max);
}
}

if (null !== $constraint->max && $value > $max) {
$this->context->addViolation($constraint->maxMessage, array(
'{{ value }}' => $value,
'{{ limit }}' => $constraint->max,
'{{ limit }}' => $this->formatValue($max, self::PRETTY_DATE),
));

return;
}

if (null !== $constraint->min && $value < $constraint->min) {
if (null !== $constraint->min && $value < $min) {
$this->context->addViolation($constraint->minMessage, array(
'{{ value }}' => $value,
'{{ limit }}' => $constraint->min,
'{{ limit }}' => $this->formatValue($min, self::PRETTY_DATE),
));
}
}
Expand Down
72 changes: 69 additions & 3 deletions Tests/Constraints/AbstractComparisonValidatorTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,40 @@ public function __toString()
*/
abstract class AbstractComparisonValidatorTestCase extends AbstractConstraintValidatorTest
{
protected static function addPhp5Dot5Comparisons(array $comparisons)
{
if (version_compare(PHP_VERSION, '5.5.0-dev', '<')) {
return $comparisons;
}

$result = $comparisons;

// Duplicate all tests involving DateTime objects to be tested with
// DateTimeImmutable objects as well
foreach ($comparisons as $comparison) {
$add = false;

foreach ($comparison as $i => $value) {
if ($value instanceof \DateTime) {
$comparison[$i] = new \DateTimeImmutable(
$value->format('Y-m-d H:i:s.u e'),
$value->getTimezone()
);
$add = true;
} elseif ('DateTime' === $value) {
$comparison[$i] = 'DateTimeImmutable';
$add = true;
}
}

if ($add) {
$result[] = $comparison;
}
}

return $result;
}

/**
* @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException
*/
Expand All @@ -45,7 +79,7 @@ public function testThrowsConstraintExceptionIfNoValueOrProperty()
}

/**
* @dataProvider provideValidComparisons
* @dataProvider provideAllValidComparisons
* @param mixed $dirtyValue
* @param mixed $comparisonValue
*/
Expand All @@ -58,13 +92,29 @@ public function testValidComparisonToValue($dirtyValue, $comparisonValue)
$this->assertNoViolation();
}

/**
* @return array
*/
public function provideAllValidComparisons()
{
// The provider runs before setUp(), so we need to manually fix
// the default timezone
$this->setDefaultTimezone('UTC');

$comparisons = self::addPhp5Dot5Comparisons($this->provideValidComparisons());

$this->restoreDefaultTimezone();

return $comparisons;
}

/**
* @return array
*/
abstract public function provideValidComparisons();

/**
* @dataProvider provideInvalidComparisons
* @dataProvider provideAllInvalidComparisons
* @param mixed $dirtyValue
* @param mixed $dirtyValueAsString
* @param mixed $comparedValue
Expand All @@ -75,7 +125,7 @@ public function testInvalidComparisonToValue($dirtyValue, $dirtyValueAsString, $
{
// Conversion of dates to string differs between ICU versions
// Make sure we have the correct version loaded
if ($dirtyValue instanceof \DateTime) {
if ($dirtyValue instanceof \DateTime || $dirtyValue instanceof \DateTimeInterface) {
IntlTestHelper::requireIntl($this);
}

Expand All @@ -91,6 +141,22 @@ public function testInvalidComparisonToValue($dirtyValue, $dirtyValueAsString, $
));
}

/**
* @return array
*/
public function provideAllInvalidComparisons()
{
// The provider runs before setUp(), so we need to manually fix
// the default timezone
$this->setDefaultTimezone('UTC');

$comparisons = self::addPhp5Dot5Comparisons($this->provideInvalidComparisons());

$this->restoreDefaultTimezone();

return $comparisons;
}

/**
* @return array
*/
Expand Down
27 changes: 27 additions & 0 deletions Tests/Constraints/AbstractConstraintValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ abstract class AbstractConstraintValidatorTest extends \PHPUnit_Framework_TestCa

protected $constraint;

protected $defaultTimezone;

protected function setUp()
{
$this->group = 'MyGroup';
Expand All @@ -74,6 +76,31 @@ protected function setUp()
$this->validator->initialize($this->context);

\Locale::setDefault('en');

$this->setDefaultTimezone('UTC');
}

protected function tearDown()
{
$this->restoreDefaultTimezone();
}

protected function setDefaultTimezone($defaultTimezone)
{
// Make sure this method can not be called twice before calling
// also restoreDefaultTimezone()
if (null === $this->defaultTimezone) {
$this->defaultTimezone = ini_get('date.timezone');
ini_set('date.timezone', $defaultTimezone);
}
}

protected function restoreDefaultTimezone()
{
if (null !== $this->defaultTimezone) {
ini_set('date.timezone', $this->defaultTimezone);
$this->defaultTimezone = null;
}
}

protected function createContext()
Expand Down
4 changes: 4 additions & 0 deletions Tests/Constraints/EqualToValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public function provideValidComparisons()
array(3, '3'),
array('a', 'a'),
array(new \DateTime('2000-01-01'), new \DateTime('2000-01-01')),
array(new \DateTime('2000-01-01'), '2000-01-01'),
array(new \DateTime('2000-01-01 UTC'), '2000-01-01 UTC'),
array(new ComparisonTest_Class(5), new ComparisonTest_Class(5)),
array(null, 1),
);
Expand All @@ -59,6 +61,8 @@ public function provideInvalidComparisons()
array(1, '1', 2, '2', 'integer'),
array('22', '"22"', '333', '"333"', 'string'),
array(new \DateTime('2001-01-01'), 'Jan 1, 2001, 12:00 AM', new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', 'DateTime'),
array(new \DateTime('2001-01-01'), 'Jan 1, 2001, 12:00 AM', '2000-01-01', 'Jan 1, 2000, 12:00 AM', 'DateTime'),
array(new \DateTime('2001-01-01 UTC'), 'Jan 1, 2001, 12:00 AM', '2000-01-01 UTC', 'Jan 1, 2000, 12:00 AM', 'DateTime'),
array(new ComparisonTest_Class(4), '4', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'),
);
}
Expand Down
6 changes: 6 additions & 0 deletions Tests/Constraints/GreaterThanOrEqualValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ public function provideValidComparisons()
array(1, 1),
array(new \DateTime('2010/01/01'), new \DateTime('2000/01/01')),
array(new \DateTime('2000/01/01'), new \DateTime('2000/01/01')),
array(new \DateTime('2010/01/01'), '2000/01/01'),
array(new \DateTime('2000/01/01'), '2000/01/01'),
array(new \DateTime('2010/01/01 UTC'), '2000/01/01 UTC'),
array(new \DateTime('2000/01/01 UTC'), '2000/01/01 UTC'),
array('a', 'a'),
array('z', 'a'),
array(null, 1),
Expand All @@ -59,6 +63,8 @@ public function provideInvalidComparisons()
return array(
array(1, '1', 2, '2', 'integer'),
array(new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', new \DateTime('2005/01/01'), 'Jan 1, 2005, 12:00 AM', 'DateTime'),
array(new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', '2005/01/01', 'Jan 1, 2005, 12:00 AM', 'DateTime'),
array(new \DateTime('2000/01/01 UTC'), 'Jan 1, 2000, 12:00 AM', '2005/01/01 UTC', 'Jan 1, 2005, 12:00 AM', 'DateTime'),
array('b', '"b"', 'c', '"c"', 'string')
);
}
Expand Down
6 changes: 6 additions & 0 deletions Tests/Constraints/GreaterThanValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public function provideValidComparisons()
return array(
array(2, 1),
array(new \DateTime('2005/01/01'), new \DateTime('2001/01/01')),
array(new \DateTime('2005/01/01'), '2001/01/01'),
array(new \DateTime('2005/01/01 UTC'), '2001/01/01 UTC'),
array(new ComparisonTest_Class(5), new ComparisonTest_Class(4)),
array('333', '22'),
array(null, 1),
Expand All @@ -59,6 +61,10 @@ public function provideInvalidComparisons()
array(2, '2', 2, '2', 'integer'),
array(new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', new \DateTime('2005/01/01'), 'Jan 1, 2005, 12:00 AM', 'DateTime'),
array(new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', 'DateTime'),
array(new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', '2005/01/01', 'Jan 1, 2005, 12:00 AM', 'DateTime'),
array(new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', '2000/01/01', 'Jan 1, 2000, 12:00 AM', 'DateTime'),
array(new \DateTime('2000/01/01 UTC'), 'Jan 1, 2000, 12:00 AM', '2005/01/01 UTC', 'Jan 1, 2005, 12:00 AM', 'DateTime'),
array(new \DateTime('2000/01/01 UTC'), 'Jan 1, 2000, 12:00 AM', '2000/01/01 UTC', 'Jan 1, 2000, 12:00 AM', 'DateTime'),
array(new ComparisonTest_Class(4), '4', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'),
array(new ComparisonTest_Class(5), '5', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'),
array('22', '"22"', '333', '"333"', 'string'),
Expand Down
22 changes: 21 additions & 1 deletion Tests/Constraints/IdenticalToValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ protected function createConstraint(array $options)
return new IdenticalTo($options);
}

public function provideAllValidComparisons()
{
$this->setDefaultTimezone('UTC');

// Don't call addPhp5Dot5Comparisons() automatically, as it does
// not take care of identical objects
$comparisons = $this->provideValidComparisons();

$this->restoreDefaultTimezone();

return $comparisons;
}

/**
* {@inheritdoc}
*/
Expand All @@ -43,13 +56,20 @@ public function provideValidComparisons()
$date = new \DateTime('2000-01-01');
$object = new ComparisonTest_Class(2);

return array(
$comparisons = array(
array(3, 3),
array('a', 'a'),
array($date, $date),
array($object, $object),
array(null, 1),
);

if (version_compare(PHP_VERSION, '>=', '5.5')) {
$immutableDate = new \DateTimeImmutable('2000-01-01');
$comparisons[] = array($immutableDate, $immutableDate);
}

return $comparisons;
}

/**
Expand Down
6 changes: 6 additions & 0 deletions Tests/Constraints/LessThanOrEqualValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ public function provideValidComparisons()
array(1, 1),
array(new \DateTime('2000-01-01'), new \DateTime('2000-01-01')),
array(new \DateTime('2000-01-01'), new \DateTime('2020-01-01')),
array(new \DateTime('2000-01-01'), '2000-01-01'),
array(new \DateTime('2000-01-01'), '2020-01-01'),
array(new \DateTime('2000-01-01 UTC'), '2000-01-01 UTC'),
array(new \DateTime('2000-01-01 UTC'), '2020-01-01 UTC'),
array(new ComparisonTest_Class(4), new ComparisonTest_Class(5)),
array(new ComparisonTest_Class(5), new ComparisonTest_Class(5)),
array('a', 'a'),
Expand All @@ -61,6 +65,8 @@ public function provideInvalidComparisons()
return array(
array(2, '2', 1, '1', 'integer'),
array(new \DateTime('2010-01-01'), 'Jan 1, 2010, 12:00 AM', new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', 'DateTime'),
array(new \DateTime('2010-01-01'), 'Jan 1, 2010, 12:00 AM', '2000-01-01', 'Jan 1, 2000, 12:00 AM', 'DateTime'),
array(new \DateTime('2010-01-01 UTC'), 'Jan 1, 2010, 12:00 AM', '2000-01-01 UTC', 'Jan 1, 2000, 12:00 AM', 'DateTime'),
array(new ComparisonTest_Class(5), '5', new ComparisonTest_Class(4), '4', __NAMESPACE__.'\ComparisonTest_Class'),
array('c', '"c"', 'b', '"b"', 'string')
);
Expand Down
6 changes: 6 additions & 0 deletions Tests/Constraints/LessThanValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public function provideValidComparisons()
return array(
array(1, 2),
array(new \DateTime('2000-01-01'), new \DateTime('2010-01-01')),
array(new \DateTime('2000-01-01'), '2010-01-01'),
array(new \DateTime('2000-01-01 UTC'), '2010-01-01 UTC'),
array(new ComparisonTest_Class(4), new ComparisonTest_Class(5)),
array('22', '333'),
array(null, 1),
Expand All @@ -59,6 +61,10 @@ public function provideInvalidComparisons()
array(2, '2', 2, '2', 'integer'),
array(new \DateTime('2010-01-01'), 'Jan 1, 2010, 12:00 AM', new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', 'DateTime'),
array(new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', 'DateTime'),
array(new \DateTime('2010-01-01'), 'Jan 1, 2010, 12:00 AM', '2000-01-01', 'Jan 1, 2000, 12:00 AM', 'DateTime'),
array(new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', '2000-01-01', 'Jan 1, 2000, 12:00 AM', 'DateTime'),
array(new \DateTime('2010-01-01 UTC'), 'Jan 1, 2010, 12:00 AM', '2000-01-01 UTC', 'Jan 1, 2000, 12:00 AM', 'DateTime'),
array(new \DateTime('2000-01-01 UTC'), 'Jan 1, 2000, 12:00 AM', '2000-01-01 UTC', 'Jan 1, 2000, 12:00 AM', 'DateTime'),
array(new ComparisonTest_Class(5), '5', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'),
array(new ComparisonTest_Class(6), '6', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'),
array('333', '"333"', '22', '"22"', 'string'),
Expand Down
Loading

0 comments on commit d44e0f3

Please sign in to comment.