Skip to content

Commit 1d7684a

Browse files
feature #11673 [Validator] Added date support to comparison constraints 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
2 parents 2293556 + 60a5863 commit 1d7684a

14 files changed

+419
-14
lines changed

src/Symfony/Component/Validator/ConstraintValidator.php

+12-1
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,22 @@ protected function formatTypeOf($value)
8989
*/
9090
protected function formatValue($value, $format = 0)
9191
{
92-
if (($format & self::PRETTY_DATE) && $value instanceof \DateTime) {
92+
$isDateTime = $value instanceof \DateTime || $value instanceof \DateTimeInterface;
93+
94+
if (($format & self::PRETTY_DATE) && $isDateTime) {
9395
if (class_exists('IntlDateFormatter')) {
9496
$locale = \Locale::getDefault();
9597
$formatter = new \IntlDateFormatter($locale, \IntlDateFormatter::MEDIUM, \IntlDateFormatter::SHORT);
9698

99+
// neither the native nor the stub IntlDateFormatter support
100+
// DateTimeImmutable as of yet
101+
if (!$value instanceof \DateTime) {
102+
$value = new \DateTime(
103+
$value->format('Y-m-d H:i:s.u e'),
104+
$value->getTimezone()
105+
);
106+
}
107+
97108
return $formatter->format($value);
98109
}
99110

src/Symfony/Component/Validator/Constraints/AbstractComparisonValidator.php

+20-3
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,28 @@ public function validate($value, Constraint $constraint)
3535
return;
3636
}
3737

38-
if (!$this->compareValues($value, $constraint->value)) {
38+
$comparedValue = $constraint->value;
39+
40+
// Convert strings to DateTimes if comparing another DateTime
41+
// This allows to compare with any date/time value supported by
42+
// the DateTime constructor:
43+
// http://php.net/manual/en/datetime.formats.php
44+
if (is_string($comparedValue)) {
45+
if ($value instanceof \DatetimeImmutable) {
46+
// If $value is immutable, convert the compared value to a
47+
// DateTimeImmutable too
48+
$comparedValue = new \DatetimeImmutable($comparedValue);
49+
} elseif ($value instanceof \DateTime || $value instanceof \DateTimeInterface) {
50+
// Otherwise use DateTime
51+
$comparedValue = new \DateTime($comparedValue);
52+
}
53+
}
54+
55+
if (!$this->compareValues($value, $comparedValue)) {
3956
$this->context->addViolation($constraint->message, array(
4057
'{{ value }}' => $this->formatValue($value, self::OBJECT_TO_STRING | self::PRETTY_DATE),
41-
'{{ compared_value }}' => $this->formatValue($constraint->value, self::OBJECT_TO_STRING | self::PRETTY_DATE),
42-
'{{ compared_value_type }}' => $this->formatTypeOf($constraint->value)
58+
'{{ compared_value }}' => $this->formatValue($comparedValue, self::OBJECT_TO_STRING | self::PRETTY_DATE),
59+
'{{ compared_value_type }}' => $this->formatTypeOf($comparedValue)
4360
));
4461
}
4562
}

src/Symfony/Component/Validator/Constraints/RangeValidator.php

+22-5
Original file line numberDiff line numberDiff line change
@@ -33,27 +33,44 @@ public function validate($value, Constraint $constraint)
3333
return;
3434
}
3535

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

4141
return;
4242
}
4343

44-
if (null !== $constraint->max && $value > $constraint->max) {
44+
$min = $constraint->min;
45+
$max = $constraint->max;
46+
47+
// Convert strings to DateTimes if comparing another DateTime
48+
// This allows to compare with any date/time value supported by
49+
// the DateTime constructor:
50+
// http://php.net/manual/en/datetime.formats.php
51+
if ($value instanceof \DateTime || $value instanceof \DateTimeInterface) {
52+
if (is_string($min)) {
53+
$min = new \DateTime($min);
54+
}
55+
56+
if (is_string($max)) {
57+
$max = new \DateTime($max);
58+
}
59+
}
60+
61+
if (null !== $constraint->max && $value > $max) {
4562
$this->context->addViolation($constraint->maxMessage, array(
4663
'{{ value }}' => $value,
47-
'{{ limit }}' => $constraint->max,
64+
'{{ limit }}' => $this->formatValue($max, self::PRETTY_DATE),
4865
));
4966

5067
return;
5168
}
5269

53-
if (null !== $constraint->min && $value < $constraint->min) {
70+
if (null !== $constraint->min && $value < $min) {
5471
$this->context->addViolation($constraint->minMessage, array(
5572
'{{ value }}' => $value,
56-
'{{ limit }}' => $constraint->min,
73+
'{{ limit }}' => $this->formatValue($min, self::PRETTY_DATE),
5774
));
5875
}
5976
}

src/Symfony/Component/Validator/Tests/Constraints/AbstractComparisonValidatorTestCase.php

+69-3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,40 @@ public function __toString()
3434
*/
3535
abstract class AbstractComparisonValidatorTestCase extends AbstractConstraintValidatorTest
3636
{
37+
protected static function addPhp5Dot5Comparisons(array $comparisons)
38+
{
39+
if (version_compare(PHP_VERSION, '5.5.0-dev', '<')) {
40+
return $comparisons;
41+
}
42+
43+
$result = $comparisons;
44+
45+
// Duplicate all tests involving DateTime objects to be tested with
46+
// DateTimeImmutable objects as well
47+
foreach ($comparisons as $comparison) {
48+
$add = false;
49+
50+
foreach ($comparison as $i => $value) {
51+
if ($value instanceof \DateTime) {
52+
$comparison[$i] = new \DateTimeImmutable(
53+
$value->format('Y-m-d H:i:s.u e'),
54+
$value->getTimezone()
55+
);
56+
$add = true;
57+
} elseif ('DateTime' === $value) {
58+
$comparison[$i] = 'DateTimeImmutable';
59+
$add = true;
60+
}
61+
}
62+
63+
if ($add) {
64+
$result[] = $comparison;
65+
}
66+
}
67+
68+
return $result;
69+
}
70+
3771
/**
3872
* @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException
3973
*/
@@ -45,7 +79,7 @@ public function testThrowsConstraintExceptionIfNoValueOrProperty()
4579
}
4680

4781
/**
48-
* @dataProvider provideValidComparisons
82+
* @dataProvider provideAllValidComparisons
4983
* @param mixed $dirtyValue
5084
* @param mixed $comparisonValue
5185
*/
@@ -58,13 +92,29 @@ public function testValidComparisonToValue($dirtyValue, $comparisonValue)
5892
$this->assertNoViolation();
5993
}
6094

95+
/**
96+
* @return array
97+
*/
98+
public function provideAllValidComparisons()
99+
{
100+
// The provider runs before setUp(), so we need to manually fix
101+
// the default timezone
102+
$this->setDefaultTimezone('UTC');
103+
104+
$comparisons = self::addPhp5Dot5Comparisons($this->provideValidComparisons());
105+
106+
$this->restoreDefaultTimezone();
107+
108+
return $comparisons;
109+
}
110+
61111
/**
62112
* @return array
63113
*/
64114
abstract public function provideValidComparisons();
65115

66116
/**
67-
* @dataProvider provideInvalidComparisons
117+
* @dataProvider provideAllInvalidComparisons
68118
* @param mixed $dirtyValue
69119
* @param mixed $dirtyValueAsString
70120
* @param mixed $comparedValue
@@ -75,7 +125,7 @@ public function testInvalidComparisonToValue($dirtyValue, $dirtyValueAsString, $
75125
{
76126
// Conversion of dates to string differs between ICU versions
77127
// Make sure we have the correct version loaded
78-
if ($dirtyValue instanceof \DateTime) {
128+
if ($dirtyValue instanceof \DateTime || $dirtyValue instanceof \DateTimeInterface) {
79129
IntlTestHelper::requireIntl($this);
80130
}
81131

@@ -91,6 +141,22 @@ public function testInvalidComparisonToValue($dirtyValue, $dirtyValueAsString, $
91141
));
92142
}
93143

144+
/**
145+
* @return array
146+
*/
147+
public function provideAllInvalidComparisons()
148+
{
149+
// The provider runs before setUp(), so we need to manually fix
150+
// the default timezone
151+
$this->setDefaultTimezone('UTC');
152+
153+
$comparisons = self::addPhp5Dot5Comparisons($this->provideInvalidComparisons());
154+
155+
$this->restoreDefaultTimezone();
156+
157+
return $comparisons;
158+
}
159+
94160
/**
95161
* @return array
96162
*/

src/Symfony/Component/Validator/Tests/Constraints/AbstractConstraintValidatorTest.php

+27
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ abstract class AbstractConstraintValidatorTest extends \PHPUnit_Framework_TestCa
5252

5353
protected $constraint;
5454

55+
protected $defaultTimezone;
56+
5557
protected function setUp()
5658
{
5759
$this->group = 'MyGroup';
@@ -74,6 +76,31 @@ protected function setUp()
7476
$this->validator->initialize($this->context);
7577

7678
\Locale::setDefault('en');
79+
80+
$this->setDefaultTimezone('UTC');
81+
}
82+
83+
protected function tearDown()
84+
{
85+
$this->restoreDefaultTimezone();
86+
}
87+
88+
protected function setDefaultTimezone($defaultTimezone)
89+
{
90+
// Make sure this method can not be called twice before calling
91+
// also restoreDefaultTimezone()
92+
if (null === $this->defaultTimezone) {
93+
$this->defaultTimezone = ini_get('date.timezone');
94+
ini_set('date.timezone', $defaultTimezone);
95+
}
96+
}
97+
98+
protected function restoreDefaultTimezone()
99+
{
100+
if (null !== $this->defaultTimezone) {
101+
ini_set('date.timezone', $this->defaultTimezone);
102+
$this->defaultTimezone = null;
103+
}
77104
}
78105

79106
protected function createContext()

src/Symfony/Component/Validator/Tests/Constraints/EqualToValidatorTest.php

+4
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ public function provideValidComparisons()
4545
array(3, '3'),
4646
array('a', 'a'),
4747
array(new \DateTime('2000-01-01'), new \DateTime('2000-01-01')),
48+
array(new \DateTime('2000-01-01'), '2000-01-01'),
49+
array(new \DateTime('2000-01-01 UTC'), '2000-01-01 UTC'),
4850
array(new ComparisonTest_Class(5), new ComparisonTest_Class(5)),
4951
array(null, 1),
5052
);
@@ -59,6 +61,8 @@ public function provideInvalidComparisons()
5961
array(1, '1', 2, '2', 'integer'),
6062
array('22', '"22"', '333', '"333"', 'string'),
6163
array(new \DateTime('2001-01-01'), 'Jan 1, 2001, 12:00 AM', new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', 'DateTime'),
64+
array(new \DateTime('2001-01-01'), 'Jan 1, 2001, 12:00 AM', '2000-01-01', 'Jan 1, 2000, 12:00 AM', 'DateTime'),
65+
array(new \DateTime('2001-01-01 UTC'), 'Jan 1, 2001, 12:00 AM', '2000-01-01 UTC', 'Jan 1, 2000, 12:00 AM', 'DateTime'),
6266
array(new ComparisonTest_Class(4), '4', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'),
6367
);
6468
}

src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorTest.php

+6
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ public function provideValidComparisons()
4545
array(1, 1),
4646
array(new \DateTime('2010/01/01'), new \DateTime('2000/01/01')),
4747
array(new \DateTime('2000/01/01'), new \DateTime('2000/01/01')),
48+
array(new \DateTime('2010/01/01'), '2000/01/01'),
49+
array(new \DateTime('2000/01/01'), '2000/01/01'),
50+
array(new \DateTime('2010/01/01 UTC'), '2000/01/01 UTC'),
51+
array(new \DateTime('2000/01/01 UTC'), '2000/01/01 UTC'),
4852
array('a', 'a'),
4953
array('z', 'a'),
5054
array(null, 1),
@@ -59,6 +63,8 @@ public function provideInvalidComparisons()
5963
return array(
6064
array(1, '1', 2, '2', 'integer'),
6165
array(new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', new \DateTime('2005/01/01'), 'Jan 1, 2005, 12:00 AM', 'DateTime'),
66+
array(new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', '2005/01/01', 'Jan 1, 2005, 12:00 AM', 'DateTime'),
67+
array(new \DateTime('2000/01/01 UTC'), 'Jan 1, 2000, 12:00 AM', '2005/01/01 UTC', 'Jan 1, 2005, 12:00 AM', 'DateTime'),
6268
array('b', '"b"', 'c', '"c"', 'string')
6369
);
6470
}

src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorTest.php

+6
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ public function provideValidComparisons()
4343
return array(
4444
array(2, 1),
4545
array(new \DateTime('2005/01/01'), new \DateTime('2001/01/01')),
46+
array(new \DateTime('2005/01/01'), '2001/01/01'),
47+
array(new \DateTime('2005/01/01 UTC'), '2001/01/01 UTC'),
4648
array(new ComparisonTest_Class(5), new ComparisonTest_Class(4)),
4749
array('333', '22'),
4850
array(null, 1),
@@ -59,6 +61,10 @@ public function provideInvalidComparisons()
5961
array(2, '2', 2, '2', 'integer'),
6062
array(new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', new \DateTime('2005/01/01'), 'Jan 1, 2005, 12:00 AM', 'DateTime'),
6163
array(new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', 'DateTime'),
64+
array(new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', '2005/01/01', 'Jan 1, 2005, 12:00 AM', 'DateTime'),
65+
array(new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', '2000/01/01', 'Jan 1, 2000, 12:00 AM', 'DateTime'),
66+
array(new \DateTime('2000/01/01 UTC'), 'Jan 1, 2000, 12:00 AM', '2005/01/01 UTC', 'Jan 1, 2005, 12:00 AM', 'DateTime'),
67+
array(new \DateTime('2000/01/01 UTC'), 'Jan 1, 2000, 12:00 AM', '2000/01/01 UTC', 'Jan 1, 2000, 12:00 AM', 'DateTime'),
6268
array(new ComparisonTest_Class(4), '4', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'),
6369
array(new ComparisonTest_Class(5), '5', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'),
6470
array('22', '"22"', '333', '"333"', 'string'),

src/Symfony/Component/Validator/Tests/Constraints/IdenticalToValidatorTest.php

+21-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ protected function createConstraint(array $options)
3535
return new IdenticalTo($options);
3636
}
3737

38+
public function provideAllValidComparisons()
39+
{
40+
$this->setDefaultTimezone('UTC');
41+
42+
// Don't call addPhp5Dot5Comparisons() automatically, as it does
43+
// not take care of identical objects
44+
$comparisons = $this->provideValidComparisons();
45+
46+
$this->restoreDefaultTimezone();
47+
48+
return $comparisons;
49+
}
50+
3851
/**
3952
* {@inheritdoc}
4053
*/
@@ -43,13 +56,20 @@ public function provideValidComparisons()
4356
$date = new \DateTime('2000-01-01');
4457
$object = new ComparisonTest_Class(2);
4558

46-
return array(
59+
$comparisons = array(
4760
array(3, 3),
4861
array('a', 'a'),
4962
array($date, $date),
5063
array($object, $object),
5164
array(null, 1),
5265
);
66+
67+
if (version_compare(PHP_VERSION, '>=', '5.5')) {
68+
$immutableDate = new \DateTimeImmutable('2000-01-01');
69+
$comparisons[] = array($immutableDate, $immutableDate);
70+
}
71+
72+
return $comparisons;
5373
}
5474

5575
/**

src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorTest.php

+6
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ public function provideValidComparisons()
4545
array(1, 1),
4646
array(new \DateTime('2000-01-01'), new \DateTime('2000-01-01')),
4747
array(new \DateTime('2000-01-01'), new \DateTime('2020-01-01')),
48+
array(new \DateTime('2000-01-01'), '2000-01-01'),
49+
array(new \DateTime('2000-01-01'), '2020-01-01'),
50+
array(new \DateTime('2000-01-01 UTC'), '2000-01-01 UTC'),
51+
array(new \DateTime('2000-01-01 UTC'), '2020-01-01 UTC'),
4852
array(new ComparisonTest_Class(4), new ComparisonTest_Class(5)),
4953
array(new ComparisonTest_Class(5), new ComparisonTest_Class(5)),
5054
array('a', 'a'),
@@ -61,6 +65,8 @@ public function provideInvalidComparisons()
6165
return array(
6266
array(2, '2', 1, '1', 'integer'),
6367
array(new \DateTime('2010-01-01'), 'Jan 1, 2010, 12:00 AM', new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', 'DateTime'),
68+
array(new \DateTime('2010-01-01'), 'Jan 1, 2010, 12:00 AM', '2000-01-01', 'Jan 1, 2000, 12:00 AM', 'DateTime'),
69+
array(new \DateTime('2010-01-01 UTC'), 'Jan 1, 2010, 12:00 AM', '2000-01-01 UTC', 'Jan 1, 2000, 12:00 AM', 'DateTime'),
6470
array(new ComparisonTest_Class(5), '5', new ComparisonTest_Class(4), '4', __NAMESPACE__.'\ComparisonTest_Class'),
6571
array('c', '"c"', 'b', '"b"', 'string')
6672
);

src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorTest.php

+6
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ public function provideValidComparisons()
4343
return array(
4444
array(1, 2),
4545
array(new \DateTime('2000-01-01'), new \DateTime('2010-01-01')),
46+
array(new \DateTime('2000-01-01'), '2010-01-01'),
47+
array(new \DateTime('2000-01-01 UTC'), '2010-01-01 UTC'),
4648
array(new ComparisonTest_Class(4), new ComparisonTest_Class(5)),
4749
array('22', '333'),
4850
array(null, 1),
@@ -59,6 +61,10 @@ public function provideInvalidComparisons()
5961
array(2, '2', 2, '2', 'integer'),
6062
array(new \DateTime('2010-01-01'), 'Jan 1, 2010, 12:00 AM', new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', 'DateTime'),
6163
array(new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', 'DateTime'),
64+
array(new \DateTime('2010-01-01'), 'Jan 1, 2010, 12:00 AM', '2000-01-01', 'Jan 1, 2000, 12:00 AM', 'DateTime'),
65+
array(new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', '2000-01-01', 'Jan 1, 2000, 12:00 AM', 'DateTime'),
66+
array(new \DateTime('2010-01-01 UTC'), 'Jan 1, 2010, 12:00 AM', '2000-01-01 UTC', 'Jan 1, 2000, 12:00 AM', 'DateTime'),
67+
array(new \DateTime('2000-01-01 UTC'), 'Jan 1, 2000, 12:00 AM', '2000-01-01 UTC', 'Jan 1, 2000, 12:00 AM', 'DateTime'),
6268
array(new ComparisonTest_Class(5), '5', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'),
6369
array(new ComparisonTest_Class(6), '6', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'),
6470
array('333', '"333"', '22', '"22"', 'string'),

0 commit comments

Comments
 (0)