From 7e8b622802d63962abd0db58e103b37c15b3d69c Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 10 Jul 2012 17:15:14 +0200 Subject: [PATCH] [Form] Added the option "format" to DateTimeType --- src/Symfony/Component/Form/CHANGELOG.md | 4 ++ .../Form/Extension/Core/Type/DateTimeType.php | 60 ++++++++++++++-- .../Form/Tests/AbstractLayoutTest.php | 72 +++++++++---------- .../Extension/Core/Type/DateTimeTypeTest.php | 17 +++-- 4 files changed, 101 insertions(+), 52 deletions(-) diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index 722d031b7a..a19b52f8f9 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -151,3 +151,7 @@ CHANGELOG of padding them automatically * [BC BREAK] DateType defaults to the format "yyyy-MM-dd" now in order to support the HTML 5 date field out of the box + * added the option "format" to DateTimeType + * [BC BREAK] DateTimeType defaults to the format "yyyy-MM-dd'T'HH:mm:ss" now. This + is almost identical to the pattern of the HTML 5 datetime input, but not quite, + because ICU cannot generate RFC 3339 dates (which have a timezone suffix). diff --git a/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php b/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php index 097d4d1c9f..233c2294d7 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormViewInterface; @@ -19,6 +20,7 @@ use Symfony\Component\Form\ReversedTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\DataTransformerChain; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToPartsTransformer; use Symfony\Component\OptionsResolver\Options; @@ -26,27 +28,67 @@ use Symfony\Component\OptionsResolver\OptionsResolverInterface; class DateTimeType extends AbstractType { + const DEFAULT_DATE_FORMAT = \IntlDateFormatter::MEDIUM; + + const DEFAULT_TIME_FORMAT = \IntlDateFormatter::MEDIUM; + + /** + * This is not quite the HTML5 format yet, because ICU lacks the + * capability of parsing and generating RFC 3339 dates, which + * are like the below pattern but with a timezone suffix. The + * timezone suffix is + * + * * "Z" for UTC + * * "(-|+)HH:mm" for other timezones (note the colon!) + * + * http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax + * http://www.w3.org/TR/html-markup/input.datetime.html + * http://tools.ietf.org/html/rfc3339 + */ + const HTML5_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"; + + private static $acceptedFormats = array( + \IntlDateFormatter::FULL, + \IntlDateFormatter::LONG, + \IntlDateFormatter::MEDIUM, + \IntlDateFormatter::SHORT, + ); + /** * {@inheritdoc} */ public function buildForm(FormBuilderInterface $builder, array $options) { $parts = array('year', 'month', 'day', 'hour', 'minute'); + $dateParts = array('year', 'month', 'day'); $timeParts = array('hour', 'minute'); - $format = 'Y-m-d H:i'; if ($options['with_seconds']) { - $format = 'Y-m-d H:i:s'; - $parts[] = 'second'; $timeParts[] = 'second'; } + $dateFormat = is_int($options['date_format']) ? $options['date_format'] : self::DEFAULT_DATE_FORMAT; + $timeFormat = self::DEFAULT_TIME_FORMAT; + $calendar = \IntlDateFormatter::GREGORIAN; + $pattern = is_string($options['format']) ? $options['format'] : null; + + if (!in_array($dateFormat, self::$acceptedFormats, true)) { + throw new InvalidOptionsException('The "date_format" option must be one of the IntlDateFormatter constants (FULL, LONG, MEDIUM, SHORT) or a string representing a custom format.'); + } + + if (null !== $pattern && (false === strpos($pattern, 'y') || false === strpos($pattern, 'M') || false === strpos($pattern, 'd') || false === strpos($pattern, 'H') || false === strpos($pattern, 'm'))) { + throw new InvalidOptionsException(sprintf('The "format" option should contain the patterns "y", "M", "d", "H" and "m". Its current value is "%s".', $pattern)); + } + if ('single_text' === $options['widget']) { - $builder->addViewTransformer(new DateTimeToStringTransformer( + $builder->addViewTransformer(new DateTimeToLocalizedStringTransformer( $options['data_timezone'], $options['user_timezone'], - $format + $dateFormat, + $timeFormat, + $calendar, + $pattern )); } else { // Only pass a subset of the options to children @@ -88,7 +130,7 @@ class DateTimeType extends AbstractType ->addViewTransformer(new DataTransformerChain(array( new DateTimeToArrayTransformer($options['data_timezone'], $options['user_timezone'], $parts), new ArrayToPartsTransformer(array( - 'date' => array('year', 'month', 'day'), + 'date' => $dateParts, 'time' => $timeParts, )), ))) @@ -119,7 +161,10 @@ class DateTimeType extends AbstractType { $view->setVar('widget', $options['widget']); - if ('single_text' === $options['widget']) { + // Change the input to a HTML5 date input if + // * the widget is set to "single_text" + // * the format matches the one expected by HTML5 + if ('single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) { $view->setVar('type', 'datetime'); } } @@ -147,6 +192,7 @@ class DateTimeType extends AbstractType 'input' => 'datetime', 'data_timezone' => null, 'user_timezone' => null, + 'format' => self::HTML5_FORMAT, 'date_format' => null, 'widget' => null, 'date_widget' => $dateWidget, diff --git a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php index fe123f3c0f..c9cc5e9135 100644 --- a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php @@ -786,14 +786,14 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [@id="name_date"] [ ./select + [@id="name_date_year"] + [./option[@value="2011"][@selected="selected"]] + /following-sibling::select [@id="name_date_month"] [./option[@value="2"][@selected="selected"]] /following-sibling::select [@id="name_date_day"] [./option[@value="3"][@selected="selected"]] - /following-sibling::select - [@id="name_date_year"] - [./option[@value="2011"][@selected="selected"]] ] /following-sibling::div [@id="name_time"] @@ -826,14 +826,14 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [@id="name_date"] [ ./select + [@id="name_date_year"] + [./option[@value=""][.="[trans]Change&Me[/trans]"]] + /following-sibling::select [@id="name_date_month"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] /following-sibling::select [@id="name_date_day"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] - /following-sibling::select - [@id="name_date_year"] - [./option[@value=""][.="[trans]Change&Me[/trans]"]] ] /following-sibling::div [@id="name_time"] @@ -866,14 +866,14 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [@id="name_date"] [ ./select + [@id="name_date_year"] + [./option[@value="2011"][@selected="selected"]] + /following-sibling::select [@id="name_date_month"] [./option[@value="2"][@selected="selected"]] /following-sibling::select [@id="name_date_day"] [./option[@value="3"][@selected="selected"]] - /following-sibling::select - [@id="name_date_year"] - [./option[@value="2011"][@selected="selected"]] ] /following-sibling::div [@id="name_time"] @@ -905,14 +905,14 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [@id="name_date"] [ ./select + [@id="name_date_year"] + [./option[@value="2011"][@selected="selected"]] + /following-sibling::select [@id="name_date_month"] [./option[@value="2"][@selected="selected"]] /following-sibling::select [@id="name_date_day"] [./option[@value="3"][@selected="selected"]] - /following-sibling::select - [@id="name_date_year"] - [./option[@value="2011"][@selected="selected"]] ] /following-sibling::div [@id="name_time"] @@ -948,7 +948,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [@type="date"] [@id="name_date"] [@name="name[date]"] - [@value="Feb 3, 2011"] + [@value="2011-02-03"] /following-sibling::input [@type="time"] [@id="name_time"] @@ -970,7 +970,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/input [@type="datetime"] [@name="name"] - [@value="2011-02-03 04:05"] + [@value="2011-02-03T04:05:06"] ' ); } @@ -988,7 +988,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/input [@type="datetime"] [@name="name"] - [@value="2011-02-03 04:05"] + [@value="2011-02-03T04:05:06"] ' ); } @@ -1004,14 +1004,14 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select + [@id="name_year"] + [./option[@value="2011"][@selected="selected"]] + /following-sibling::select [@id="name_month"] [./option[@value="2"][@selected="selected"]] /following-sibling::select [@id="name_day"] [./option[@value="3"][@selected="selected"]] - /following-sibling::select - [@id="name_year"] - [./option[@value="2011"][@selected="selected"]] ] [count(./select)=3] ' @@ -1031,14 +1031,14 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select + [@id="name_year"] + [./option[@value=""][.="[trans]Change&Me[/trans]"]] + /following-sibling::select [@id="name_month"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] /following-sibling::select [@id="name_day"] [./option[@value=""][.="[trans]Change&Me[/trans]"]] - /following-sibling::select - [@id="name_year"] - [./option[@value=""][.="[trans]Change&Me[/trans]"]] ] [count(./select)=3] ' @@ -1058,14 +1058,14 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select + [@id="name_year"] + [./option[@value=""][.="[trans]Change&Me[/trans]"]] + /following-sibling::select [@id="name_month"] [./option[@value="1"]] /following-sibling::select [@id="name_day"] [./option[@value="1"]] - /following-sibling::select - [@id="name_year"] - [./option[@value=""][.="[trans]Change&Me[/trans]"]] ] [count(./select)=3] ' @@ -1083,6 +1083,10 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./input + [@id="name_year"] + [@type="text"] + [@value="2011"] + /following-sibling::input [@id="name_month"] [@type="text"] [@value="2"] @@ -1090,10 +1094,6 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [@id="name_day"] [@type="text"] [@value="3"] - /following-sibling::input - [@id="name_year"] - [@type="text"] - [@value="2011"] ] [count(./input)=3] ' @@ -1111,7 +1111,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/input [@type="date"] [@name="name"] - [@value="Feb 3, 2011"] + [@value="2011-02-03"] ' ); } @@ -1137,14 +1137,14 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select + [@id="name_year"] + [./option[@value="2000"][@selected="selected"]] + /following-sibling::select [@id="name_month"] [./option[@value="2"][@selected="selected"]] /following-sibling::select [@id="name_day"] [./option[@value="3"][@selected="selected"]] - /following-sibling::select - [@id="name_year"] - [./option[@value="2000"][@selected="selected"]] ] [count(./select)=3] ' @@ -1163,6 +1163,10 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase '/div [ ./select + [@id="name_year"] + [./option[@value=""][.="[trans][/trans]"]] + [./option[@value="1950"][@selected="selected"]] + /following-sibling::select [@id="name_month"] [./option[@value=""][.="[trans][/trans]"]] [./option[@value="1"][@selected="selected"]] @@ -1170,10 +1174,6 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase [@id="name_day"] [./option[@value=""][.="[trans][/trans]"]] [./option[@value="1"][@selected="selected"]] - /following-sibling::select - [@id="name_year"] - [./option[@value=""][.="[trans][/trans]"]] - [./option[@value="1950"][@selected="selected"]] ] [count(./select)=3] ' diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTimeTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTimeTypeTest.php index f48ed69fa2..e83d187fb7 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTimeTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTimeTypeTest.php @@ -163,15 +163,14 @@ class DateTimeTypeTest extends LocalizedTestCase 'input' => 'datetime', )); - $dateTime = new \DateTime('2010-06-02 03:04:05 America/New_York'); - - $form->bind('2010-06-02 03:04:05'); - $outputTime = new \DateTime('2010-06-02 03:04:00 Pacific/Tahiti'); + + $form->bind('2010-06-02T03:04:00'); + $outputTime->setTimezone(new \DateTimeZone('America/New_York')); $this->assertDateTimeEquals($outputTime, $form->getData()); - $this->assertEquals('2010-06-02 03:04', $form->getViewData()); + $this->assertEquals('2010-06-02T03:04:00', $form->getViewData()); } public function testSubmit_stringSingleText() @@ -183,10 +182,10 @@ class DateTimeTypeTest extends LocalizedTestCase 'widget' => 'single_text', )); - $form->bind('2010-06-02 03:04:05'); + $form->bind('2010-06-02T03:04:00'); $this->assertEquals('2010-06-02 03:04:00', $form->getData()); - $this->assertEquals('2010-06-02 03:04', $form->getViewData()); + $this->assertEquals('2010-06-02T03:04:00', $form->getViewData()); } public function testSubmit_stringSingleText_withSeconds() @@ -199,10 +198,10 @@ class DateTimeTypeTest extends LocalizedTestCase 'with_seconds' => true, )); - $form->bind('2010-06-02 03:04:05'); + $form->bind('2010-06-02T03:04:05'); $this->assertEquals('2010-06-02 03:04:05', $form->getData()); - $this->assertEquals('2010-06-02 03:04:05', $form->getViewData()); + $this->assertEquals('2010-06-02T03:04:05', $form->getViewData()); } public function testSubmit_differentPattern()