8000 merged branch bschussek/issue3162 (PR #4839) · jacobmaster/symfony@75dfc8f · GitHub
[go: up one dir, main page]

Skip to content

Commit 75dfc8f

Browse files
committed
merged branch bschussek/issue3162 (PR symfony#4839)
Commits ------- ded6c03 [Form] DateTimeType now handles RFC 3339 dates as provided by HTML5 7e8b622 [Form] Added the option "format" to DateTimeType 9eeb200 [Form] Changed the default format of DateType to "yyyy-MM-dd" to support HTML 5 out of the box d621a76 [Form] Improved DateTimeType code Discussion ---------- [Form] Changed DateType and DateTimeType to support HTML5 by default Bug fix: no Feature addition: yes Backwards compatibility break: yes Symfony2 tests pass: yes Fixes the following tickets: symfony#2849, symfony#3162 Todo: - This PR changes DateType and DateTimeType to support HTML5 by default when setting the option "widget" to "single_text". Also, the option "format" was added to DateTimeType. --------------------------------------------------------------------------- by stof at 2012-07-10T15:38:44Z This loos OK to me --------------------------------------------------------------------------- by MDrollette at 2012-07-10T16:36:26Z @stof typo: "looks" #meta-stoffed
2 parents a94d41d + ded6c03 commit 75dfc8f

File tree

8 files changed

+451
-77
lines changed

8 files changed

+451
-77
lines changed

src/Symfony/Component/Form/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,8 @@ CHANGELOG
149149
* fixed: the "data" option supersedes default values from the model
150150
* changed DateType to refer to the "format" option for calculating the year and day choices instead
151151
of padding them automatically
152+
* [BC BREAK] DateType defaults to the format "yyyy-MM-dd" now if the widget is
153+
"single_text", in order to support the HTML 5 date field out of the box
154+
* added the option "format" to DateTimeType
155+
* [BC BREAK] DateTimeType now outputs RFC 3339 dates by default, as generated and
156+
consumed by HTML5 browsers, if the widget is "single_text"
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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\UnexpectedTypeException;
15+
use Symfony\Component\Form\Exception\TransformationFailedException;
16+
17+
/**
18+
* @author Bernhard Schussek <bschussek@gmail.com>
19+
*/
20+
class DateTimeToRfc3339Transformer extends BaseDateTimeTransformer
21+
{
22+
/**
23+
* {@inheritDoc}
24+
*/
25+
public function transform($dateTime)
26+
{
27+
if (null === $dateTime) {
28+
return '';
29+
}
30+
31+
if (!$dateTime instanceof \DateTime) {
32+
throw new UnexpectedTypeException($dateTime, '\DateTime');
33+
}
34+
35+
if ($this->inputTimezone !== $this->outputTimezone) {
36+
$dateTime = clone $dateTime;
37+
$dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
38+
}
39+
40+
return preg_replace('/\+00:00$/', 'Z', $dateTime->format('c'));
41+
}
42+
43+
/**
44+
* {@inheritDoc}
45+
*/
46+
public function reverseTransform($rfc3339)
47+
{
48+
if (!is_string($rfc3339)) {
49+
throw new UnexpectedTypeException($rfc3339, 'string');
50+
}
51+
52+
if ('' === $rfc3339) {
53+
return null;
54+
}
55+
56+
57+
$dateTime = new \DateTime($rfc3339);
58+
59+
if ($this->outputTimezone !== $this->inputTimezone) {
60+
try {
61+
$dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
62+
} catch (\Exception $e) {
63+
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
64+
}
65+
}
66+
67+
if (preg_match('/(\d{4})-(\d{2})-(\d{2})/', $rfc3339, $matches)) {
68+
if (!checkdate($matches[2], $matches[3], $matches[1])) {
69+
throw new TransformationFailedException(sprintf(
70+
'The date "%s-%s-%s" is not a valid date.',
71+
$matches[1],
72+
$matches[2],
73+
$matches[3]
74+
));
75+
}
76+
}
77+
78+
return $dateTime;
79+
}
80+
}

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

Lines changed: 99 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,38 +12,101 @@
1212
namespace Symfony\Component\Form\Extension\Core\Type;
1313

1414
use Symfony\Component\Form\AbstractType;
15+
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
1516
use Symfony\Component\Form\FormInterface;
1617
use Symfony\Component\Form\FormBuilderInterface;
1718
use Symfony\Component\Form\FormViewInterface;
1819
use Symfony\Component\Form\ReversedTransformer;
1920
use Symfony\Component\Form\Extension\Core\DataTransformer\DataTransformerChain;
2021
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
2122
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
23+
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
2224
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
25+
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToRfc3339Transformer;
2326
use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToPartsTransformer;
2427
use Symfony\Component\OptionsResolver\Options;
2528
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
2629

2730
class DateTimeType extends AbstractType
2831
{
32+
const DEFAULT_DATE_FORMAT = \IntlDateFormatter::MEDIUM;
33+
34+
const DEFAULT_TIME_FORMAT = \IntlDateFormatter::MEDIUM;
35+
36+
/**
37+
* This is not quite the HTML5 format yet, because ICU lacks the
38+
* capability of parsing and generating RFC 3339 dates, which
39+
* are like the below pattern but with a timezone suffix. The
40+
* timezone suffix is
41+
*
42+
* * "Z" for UTC
43+
* * "(-|+)HH:mm" for other timezones (note the colon!)
44+
*
45+
* http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax
46+
* http://www.w3.org/TR/html-markup/input.datetime.html
47+
* http://tools.ietf.org/html/rfc3339
48+
*
49+
* An ICU ticket was created:
50+
* http://icu-project.org/trac/ticket/9421
51+
*
52+
* To temporarily circumvent this issue, DateTimeToRfc3339Transformer is used
53+
* when the format matches this constant.
54+
*
55+
* ("ZZZZZZ" is not recognized by ICU and used here to differentiate this
56+
* pattern from custom patterns).
57+
*/
58+
const HTML5_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZZZZZZ";
59+
60+
private static $acceptedFormats = array(
61+
\IntlDateFormatter::FULL,
62+
\IntlDateFormatter::LONG,
63+
\IntlDateFormatter::MEDIUM,
64+
\IntlDateFormatter::SHORT,
65+
);
66+
2967
/**
3068
* {@inheritdoc}
3169
*/
3270
public function buildForm(FormBuilderInterface $builder, array $options)
3371
{
3472
$parts = array('year', 'month', 'day', 'hour', 'minute');
73+
$dateParts = array('year', 'month', 'day');
3574
$timeParts = array('hour', 'minute');
3675

37-
$format = 'Y-m-d H:i';
3876
if ($options['with_seconds']) {
39-
$format = 'Y-m-d H:i:s';
40-
4177
$parts[] = 'second';
4278
$timeParts[] = 'second';
4379
}
4480

81+
$dateFormat = is_int($options['date_format']) ? $options['date_format'] : self::DEFAULT_DATE_FORMAT;
82+
$timeFormat = self::DEFAULT_TIME_FORMAT;
83+
$calendar = \IntlDateFormatter::GREGORIAN;
84+
$pattern = is_string($options['format']) ? $options['format'] : null;
85+
86+
if (!in_array($dateFormat, self::$acceptedFormats, true)) {
87+
throw new InvalidOptionsException('The "date_format" option must be one of the IntlDateFormatter constants (FULL, LONG, MEDIUM, SHORT) or a string representing a custom format.');
88+
}
89+
90+
if (null !== $pattern && (false === strpos($pattern, 'y') || false === strpos($pattern, 'M') || false === strpos($pattern, 'd') || false === strpos($pattern, 'H') || false === strpos($pattern, 'm'))) {
91+
throw new InvalidOptionsException(sprintf('The "format" option should contain the letters "y", "M", "d", "H" and "m". Its current value is "%s".', $pattern));
92+
}
93+
4594
if ('single_text' === $options['widget']) {
46-
$builder->addViewTransformer(new DateTimeToStringTransformer($options['data_timezone'], $options['user_timezone'], $format));
95+
if (self::HTML5_FORMAT === $pattern) {
96+
$builder->addViewTransformer(new DateTimeToRfc3339Transformer(
97+
$options['data_timezone'],
98+
$options['user_timezone']
99+
));
100+
} else {
101+
$builder->addViewTransformer(new DateTimeToLocalizedStringTransformer(
102+
$options['data_timezone'],
103+
$options['user_timezone'],
104+
$dateFormat,
105+
$timeFormat,
106+
$calendar,
107+
$pattern
108+
));
109+
}
47110
} else {
48111
// Only pass a subset of the options to children
49112
$dateOptions = array_intersect_key($options, array_flip(array(
@@ -54,6 +117,7 @@ public function buildForm(FormBuilderInterface $builder, array $options)
54117
'required',
55118
'translation_domain',
56119
)));
120+
57121
$timeOptions = array_intersect_key($options, array_flip(array(
58122
'hours',
59123
'minutes',
@@ -64,21 +128,15 @@ public function buildForm(FormBuilderInterface $builder, array $options)
64128
'translation_domain',
65129
)));
66130

67-
// If `widget` is set, overwrite widget options from `date` and `time`
68-
if (isset($options['widget'])) {
69-
$dateOptions['widget'] = $options['widget'];
70-
$timeOptions['widget'] = $options['widget'];
71-
} else {
72-
if (isset($options['date_widget'])) {
73-
$dateOptions['widget'] = $options['date_widget'];
74-
}
131+
if (null !== $options['date_widget']) {
132+
$dateOptions['widget'] = $options['date_widget'];
133+
}
75134

76-
if (isset($options['time_widget'])) {
77-
$timeOptions['widget'] = $options['time_widget'];
78-
}
135+
if (null !== $options['time_widget']) {
136+
$timeOptions['widget'] = $options['time_widget'];
79137
}
80138

81-
if (isset($options['date_format'])) {
139+
if (null !== $options['date_format']) {
82140
$dateOptions['format'] = $options['date_format'];
83141
}
84142

@@ -89,7 +147,7 @@ public function buildForm(FormBuilderInterface $builder, array $options)
89147
->addViewTransformer(new DataTransformerChain(array(
90148
new DateTimeToArrayTransformer($options['data_timezone'], $options['user_timezone'], $parts),
91149
new ArrayToPartsTransformer(array(
92-
'date' => array('year', 'month', 'day'),
150+
'date' => $dateParts,
93151
'time' => $timeParts,
94152
)),
95153
)))
@@ -120,7 +178,10 @@ public function buildView(FormViewInterface $view, FormInterface $form, array $o
120178
{
121179
$view->setVar('widget', $options['widget']);
122180

123-
if ('single_text' === $options['widget']) {
181+
// Change the input to a HTML5 date input if
182+
// * the widget is set to "single_text"
183+
// * the format matches the one expected by HTML5
184+
if ('single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) {
124185
$view->setVar('type', 'datetime');
125186
}
126187
}
@@ -134,27 +195,29 @@ public function setDefaultOptions(OptionsResolverInterface $resolver)
134195
return $options['widget'] !== 'single_text';
135196
};
136197

198+
// Defaults to the value of "widget"
199+
$dateWidget = function (Options $options) {
200+
return $options['widget'];
201+
};
202+
203+
// Defaults to the value of "widget"
204+
$timeWidget = function (Options $options) {
205+
return $options['widget'];
206+
};
207+
137208
$resolver->setDefaults(array(
138209
'input' => 'datetime',
139210
'data_timezone' => null,
140211
'user_timezone' => null,
141-
'date_widget' => null,
212+
'format' => self::HTML5_FORMAT,
142213
'date_format' => null,
143-
'time_widget' => null,
144-
/* Defaults for date field */
145-
'years' => range(date('Y') - 5, date('Y') + 5),
146-
'months' => range(1, 12),
147-
'days' => range(1, 31),
148-
/* Defaults for time field */
149-
'hours' => range(0, 23),
150-
'minutes' => range(0, 59),
151-
'seconds' => range(0, 59),
214+
'widget' => null,
215+
'date_widget' => $dateWidget,
216+
'time_widget' => $timeWidget,
152217
'with_seconds' => false,
153218
// Don't modify \DateTime classes by reference, we treat
154219
// them like immutable value objects
155220
'by_reference' => false,
156-
// This will overwrite "widget" child options
157-
'widget' => null,
158221
// If initialized with a \DateTime object, FormType initializes
159222
// this option to "\DateTime". Since the internal, normalized
160223
// representation is not \DateTime, but an array, we need to unset
@@ -167,6 +230,12 @@ public function setDefaultOptions(OptionsResolverInterface $resolver)
167230
// set in DateType and TimeType
168231
$resolver->setOptional(array(
169232
'empty_value',
233+
'years',
234+
'months',
235+
'days',
236+
'hours',
237+
'minutes',
238+
'seconds',
170239
));
171240

172241
$resolver->setAllowedValues(array(

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ class DateType extends AbstractType
2929
{
3030
const DEFAULT_FORMAT = \IntlDateFormatter::MEDIUM;
3131

32+
const HTML5_FORMAT = 'yyyy-MM-dd';
33+
3234
private static $acceptedFormats = array(
3335
\IntlDateFormatter::FULL,
3436
\IntlDateFormatter::LONG,
@@ -51,7 +53,7 @@ public function buildForm(FormBuilderInterface $builder, array $options)
5153
}
5254

5355
if (null !== $pattern && (false === strpos($pattern, 'y') || false === strpos($pattern, 'M') || false === strpos($pattern, 'd'))) {
54-
throw new InvalidOptionsException(sprintf('The "format" option should contain the patterns "y", "M" and "d". Its current value is "%s".', $pattern));
56+
throw new InvalidOptionsException(sprintf('The "format" option should contain the letters "y", "M" and "d". Its current value is "%s".', $pattern));
5557
}
5658

5759
if ('single_text' === $options['widget']) {
@@ -130,7 +132,10 @@ public function finishView(FormViewInterface $view, FormInterface $form, array $
130132
{
131133
$view->setVar('widget', $options['widget']);
132134

133-
if ('single_text' === $options['widget']) {
135+
// Change the input to a HTML5 date input if
136+
// * the widget is set to "single_text"
137+
// * the format matches the one expected by HTML5
138+
if ('single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) {
134139
$view->setVar('type', 'date');
135140
}
136141

@@ -186,7 +191,7 @@ public function setDefaultOptions(OptionsResolverInterface $resolver)
186191
'days' => range(1, 31),
187192
'widget' => 'choice',
188193
'input' => 'datetime',
189-
'format' => self::DEFAULT_FORMAT,
194+
'format' => self::HTML5_FORMAT,
190195
'data_timezone' => null,
191196
'user_timezone' => null,
192197
'empty_value' => $emptyValue,

0 commit comments

Comments
 (0)
0