Skip to content

Commit cd93b98

Browse files
franzwildingnicolas-grekas
authored andcommitted
[Form] Fix DateTimeType html5 input format
1 parent 528eef3 commit cd93b98

File tree

6 files changed

+267
-25
lines changed

6 files changed

+267
-25
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Extension\Core\DataTransformer;
13+
14+
use Symfony\Component\Form\Exception\TransformationFailedException;
15+
16+
/**
17+
* @author Franz Wilding <franz.wilding@me.com>
18+
* @author Bernhard Schussek <bschussek@gmail.com>
19+
*/
20+
class DateTimeToHtml5DateTimeLocalTransformer extends BaseDateTimeTransformer
21+
{
22+
/**
23+
* Transforms a normalized date into a localized date without trailing timezone.
24+
*
25+
* According to the HTML standard, the input string of a datetime-local
26+
* input is a RFC3339 date followed by 'T', followed by a RFC3339 time.
27+
* http://w3c.github.io/html-reference/datatypes.html#form.data.datetime-local
28+
*
29+
* @param \DateTime|\DateTimeInterface $dateTime A DateTime object
30+
*
31+
* @return string The formatted date
32+
*
33+
* @throws TransformationFailedException If the given value is not an
34+
* instance of \DateTime or \DateTimeInterface
35+
*/
36+
public function transform($dateTime)
37+
{
38+
if (null === $dateTime) {
39+
return '';
40+
}
41+
42+
if (!$dateTime instanceof \DateTime && !$dateTime instanceof \DateTimeInterface) {
43+
throw new TransformationFailedException('Expected a \DateTime or \DateTimeInterface.');
44+
}
45+
46+
if ($this->inputTimezone !== $this->outputTimezone) {
47+
if (!$dateTime instanceof \DateTimeImmutable) {
48+
$dateTime = clone $dateTime;
49+
}
50+
51+
$dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
52+
}
53+
54+
return preg_replace('/\+00:00$/', '', $dateTime->format('c'));
55+
}
56+
57+
/**
58+
* Transforms a formatted datetime-local string into a normalized date.
59+
*
60+
* @param string $dateTimeLocal Formatted string
61+
*
62+
* @return \DateTime Normalized date
63+
*
64+
* @throws TransformationFailedException If the given value is not a string,
65+
* if the value could not be transformed
66+
*/
67+
public function reverseTransform($dateTimeLocal)
68+
{
69+
if (!\is_string($dateTimeLocal)) {
70+
throw new TransformationFailedException('Expected a string.');
71+
}
72+
73+
if ('' === $dateTimeLocal) {
74+
return;
75+
}
76+
77+
if ('Z' !== substr($dateTimeLocal, -1)) {
78+
$dateTimeLocal .= 'Z';
79+
}
80+
81+
try {
82+
$dateTime = new \DateTime($dateTimeLocal);
83+
} catch (\Exception $e) {
84+
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
85+
}
86+
87+
if ($this->inputTimezone !== $dateTime->getTimezone()->getName()) {
88+
$dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
89+
}
90+
91+
if (preg_match('/(\d{4})-(\d{2})-(\d{2})/', $dateTimeLocal, $matches)) {
92+
if (!checkdate($matches[2], $matches[3], $matches[1])) {
93+
throw new TransformationFailedException(sprintf(
94+
'The date "%s-%s-%s" is not a valid date.',
95+
$matches[1],
96+
$matches[2],
97+
$matches[3]
98+
));
99+
}
100+
}
101+
102+
return $dateTime;
103+
}
104+
}

src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToPartsTransformer;
1616
use Symfony\Component\Form\Extension\Core\DataTransformer\DataTransformerChain;
1717
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
18+
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToHTML5DateTimeLocalTransformer;
1819
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
19-
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToRfc3339Transformer;
2020
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
2121
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
2222
use Symfony\Component\Form\FormBuilderInterface;
@@ -33,21 +33,8 @@ class DateTimeType extends AbstractType
3333
const DEFAULT_TIME_FORMAT = \IntlDateFormatter::MEDIUM;
3434

3535
/**
36-
* This is not quite the HTML5 format yet, because ICU lacks the
37-
* capability of parsing and generating RFC 3339 dates.
38-
*
39-
* For more information see:
40-
*
41-
* http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax
42-
* https://www.w3.org/TR/html5/sec-forms.html#local-date-and-time-state-typedatetimelocal
43-
* http://tools.ietf.org/html/rfc3339
44-
*
45-
* An ICU ticket was created:
46-
* http://icu-project.org/trac/ticket/9421
47-
*
48-
* It was supposedly fixed, but is not available in all PHP installations
49-
* yet. To temporarily circumvent this issue, DateTimeToRfc3339Transformer
50-
* is used when the format matches this constant.
36+
* The HTML5 datetime-local format as defined in
37+
* http://w3c.github.io/html-reference/datatypes.html#form.data.datetime-local.
5138
*/
5239
const HTML5_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
5340

@@ -88,7 +75,7 @@ public function buildForm(FormBuilderInterface $builder, array $options)
8875

8976
if ('single_text' === $options['widget']) {
9077
if (self::HTML5_FORMAT === $pattern) {
91-
$builder->addViewTransformer(new DateTimeToRfc3339Transformer(
78+
$builder->addViewTransformer(new DateTimeToHtml5DateTimeLocalTransformer(
9279
$options['model_timezone'],
9380
$options['view_timezone']
9481
));

src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1603,7 +1603,7 @@ public function testDateTimeWithWidgetSingleText()
16031603
[@type="datetime-local"]
16041604
[@name="name"]
16051605
[@class="my&class form-control"]
1606-
[@value="2011-02-03T04:05:06Z"]
1606+
[@value="2011-02-03T04:05:06"]
16071607
'
16081608
);
16091609
}
@@ -1624,7 +1624,7 @@ public function testDateTimeWithWidgetSingleTextIgnoreDateAndTimeWidgets()
16241624
[@type="datetime-local"]
16251625
[@name="name"]
16261626
[@class="my&class form-control"]
1627-
[@value="2011-02-03T04:05:06Z"]
1627+
[@value="2011-02-03T04:05:06"]
16281628
'
16291629
);
16301630
}

src/Symfony/Component/Form/Tests/AbstractLayoutTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1502,7 +1502,7 @@ public function testDateTimeWithWidgetSingleText()
15021502
'/input
15031503
[@type="datetime-local"]
15041504
[@name="name"]
1505-
[@value="2011-02-03T04:05:06Z"]
1505+
[@value="2011-02-03T04:05:06"]
15061506
'
15071507
);
15081508
}
@@ -1522,7 +1522,7 @@ public function testDateTimeWithWidgetSingleTextIgnoreDateAndTimeWidgets()
15221522
'/input
15231523
[@type="datetime-local"]
15241524
[@name="name"]
1525-
[@value="2011-02-03T04:05:06Z"]
1525+
[@value="2011-02-03T04:05:06"]
15261526
'
15271527
);
15281528
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Tests\Extension\Core\DataTransformer;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToHtml5DateTimeLocalTransformer;
16+
17+
class DateTimeToHtml5DateTimeLocaleTransformerTest extends TestCase
18+
{
19+
protected $dateTime;
20+
protected $dateTimeWithoutSeconds;
21+
22+
protected function setUp()
23+
{
24+
parent::setUp();
25+
26+
$this->dateTime = new \DateTime('2010-02-03 04:05:06 UTC');
27+
$this->dateTimeWithoutSeconds = new \DateTime('2010-02-03 04:05:00 UTC');
28+
}
29+
30+
protected function tearDown()
31+
{
32+
$this->dateTime = null;
33+
$this->dateTimeWithoutSeconds = null;
34+
}
35+
36+
public static function assertEquals($expected, $actual, $message = '', $delta = 0, $maxDepth = 10, $canonicalize = false, $ignoreCase = false)
37+
{
38+
if ($expected instanceof \DateTime && $actual instanceof \DateTime) {
39+
$expected = $expected->format('c');
40+
$actual = $actual->format('c');
41+
}
42+
43+
parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
44+
}
45+
46+
public function allProvider()
47+
{
48+
return array(
49+
array('UTC', 'UTC', '2010-02-03 04:05:06 UTC', '2010-02-03T04:05:06'),
50+
array('UTC', 'UTC', null, ''),
51+
array('America/New_York', 'Asia/Hong_Kong', '2010-02-03 04:05:06 America/New_York', '2010-02-03T17:05:06+08:00'),
52+
array('America/New_York', 'Asia/Hong_Kong', null, ''),
53+
array('UTC', 'Asia/Hong_Kong', '2010-02-03 04:05:06 UTC', '2010-02-03T12:05:06+08:00'),
54+
array('America/New_York', 'UTC', '2010-02-03 04:05:06 America/New_York', '2010-02-03T09:05:06'),
55+
);
56+
}
57+
58+
public function transformProvider()
59+
{
60+
return $this->allProvider();
61+
}
62+
63+
public function reverseTransformProvider()
64+
{
65+
return array(
66+
// format without seconds, as appears in some browsers
67+
array('UTC', 'UTC', '2010-02-03 04:05:06 UTC', '2010-02-03T04:05:06'),
68+
array('UTC', 'UTC', null, ''),
69+
array('America/New_York', 'Asia/Hong_Kong', '2010-02-03 04:05:06 America/New_York', '2010-02-03T17:05:06+08:00'),
70+
array('America/New_York', 'Asia/Hong_Kong', null, ''),
71+
array('UTC', 'Asia/Hong_Kong', '2010-02-03 04:05:06 UTC', '2010-02-03T12:05:06+08:00'),
72+
array('America/New_York', 'UTC', '2010-02-03 04:05:06 America/New_York', '2010-02-03T09:05:06'),
73+
array('UTC', 'UTC', '2010-02-03 04:05:00 UTC', '2010-02-03T04:05'),
74+
array('America/New_York', 'Asia/Hong_Kong', '2010-02-03 04:05:00 America/New_York', '2010-02-03T17:05+08:00'),
75+
array('Europe/Amsterdam', 'Europe/Amsterdam', '2013-08-21 10:30:00 Europe/Amsterdam', '2013-08-21T08:30:00'),
76+
);
77+
}
78+
79+
/**
80+
* @dataProvider transformProvider
81+
*/
82+
public function testTransform($fromTz, $toTz, $from, $to)
83+
{
84+
$transformer = new DateTimeToHtml5DateTimeLocalTransformer($fromTz, $toTz);
85+
86+
$this->assertSame($to, $transformer->transform(null !== $from ? new \DateTime($from) : null));
87+
}
88+
89+
/**
90+
* @dataProvider transformProvider
91+
* @requires PHP 5.5
92+
*/
93+
public function testTransformDateTimeImmutable($fromTz, $toTz, $from, $to)
94+
{
95+
$transformer = new DateTimeToHtml5DateTimeLocalTransformer($fromTz, $toTz);
96+
97+
$this->assertSame($to, $transformer->transform(null !== $from ? new \DateTimeImmutable($from) : null));
98+
}
99+
100+
/**
101+
* @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
102+
*/
103+
public function testTransformRequiresValidDateTime()
104+
{
105+
$transformer = new DateTimeToHtml5DateTimeLocalTransformer();
106+
$transformer->transform('2010-01-01');
107+
}
108+
109+
/**
110+
* @dataProvider reverseTransformProvider
111+
*/
112+
public function testReverseTransform($toTz, $fromTz, $to, $from)
113+
{
114+
$transformer = new DateTimeToHtml5DateTimeLocalTransformer($toTz, $fromTz);
115+
116+
if (null !== $to) {
117+
$this->assertEquals(new \DateTime($to), $transformer->reverseTransform($from));
118+
} else {
119+
$this->assertNull($transformer->reverseTransform($from));
120+
}
121+
}
122+
123+
/**
124+
* @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
125+
*/
126+
public function testReverseTransformRequiresString()
127+
{
128+
$transformer = new DateTimeToHtml5DateTimeLocalTransformer();
129+
$transformer->reverseTransform(12345);
130+
}
131+
132+
/**
133+
* @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
134+
*/
135+
public function testReverseTransformWithNonExistingDate()
136+
{
137+
$transformer = new DateTimeToHtml5DateTimeLocalTransformer('UTC', 'UTC');
138+
139+
$transformer->reverseTransform('2010-04-31T04:05Z');
140+
}
141+
142+
/**
143+
* @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
144+
*/
145+
public function testReverseTransformExpectsValidDateString()
146+
{
147+
$transformer = new DateTimeToHtml5DateTimeLocalTransformer('UTC', 'UTC');
148+
149+
$transformer->reverseTransform('2010-2010-2010');
150+
}
151+
}

src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTimeTypeTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,10 +238,10 @@ public function testSubmitStringSingleText()
238238
'widget' => 'single_text',
239239
));
240240

241-
$form->submit('2010-06-02T03:04:00Z');
241+
$form->submit('2010-06-02T03:04:00');
242242

243243
$this->assertEquals('2010-06-02 03:04:00', $form->getData());
244-
$this->assertEquals('2010-06-02T03:04:00Z', $form->getViewData());
244+
$this->assertEquals('2010-06-02T03:04:00', $form->getViewData());
245245
}
246246

247247
public function testSubmitStringSingleTextWithSeconds()
@@ -254,10 +254,10 @@ public function testSubmitStringSingleTextWithSeconds()
254254
'with_seconds' => true,
255255
));
256256

257-
$form->submit('2010-06-02T03:04:05Z');
257+
$form->submit('2010-06-02T03:04:05');
258258

259259
$this->assertEquals('2010-06-02 03:04:05', $form->getData());
260-
$this->assertEquals('2010-06-02T03:04:05Z', $form->getViewData());
260+
$this->assertEquals('2010-06-02T03:04:05', $form->getViewData());
261261
}
262262

263263
public function testSubmitDifferentPattern()

0 commit comments

Comments
 (0)