PageRenderTime 38ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

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

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