]> git.immae.eu Git - github/wallabag/wallabag.git/blob - vendor/symfony/form/Symfony/Component/Form/Extension/Core/Type/DateType.php
twig implementation
[github/wallabag/wallabag.git] / vendor / symfony / form / Symfony / Component / Form / Extension / Core / Type / DateType.php
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 }