Skip to content

Commit

Permalink
[Form] Fixed reverse transformation of values in DateTimeToStringTran…
Browse files Browse the repository at this point in the history
…sformer

The parts not given in the format are reset to the corresponding values of
the UNIX base timestamp. For example, when parsing with the format "Y-m-d",
parsing

    "2012-05-18"

now results in the date

    "2012-05-18 00:00:00 UTC"

instead of

    "2012-05-18 12:58:27 UTC"

as before, where the time part corresponded to the local server time.

Another example: When parsing with the format "H:i:s", parsing

    "12:58:27"

now results in

    "1970-01-01 12:58:27 UTC"

instead of

    "2012-12-13 12:58:27 UTC"

as before, where again the date part corresponded to the local server time.

This behavior is now consistent with DateTimeToArrayTransformer and
DateTimeToLocalizedStringTransformer.
  • Loading branch information
webmozart committed Dec 13, 2012
1 parent 722c19b commit b20c5ca
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 157 deletions.
Expand Up @@ -32,12 +32,12 @@ class DateTimeToLocalizedStringTransformer extends BaseDateTimeTransformer
*
* @see BaseDateTimeTransformer::formats for available format options
*
* @param string $inputTimezone The name of the input timezone
* @param string $outputTimezone The name of the output timezone
* @param integer $dateFormat The date format
* @param integer $timeFormat The time format
* @param \IntlDateFormatter $calendar An \IntlDateFormatter instance
* @param string $pattern A pattern to pass to \IntlDateFormatter
* @param string $inputTimezone The name of the input timezone
* @param string $outputTimezone The name of the output timezone
* @param integer $dateFormat The date format
* @param integer $timeFormat The time format
* @param integer $calendar One of the \IntlDateFormatter calendar constants
* @param string $pattern A pattern to pass to \IntlDateFormatter
*
* @throws UnexpectedTypeException If a format is not supported
* @throws UnexpectedTypeException if a timezone is not a string
Expand Down
Expand Up @@ -22,7 +22,22 @@
*/
class DateTimeToStringTransformer extends BaseDateTimeTransformer
{
private $format;
/**
* Format used for generating strings
* @var string
*/
private $generateFormat;

/**
* Format used for parsing strings
*
* Different than the {@link $generateFormat} because formats for parsing
* support additional characters in PHP that are not supported for
* generating strings.
*
* @var string
*/
private $parseFormat;

/**
* Transforms a \DateTime instance to a string
Expand All @@ -39,14 +54,26 @@ public function __construct($inputTimezone = null, $outputTimezone = null, $form
{
parent::__construct($inputTimezone, $outputTimezone);

$this->format = $format;
$this->generateFormat = $this->parseFormat = $format;

// See http://php.net/manual/en/datetime.createfromformat.php
// The character "|" in the format makes sure that the parts of a date
// that are *not* specified in the format are reset to the corresponding
// values from 1970-01-01 00:00:00 instead of the current time.
// Without "|" and "Y-m-d", "2010-02-03" becomes "2010-02-03 12:32:47",
// where the time corresponds to the current server time.
// With "|" and "Y-m-d", "2010-02-03" becomes "2010-02-03 00:00:00",
// which is at least deterministic and thus used here.
if (false === strpos($this->parseFormat, '|')) {
$this->parseFormat .= '|';
}
}

/**
* Transforms a DateTime object into a date string with the configured format
* and timezone
*
* @param DateTime $value A DateTime object
* @param \DateTime $value A DateTime object
*
* @return string A value as produced by PHP's date() function
*
Expand All @@ -70,15 +97,15 @@ public function transform($value)
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
}

return $value->format($this->format);
return $value->format($this->generateFormat);
}

/**
* Transforms a date string in the configured timezone into a DateTime object.
*
* @param string $value A value as produced by PHP's date() function
*
* @return \DateTime An instance of \DateTime
* @return \DateTime An instance of \DateTime
*
* @throws UnexpectedTypeException if the given value is not a string
* @throws TransformationFailedException if the date could not be parsed
Expand All @@ -95,20 +122,25 @@ public function reverseTransform($value)
}

try {
$dateTime = new \DateTime($value, new \DateTimeZone($this->outputTimezone));
$outputTz = new \DateTimeZone($this->outputTimezone);
$dateTime = \DateTime::createFromFormat($this->parseFormat, $value, $outputTz);

$lastErrors = \DateTime::getLastErrors();
if (0 < $lastErrors['warning_count'] || 0 < $lastErrors['error_count']) {
throw new \UnexpectedValueException(implode(', ', array_merge(array_values($lastErrors['warnings']), array_values($lastErrors['errors']))));
}

// Force value to be in same format as given to transform
if ($value !== $dateTime->format($this->format)) {
$dateTime = new \DateTime($dateTime->format($this->format), new \DateTimeZone($this->outputTimezone));
if (0 < $lastErrors['warning_count'] || 0 < $lastErrors['error_count']) {
throw new TransformationFailedException(
implode(', ', array_merge(
array_values($lastErrors['warnings']),
array_values($lastErrors['errors'])
))
);
}

if ($this->inputTimezone !== $this->outputTimezone) {
$dateTime->setTimeZone(new \DateTimeZone($this->inputTimezone));
}
} catch (TransformationFailedException $e) {
throw $e;
} catch (\Exception $e) {
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
}
Expand Down
Expand Up @@ -34,7 +34,7 @@ protected function tearDown()
$this->dateTimeWithoutSeconds = null;
}

public static function assertEquals($expected, $actual, $message = '', $delta = 0, $maxDepth = 10, $canonicalize = FALSE, $ignoreCase = FALSE)
public static function assertEquals($expected, $actual, $message = '', $delta = 0, $maxDepth = 10, $canonicalize = false, $ignoreCase = false)
{
if ($expected instanceof \DateTime && $actual instanceof \DateTime) {
$expected = $expected->format('c');
Expand All @@ -44,54 +44,59 @@ public static function assertEquals($expected, $actual, $message = '', $delta =
parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
}

public function testTransformShortDate()
{
$transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC', \IntlDateFormatter::SHORT);
$this->assertEquals('03.02.10 04:05', $transformer->transform($this->dateTime));
}

public function testTransformMediumDate()
{
$transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC', \IntlDateFormatter::MEDIUM);

$this->assertEquals('03.02.2010 04:05', $transformer->transform($this->dateTime));
}

public function testTransformLongDate()
{
$transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC', \IntlDateFormatter::LONG);

$this->assertEquals('03. Februar 2010 04:05', $transformer->transform($this->dateTime));
}

public function testTransformFullDate()
{
$transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC', \IntlDateFormatter::FULL);

$this->assertEquals('Mittwoch, 03. Februar 2010 04:05', $transformer->transform($this->dateTime));
public function dataProvider()
{
return array(
array(\IntlDateFormatter::SHORT, null, null, '03.02.10 04:05', '2010-02-03 04:05:00 UTC'),
array(\IntlDateFormatter::MEDIUM, null, null, '03.02.2010 04:05', '2010-02-03 04:05:00 UTC'),
array(\IntlDateFormatter::LONG, null, null, '03. Februar 2010 04:05', '2010-02-03 04:05:00 UTC'),
array(\IntlDateFormatter::FULL, null, null, 'Mittwoch, 03. Februar 2010 04:05', '2010-02-03 04:05:00 UTC'),
array(\IntlDateFormatter::SHORT, \IntlDateFormatter::NONE, null, '03.02.10', '2010-02-03 00:00:00 UTC'),
array(\IntlDateFormatter::MEDIUM, \IntlDateFormatter::NONE, null, '03.02.2010', '2010-02-03 00:00:00 UTC'),
array(\IntlDateFormatter::LONG, \IntlDateFormatter::NONE, null, '03. Februar 2010', '2010-02-03 00:00:00 UTC'),
array(\IntlDateFormatter::FULL, \IntlDateFormatter::NONE, null, 'Mittwoch, 03. Februar 2010', '2010-02-03 00:00:00 UTC'),
array(null, \IntlDateFormatter::SHORT, null, '03.02.2010 04:05', '2010-02-03 04:05:00 UTC'),
array(null, \IntlDateFormatter::MEDIUM, null, '03.02.2010 04:05:06', '2010-02-03 04:05:06 UTC'),
array(null, \IntlDateFormatter::LONG, null,
'03.02.2010 04:05:06 GMT' . ($this->isLowerThanIcuVersion('4.8') ? '+00:00' : ''),
'2010-02-03 04:05:06 UTC'),
// see below for extra test case for time format FULL
array(\IntlDateFormatter::NONE, \IntlDateFormatter::SHORT, null, '04:05', '1970-01-01 04:05:00 UTC'),
array(\IntlDateFormatter::NONE, \IntlDateFormatter::MEDIUM, null, '04:05:06', '1970-01-01 04:05:06 UTC'),
array(\IntlDateFormatter::NONE, \IntlDateFormatter::LONG, null,
'04:05:06 GMT' . ($this->isLowerThanIcuVersion('4.8') ? '+00:00' : ''),
'1970-01-01 04:05:06 UTC'),
array(null, null, 'yyyy-MM-dd HH:mm:00', '2010-02-03 04:05:00', '2010-02-03 04:05:00 UTC'),
array(null, null, 'yyyy-MM-dd HH:mm', '2010-02-03 04:05', '2010-02-03 04:05:00 UTC'),
array(null, null, 'yyyy-MM-dd HH', '2010-02-03 04', '2010-02-03 04:00:00 UTC'),
array(null, null, 'yyyy-MM-dd', '2010-02-03', '2010-02-03 00:00:00 UTC'),
array(null, null, 'yyyy-MM', '2010-02', '2010-02-01 00:00:00 UTC'),
array(null, null, 'yyyy', '2010', '2010-01-01 00:00:00 UTC'),
array(null, null, 'dd-MM-yyyy', '03-02-2010', '2010-02-03 00:00:00 UTC'),
array(null, null, 'HH:mm:ss', '04:05:06', '1970-01-01 04:05:06 UTC'),
array(null, null, 'HH:mm:00', '04:05:00', '1970-01-01 04:05:00 UTC'),
array(null, null, 'HH:mm', '04:05', '1970-01-01 04:05:00 UTC'),
array(null, null, 'HH', '04', '1970-01-01 04:00:00 UTC'),
);
}

public function testTransformShortTime()
{
$transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC', null, \IntlDateFormatter::SHORT);

$this->assertEquals('03.02.2010 04:05', $transformer->transform($this->dateTime));
}

public function testTransformMediumTime()
{
$transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC', null, \IntlDateFormatter::MEDIUM);

$this->assertEquals('03.02.2010 04:05:06', $transformer->transform($this->dateTime));
}

public function testTransformLongTime()
/**
* @dataProvider dataProvider
*/
public function testTransform($dateFormat, $timeFormat, $pattern, $output, $input)
{
$transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC', null, \IntlDateFormatter::LONG);
$transformer = new DateTimeToLocalizedStringTransformer(
'UTC',
'UTC',
$dateFormat,
$timeFormat,
\IntlDateFormatter::GREGORIAN,
$pattern
);

$expected = $this->isLowerThanIcuVersion('4.8') ? '03.02.2010 04:05:06 GMT+00:00' : '03.02.2010 04:05:06 GMT';
$input = new \DateTime($input);

$this->assertEquals($expected, $transformer->transform($this->dateTime));
$this->assertEquals($output, $transformer->transform($input));
}

public function testTransformFullTime()
Expand Down Expand Up @@ -143,7 +148,7 @@ public function testTransform_differentPatterns()
}

/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testTransformRequiresValidDateTime()
{
Expand All @@ -162,53 +167,23 @@ public function testTransformWrapsIntlErrors()
//$transformer->transform(1.5);
}

public function testReverseTransformShortDate()
{
$transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC', \IntlDateFormatter::SHORT);

$this->assertDateTimeEquals($this->dateTimeWithoutSeconds, $transformer->reverseTransform('03.02.10 04:05'));
}

public function testReverseTransformMediumDate()
{
$transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC', \IntlDateFormatter::MEDIUM);

$this->assertDateTimeEquals($this->dateTimeWithoutSeconds, $transformer->reverseTransform('03.02.2010 04:05'));
}

public function testReverseTransformLongDate()
{
$transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC', \IntlDateFormatter::LONG);

$this->assertDateTimeEquals($this->dateTimeWithoutSeconds, $transformer->reverseTransform('03. Februar 2010 04:05'));
}

public function testReverseTransformFullDate()
{
$transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC', \IntlDateFormatter::FULL);

$this->assertDateTimeEquals($this->dateTimeWithoutSeconds, $transformer->reverseTransform('Mittwoch, 03. Februar 2010 04:05'));
}

public function testReverseTransformShortTime()
{
$transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC', null, \IntlDateFormatter::SHORT);

$this->assertDateTimeEquals($this->dateTimeWithoutSeconds, $transformer->reverseTransform('03.02.2010 04:05'));
}

public function testReverseTransformMediumTime()
/**
* @dataProvider dataProvider
*/
public function testReverseTransform($dateFormat, $timeFormat, $pattern, $input, $output)
{
$transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC', null, \IntlDateFormatter::MEDIUM);

$this->assertDateTimeEquals($this->dateTime, $transformer->reverseTransform('03.02.2010 04:05:06'));
}
$transformer = new DateTimeToLocalizedStringTransformer(
'UTC',
'UTC',
$dateFormat,
$timeFormat,
\IntlDateFormatter::GREGORIAN,
$pattern
);

public function testReverseTransformLongTime()
{
$transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC', null, \IntlDateFormatter::LONG);
$output = new \DateTime($output);

$this->assertDateTimeEquals($this->dateTime, $transformer->reverseTransform('03.02.2010 04:05:06 GMT+00:00'));
$this->assertEquals($output, $transformer->reverseTransform($input));
}

public function testReverseTransformFullTime()
Expand Down Expand Up @@ -256,7 +231,7 @@ public function testReverseTransform_empty()
}

/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testReverseTransformRequiresString()
{
Expand All @@ -265,7 +240,7 @@ public function testReverseTransformRequiresString()
}

/**
* @expectedException Symfony\Component\Form\Exception\TransformationFailedException
* @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
*/
public function testReverseTransformWrapsIntlErrors()
{
Expand All @@ -274,23 +249,23 @@ public function testReverseTransformWrapsIntlErrors()
}

/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testValidateDateFormatOption()
{
new DateTimeToLocalizedStringTransformer(null, null, 'foobar');
}

/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testValidateTimeFormatOption()
{
new DateTimeToLocalizedStringTransformer(null, null, null, 'foobar');
}

/**
* @expectedException Symfony\Component\Form\Exception\TransformationFailedException
* @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
*/
public function testReverseTransformWithNonExistingDate()
{
Expand Down

5 comments on commit b20c5ca

@ahundiak
Copy link
Contributor

Choose a reason for hiding this comment

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

   if (false === strpos($this->parseFormat, '|')) {
        $this->parseFormat .= '|';
    }

According to notes in the php manual, the trailing | does not work correctly on all php versions:
"It seems that a pipe ('|') option in formating string works only with PHP version 5.3.8 and newer. We had an issue with it on versions 5.3.2, 5.3.3, 5.3.6. Yet it was fine with 5.3.8 and 5.3.10."

This fix broke my 5.3.3 and 5.5.5 applications. Since 5.5.3 is officially supported by S2 we might consider an adjustment somewhere.

@fabpot
Copy link
Member

@fabpot fabpot commented on b20c5ca Dec 14, 2012

Choose a reason for hiding this comment

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

@ahundiak Can you create a ticket instead so that we don't loose your comment?

@webmozart
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ahundiak Did you mean to say "5.5.5" and "5.5.3" or was this a typo?

@ahundiak
Copy link
Contributor

Choose a reason for hiding this comment

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

@bschussek - PHP version 5.3.3 and 5.3.5 is what I meant. Just happens to be the versions I have on my machines. I'm assuming the | works on newer versions based on the comments in the php manual and based on the notion that the unit tests would fail otherwise. For now, I just commented out the one line and it seems to work.

I will point out that before this commit, I could pass a string like '20120102' (without dashes) to the form element and it would work. Now it needs dashes which is fine. A transformer takes care of that issue.

@webmozart
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ahundiak Thanks for the report. This is fixed in #6353.

Please sign in to comment.