12
12
namespace Symfony \Component \Form \Extension \Core \Type ;
13
13
14
14
use Symfony \Component \Form \AbstractType ;
15
+ use Symfony \Component \OptionsResolver \Exception \InvalidOptionsException ;
15
16
use Symfony \Component \Form \FormInterface ;
16
17
use Symfony \Component \Form \FormBuilderInterface ;
17
18
use Symfony \Component \Form \FormViewInterface ;
18
19
use Symfony \Component \Form \ReversedTransformer ;
19
20
use Symfony \Component \Form \Extension \Core \DataTransformer \DataTransformerChain ;
20
21
use Symfony \Component \Form \Extension \Core \DataTransformer \DateTimeToArrayTransformer ;
21
22
use Symfony \Component \Form \Extension \Core \DataTransformer \DateTimeToStringTransformer ;
23
+ use Symfony \Component \Form \Extension \Core \DataTransformer \DateTimeToLocalizedStringTransformer ;
22
24
use Symfony \Component \Form \Extension \Core \DataTransformer \DateTimeToTimestampTransformer ;
25
+ use Symfony \Component \Form \Extension \Core \DataTransformer \DateTimeToRfc3339Transformer ;
23
26
use Symfony \Component \Form \Extension \Core \DataTransformer \ArrayToPartsTransformer ;
24
27
use Symfony \Component \OptionsResolver \Options ;
25
28
use Symfony \Component \OptionsResolver \OptionsResolverInterface ;
26
29
27
30
class DateTimeType extends AbstractType
28
31
{
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
+
29
67
/**
30
68
* {@inheritdoc}
31
69
*/
32
70
public function buildForm (FormBuilderInterface $ builder , array $ options )
33
71
{
34
72
$ parts = array ('year ' , 'month ' , 'day ' , 'hour ' , 'minute ' );
73
+ $ dateParts = array ('year ' , 'month ' , 'day ' );
35
74
$ timeParts = array ('hour ' , 'minute ' );
36
75
37
- $ format = 'Y-m-d H:i ' ;
38
76
if ($ options ['with_seconds ' ]) {
39
- $ format = 'Y-m-d H:i:s ' ;
40
-
41
77
$ parts [] = 'second ' ;
42
78
$ timeParts [] = 'second ' ;
43
79
}
44
80
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
+
45
94
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
+ }
47
110
} else {
48
111
// Only pass a subset of the options to children
49
112
$ dateOptions = array_intersect_key ($ options , array_flip (array (
@@ -54,6 +117,7 @@ public function buildForm(FormBuilderInterface $builder, array $options)
54
117
'required ' ,
55
118
'translation_domain ' ,
56
119
)));
120
+
57
121
$ timeOptions = array_intersect_key ($ options , array_flip (array (
58
122
'hours ' ,
59
123
'minutes ' ,
@@ -64,21 +128,15 @@ public function buildForm(FormBuilderInterface $builder, array $options)
64
128
'translation_domain ' ,
65
129
)));
66
130
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
+ }
75
134
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 ' ];
79
137
}
80
138
81
- if (isset ( $ options ['date_format ' ]) ) {
139
+ if (null !== $ options ['date_format ' ]) {
82
140
$ dateOptions ['format ' ] = $ options ['date_format ' ];
83
141
}
84
142
@@ -89,7 +147,7 @@ public function buildForm(FormBuilderInterface $builder, array $options)
89
147
->addViewTransformer (new DataTransformerChain (array (
90
148
new DateTimeToArrayTransformer ($ options ['data_timezone ' ], $ options ['user_timezone ' ], $ parts ),
91
149
new ArrayToPartsTransformer (array (
92
- 'date ' => array ( ' year ' , ' month ' , ' day ' ) ,
150
+ 'date ' => $ dateParts ,
93
151
'time ' => $ timeParts ,
94
152
)),
95
153
)))
@@ -120,7 +178,10 @@ public function buildView(FormViewInterface $view, FormInterface $form, array $o
120
178
{
121
179
$ view ->setVar ('widget ' , $ options ['widget ' ]);
122
180
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 ' ]) {
124
185
$ view ->setVar ('type ' , 'datetime ' );
125
186
}
126
187
}
@@ -134,27 +195,29 @@ public function setDefaultOptions(OptionsResolverInterface $resolver)
134
195
return $ options ['widget ' ] !== 'single_text ' ;
135
196
};
136
197
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
+
137
208
$ resolver ->setDefaults (array (
138
209
'input ' => 'datetime ' ,
139
210
'data_timezone ' => null ,
140
211
'user_timezone ' => null ,
141
- 'date_widget ' => null ,
212
+ 'format ' => self :: HTML5_FORMAT ,
142
213
'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 ,
152
217
'with_seconds ' => false ,
153
218
// Don't modify \DateTime classes by reference, we treat
154
219
// them like immutable value objects
155
220
'by_reference ' => false ,
156
- // This will overwrite "widget" child options
157
- 'widget ' => null ,
158
221
// If initialized with a \DateTime object, FormType initializes
159
222
// this option to "\DateTime". Since the internal, normalized
160
223
// representation is not \DateTime, but an array, we need to unset
@@ -167,6 +230,12 @@ public function setDefaultOptions(OptionsResolverInterface $resolver)
167
230
// set in DateType and TimeType
168
231
$ resolver ->setOptional (array (
169
232
'empty_value ' ,
233
+ 'years ' ,
234
+ 'months ' ,
235
+ 'days ' ,
236
+ 'hours ' ,
237
+ 'minutes ' ,
238
+ 'seconds ' ,
170
239
));
171
240
172
241
$ resolver ->setAllowedValues (array (
0 commit comments