diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index 09e0a30112..54efc1dd29 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -147,3 +147,5 @@ CHANGELOG * [BC BREAK] fixed: form constraints are only validated if they belong to the validated group * deprecated `bindRequest` in `Form` and replaced it by a listener to FormEvents::PRE_BIND * fixed: the "data" option supersedes default values from the model + * changed DateType to refer to the "format" option for calculating the year and day choices instead + of padding them automatically diff --git a/src/Symfony/Component/Form/Extension/Core/Type/DateType.php b/src/Symfony/Component/Form/Extension/Core/Type/DateType.php index e1bf5ebeb1..bd3b16f822 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/DateType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/DateType.php @@ -23,82 +23,78 @@ use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTra use Symfony\Component\Form\ReversedTransformer; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolverInterface; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; class DateType extends AbstractType { const DEFAULT_FORMAT = \IntlDateFormatter::MEDIUM; + private static $acceptedFormats = array( + \IntlDateFormatter::FULL, + \IntlDateFormatter::LONG, + \IntlDateFormatter::MEDIUM, + \IntlDateFormatter::SHORT, + ); + /** * {@inheritdoc} */ public function buildForm(FormBuilderInterface $builder, array $options) { - $format = $options['format']; - $pattern = null; + $dateFormat = is_int($options['format']) ? $options['format'] : self::DEFAULT_FORMAT; + $timeFormat = \IntlDateFormatter::NONE; + $calendar = \IntlDateFormatter::GREGORIAN; + $pattern = is_string($options['format']) ? $options['format'] : null; - $allowedFormats = array( - \IntlDateFormatter::FULL, - \IntlDateFormatter::LONG, - \IntlDateFormatter::MEDIUM, - \IntlDateFormatter::SHORT, - ); - - // If $format is not in the allowed options, it's considered as the pattern of the formatter if it is a string - if (!in_array($format, $allowedFormats, true)) { - if (is_string($format)) { - $format = self::DEFAULT_FORMAT; - $pattern = $options['format']; - } else { - throw new CreationException('The "format" option must be one of the IntlDateFormatter constants (FULL, LONG, MEDIUM, SHORT) or a string representing a custom pattern'); - } + if (!in_array($dateFormat, self::$acceptedFormats, true)) { + throw new InvalidOptionsException('The "format" option must be one of the IntlDateFormatter constants (FULL, LONG, MEDIUM, SHORT) or a string representing a custom format.'); } - $formatter = new \IntlDateFormatter( - \Locale::getDefault(), - $format, - \IntlDateFormatter::NONE, - 'UTC', - \IntlDateFormatter::GREGORIAN, - $pattern - ); - $formatter->setLenient(false); + if (null !== $pattern && (false === strpos($pattern, 'y') || false === strpos($pattern, 'M') || false === strpos($pattern, 'd'))) { + throw new InvalidOptionsException(sprintf('The "format" option should contain the patterns "y", "M" and "d". Its current value is "%s".', $pattern)); + } if ('single_text' === $options['widget']) { - $builder->addViewTransformer(new DateTimeToLocalizedStringTransformer($options['data_timezone'], $options['user_timezone'], $format, \IntlDateFormatter::NONE, \IntlDateFormatter::GREGORIAN, $pattern)); + $builder->addViewTransformer(new DateTimeToLocalizedStringTransformer( + $options['data_timezone'], + $options['user_timezone'], + $dateFormat, + $timeFormat, + $calendar, + $pattern + )); } else { $yearOptions = $monthOptions = $dayOptions = array(); + $formatter = new \IntlDateFormatter( + \Locale::getDefault(), + $dateFormat, + $timeFormat, + 'UTC', + $calendar, + $pattern + ); + $formatter->setLenient(false); + if ('choice' === $options['widget']) { - $years = $months = $days = array(); - - foreach ($options['years'] as $year) { - $years[$year] = str_pad($year, 4, '0', STR_PAD_LEFT); - } - foreach ($options['months'] as $month) { - $months[$month] = str_pad($month, 2, '0', STR_PAD_LEFT); - } - foreach ($options['days'] as $day) { - $days[$day] = str_pad($day, 2, '0', STR_PAD_LEFT); - } - // Only pass a subset of the options to children $yearOptions = array( - 'choices' => $years, + 'choices' => $this->formatTimestamps($formatter, '/y+/', $this->listYears($options['years'])), 'empty_value' => $options['empty_value']['year'], ); $monthOptions = array( - 'choices' => $this->formatMonths($formatter, $months), + 'choices' => $this->formatTimestamps($formatter, '/M+/', $this->listMonths($options['months'])), 'empty_value' => $options['empty_value']['month'], ); $dayOptions = array( - 'choices' => $days, + 'choices' => $this->formatTimestamps($formatter, '/d+/', $this->listDays($options['days'])), 'empty_value' => $options['empty_value']['day'], ); + } - // Append generic carry-along options - foreach (array('required', 'translation_domain') as $passOpt) { - $yearOptions[$passOpt] = $monthOptions[$passOpt] = $dayOptions[$passOpt] = $options[$passOpt]; - } + // Append generic carry-along options + foreach (array('required', 'translation_domain') as $passOpt) { + $yearOptions[$passOpt] = $monthOptions[$passOpt] = $dayOptions[$passOpt] = $options[$passOpt]; } $builder @@ -108,6 +104,7 @@ class DateType extends AbstractType ->addViewTransformer(new DateTimeToArrayTransformer( $options['data_timezone'], $options['user_timezone'], array('year', 'month', 'day') )) + ->setAttribute('formatter', $formatter) ; } @@ -124,8 +121,6 @@ class DateType extends AbstractType new DateTimeToArrayTransformer($options['data_timezone'], $options['data_timezone'], array('year', 'month', 'day')) )); } - - $builder->setAttribute('formatter', $formatter); } /** @@ -139,7 +134,7 @@ class DateType extends AbstractType $view->setVar('type', 'date'); } - if (count($view) > 0) { + if ($form->getConfig()->hasAttribute('formatter')) { $pattern = $form->getConfig()->getAttribute('formatter')->getPattern(); // set right order with respect to locale (e.g.: de_DE=dd.MM.yy; en_US=M/d/yy) @@ -224,6 +219,10 @@ class DateType extends AbstractType 'choice', ), )); + + $resolver->setAllowedTypes(array( + 'format' => array('int', 'string'), + )); } /** @@ -242,18 +241,18 @@ class DateType extends AbstractType return 'date'; } - private function formatMonths(\IntlDateFormatter $formatter, array $months) + private function formatTimestamps(\IntlDateFormatter $formatter, $regex, array $timestamps) { $pattern = $formatter->getPattern(); $timezone = $formatter->getTimezoneId(); $formatter->setTimezoneId(\DateTimeZone::UTC); - if (preg_match('/M+/', $pattern, $matches)) { + if (preg_match($regex, $pattern, $matches)) { $formatter->setPattern($matches[0]); - foreach ($months as $key => $value) { - $months[$key] = $formatter->format(gmmktime(0, 0, 0, $key, 15)); + foreach ($timestamps as $key => $timestamp) { + $timestamps[$key] = $formatter->format($timestamp); } // I'd like to clone the formatter above, but then we get a @@ -263,6 +262,39 @@ class DateType extends AbstractType $formatter->setTimezoneId($timezone); - return $months; + return $timestamps; + } + + private function listYears(array $years) + { + $result = array(); + + foreach ($years as $year) { + $result[$year] = gmmktime(0, 0, 0, 6, 15, $year); + } + + return $result; + } + + private function listMonths(array $months) + { + $result = array(); + + foreach ($months as $month) { + $result[$month] = gmmktime(0, 0, 0, $month, 15); + } + + return $result; + } + + private function listDays(array $days) + { + $result = array(); + + foreach ($days as $day) { + $result[$day] = gmmktime(0, 0, 0, 5, $day); + } + + return $result; } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php index 29ed44ea21..d6ddd814d3 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php @@ -250,37 +250,45 @@ class DateTypeTest extends LocalizedTestCase /** * This test is to check that the strings '0', '1', '2', '3' are no accepted * as valid IntlDateFormatter constants for FULL, LONG, MEDIUM or SHORT respectively. + * + * @expectedException Symfony\Component\OptionsResolver\Exception\InvalidOptionsException */ - public function testFormatOptionCustomPatternCollapsingIntlDateFormatterConstant() + public function testThrowExceptionIfFormatIsNoPattern() { - $form = $this->factory->create('date', null, array( + $this->factory->create('date', null, array( 'format' => '0', 'widget' => 'single_text', 'input' => 'string', )); - - $form->setData('2010-06-02'); - - // This would be what would be outputed if '0' was mistaken for \IntlDateFormatter::FULL - $this->assertNotEquals('Mittwoch, 02. Juni 2010', $form->getViewData()); } /** - * @expectedException Symfony\Component\Form\Exception\CreationException + * @expectedException Symfony\Component\OptionsResolver\Exception\InvalidOptionsException */ - public function testValidateFormatOptionGivenWrongConstants() + public function testThrowExceptionIfFormatDoesNotContainYearMonthAndDay() { - $form = $this->factory->create('date', null, array( + $this->factory->create('date', null, array( + 'months' => array(6, 7), + 'format' => 'yy', + )); + } + + /** + * @expectedException Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testThrowExceptionIfFormatIsNoConstant() + { + $this->factory->create('date', null, array( 'format' => 105, )); } /** - * @expectedException Symfony\Component\Form\Exception\CreationException + * @expectedException Symfony\Component\OptionsResolver\Exception\InvalidOptionsException */ - public function testValidateFormatOptionGivenArrayValue() + public function testThrowExceptionIfFormatIsInvalid() { - $form = $this->factory->create('date', null, array( + $this->factory->create('date', null, array( 'format' => array(), )); } @@ -344,21 +352,6 @@ class DateTypeTest extends LocalizedTestCase ), $view->get('month')->getVar('choices')); } - public function testMonthsOptionNumericIfFormatContainsNoMonth() - { - $form = $this->factory->create('date', null, array( - 'months' => array(6, 7), - 'format' => 'yy', - )); - - $view = $form->createView(); - - $this->assertEquals(array( - new ChoiceView('6', '06'), - new ChoiceView('7', '07'), - ), $view->get('month')->getVar('choices')); - } - public function testMonthsOptionShortFormat() { $form = $this->factory->create('date', null, array(