Skip to content

Commit d9a1fe3

Browse files
Merge branch '3.4' into 4.1
* 3.4: Think positive KernelInterface can return null container [DI] Detect circular references with ChildDefinition parent [VarDumper] Fix global dump function return value for PHP7 [Ldap] Use shut up operator on connection errors at ldap_start_tls Implement startTest rather than startTestSuite [OptionsResolver] remove dead code and useless else [HttpFoundation] don't override StreamedResponse::setNotModified() Added relevent links for parsing to the phpdoc Add stricter checking for valid date time string Fix symfony/console (optional) dependency for MonologBridge [Form] Fix DateTimeType html5 input format
2 parents 6336168 + dad1e12 commit d9a1fe3

File tree

5 files changed

+244
-25
lines changed

5 files changed

+244
-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 <[email protected]>
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 <[email protected]>
18+
* @author Bernhard Schussek <[email protected]>
19+
* @author Fred Cox <[email protected]>
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+
}

Extension/Core/Type/DateTimeType.php

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
use Symfony\Component\Form\Extension\Core\DataTransformer\DataTransformerChain;
1717
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeImmutableToDateTimeTransformer;
1818
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
19+
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToHtml5LocalDateTimeTransformer;
1920
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
20-
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToRfc3339Transformer;
2121
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
2222
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
2323
use Symfony\Component\Form\FormBuilderInterface;
@@ -34,21 +34,8 @@ class DateTimeType extends AbstractType
3434
const DEFAULT_TIME_FORMAT = \IntlDateFormatter::MEDIUM;
3535

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

@@ -89,7 +76,7 @@ public function buildForm(FormBuilderInterface $builder, array $options)
8976

9077
if ('single_text' === $options['widget']) {
9178
if (self::HTML5_FORMAT === $pattern) {
92-
$builder->addViewTransformer(new DateTimeToRfc3339Transformer(
79+
$builder->addViewTransformer(new DateTimeToHtml5LocalDateTimeTransformer(
9380
$options['model_timezone'],
9481
$options['view_timezone']
9582
));

Tests/AbstractLayoutTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1501,7 +1501,7 @@ public function testDateTimeWithWidgetSingleText()
15011501
'/input
15021502
[@type="datetime-local"]
15031503
[@name="name"]
1504-
[@value="2011-02-03T04:05:06Z"]
1504+
[@value="2011-02-03T04:05:06"]
15051505
'
15061506
);
15071507
}
@@ -1521,7 +1521,7 @@ public function testDateTimeWithWidgetSingleTextIgnoreDateAndTimeWidgets()
15211521
'/input
15221522
[@type="datetime-local"]
15231523
[@name="name"]
1524-
[@value="2011-02-03T04:05:06Z"]
1524+
[@value="2011-02-03T04:05:06"]
15251525
'
15261526
);
15271527
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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+
*/
70+
public function testTransformDateTimeImmutable($fromTz, $toTz, $from, $to)
71+
{
72+
$transformer = new DateTimeToHtml5LocalDateTimeTransformer($fromTz, $toTz);
73+
74+
$this->assertSame($to, $transformer->transform(null !== $from ? new \DateTimeImmutable($from) : null));
75+
}
76+
77+
/**
78+
* @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
79+
*/
80+
public function testTransformRequiresValidDateTime()
81+
{
82+
$transformer = new DateTimeToHtml5LocalDateTimeTransformer();
83+
$transformer->transform('2010-01-01');
84+
}
85+
86+
/**
87+
* @dataProvider reverseTransformProvider
88+
*/
89+
public function testReverseTransform($toTz, $fromTz, $to, $from)
90+
{
91+
$transformer = new DateTimeToHtml5LocalDateTimeTransformer($toTz, $fromTz);
92+
93+
if (null !== $to) {
94+
$this->assertEquals(new \DateTime($to), $transformer->reverseTransform($from));
95+
} else {
96+
$this->assertNull($transformer->reverseTransform($from));
97+
}
98+
}
99+
100+
/**
101+
* @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
102+
*/
103+
public function testReverseTransformRequiresString()
104+
{
105+
$transformer = new DateTimeToHtml5LocalDateTimeTransformer();
106+
$transformer->reverseTransform(12345);
107+
}
108+
109+
/**
110+
* @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
111+
*/
112+
public function testReverseTransformWithNonExistingDate()
113+
{
114+
$transformer = new DateTimeToHtml5LocalDateTimeTransformer('UTC', 'UTC');
115+
116+
$transformer->reverseTransform('2010-04-31T04:05');
117+
}
118+
119+
/**
120+
* @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
121+
*/
122+
public function testReverseTransformExpectsValidDateString()
123+
{
124+
$transformer = new DateTimeToHtml5LocalDateTimeTransformer('UTC', 'UTC');
125+
126+
$transformer->reverseTransform('2010-2010-2010');
127+
}
128+
}

Tests/Extension/Core/Type/DateTimeTypeTest.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -239,12 +239,12 @@ public function testSubmitDifferentTimezonesDateTime()
239239

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

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

244244
$outputTime->setTimezone(new \DateTimeZone('America/New_York'));
245245

246246
$this->assertEquals($outputTime, $form->getData());
247-
$this->assertEquals('2010-06-02T03:04:00-10:00', $form->getViewData());
247+
$this->assertEquals('2010-06-02T03:04:00', $form->getViewData());
248248
}
249249

250250
public function testSubmitDifferentTimezonesDateTimeImmutable()
@@ -276,10 +276,10 @@ public function testSubmitStringSingleText()
276276
'widget' => 'single_text',
277277
));
278278

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

281281
$this->assertEquals('2010-06-02 03:04:00', $form->getData());
282-
$this->assertEquals('2010-06-02T03:04:00Z', $form->getViewData());
282+
$this->assertEquals('2010-06-02T03:04:00', $form->getViewData());
283283
}
284284

285285
public function testSubmitStringSingleTextWithSeconds()
@@ -292,10 +292,10 @@ public function testSubmitStringSingleTextWithSeconds()
292292
'with_seconds' => true,
293293
));
294294

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

297297
$this->assertEquals('2010-06-02 03:04:05', $form->getData());
298-
$this->assertEquals('2010-06-02T03:04:05Z', $form->getViewData());
298+
$this->assertEquals('2010-06-02T03:04:05', $form->getViewData());
299299
}
300300

301301
public function testSubmitDifferentPattern()

0 commit comments

Comments
 (0)