FormFactory.php 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien.potencier@symfony-project.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;
  11. use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
  12. use Symfony\Component\Form\ChoiceList\DefaultChoiceList;
  13. use Symfony\Component\Form\ChoiceList\PaddedChoiceList;
  14. use Symfony\Component\Form\ChoiceList\MonthChoiceList;
  15. use Symfony\Component\Form\ChoiceList\TimeZoneChoiceList;
  16. use Symfony\Component\Form\ChoiceList\EntityChoiceList;
  17. use Symfony\Component\Form\CsrfProvider\CsrfProviderInterface;
  18. use Symfony\Component\Form\DataProcessor\RadioToArrayConverter;
  19. use Symfony\Component\Form\DataProcessor\UrlProtocolFixer;
  20. use Symfony\Component\Form\DataProcessor\CollectionMerger;
  21. use Symfony\Component\Form\FieldFactory\FieldFactoryInterface;
  22. use Symfony\Component\Form\Renderer\DefaultRenderer;
  23. use Symfony\Component\Form\Renderer\Theme\ThemeInterface;
  24. use Symfony\Component\Form\Renderer\Plugin\IdPlugin;
  25. use Symfony\Component\Form\Renderer\Plugin\NamePlugin;
  26. use Symfony\Component\Form\Renderer\Plugin\ParameterPlugin;
  27. use Symfony\Component\Form\Renderer\Plugin\ChoicePlugin;
  28. use Symfony\Component\Form\Renderer\Plugin\ParentNamePlugin;
  29. use Symfony\Component\Form\Renderer\Plugin\DatePatternPlugin;
  30. use Symfony\Component\Form\Renderer\Plugin\MoneyPatternPlugin;
  31. use Symfony\Component\Form\Renderer\Plugin\ValuePlugin;
  32. use Symfony\Component\Form\Renderer\Plugin\PasswordValuePlugin;
  33. use Symfony\Component\Form\Renderer\Plugin\SelectMultipleNamePlugin;
  34. use Symfony\Component\Form\ValueTransformer\BooleanToStringTransformer;
  35. use Symfony\Component\Form\ValueTransformer\NumberToLocalizedStringTransformer;
  36. use Symfony\Component\Form\ValueTransformer\IntegerToLocalizedStringTransformer;
  37. use Symfony\Component\Form\ValueTransformer\MoneyToLocalizedStringTransformer;
  38. use Symfony\Component\Form\ValueTransformer\PercentToLocalizedStringTransformer;
  39. use Symfony\Component\Form\ValueTransformer\ScalarToChoicesTransformer;
  40. use Symfony\Component\Form\ValueTransformer\DateTimeToArrayTransformer;
  41. use Symfony\Component\Form\ValueTransformer\DateTimeToStringTransformer;
  42. use Symfony\Component\Form\ValueTransformer\DateTimeToLocalizedStringTransformer;
  43. use Symfony\Component\Form\ValueTransformer\DateTimeToTimestampTransformer;
  44. use Symfony\Component\Form\ValueTransformer\ReversedTransformer;
  45. use Symfony\Component\Form\ValueTransformer\EntityToIdTransformer;
  46. use Symfony\Component\Form\ValueTransformer\EntitiesToArrayTransformer;
  47. use Symfony\Component\Form\ValueTransformer\ValueTransformerChain;
  48. use Symfony\Component\Form\ValueTransformer\ArrayToChoicesTransformer;
  49. use Symfony\Component\Form\ValueTransformer\ArrayToPartsTransformer;
  50. use Symfony\Component\Validator\ValidatorInterface;
  51. use Symfony\Component\Locale\Locale;
  52. class FormFactory
  53. {
  54. private $theme;
  55. private $csrfProvider;
  56. private $validator;
  57. private $fieldFactory;
  58. public function __construct(ThemeInterface $theme, CsrfProviderInterface $csrfProvider, ValidatorInterface $validator, FieldFactoryInterface $fieldFactory)
  59. {
  60. $this->theme = $theme;
  61. $this->csrfProvider = $csrfProvider;
  62. $this->validator = $validator;
  63. $this->fieldFactory = $fieldFactory;
  64. }
  65. protected function getTheme()
  66. {
  67. return $this->theme;
  68. }
  69. protected function getCsrfProvider()
  70. {
  71. return $this->csrfProvider;
  72. }
  73. protected function getValidator()
  74. {
  75. return $this->validator;
  76. }
  77. protected function getFieldFactory()
  78. {
  79. return $this->fieldFactory;
  80. }
  81. protected function initField(FieldInterface $field, array $options = array())
  82. {
  83. $options = array_merge(array(
  84. 'template' => 'text',
  85. 'data' => null,
  86. 'property_path' => (string)$field->getKey(),
  87. 'trim' => true,
  88. 'required' => true,
  89. 'disabled' => false,
  90. 'value_transformer' => null,
  91. 'normalization_transformer' => null,
  92. ), $options);
  93. return $field
  94. ->setData($options['data'])
  95. ->setPropertyPath($options['property_path'])
  96. ->setTrim($options['trim'])
  97. ->setRequired($options['required'])
  98. ->setDisabled($options['disabled'])
  99. ->setValueTransformer($options['value_transformer'])
  100. ->setNormalizationTransformer($options['normalization_transformer'])
  101. ->setRenderer(new DefaultRenderer($this->theme, $options['template']))
  102. ->addRendererPlugin(new IdPlugin($field))
  103. ->addRendererPlugin(new NamePlugin($field))
  104. ->addRendererPlugin(new ValuePlugin($field))
  105. ->setRendererVar('field', $field)
  106. ->setRendererVar('class', null)
  107. ->setRendererVar('max_length', null)
  108. ->setRendererVar('size', null)
  109. ->setRendererVar('label', ucfirst(strtolower(str_replace('_', ' ', $field->getKey()))));
  110. }
  111. protected function initForm(FormInterface $form, array $options = array())
  112. {
  113. $options = array_merge(array(
  114. 'template' => 'form',
  115. 'data_class' => null,
  116. 'data_constructor' => null,
  117. 'csrf_protection' => true,
  118. 'csrf_field_name' => '_token',
  119. 'csrf_provider' => $this->csrfProvider,
  120. 'field_factory' => $this->fieldFactory,
  121. 'validation_groups' => null,
  122. 'virtual' => false,
  123. 'validator' => $this->validator,
  124. ), $options);
  125. $this->initField($form, $options);
  126. if ($options['csrf_protection']) {
  127. $form->enableCsrfProtection($options['csrf_provider'], $options['csrf_field_name']);
  128. }
  129. return $form
  130. ->setDataClass($options['data_class'])
  131. ->setDataConstructor($options['data_constructor'])
  132. ->setFieldFactory($options['field_factory'])
  133. ->setValidationGroups($options['validation_groups'])
  134. ->setVirtual($options['virtual'])
  135. ->setValidator($options['validator']);
  136. }
  137. public function getField($key, array $options = array())
  138. {
  139. $field = new Field($key);
  140. $this->initField($field, $options);
  141. return $field;
  142. }
  143. public function getForm($key, array $options = array())
  144. {
  145. $form = new Form($key);
  146. $this->initForm($form, $options);
  147. return $form;
  148. }
  149. public function getTextField($key, array $options = array())
  150. {
  151. $options = array_merge(array(
  152. 'template' => 'text',
  153. 'max_length' => null,
  154. ), $options);
  155. return $this->getField($key, $options)
  156. ->setRendererVar('max_length', $options['max_length']);
  157. }
  158. public function getTextareaField($key, array $options = array())
  159. {
  160. $options = array_merge(array(
  161. 'template' => 'textarea',
  162. ), $options);
  163. return $this->getField($key, $options);
  164. }
  165. public function getPasswordField($key, array $options = array())
  166. {
  167. $options = array_merge(array(
  168. 'template' => 'password',
  169. 'always_empty' => true,
  170. ), $options);
  171. $field = $this->getTextField($key, $options);
  172. return $field
  173. ->addRendererPlugin(new PasswordValuePlugin($field, $options['always_empty']));
  174. }
  175. public function getHiddenField($key, array $options = array())
  176. {
  177. $options = array_merge(array(
  178. 'template' => 'hidden',
  179. ), $options);
  180. return $this->getField($key, $options)
  181. ->setHidden(true);
  182. }
  183. public function getNumberField($key, array $options = array())
  184. {
  185. $options = array_merge(array(
  186. 'template' => 'number',
  187. // default precision is locale specific (usually around 3)
  188. 'precision' => null,
  189. 'grouping' => false,
  190. 'rounding_mode' => NumberToLocalizedStringTransformer::ROUND_HALFUP,
  191. ), $options);
  192. return $this->getField($key, $options)
  193. ->setValueTransformer(new NumberToLocalizedStringTransformer(array(
  194. 'precision' => $options['precision'],
  195. 'grouping' => $options['grouping'],
  196. 'rounding-mode' => $options['rounding_mode'],
  197. )));
  198. }
  199. public function getIntegerField($key, array $options = array())
  200. {
  201. $options = array_merge(array(
  202. 'template' => 'integer',
  203. // default precision is locale specific (usually around 3)
  204. 'precision' => null,
  205. 'grouping' => false,
  206. // Integer cast rounds towards 0, so do the same when displaying fractions
  207. 'rounding_mode' => IntegerToLocalizedStringTransformer::ROUND_DOWN,
  208. ), $options);
  209. return $this->getField($key, $options)
  210. ->setValueTransformer(new IntegerToLocalizedStringTransformer(array(
  211. 'precision' => $options['precision'],
  212. 'grouping' => $options['grouping'],
  213. 'rounding-mode' => $options['rounding_mode'],
  214. )));
  215. }
  216. public function getMoneyField($key, array $options = array())
  217. {
  218. $options = array_merge(array(
  219. 'template' => 'money',
  220. 'precision' => 2,
  221. 'grouping' => false,
  222. 'divisor' => 1,
  223. 'currency' => 'EUR',
  224. ), $options);
  225. return $this->getField($key, $options)
  226. ->setValueTransformer(new MoneyToLocalizedStringTransformer(array(
  227. 'precision' => $options['precision'],
  228. 'grouping' => $options['grouping'],
  229. 'divisor' => $options['divisor'],
  230. )))
  231. ->addRendererPlugin(new MoneyPatternPlugin($options['currency']));
  232. }
  233. public function getPercentField($key, array $options = array())
  234. {
  235. $options = array_merge(array(
  236. 'template' => 'percent',
  237. 'precision' => 0,
  238. 'type' => 'fractional',
  239. ), $options);
  240. return $this->getField($key, $options)
  241. ->setValueTransformer(new PercentToLocalizedStringTransformer(array(
  242. 'precision' => $options['precision'],
  243. 'type' => $options['type'],
  244. )));
  245. }
  246. public function getCheckboxField($key, array $options = array())
  247. {
  248. $options = array_merge(array(
  249. 'template' => 'checkbox',
  250. 'value' => '1',
  251. ), $options);
  252. return $this->getField($key, $options)
  253. ->setValueTransformer(new BooleanToStringTransformer())
  254. ->setRendererVar('value', $options['value']);
  255. }
  256. public function getRadioField($key, array $options = array())
  257. {
  258. $options = array_merge(array(
  259. 'template' => 'radio',
  260. 'value' => null,
  261. ), $options);
  262. $field = $this->getField($key, $options);
  263. return $field
  264. ->setValueTransformer(new BooleanToStringTransformer())
  265. ->addRendererPlugin(new ParentNamePlugin($field))
  266. ->setRendererVar('value', $options['value']);
  267. }
  268. public function getUrlField($key, array $options = array())
  269. {
  270. $options = array_merge(array(
  271. 'default_protocol' => 'http',
  272. ), $options);
  273. return $this->getTextField($key, $options)
  274. ->setDataProcessor(new UrlProtocolFixer($options['default_protocol']));
  275. }
  276. protected function getChoiceFieldForList($key, ChoiceListInterface $choiceList, array $options = array())
  277. {
  278. $options = array_merge(array(
  279. 'template' => 'choice',
  280. 'multiple' => false,
  281. 'expanded' => false,
  282. ), $options);
  283. if (!$options['expanded']) {
  284. $field = $this->getField($key, $options);
  285. } else {
  286. $field = $this->getForm($key, $options);
  287. $choices = array_replace($choiceList->getPreferredChoices(), $choiceList->getOtherChoices());
  288. foreach ($choices as $choice => $value) {
  289. if ($options['multiple']) {
  290. $field->add($this->getCheckboxField($choice, array(
  291. 'value' => $choice,
  292. )));
  293. } else {
  294. $field->add($this->getRadioField($choice, array(
  295. 'value' => $choice,
  296. )));
  297. }
  298. }
  299. }
  300. $field->addRendererPlugin(new ChoicePlugin($choiceList))
  301. ->setRendererVar('multiple', $options['multiple'])
  302. ->setRendererVar('expanded', $options['expanded']);
  303. if ($options['multiple'] && $options['expanded']) {
  304. $field->setValueTransformer(new ArrayToChoicesTransformer($choiceList));
  305. }
  306. if (!$options['multiple'] && $options['expanded']) {
  307. $field->setValueTransformer(new ScalarToChoicesTransformer($choiceList));
  308. $field->setDataPreprocessor(new RadioToArrayConverter());
  309. }
  310. if ($options['multiple'] && !$options['expanded']) {
  311. $field->addRendererPlugin(new SelectMultipleNamePlugin($field));
  312. }
  313. return $field;
  314. }
  315. public function getChoiceField($key, array $options = array())
  316. {
  317. $options = array_merge(array(
  318. 'choices' => array(),
  319. 'preferred_choices' => array(),
  320. ), $options);
  321. $choiceList = new DefaultChoiceList(
  322. $options['choices'],
  323. $options['preferred_choices']
  324. );
  325. return $this->getChoiceFieldForList($key, $choiceList, $options);
  326. }
  327. public function getEntityChoiceField($key, array $options = array())
  328. {
  329. $options = array_merge(array(
  330. 'em' => null,
  331. 'class' => null,
  332. 'property' => null,
  333. 'query_builder' => null,
  334. 'choices' => array(),
  335. 'preferred_choices' => array(),
  336. 'multiple' => false,
  337. 'expanded' => false,
  338. ), $options);
  339. $choiceList = new EntityChoiceList(
  340. $options['em'],
  341. $options['class'],
  342. $options['property'],
  343. $options['query_builder'],
  344. $options['choices'],
  345. $options['preferred_choices']
  346. );
  347. $field = $this->getChoiceFieldForList($key, $choiceList, $options);
  348. $transformers = array();
  349. if ($options['multiple']) {
  350. $field->setDataProcessor(new CollectionMerger($field));
  351. $transformers[] = new EntitiesToArrayTransformer($choiceList);
  352. if ($options['expanded']) {
  353. $transformers[] = new ArrayToChoicesTransformer($choiceList);
  354. }
  355. } else {
  356. $transformers[] = new EntityToIdTransformer($choiceList);
  357. if ($options['expanded']) {
  358. $transformers[] = new ScalarToChoicesTransformer($choiceList);
  359. }
  360. }
  361. if (count($transformers) > 1) {
  362. $field->setValueTransformer(new ValueTransformerChain($transformers));
  363. } else {
  364. $field->setValueTransformer(current($transformers));
  365. }
  366. return $field;
  367. }
  368. public function getCountryField($key, array $options = array())
  369. {
  370. $options = array_merge(array(
  371. 'choices' => Locale::getDisplayCountries(\Locale::getDefault()),
  372. ), $options);
  373. return $this->getChoiceField($key, $options);
  374. }
  375. public function getLanguageField($key, array $options = array())
  376. {
  377. $options = array_merge(array(
  378. 'choices' => Locale::getDisplayLanguages(\Locale::getDefault()),
  379. ), $options);
  380. return $this->getChoiceField($key, $options);
  381. }
  382. public function getLocaleField($key, array $options = array())
  383. {
  384. $options = array_merge(array(
  385. 'choices' => Locale::getDisplayLocales(\Locale::getDefault()),
  386. ), $options);
  387. return $this->getChoiceField($key, $options);
  388. }
  389. public function getTimeZoneField($key, array $options = array())
  390. {
  391. $options = array_merge(array(
  392. 'preferred_choices' => array(),
  393. ), $options);
  394. $choiceList = new TimeZoneChoiceList($options['preferred_choices']);
  395. return $this->getChoiceFieldForList($key, $choiceList, $options);
  396. }
  397. protected function getDayField($key, array $options = array())
  398. {
  399. $options = array_merge(array(
  400. 'days' => range(1, 31),
  401. 'preferred_choices' => array(),
  402. ), $options);
  403. $choiceList = new PaddedChoiceList(
  404. $options['days'], 2, '0', STR_PAD_LEFT, $options['preferred_choices']
  405. );
  406. return $this->getChoiceFieldForList($key, $choiceList, $options);
  407. }
  408. protected function getMonthField($key, \IntlDateFormatter $formatter, array $options = array())
  409. {
  410. $options = array_merge(array(
  411. 'months' => range(1, 12),
  412. 'preferred_choices' => array(),
  413. ), $options);
  414. $choiceList = new MonthChoiceList(
  415. $formatter, $options['months'], $options['preferred_choices']
  416. );
  417. return $this->getChoiceFieldForList($key, $choiceList, $options);
  418. }
  419. protected function getYearField($key, array $options = array())
  420. {
  421. $options = array_merge(array(
  422. 'years' => range(date('Y') - 5, date('Y') + 5),
  423. 'preferred_choices' => array(),
  424. ), $options);
  425. $choiceList = new PaddedChoiceList(
  426. $options['years'], 4, '0', STR_PAD_LEFT, $options['preferred_choices']
  427. );
  428. return $this->getChoiceFieldForList($key, $choiceList, $options);
  429. }
  430. protected function getHourField($key, array $options = array())
  431. {
  432. $options = array_merge(array(
  433. 'widget' => 'choice',
  434. 'hours' => range(0, 23),
  435. 'preferred_choices' => array(),
  436. ), $options);
  437. if ($options['widget'] == 'text') {
  438. return $this->getTextField($key, array('max_length' => 2));
  439. } else {
  440. $choiceList = new PaddedChoiceList(
  441. $options['hours'], 2, '0', STR_PAD_LEFT, $options['preferred_choices']
  442. );
  443. return $this->getChoiceFieldForList($key, $choiceList, $options);
  444. }
  445. }
  446. protected function getMinuteField($key, array $options = array())
  447. {
  448. $options = array_merge(array(
  449. 'widget' => 'choice',
  450. 'minutes' => range(0, 59),
  451. 'preferred_choices' => array(),
  452. ), $options);
  453. if ($options['widget'] == 'text') {
  454. return $this->getTextField($key, array('max_length' => 2));
  455. } else {
  456. $choiceList = new PaddedChoiceList(
  457. $options['minutes'], 2, '0', STR_PAD_LEFT, $options['preferred_choices']
  458. );
  459. return $this->getChoiceFieldForList($key, $choiceList, $options);
  460. }
  461. }
  462. protected function getSecondField($key, array $options = array())
  463. {
  464. $options = array_merge(array(
  465. 'widget' => 'choice',
  466. 'seconds' => range(0, 59),
  467. 'preferred_choices' => array(),
  468. ), $options);
  469. if ($options['widget'] == 'text') {
  470. return $this->getTextField($key, array('max_length' => 2));
  471. } else {
  472. $choiceList = new PaddedChoiceList(
  473. $options['seconds'], 2, '0', STR_PAD_LEFT, $options['preferred_choices']
  474. );
  475. return $this->getChoiceFieldForList($key, $choiceList, $options);
  476. }
  477. }
  478. public function getDateField($key, array $options = array())
  479. {
  480. $options = array_merge(array(
  481. 'template' => 'date',
  482. 'widget' => 'choice',
  483. 'type' => 'datetime',
  484. 'pattern' => null,
  485. 'format' => \IntlDateFormatter::MEDIUM,
  486. 'data_timezone' => date_default_timezone_get(),
  487. 'user_timezone' => date_default_timezone_get(),
  488. ), $options);
  489. $formatter = new \IntlDateFormatter(
  490. \Locale::getDefault(),
  491. $options['format'],
  492. \IntlDateFormatter::NONE
  493. );
  494. if ($options['widget'] === 'text') {
  495. $field = $this->getField($key, $options)
  496. ->setValueTransformer(new DateTimeToLocalizedStringTransformer(array(
  497. 'date_format' => $options['format'],
  498. 'time_format' => \IntlDateFormatter::NONE,
  499. 'input_timezone' => $options['data_timezone'],
  500. 'output_timezone' => $options['user_timezone'],
  501. )));
  502. } else {
  503. // Only pass a subset of the options to children
  504. $childOptions = array_intersect_key($options, array_flip(array(
  505. 'years',
  506. 'months',
  507. 'days',
  508. )));
  509. $field = $this->getForm($key, $options)
  510. ->add($this->getYearField('year', $childOptions))
  511. ->add($this->getMonthField('month', $formatter, $childOptions))
  512. ->add($this->getDayField('day', $childOptions))
  513. ->setValueTransformer(new DateTimeToArrayTransformer(array(
  514. 'input_timezone' => $options['data_timezone'],
  515. 'output_timezone' => $options['user_timezone'],
  516. 'fields' => array('year', 'month', 'day'),
  517. )))
  518. ->addRendererPlugin(new DatePatternPlugin($formatter))
  519. // Don't modify \DateTime classes by reference, we treat
  520. // them like immutable value objects
  521. ->setModifyByReference(false);
  522. }
  523. if ($options['type'] === 'string') {
  524. $field->setNormalizationTransformer(new ReversedTransformer(
  525. new DateTimeToStringTransformer(array(
  526. 'input_timezone' => $options['data_timezone'],
  527. 'output_timezone' => $options['data_timezone'],
  528. 'format' => 'Y-m-d',
  529. ))
  530. ));
  531. } else if ($options['type'] === 'timestamp') {
  532. $field->setNormalizationTransformer(new ReversedTransformer(
  533. new DateTimeToTimestampTransformer(array(
  534. 'output_timezone' => $options['data_timezone'],
  535. 'input_timezone' => $options['data_timezone'],
  536. ))
  537. ));
  538. } else if ($options['type'] === 'array') {
  539. $field->setNormalizationTransformer(new ReversedTransformer(
  540. new DateTimeToArrayTransformer(array(
  541. 'input_timezone' => $options['data_timezone'],
  542. 'output_timezone' => $options['data_timezone'],
  543. 'fields' => array('year', 'month', 'day'),
  544. ))
  545. ));
  546. }
  547. $field->setRendererVar('widget', $options['widget']);
  548. return $field;
  549. }
  550. public function getBirthdayField($key, array $options = array())
  551. {
  552. $options = array_merge(array(
  553. 'years' => range($currentYear-120, $currentYear),
  554. ), $options);
  555. return $this->getDateField($key, $options);
  556. }
  557. public function getTimeField($key, array $options = array())
  558. {
  559. $options = array_merge(array(
  560. 'template' => 'time',
  561. 'widget' => 'choice',
  562. 'type' => 'datetime',
  563. 'with_seconds' => false,
  564. 'pattern' => null,
  565. 'data_timezone' => date_default_timezone_get(),
  566. 'user_timezone' => date_default_timezone_get(),
  567. ), $options);
  568. // Only pass a subset of the options to children
  569. $childOptions = array_intersect_key($options, array_flip(array(
  570. 'hours',
  571. 'minutes',
  572. 'seconds',
  573. 'widget',
  574. )));
  575. $parts = array('hour', 'minute');
  576. $field = $this->getForm($key, $options)
  577. ->add($this->getHourField('hour', $childOptions))
  578. ->add($this->getMinuteField('minute', $childOptions))
  579. // Don't modify \DateTime classes by reference, we treat
  580. // them like immutable value objects
  581. ->setModifyByReference(false);
  582. if ($options['with_seconds']) {
  583. $parts[] = 'second';
  584. $field->add($this->getSecondField('second', $childOptions));
  585. }
  586. if ($options['type'] == 'string') {
  587. $field->setNormalizationTransformer(new ReversedTransformer(
  588. new DateTimeToStringTransformer(array(
  589. 'format' => 'H:i:s',
  590. 'input_timezone' => $options['data_timezone'],
  591. 'output_timezone' => $options['data_timezone'],
  592. ))
  593. ));
  594. } else if ($options['type'] == 'timestamp') {
  595. $field->setNormalizationTransformer(new ReversedTransformer(
  596. new DateTimeToTimestampTransformer(array(
  597. 'input_timezone' => $options['data_timezone'],
  598. 'output_timezone' => $options['data_timezone'],
  599. ))
  600. ));
  601. } else if ($options['type'] === 'array') {
  602. $field->setNormalizationTransformer(new ReversedTransformer(
  603. new DateTimeToArrayTransformer(array(
  604. 'input_timezone' => $options['data_timezone'],
  605. 'output_timezone' => $options['data_timezone'],
  606. 'fields' => $parts,
  607. ))
  608. ));
  609. }
  610. $field
  611. ->setValueTransformer(new DateTimeToArrayTransformer(array(
  612. 'input_timezone' => $options['data_timezone'],
  613. 'output_timezone' => $options['user_timezone'],
  614. // if the field is rendered as choice field, the values should be trimmed
  615. // of trailing zeros to render the selected choices correctly
  616. 'pad' => $options['widget'] === 'text',
  617. 'fields' => $parts,
  618. )))
  619. ->setRendererVar('widget', $options['widget'])
  620. ->setRendererVar('with_seconds', $options['with_seconds']);
  621. return $field;
  622. }
  623. public function getDateTimeField($key, array $options = array())
  624. {
  625. $options = array_merge(array(
  626. 'template' => 'datetime',
  627. 'type' => 'datetime',
  628. 'with_seconds' => false,
  629. 'data_timezone' => date_default_timezone_get(),
  630. 'user_timezone' => date_default_timezone_get(),
  631. ), $options);
  632. // Only pass a subset of the options to children
  633. $dateFieldOptions = array_intersect_key($options, array_flip(array(
  634. 'years',
  635. 'months',
  636. 'days',
  637. )));
  638. $timeFieldOptions = array_intersect_key($options, array_flip(array(
  639. 'hours',
  640. 'minutes',
  641. 'seconds',
  642. 'with_seconds',
  643. )));
  644. if (isset($options['date_pattern'])) {
  645. $dateFieldOptions['pattern'] = $options['date_pattern'];
  646. }
  647. if (isset($options['date_widget'])) {
  648. $dateFieldOptions['widget'] = $options['date_widget'];
  649. }
  650. if (isset($options['date_format'])) {
  651. $dateFieldOptions['format'] = $options['date_format'];
  652. }
  653. $dateFieldOptions['type'] = 'array';
  654. if (isset($options['time_pattern'])) {
  655. $timeFieldOptions['pattern'] = $options['time_pattern'];
  656. }
  657. if (isset($options['time_widget'])) {
  658. $timeFieldOptions['widget'] = $options['time_widget'];
  659. }
  660. if (isset($options['time_format'])) {
  661. $timeFieldOptions['format'] = $options['time_format'];
  662. }
  663. $timeFieldOptions['type'] = 'array';
  664. $parts = array('year', 'month', 'day', 'hour', 'minute');
  665. $timeParts = array('hour', 'minute');
  666. if ($options['with_seconds']) {
  667. $parts[] = 'second';
  668. $timeParts[] = 'second';
  669. }
  670. $field = $this->getForm($key, $options)
  671. ->setValueTransformer(new ValueTransformerChain(array(
  672. new DateTimeToArrayTransformer(array(
  673. 'input_timezone' => $options['data_timezone'],
  674. 'output_timezone' => $options['user_timezone'],
  675. )),
  676. new ArrayToPartsTransformer(array(
  677. 'date' => array('year', 'month', 'day'),
  678. 'time' => $timeParts,
  679. )),
  680. )))
  681. ->add($this->getDateField('date', $dateFieldOptions))
  682. ->add($this->getTimeField('time', $timeFieldOptions))
  683. // Don't modify \DateTime classes by reference, we treat
  684. // them like immutable value objects
  685. ->setModifyByReference(false)
  686. ->setData(null); // hack: should be invoked automatically
  687. if ($options['type'] == 'string') {
  688. $field->setNormalizationTransformer(new ReversedTransformer(
  689. new DateTimeToStringTransformer(array(
  690. 'format' => 'Y-m-d H:i:s',
  691. 'input_timezone' => $options['data_timezone'],
  692. 'output_timezone' => $options['data_timezone'],
  693. ))
  694. ));
  695. } else if ($options['type'] == 'timestamp') {
  696. $field->setNormalizationTransformer(new ReversedTransformer(
  697. new DateTimeToTimestampTransformer(array(
  698. 'input_timezone' => $options['data_timezone'],
  699. 'output_timezone' => $options['data_timezone'],
  700. ))
  701. ));
  702. } else if ($options['type'] === 'array') {
  703. $field->setNormalizationTransformer(new ReversedTransformer(
  704. new DateTimeToArrayTransformer(array(
  705. 'input_timezone' => $options['data_timezone'],
  706. 'output_timezone' => $options['data_timezone'],
  707. 'fields' => $parts,
  708. ))
  709. ));
  710. }
  711. return $field;
  712. }
  713. }