Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 8d90df7

Browse filesBrowse files
bug #28372 [Form] Fix DateTimeType html5 input format (franzwilding, mcfedr)
This PR was merged into the 2.8 branch. Discussion ---------- [Form] Fix DateTimeType html5 input format | Q | A | ------------- | --- | Branch? | 2.8 | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #27233, #27254 | License | MIT | Doc PR | N/A Fix DateTimeType' HTML input format according to HTML specs. Currently `DateTimeType` produces html with format `yyyy-MM-dd'T'HH:mm:ssZ` but the HTML5 spec expects `yyyy-MM-dd'T'HH:mm:ss` (i.e. no `Z`). Chrome presents an empty date picker meaning edits or having a default date are broken. Also the reverseTransform was expect to have a timezone attached, which it does not - and incorrectly marks it as being a UTC time in this case, instead of using the Transformers output TZ. This is same as @franzwilding #27254 but with change to just straight use of `DateTime::format` and handling TZ in reverseTransform Commits ------- e21a1a4 Added relevent links for parsing to the phpdoc 4f06f15 Add stricter checking for valid date time string 253d0a6 [Form] Fix DateTimeType html5 input format
2 parents e40bb0f + e21a1a4 commit 8d90df7
Copy full SHA for 8d90df7

File tree

Expand file treeCollapse file tree

6 files changed

+247
-27
lines changed
Filter options
Expand file treeCollapse file tree

6 files changed

+247
-27
lines changed
+104Lines 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+
* @author Fred Cox <mcfedr@gmail.com>
20+
*/
21+
class DateTimeToHtml5LocalDateTimeTransformer extends BaseDateTimeTransformer
22+
{
23+
const HTML5_FORMAT = 'Y-m-d\\TH:i:s';
24+
25+
/**
26+
* Transforms a \DateTime into a local date and time string.
27+
*
28+
* According to the HTML standard, the input string of a datetime-local
29+
* input is a RFC3339 date followed by 'T', followed by a RFC3339 time.
30+
* https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-local-date-and-time-string
31+
*
32+
* @param \DateTime|\DateTimeInterface $dateTime A DateTime object
33+
*
34+
* @return string The formatted date
35+
*
36+
* @throws TransformationFailedException If the given value is not an
37+
* instance of \DateTime or \DateTimeInterface
38+
*/
39+
public function transform($dateTime)
40+
{
41+
if (null === $dateTime) {
42+
return '';
43+
}
44+
45+
if (!$dateTime instanceof \DateTime && !$dateTime instanceof \DateTimeInterface) {
46+
throw new TransformationFailedException('Expected a \DateTime or \DateTimeInterface.');
47+
}
48+
49+
if ($this->inputTimezone !== $this->outputTimezone) {
50+
if (!$dateTime instanceof \DateTimeImmutable) {
51+
$dateTime = clone $dateTime;
52+
}
53+
54+
$dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
55+
}
56+
57+
return $dateTime->format(self::HTML5_FORMAT);
58+
}
59+
60+
/**
61+
* Transforms a local date and time string into a \DateTime.
62+
*
63+
* When transforming back to DateTime the regex is slightly laxer, taking into
64+
* account rules for parsing a local date and time string
65+
* https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-local-date-and-time-string
66+
*
67+
* @param string $dateTimeLocal Formatted string
68+
*
69+
* @return \DateTime Normalized date
70+
*
71+
* @throws TransformationFailedException If the given value is not a string,
72+
* if the value could not be transformed
73+
*/
74+
public function reverseTransform($dateTimeLocal)
75+
{
76+
if (!\is_string($dateTimeLocal)) {
77+
throw new TransformationFailedException('Expected a string.');
78+
}
79+
80+
if ('' === $dateTimeLocal) {
81+
return;
82+
}
83+
84+
if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})[T ]\d{2}:\d{2}(?::\d{2})?$/', $dateTimeLocal, $matches)) {
85+
throw new TransformationFailedException(sprintf('The date "%s" is not a valid date.', $dateTimeLocal));
86+
}
87+
88+
try {
89+
$dateTime = new \DateTime($dateTimeLocal, new \DateTimeZone($this->outputTimezone));
90+
} catch (\Exception $e) {
91+
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
92+
}
93+
94+
if ($this->inputTimezone !== $dateTime->getTimezone()->getName()) {
95+
$dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
96+
}
97+
98+
if (!checkdate($matches[2], $matches[3], $matches[1])) {
99+
throw new TransformationFailedException(sprintf('The date "%s-%s-%s" is not a valid date.', $matches[1], $matches[2], $matches[3]));
100+
}
101+
102+
return $dateTime;
103+
}
104+
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php
+4-17Lines 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\DateTimeToHtml5LocalDateTimeTransformer;
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 DateTimeToHtml5LocalDateTimeTransformer(
9279
$options['model_timezone'],
9380
$options['view_timezone']
9481
));

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php
+2-2Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/Tests/AbstractLayoutTest.php
+2-2Lines 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,129 @@
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\DateTimeToHtml5LocalDateTimeTransformer;
16+
17+
class DateTimeToHtml5LocalDateTimeTransformerTest extends TestCase
18+
{
19+
public static function assertEquals($expected, $actual, $message = '', $delta = 0, $maxDepth = 10, $canonicalize = false, $ignoreCase = false)
20+
{
21+
if ($expected instanceof \DateTime && $actual instanceof \DateTime) {
22+
$expected = $expected->format('c');
23+
$actual = $actual->format('c');
24+
}
25+
26+
parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
27+
}
28+
29+
public function transformProvider()
30+
{
31+
return array(
32+
array('UTC', 'UTC', '2010-02-03 04:05:06 UTC', '2010-02-03T04:05:06'),
33+
array('UTC', 'UTC', null, ''),
34+
array('America/New_York', 'Asia/Hong_Kong', '2010-02-03 04:05:06 America/New_York', '2010-02-03T17:05:06'),
35+
array('America/New_York', 'Asia/Hong_Kong', null, ''),
36+
array('UTC', 'Asia/Hong_Kong', '2010-02-03 04:05:06 UTC', '2010-02-03T12:05:06'),
37+
array('America/New_York', 'UTC', '2010-02-03 04:05:06 America/New_York', '2010-02-03T09:05:06'),
38+
);
39+
}
40+
41+
public function reverseTransformProvider()
42+
{
43+
return array(
44+
// format without seconds, as appears in some browsers
45+
array('UTC', 'UTC', '2010-02-03 04:05:06 UTC', '2010-02-03T04:05:06'),
46+
array('UTC', 'UTC', null, ''),
47+
array('America/New_York', 'Asia/Hong_Kong', '2010-02-03 04:05:06 America/New_York', '2010-02-03T17:05:06'),
48+
array('America/New_York', 'Asia/Hong_Kong', null, ''),
49+
array('UTC', 'Asia/Hong_Kong', '2010-02-03 04:05:06 UTC', '2010-02-03T12:05:06'),
50+
array('America/New_York', 'UTC', '2010-02-03 04:05:06 America/New_York', '2010-02-03T09:05:06'),
51+
array('UTC', 'UTC', '2010-02-03 04:05:00 UTC', '2010-02-03T04:05'),
52+
array('America/New_York', 'Asia/Hong_Kong', '2010-02-03 04:05:00 America/New_York', '2010-02-03T17:05'),
53+
array('Europe/Amsterdam', 'Europe/Amsterdam', '2013-08-21 10:30:00 Europe/Amsterdam', '2013-08-21T10:30:00'),
54+
);
55+
}
56+
57+
/**
58+
* @dataProvider transformProvider
59+
*/
60+
public function testTransform($fromTz, $toTz, $from, $to)
61+
{
62+
$transformer = new DateTimeToHtml5LocalDateTimeTransformer($fromTz, $toTz);
63+
64+
$this->assertSame($to, $transformer->transform(null !== $from ? new \DateTime($from) : null));
65+
}
66+
67+
/**
68+
* @dataProvider transformProvider
69+
* @requires PHP 5.5
70+
*/
71+
public function testTransformDateTimeImmutable($fromTz, $toTz, $from, $to)
72+
{
73+
$transformer = new DateTimeToHtml5LocalDateTimeTransformer($fromTz, $toTz);
74+
75+
$this->assertSame($to, $transformer->transform(null !== $from ? new \DateTimeImmutable($from) : null));
76+
}
77+
78+
/**
79+
* @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
80+
*/
81+
public function testTransformRequiresValidDateTime()
82+
{
83+
$transformer = new DateTimeToHtml5LocalDateTimeTransformer();
84+
$transformer->transform('2010-01-01');
85+
}
86+
87+
/**
88+
* @dataProvider reverseTransformProvider
89+
*/
90+
public function testReverseTransform($toTz, $fromTz, $to, $from)
91+
{
92+
$transformer = new DateTimeToHtml5LocalDateTimeTransformer($toTz, $fromTz);
93+
94+
if (null !== $to) {
95+
$this->assertEquals(new \DateTime($to), $transformer->reverseTransform($from));
96+
} else {
97+
$this->assertNull($transformer->reverseTransform($from));
98+
}
99+
}
100+
101+
/**
102+
* @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
103+
*/
104+
public function testReverseTransformRequiresString()
105+
{
106+
$transformer = new DateTimeToHtml5LocalDateTimeTransformer();
107+
$transformer->reverseTransform(12345);
108+
}
109+
110+
/**
111+
* @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
112+
*/
113+
public function testReverseTransformWithNonExistingDate()
114+
{
115+
$transformer = new DateTimeToHtml5LocalDateTimeTransformer('UTC', 'UTC');
116+
117+
$transformer->reverseTransform('2010-04-31T04:05');
118+
}
119+
120+
/**
121+
* @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
122+
*/
123+
public function testReverseTransformExpectsValidDateString()
124+
{
125+
$transformer = new DateTimeToHtml5LocalDateTimeTransformer('UTC', 'UTC');
126+
127+
$transformer->reverseTransform('2010-2010-2010');
128+
}
129+
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTimeTypeTest.php
+6-6Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -221,12 +221,12 @@ public function testSubmitDifferentTimezonesDateTime()
221221

222222
$outputTime = new \DateTime('2010-06-02 03:04:00 Pacific/Tahiti');
223223

224-
$form->submit('2010-06-02T03:04:00-10:00');
224+
$form->submit('2010-06-02T03:04:00');
225225

226226
$outputTime->setTimezone(new \DateTimeZone('America/New_York'));
227227

228228
$this->assertEquals($outputTime, $form->getData());
229-
$this->assertEquals('2010-06-02T03:04:00-10:00', $form->getViewData());
229+
$this->assertEquals('2010-06-02T03:04:00', $form->getViewData());
230230
}
231231

232232
public function testSubmitStringSingleText()
@@ -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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.