DateField.php 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. <?php
  2. namespace Symfony\Component\Form;
  3. /*
  4. * This file is part of the Symfony framework.
  5. *
  6. * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
  7. *
  8. * This source file is subject to the MIT license that is bundled
  9. * with this source code in the file LICENSE.
  10. */
  11. use Symfony\Component\Form\ValueTransformer\ReversedTransformer;
  12. use Symfony\Component\Form\ValueTransformer\StringToDateTimeTransformer;
  13. use Symfony\Component\Form\ValueTransformer\TimestampToDateTimeTransformer;
  14. use Symfony\Component\Form\ValueTransformer\ValueTransformerChain;
  15. use Symfony\Component\Form\ValueTransformer\DateTimeToLocalizedStringTransformer;
  16. use Symfony\Component\Form\ValueTransformer\DateTimeToArrayTransformer;
  17. class DateField extends HybridField
  18. {
  19. const FULL = 'full';
  20. const LONG = 'long';
  21. const MEDIUM = 'medium';
  22. const SHORT = 'short';
  23. const DATETIME = 'datetime';
  24. const STRING = 'string';
  25. const TIMESTAMP = 'timestamp';
  26. const RAW = 'raw';
  27. const INPUT = 'input';
  28. const CHOICE = 'choice';
  29. protected static $formats = array(
  30. self::FULL,
  31. self::LONG,
  32. self::MEDIUM,
  33. self::SHORT,
  34. );
  35. protected static $intlFormats = array(
  36. self::FULL => \IntlDateFormatter::FULL,
  37. self::LONG => \IntlDateFormatter::LONG,
  38. self::MEDIUM => \IntlDateFormatter::MEDIUM,
  39. self::SHORT => \IntlDateFormatter::SHORT,
  40. );
  41. protected static $widgets = array(
  42. self::INPUT,
  43. self::CHOICE,
  44. );
  45. protected static $types = array(
  46. self::DATETIME,
  47. self::STRING,
  48. self::TIMESTAMP,
  49. self::RAW,
  50. );
  51. /**
  52. * The ICU formatter instance
  53. * @var \IntlDateFormatter
  54. */
  55. protected $formatter;
  56. /**
  57. * Configures the text field.
  58. *
  59. * Available options:
  60. *
  61. * * widget: How to render the field ("input" or "select"). Default: "input"
  62. * * years: An array of years for the year select tag (optional)
  63. * * months: An array of months for the month select tag (optional)
  64. * * days: An array of days for the day select tag (optional)
  65. * * format: See DateValueTransformer. Default: medium
  66. * * type: The type of the date ("date", "datetime" or "timestamp"). Default: "date"
  67. * * data_timezone: The timezone of the data
  68. * * user_timezone: The timezone of the user entering a new value
  69. * * pattern: The pattern for the select boxes when "widget" is "select".
  70. * You can use the placeholders "%year%", "%month%" and "%day%".
  71. * Default: locale dependent
  72. *
  73. * @param array $options Options for this field
  74. * @throws \InvalidArgumentException Thrown if you want to show a timestamp with the select widget.
  75. */
  76. protected function configure()
  77. {
  78. $this->addOption('years', range(date('Y') - 5, date('Y') + 5));
  79. $this->addOption('months', range(1, 12));
  80. $this->addOption('days', range(1, 31));
  81. $this->addOption('format', self::MEDIUM, self::$formats);
  82. $this->addOption('type', self::DATETIME, self::$types);
  83. $this->addOption('data_timezone', 'UTC');
  84. $this->addOption('user_timezone', 'UTC');
  85. $this->addOption('widget', self::CHOICE, self::$widgets);
  86. $this->addOption('pattern');
  87. $this->initFormatter();
  88. $transformers = array();
  89. if ($this->getOption('type') === self::STRING) {
  90. $transformers[] = new StringToDateTimeTransformer(array(
  91. 'input_timezone' => $this->getOption('data_timezone'),
  92. 'output_timezone' => $this->getOption('data_timezone'),
  93. 'format' => 'Y-m-d',
  94. ));
  95. } else if ($this->getOption('type') === self::TIMESTAMP) {
  96. $transformers[] = new TimestampToDateTimeTransformer(array(
  97. 'output_timezone' => $this->getOption('data_timezone'),
  98. 'input_timezone' => $this->getOption('data_timezone'),
  99. ));
  100. } else if ($this->getOption('type') === self::RAW) {
  101. $transformers[] = new ReversedTransformer(new DateTimeToArrayTransformer(array(
  102. 'input_timezone' => $this->getOption('data_timezone'),
  103. 'output_timezone' => $this->getOption('data_timezone'),
  104. 'fields' => array('year', 'month', 'day'),
  105. )));
  106. }
  107. if ($this->getOption('widget') === self::INPUT) {
  108. $transformers[] = new DateTimeToLocalizedStringTransformer(array(
  109. 'date_format' => $this->getOption('format'),
  110. 'time_format' => DateTimeToLocalizedStringTransformer::NONE,
  111. 'input_timezone' => $this->getOption('data_timezone'),
  112. 'output_timezone' => $this->getOption('user_timezone'),
  113. ));
  114. $this->setFieldMode(self::FIELD);
  115. } else {
  116. $transformers[] = new DateTimeToArrayTransformer(array(
  117. 'input_timezone' => $this->getOption('data_timezone'),
  118. 'output_timezone' => $this->getOption('user_timezone'),
  119. ));
  120. $this->setFieldMode(self::GROUP);
  121. $this->addChoiceFields();
  122. }
  123. if (count($transformers) > 0) {
  124. $this->setValueTransformer(new ValueTransformerChain($transformers));
  125. }
  126. }
  127. /**
  128. * Generates an array of choices for the given values
  129. *
  130. * If the values are shorter than $padLength characters, they are padded with
  131. * zeros on the left side.
  132. *
  133. * @param array $values The available choices
  134. * @param integer $padLength The length to pad the choices
  135. * @return array An array with the input values as keys and the
  136. * padded values as values
  137. */
  138. protected function generatePaddedChoices(array $values, $padLength)
  139. {
  140. $choices = array();
  141. foreach ($values as $value) {
  142. $choices[$value] = str_pad($value, $padLength, '0', STR_PAD_LEFT);
  143. }
  144. return $choices;
  145. }
  146. /**
  147. * Generates an array of localized month choices
  148. *
  149. * @param array $months The month numbers to generate
  150. * @return array The localized months respecting the configured
  151. * locale and date format
  152. */
  153. protected function generateMonthChoices(array $months)
  154. {
  155. $pattern = $this->formatter->getPattern();
  156. if (preg_match('/M+/', $pattern, $matches)) {
  157. $this->formatter->setPattern($matches[0]);
  158. $choices = array();
  159. foreach ($months as $month) {
  160. $choices[$month] = $this->formatter->format(gmmktime(0, 0, 0, $month));
  161. }
  162. $this->formatter->setPattern($pattern);
  163. } else {
  164. $choices = $this->generatePaddedChoices($months, 2);
  165. }
  166. return $choices;
  167. }
  168. /**
  169. * {@inheritDoc}
  170. */
  171. public function render(array $attributes = array())
  172. {
  173. if ($this->getOption('widget') === self::INPUT) {
  174. return $this->generator->tag('input', array_merge(array(
  175. 'id' => $this->getId(),
  176. 'name' => $this->getName(),
  177. 'value' => $this->getDisplayedData(),
  178. 'type' => 'text',
  179. ), $attributes));
  180. } else {
  181. // set order as specified in the pattern
  182. if ($this->getOption('pattern')) {
  183. $pattern = $this->getOption('pattern');
  184. }
  185. // set right order with respect to locale (e.g.: de_DE=dd.MM.yy; en_US=M/d/yy)
  186. // lookup various formats at http://userguide.icu-project.org/formatparse/datetime
  187. else if (preg_match('/^([yMd]+).+([yMd]+).+([yMd]+)$/', $this->formatter->getPattern())) {
  188. $pattern = preg_replace(array('/y+/', '/M+/', '/d+/'), array('%year%', '%month%', '%day%'), $this->formatter->getPattern());
  189. }
  190. // default fallback
  191. else {
  192. $pattern = '%year%-%month%-%day%';
  193. }
  194. return str_replace(array('%year%', '%month%', '%day%'), array(
  195. $this->get('year')->render($attributes),
  196. $this->get('month')->render($attributes),
  197. $this->get('day')->render($attributes),
  198. ), $pattern);
  199. }
  200. }
  201. /**
  202. * Sets the locale of this field.
  203. *
  204. * @see Localizable
  205. */
  206. public function setLocale($locale)
  207. {
  208. parent::setLocale($locale);
  209. $this->initFormatter();
  210. if ($this->getOption('widget') === self::CHOICE) {
  211. $this->addChoiceFields();
  212. }
  213. }
  214. /**
  215. * Initializes (or reinitializes) the formatter
  216. */
  217. protected function initFormatter()
  218. {
  219. $this->formatter = new \IntlDateFormatter(
  220. $this->locale,
  221. self::$intlFormats[$this->getOption('format')],
  222. \IntlDateFormatter::NONE
  223. );
  224. }
  225. /**
  226. * Adds (or replaces if already added) the fields used when widget=CHOICE
  227. */
  228. protected function addChoiceFields()
  229. {
  230. $this->add(new ChoiceField('year', array(
  231. 'choices' => $this->generatePaddedChoices($this->getOption('years'), 4),
  232. )));
  233. $this->add(new ChoiceField('month', array(
  234. 'choices' => $this->generateMonthChoices($this->getOption('months')),
  235. )));
  236. $this->add(new ChoiceField('day', array(
  237. 'choices' => $this->generatePaddedChoices($this->getOption('days'), 2),
  238. )));
  239. }
  240. }