Skip to content

[Form][Validator] Extended Date Validator with before/after options #8034

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

Closed
wants to merge 1 commit into from
Closed
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
49 changes: 47 additions & 2 deletions src/Symfony/Component/Validator/Constraints/Date.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,50 @@
*/
class Date extends Constraint
{
public $message = 'This value is not a valid date.';
}
public $message = 'This value is not a valid date.';
public $messageBeforeDate = 'The date should be before {{ before }}.';
public $messageAfterDate = 'The date should be after {{ after }}.';
public $dateFormatMessages = 'yyyy-MM-dd'; // see http://userguide.icu-project.org/formatparse/datetime
public $dateFormatter;

public $before;
public $after;

public function __construct($options = null)
{
if (isset($options['after']) && !$options['after'] instanceof \DateTime) {
$options['after'] = new \DateTime($options['after']);
}

if (isset($options['before']) && !$options['before'] instanceof \DateTime) {
$options['before'] = new \DateTime($options['before']);
}

if (isset($options['before']) && isset($options['after'])) {
if ($options['before'] == $options['after']) {
throw new InvalidOptionsException('The options "after" and "before" may not have the same value. ' . __CLASS__, array('after', 'before'));
}
if ($options['before'] < $options['after']) {
throw new InvalidOptionsException('The value of "before" must be a date later than the value of "after". ' . __CLASS__, array('after', 'before'));
}
}

if (isset($options['dateFormatter'])) {
if (!$options['dateFormatter'] instanceof \IntlDateFormatter) {
throw new InvalidOptionsException('The option "dateFormatter" must be an instance of \IntlDateFormatter.' . __CLASS__, array('dateFormatter'));
}
} else {
$options['dateFormatter'] = new \IntlDateFormatter('en_US', \IntlDateFormatter::SHORT, \IntlDateFormatter::NONE);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The IntlDateFormatter has nothing to do in the constraint. It is not something you can configure in XML, Yaml or annotations

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since Symfony uses IntlDateFormatter anyways to format dates (e.q. in Symfony\Component\Form\Extension\Core\DataTransformer\BaseDateTimeTransformer) I thought it would be ok to use it here, too. Isn't it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constraint classes are meant to be created based on YAML or XML files or as annotations. You cannot provide an IntlDateFormatter there.
Using it could be fine, but it must be created be created the constraint validator, not by the constraint

}

if (isset($options['dateFormatMessages'])) {
if (!$options['dateFormatter']->setPattern($options['dateFormatMessages'])) {
throw new InvalidOptionsException('The value of the option "dateFormatMessages" is invalid. ' . __CLASS__, array('dateFormatMessages'));
}
} else {
$options['dateFormatter']->setPattern($this->dateFormatMessages);
}

parent::__construct($options);
}
}
35 changes: 29 additions & 6 deletions src/Symfony/Component/Validator/Constraints/DateValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,41 @@ class DateValidator extends ConstraintValidator
*/
public function validate($value, Constraint $constraint)
{
if (null === $value || '' === $value || $value instanceof \DateTime) {
if (null === $value || '' === $value) {
return;
}

if (!is_scalar($value) && !(is_object($value) && method_exists($value, '__toString'))) {
throw new UnexpectedTypeException($value, 'string');
if($value instanceof \DateTime) {
$dateTime = $value;
} else {

if (!is_scalar($value) && !(is_object($value) && method_exists($value, '__toString'))) {
throw new UnexpectedTypeException($value, 'string');
}

$value = (string) $value;

if (!preg_match(static::PATTERN, $value, $matches) || !checkdate($matches[2], $matches[3], $matches[1])) {
$this->context->addViolation($constraint->message, array('{{ value }}' => $value));

return;
}

$dateTime = new \DateTime($matches[1].'-'.$matches[2].'-'.$matches[3]);
}

$value = (string) $value;
if (null !== $constraint->before && $dateTime >= $constraint->before) {
$formattedBeforeDate = $constraint->dateFormatter->format($constraint->before);
$this->context->addViolation($constraint->messageBeforeDate, array('{{ value }}' => $value, '{{ before }}' => $formattedBeforeDate));

if (!preg_match(static::PATTERN, $value, $matches) || !checkdate($matches[2], $matches[3], $matches[1])) {
$this->context->addViolation($constraint->message, array('{{ value }}' => $value));
return;
}

if (null !== $constraint->after && $dateTime <= $constraint->after) {
$formattedAfterDate = $constraint->dateFormatter->format($constraint->after);
$this->context->addViolation($constraint->messageAfterDate, array('{{ value }}' => $value, '{{ after }}' => $formattedAfterDate));

return;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public function testValidDates($date)
public function getValidDates()
{
return array(
array(''),
array('2010-01-01'),
array('1955-12-12'),
array('2030-05-31'),
Expand Down Expand Up @@ -113,4 +114,228 @@ public function getInvalidDates()
array('2010-02-29'),
);
}

/**
* @dataProvider getValidBeforeDates
*/
public function testValidBeforeDates($date)
{
$constraint = new Date(array(
'before' => '2015-01-01'
));

$this->context->expects($this->never())
->method('addViolation');

$this->validator->validate($date, $constraint);
}

public function getValidBeforeDates()
{
return array(
array('2014-12-31'),
array('2000-01-01'),
array('1980-01-01'),
);
}

/**
* @dataProvider getInvalidBeforeDates
*/
public function testInvalidBeforeDates($date)
{
$constraint = new Date(array(
'messageBeforeDate' => 'myMessage',
'before' => '2015-01-01'
));

$this->context->expects($this->once())
->method('addViolation')
->with('myMessage', array(
'{{ value }}' => $date,
'{{ before }}' => '2015-01-01',
));

$this->validator->validate($date, $constraint);
}

public function getInvalidBeforeDates()
{
return array(
array('2015-01-01'),
array('2018-12-20'),
array('2016-02-29'),
);
}

/**
* @dataProvider getValidAfterDates
*/
public function testValidAfterDates($date)
{
$constraint = new Date(array(
'after' => '2015-01-01'
));

$this->context->expects($this->never())
->method('addViolation');

$this->validator->validate($date, $constraint);
}

public function getValidAfterDates()
{
return array(
array('2015-01-02'),
array('2016-02-29'),
array('2100-12-31'),
);
}

/**
* @dataProvider getInvalidAfterDates
*/
public function testInvalidAfterDates($date)
{
$constraint = new Date(array(
'messageAfterDate' => 'myMessage',
'after' => '2015-01-01'
));

$this->context->expects($this->once())
->method('addViolation')
->with('myMessage', array(
'{{ value }}' => $date,
'{{ after }}' => '2015-01-01',
));

$this->validator->validate($date, $constraint);
}

public function getInvalidAfterDates()
{
return array(
array('2015-01-01'),
array('2014-12-31'),
array('1980-01-01'),
);
}


/**
* @dataProvider getValidInBetweenDates
*/
public function testValidInBetweenDates($date)
{
$constraint = new Date(array(
'after' => '2014-12-31',
'before' => '2017-01-01'
));

$this->context->expects($this->never())
->method('addViolation');

$this->validator->validate($date, $constraint);
}

public function getValidInBetweenDates()
{
return array(
array('2015-01-01'),
array('2015-12-31'),
array('2016-12-31'),
);
}

/**
* @dataProvider getTooLateInBetweenDates
*/
public function testTooLateInBetweenDates($date)
{
$constraint = new Date(array(
'messageAfterDate' => 'myMessage',
'messageBeforeDate' => 'myMessage',
'after' => '2014-12-31',
'before' => '2017-01-01'
));

$this->context->expects($this->once())
->method('addViolation')
->with('myMessage', array(
'{{ value }}' => $date,
'{{ before }}' => '2017-01-01',
));

$this->validator->validate($date, $constraint);
}

public function getTooLateInBetweenDates()
{
return array(
array('2017-01-01'),
array('2020-06-06'),
);
}

/**
* @dataProvider getTooEarlyInBetweenDates
*/
public function testTooEarlyInBetweenDates($date)
{
$constraint = new Date(array(
'messageAfterDate' => 'myMessage',
'messageBeforeDate' => 'myMessage',
'after' => '2014-12-31',
'before' => '2017-01-01'
));

$this->context->expects($this->once())
->method('addViolation')
->with('myMessage', array(
'{{ value }}' => $date,
'{{ after }}' => '2014-12-31',
));

$this->validator->validate($date, $constraint);
}

public function getTooEarlyInBetweenDates()
{
return array(
array('2014-12-31'),
array('2000-06-06'),
);
}

/**
* @dataProvider getDateFormatMessages
*/
public function testDateFormatMessagesFormatter($format, $expected)
{
$constraint = new Date(array(
'messageAfterDate' => 'myMessage',
'after' => '2014-12-31',
'dateFormatMessages' => $format,
));

$date = '2000-01-01';

$this->context->expects($this->once())
->method('addViolation')
->with('myMessage', array(
'{{ value }}' => $date,
'{{ after }}' => $expected,
));

$this->validator->validate($date, $constraint);
}

public function getDateFormatMessages()
{
return array(
array('dd/MM/yy', '31/12/14'),
array('yyyy-MM-dd', '2014-12-31'),
array('yyyyMMdd HH:mm:ss', '20141231 00:00:00'),
);
}
}