]>
Commit | Line | Data |
---|---|---|
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\Type; | |
13 | ||
14 | use Symfony\Component\Form\AbstractType; | |
15 | use Symfony\Component\Form\FormInterface; | |
16 | use Symfony\Component\Form\FormBuilderInterface; | |
17 | use Symfony\Component\Form\FormView; | |
18 | use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer; | |
19 | use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer; | |
20 | use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer; | |
21 | use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer; | |
22 | use Symfony\Component\Form\ReversedTransformer; | |
23 | use Symfony\Component\OptionsResolver\Options; | |
24 | use Symfony\Component\OptionsResolver\OptionsResolverInterface; | |
25 | use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; | |
26 | ||
27 | class DateType extends AbstractType | |
28 | { | |
29 | const DEFAULT_FORMAT = \IntlDateFormatter::MEDIUM; | |
30 | ||
31 | const HTML5_FORMAT = 'yyyy-MM-dd'; | |
32 | ||
33 | private static $acceptedFormats = array( | |
34 | \IntlDateFormatter::FULL, | |
35 | \IntlDateFormatter::LONG, | |
36 | \IntlDateFormatter::MEDIUM, | |
37 | \IntlDateFormatter::SHORT, | |
38 | ); | |
39 | ||
40 | /** | |
41 | * {@inheritdoc} | |
42 | */ | |
43 | public function buildForm(FormBuilderInterface $builder, array $options) | |
44 | { | |
45 | $dateFormat = is_int($options['format']) ? $options['format'] : self::DEFAULT_FORMAT; | |
46 | $timeFormat = \IntlDateFormatter::NONE; | |
47 | $calendar = \IntlDateFormatter::GREGORIAN; | |
48 | $pattern = is_string($options['format']) ? $options['format'] : null; | |
49 | ||
50 | if (!in_array($dateFormat, self::$acceptedFormats, true)) { | |
51 | throw new InvalidOptionsException('The "format" option must be one of the IntlDateFormatter constants (FULL, LONG, MEDIUM, SHORT) or a string representing a custom format.'); | |
52 | } | |
53 | ||
54 | if (null !== $pattern && (false === strpos($pattern, 'y') || false === strpos($pattern, 'M') || false === strpos($pattern, 'd'))) { | |
55 | throw new InvalidOptionsException(sprintf('The "format" option should contain the letters "y", "M" and "d". Its current value is "%s".', $pattern)); | |
56 | } | |
57 | ||
58 | if ('single_text' === $options['widget']) { | |
59 | $builder->addViewTransformer(new DateTimeToLocalizedStringTransformer( | |
60 | $options['model_timezone'], | |
61 | $options['view_timezone'], | |
62 | $dateFormat, | |
63 | $timeFormat, | |
64 | $calendar, | |
65 | $pattern | |
66 | )); | |
67 | } else { | |
68 | $yearOptions = $monthOptions = $dayOptions = array( | |
69 | 'error_bubbling' => true, | |
70 | ); | |
71 | ||
72 | $formatter = new \IntlDateFormatter( | |
73 | \Locale::getDefault(), | |
74 | $dateFormat, | |
75 | $timeFormat, | |
76 | 'UTC', | |
77 | $calendar, | |
78 | $pattern | |
79 | ); | |
80 | $formatter->setLenient(false); | |
81 | ||
82 | if ('choice' === $options['widget']) { | |
83 | // Only pass a subset of the options to children | |
84 | $yearOptions['choices'] = $this->formatTimestamps($formatter, '/y+/', $this->listYears($options['years'])); | |
85 | $yearOptions['empty_value'] = $options['empty_value']['year']; | |
86 | $monthOptions['choices'] = $this->formatTimestamps($formatter, '/[M|L]+/', $this->listMonths($options['months'])); | |
87 | $monthOptions['empty_value'] = $options['empty_value']['month']; | |
88 | $dayOptions['choices'] = $this->formatTimestamps($formatter, '/d+/', $this->listDays($options['days'])); | |
89 | $dayOptions['empty_value'] = $options['empty_value']['day']; | |
90 | } | |
91 | ||
92 | // Append generic carry-along options | |
93 | foreach (array('required', 'translation_domain') as $passOpt) { | |
94 | $yearOptions[$passOpt] = $monthOptions[$passOpt] = $dayOptions[$passOpt] = $options[$passOpt]; | |
95 | } | |
96 | ||
97 | $builder | |
98 | ->add('year', $options['widget'], $yearOptions) | |
99 | ->add('month', $options['widget'], $monthOptions) | |
100 | ->add('day', $options['widget'], $dayOptions) | |
101 | ->addViewTransformer(new DateTimeToArrayTransformer( | |
102 | $options['model_timezone'], $options['view_timezone'], array('year', 'month', 'day') | |
103 | )) | |
104 | ->setAttribute('formatter', $formatter) | |
105 | ; | |
106 | } | |
107 | ||
108 | if ('string' === $options['input']) { | |
109 | $builder->addModelTransformer(new ReversedTransformer( | |
110 | new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'], 'Y-m-d') | |
111 | )); | |
112 | } elseif ('timestamp' === $options['input']) { | |
113 | $builder->addModelTransformer(new ReversedTransformer( | |
114 | new DateTimeToTimestampTransformer($options['model_timezone'], $options['model_timezone']) | |
115 | )); | |
116 | } elseif ('array' === $options['input']) { | |
117 | $builder->addModelTransformer(new ReversedTransformer( | |
118 | new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], array('year', 'month', 'day')) | |
119 | )); | |
120 | } | |
121 | } | |
122 | ||
123 | /** | |
124 | * {@inheritdoc} | |
125 | */ | |
126 | public function finishView(FormView $view, FormInterface $form, array $options) | |
127 | { | |
128 | $view->vars['widget'] = $options['widget']; | |
129 | ||
130 | // Change the input to a HTML5 date input if | |
131 | // * the widget is set to "single_text" | |
132 | // * the format matches the one expected by HTML5 | |
133 | if ('single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) { | |
134 | $view->vars['type'] = 'date'; | |
135 | } | |
136 | ||
137 | if ($form->getConfig()->hasAttribute('formatter')) { | |
138 | $pattern = $form->getConfig()->getAttribute('formatter')->getPattern(); | |
139 | ||
140 | // remove special characters unless the format was explicitly specified | |
141 | if (!is_string($options['format'])) { | |
142 | $pattern = preg_replace('/[^yMd]+/', '', $pattern); | |
143 | } | |
144 | ||
145 | // set right order with respect to locale (e.g.: de_DE=dd.MM.yy; en_US=M/d/yy) | |
146 | // lookup various formats at http://userguide.icu-project.org/formatparse/datetime | |
147 | if (preg_match('/^([yMd]+)[^yMd]*([yMd]+)[^yMd]*([yMd]+)$/', $pattern)) { | |
148 | $pattern = preg_replace(array('/y+/', '/M+/', '/d+/'), array('{{ year }}', '{{ month }}', '{{ day }}'), $pattern); | |
149 | } else { | |
150 | // default fallback | |
151 | $pattern = '{{ year }}{{ month }}{{ day }}'; | |
152 | } | |
153 | ||
154 | $view->vars['date_pattern'] = $pattern; | |
155 | } | |
156 | } | |
157 | ||
158 | /** | |
159 | * {@inheritdoc} | |
160 | */ | |
161 | public function setDefaultOptions(OptionsResolverInterface $resolver) | |
162 | { | |
163 | $compound = function (Options $options) { | |
164 | return $options['widget'] !== 'single_text'; | |
165 | }; | |
166 | ||
167 | $emptyValue = $emptyValueDefault = function (Options $options) { | |
168 | return $options['required'] ? null : ''; | |
169 | }; | |
170 | ||
171 | $emptyValueNormalizer = function (Options $options, $emptyValue) use ($emptyValueDefault) { | |
172 | if (is_array($emptyValue)) { | |
173 | $default = $emptyValueDefault($options); | |
174 | ||
175 | return array_merge( | |
176 | array('year' => $default, 'month' => $default, 'day' => $default), | |
177 | $emptyValue | |
178 | ); | |
179 | } | |
180 | ||
181 | return array( | |
182 | 'year' => $emptyValue, | |
183 | 'month' => $emptyValue, | |
184 | 'day' => $emptyValue | |
185 | ); | |
186 | }; | |
187 | ||
188 | $format = function (Options $options) { | |
189 | return $options['widget'] === 'single_text' ? DateType::HTML5_FORMAT : DateType::DEFAULT_FORMAT; | |
190 | }; | |
191 | ||
192 | $resolver->setDefaults(array( | |
193 | 'years' => range(date('Y') - 5, date('Y') + 5), | |
194 | 'months' => range(1, 12), | |
195 | 'days' => range(1, 31), | |
196 | 'widget' => 'choice', | |
197 | 'input' => 'datetime', | |
198 | 'format' => $format, | |
199 | 'model_timezone' => null, | |
200 | 'view_timezone' => null, | |
201 | 'empty_value' => $emptyValue, | |
202 | // Don't modify \DateTime classes by reference, we treat | |
203 | // them like immutable value objects | |
204 | 'by_reference' => false, | |
205 | 'error_bubbling' => false, | |
206 | // If initialized with a \DateTime object, FormType initializes | |
207 | // this option to "\DateTime". Since the internal, normalized | |
208 | // representation is not \DateTime, but an array, we need to unset | |
209 | // this option. | |
210 | 'data_class' => null, | |
211 | 'compound' => $compound, | |
212 | )); | |
213 | ||
214 | $resolver->setNormalizers(array( | |
215 | 'empty_value' => $emptyValueNormalizer, | |
216 | )); | |
217 | ||
218 | $resolver->setAllowedValues(array( | |
219 | 'input' => array( | |
220 | 'datetime', | |
221 | 'string', | |
222 | 'timestamp', | |
223 | 'array', | |
224 | ), | |
225 | 'widget' => array( | |
226 | 'single_text', | |
227 | 'text', | |
228 | 'choice', | |
229 | ), | |
230 | )); | |
231 | ||
232 | $resolver->setAllowedTypes(array( | |
233 | 'format' => array('int', 'string'), | |
234 | )); | |
235 | } | |
236 | ||
237 | /** | |
238 | * {@inheritdoc} | |
239 | */ | |
240 | public function getName() | |
241 | { | |
242 | return 'date'; | |
243 | } | |
244 | ||
245 | private function formatTimestamps(\IntlDateFormatter $formatter, $regex, array $timestamps) | |
246 | { | |
247 | $pattern = $formatter->getPattern(); | |
248 | $timezone = $formatter->getTimezoneId(); | |
249 | ||
250 | if (version_compare(\PHP_VERSION, '5.5.0-dev', '>=')) { | |
251 | $formatter->setTimeZone(\DateTimeZone::UTC); | |
252 | } else { | |
253 | $formatter->setTimeZoneId(\DateTimeZone::UTC); | |
254 | } | |
255 | ||
256 | if (preg_match($regex, $pattern, $matches)) { | |
257 | $formatter->setPattern($matches[0]); | |
258 | ||
259 | foreach ($timestamps as $key => $timestamp) { | |
260 | $timestamps[$key] = $formatter->format($timestamp); | |
261 | } | |
262 | ||
263 | // I'd like to clone the formatter above, but then we get a | |
264 | // segmentation fault, so let's restore the old state instead | |
265 | $formatter->setPattern($pattern); | |
266 | } | |
267 | ||
268 | if (version_compare(\PHP_VERSION, '5.5.0-dev', '>=')) { | |
269 | $formatter->setTimeZone($timezone); | |
270 | } else { | |
271 | $formatter->setTimeZoneId($timezone); | |
272 | } | |
273 | ||
274 | return $timestamps; | |
275 | } | |
276 | ||
277 | private function listYears(array $years) | |
278 | { | |
279 | $result = array(); | |
280 | ||
281 | foreach ($years as $year) { | |
282 | $result[$year] = gmmktime(0, 0, 0, 6, 15, $year); | |
283 | } | |
284 | ||
285 | return $result; | |
286 | } | |
287 | ||
288 | private function listMonths(array $months) | |
289 | { | |
290 | $result = array(); | |
291 | ||
292 | foreach ($months as $month) { | |
293 | $result[$month] = gmmktime(0, 0, 0, $month, 15); | |
294 | } | |
295 | ||
296 | return $result; | |
297 | } | |
298 | ||
299 | private function listDays(array $days) | |
300 | { | |
301 | $result = array(); | |
302 | ||
303 | foreach ($days as $day) { | |
304 | $result[$day] = gmmktime(0, 0, 0, 5, $day); | |
305 | } | |
306 | ||
307 | return $result; | |
308 | } | |
309 | } |