[Form] DateTimeType now handles RFC 3339 dates as provided by HTML5
This commit is contained in:
parent
7e8b622802
commit
ded6c0305c
@ -149,9 +149,8 @@ CHANGELOG
|
|||||||
* fixed: the "data" option supersedes default values from the model
|
* 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
|
* changed DateType to refer to the "format" option for calculating the year and day choices instead
|
||||||
of padding them automatically
|
of padding them automatically
|
||||||
* [BC BREAK] DateType defaults to the format "yyyy-MM-dd" now in order to support
|
* [BC BREAK] DateType defaults to the format "yyyy-MM-dd" now if the widget is
|
||||||
the HTML 5 date field out of the box
|
"single_text", in order to support the HTML 5 date field out of the box
|
||||||
* added the option "format" to DateTimeType
|
* added the option "format" to DateTimeType
|
||||||
* [BC BREAK] DateTimeType defaults to the format "yyyy-MM-dd'T'HH:mm:ss" now. This
|
* [BC BREAK] DateTimeType now outputs RFC 3339 dates by default, as generated and
|
||||||
is almost identical to the pattern of the HTML 5 datetime input, but not quite,
|
consumed by HTML5 browsers, if the widget is "single_text"
|
||||||
because ICU cannot generate RFC 3339 dates (which have a timezone suffix).
|
|
||||||
|
@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Form\Extension\Core\DataTransformer;
|
||||||
|
|
||||||
|
use Symfony\Component\Form\Exception\UnexpectedTypeException;
|
||||||
|
use Symfony\Component\Form\Exception\TransformationFailedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||||
|
*/
|
||||||
|
class DateTimeToRfc3339Transformer extends BaseDateTimeTransformer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function transform($dateTime)
|
||||||
|
{
|
||||||
|
if (null === $dateTime) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$dateTime instanceof \DateTime) {
|
||||||
|
throw new UnexpectedTypeException($dateTime, '\DateTime');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->inputTimezone !== $this->outputTimezone) {
|
||||||
|
$dateTime = clone $dateTime;
|
||||||
|
$dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
|
||||||
|
}
|
||||||
|
|
||||||
|
return preg_replace('/\+00:00$/', 'Z', $dateTime->format('c'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function reverseTransform($rfc3339)
|
||||||
|
{
|
||||||
|
if (!is_string($rfc3339)) {
|
||||||
|
throw new UnexpectedTypeException($rfc3339, 'string');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('' === $rfc3339) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$dateTime = new \DateTime($rfc3339);
|
||||||
|
|
||||||
|
if ($this->outputTimezone !== $this->inputTimezone) {
|
||||||
|
try {
|
||||||
|
$dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/(\d{4})-(\d{2})-(\d{2})/', $rfc3339, $matches)) {
|
||||||
|
if (!checkdate($matches[2], $matches[3], $matches[1])) {
|
||||||
|
throw new TransformationFailedException(sprintf(
|
||||||
|
'The date "%s-%s-%s" is not a valid date.',
|
||||||
|
$matches[1],
|
||||||
|
$matches[2],
|
||||||
|
$matches[3]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dateTime;
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,7 @@ use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransfo
|
|||||||
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
|
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
|
||||||
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
|
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
|
||||||
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
|
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
|
||||||
|
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToRfc3339Transformer;
|
||||||
use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToPartsTransformer;
|
use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToPartsTransformer;
|
||||||
use Symfony\Component\OptionsResolver\Options;
|
use Symfony\Component\OptionsResolver\Options;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
|
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
|
||||||
@ -44,8 +45,17 @@ class DateTimeType extends AbstractType
|
|||||||
* http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax
|
* http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax
|
||||||
* http://www.w3.org/TR/html-markup/input.datetime.html
|
* http://www.w3.org/TR/html-markup/input.datetime.html
|
||||||
* http://tools.ietf.org/html/rfc3339
|
* http://tools.ietf.org/html/rfc3339
|
||||||
|
*
|
||||||
|
* An ICU ticket was created:
|
||||||
|
* http://icu-project.org/trac/ticket/9421
|
||||||
|
*
|
||||||
|
* To temporarily circumvent this issue, DateTimeToRfc3339Transformer is used
|
||||||
|
* when the format matches this constant.
|
||||||
|
*
|
||||||
|
* ("ZZZZZZ" is not recognized by ICU and used here to differentiate this
|
||||||
|
* pattern from custom patterns).
|
||||||
*/
|
*/
|
||||||
const HTML5_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
|
const HTML5_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZZZZZZ";
|
||||||
|
|
||||||
private static $acceptedFormats = array(
|
private static $acceptedFormats = array(
|
||||||
\IntlDateFormatter::FULL,
|
\IntlDateFormatter::FULL,
|
||||||
@ -78,10 +88,16 @@ class DateTimeType extends AbstractType
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $pattern && (false === strpos($pattern, 'y') || false === strpos($pattern, 'M') || false === strpos($pattern, 'd') || false === strpos($pattern, 'H') || false === strpos($pattern, 'm'))) {
|
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));
|
throw new InvalidOptionsException(sprintf('The "format" option should contain the letters "y", "M", "d", "H" and "m". Its current value is "%s".', $pattern));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('single_text' === $options['widget']) {
|
if ('single_text' === $options['widget']) {
|
||||||
|
if (self::HTML5_FORMAT === $pattern) {
|
||||||
|
$builder->addViewTransformer(new DateTimeToRfc3339Transformer(
|
||||||
|
$options['data_timezone'],
|
||||||
|
$options['user_timezone']
|
||||||
|
));
|
||||||
|
} else {
|
||||||
$builder->addViewTransformer(new DateTimeToLocalizedStringTransformer(
|
$builder->addViewTransformer(new DateTimeToLocalizedStringTransformer(
|
||||||
$options['data_timezone'],
|
$options['data_timezone'],
|
||||||
$options['user_timezone'],
|
$options['user_timezone'],
|
||||||
@ -90,6 +106,7 @@ class DateTimeType extends AbstractType
|
|||||||
$calendar,
|
$calendar,
|
||||||
$pattern
|
$pattern
|
||||||
));
|
));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Only pass a subset of the options to children
|
// Only pass a subset of the options to children
|
||||||
$dateOptions = array_intersect_key($options, array_flip(array(
|
$dateOptions = array_intersect_key($options, array_flip(array(
|
||||||
|
@ -53,7 +53,7 @@ class DateType extends AbstractType
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $pattern && (false === strpos($pattern, 'y') || false === strpos($pattern, 'M') || false === strpos($pattern, 'd'))) {
|
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));
|
throw new InvalidOptionsException(sprintf('The "format" option should contain the letters "y", "M" and "d". Its current value is "%s".', $pattern));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('single_text' === $options['widget']) {
|
if ('single_text' === $options['widget']) {
|
||||||
|
@ -970,7 +970,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
|
|||||||
'/input
|
'/input
|
||||||
[@type="datetime"]
|
[@type="datetime"]
|
||||||
[@name="name"]
|
[@name="name"]
|
||||||
[@value="2011-02-03T04:05:06"]
|
[@value="2011-02-03T04:05:06+01:00"]
|
||||||
'
|
'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -988,7 +988,7 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
|
|||||||
'/input
|
'/input
|
||||||
[@type="datetime"]
|
[@type="datetime"]
|
||||||
[@name="name"]
|
[@name="name"]
|
||||||
[@value="2011-02-03T04:05:06"]
|
[@value="2011-02-03T04:05:06+01:00"]
|
||||||
'
|
'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Form\Tests\Extension\Core\DataTransformer;
|
||||||
|
|
||||||
|
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToRfc3339Transformer;
|
||||||
|
|
||||||
|
class DateTimeToRfc3339TransformerTest extends DateTimeTestCase
|
||||||
|
{
|
||||||
|
protected $dateTime;
|
||||||
|
protected $dateTimeWithoutSeconds;
|
||||||
|
|
||||||
|
protected function setUp()
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->dateTime = new \DateTime('2010-02-03 04:05:06 UTC');
|
||||||
|
$this->dateTimeWithoutSeconds = new \DateTime('2010-02-03 04:05:00 UTC');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown()
|
||||||
|
{
|
||||||
|
$this->dateTime = null;
|
||||||
|
$this->dateTimeWithoutSeconds = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function assertEquals($expected, $actual, $message = '', $delta = 0, $maxDepth = 10, $canonicalize = FALSE, $ignoreCase = FALSE)
|
||||||
|
{
|
||||||
|
if ($expected instanceof \DateTime && $actual instanceof \DateTime) {
|
||||||
|
$expected = $expected->format('c');
|
||||||
|
$actual = $actual->format('c');
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function allProvider()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
array('UTC', 'UTC', '2010-02-03 04:05:06 UTC', '2010-02-03T04:05:06Z'),
|
||||||
|
array('UTC', 'UTC', null, ''),
|
||||||
|
array('America/New_York', 'Asia/Hong_Kong', '2010-02-03 04:05:06 America/New_York', '2010-02-03T17:05:06+08:00'),
|
||||||
|
array('America/New_York', 'Asia/Hong_Kong', null, ''),
|
||||||
|
array('UTC', 'Asia/Hong_Kong', '2010-02-03 04:05:06 UTC', '2010-02-03T12:05:06+08:00'),
|
||||||
|
array('America/New_York', 'UTC', '2010-02-03 04:05:06 America/New_York', '2010-02-03T09:05:06Z'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function transformProvider()
|
||||||
|
{
|
||||||
|
return $this->allProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reverseTransformProvider()
|
||||||
|
{
|
||||||
|
return array_merge($this->allProvider(), array(
|
||||||
|
// format without seconds, as appears in some browsers
|
||||||
|
array('UTC', 'UTC', '2010-02-03 04:05:00 UTC', '2010-02-03T04:05Z'),
|
||||||
|
array('America/New_York', 'Asia/Hong_Kong', '2010-02-03 04:05:00 America/New_York', '2010-02-03T17:05+08:00'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider transformProvider
|
||||||
|
*/
|
||||||
|
public function testTransform($fromTz, $toTz, $from, $to)
|
||||||
|
{
|
||||||
|
$transformer = new DateTimeToRfc3339Transformer($fromTz, $toTz);
|
||||||
|
|
||||||
|
$this->assertSame($to, $transformer->transform(null !== $from ? new \DateTime($from) : null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
|
||||||
|
*/
|
||||||
|
public function testTransformRequiresValidDateTime()
|
||||||
|
{
|
||||||
|
$transformer = new DateTimeToRfc3339Transformer();
|
||||||
|
$transformer->transform('2010-01-01');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider reverseTransformProvider
|
||||||
|
*/
|
||||||
|
public function testReverseTransform($toTz, $fromTz, $to, $from)
|
||||||
|
{
|
||||||
|
$transformer = new DateTimeToRfc3339Transformer($toTz, $fromTz);
|
||||||
|
|
||||||
|
if (null !== $to) {
|
||||||
|
$this->assertDateTimeEquals(new \DateTime($to), $transformer->reverseTransform($from));
|
||||||
|
} else {
|
||||||
|
$this->assertSame($to, $transformer->reverseTransform($from));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
|
||||||
|
*/
|
||||||
|
public function testReverseTransformRequiresString()
|
||||||
|
{
|
||||||
|
$transformer = new DateTimeToRfc3339Transformer();
|
||||||
|
$transformer->reverseTransform(12345);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException Symfony\Component\Form\Exception\TransformationFailedException
|
||||||
|
*/
|
||||||
|
public function testReverseTransformWithNonExistingDate()
|
||||||
|
{
|
||||||
|
$transformer = new DateTimeToRfc3339Transformer('UTC', 'UTC');
|
||||||
|
|
||||||
|
var_dump($transformer->reverseTransform('2010-04-31T04:05Z'));
|
||||||
|
}
|
||||||
|
}
|
@ -165,12 +165,12 @@ class DateTimeTypeTest extends LocalizedTestCase
|
|||||||
|
|
||||||
$outputTime = new \DateTime('2010-06-02 03:04:00 Pacific/Tahiti');
|
$outputTime = new \DateTime('2010-06-02 03:04:00 Pacific/Tahiti');
|
||||||
|
|
||||||
$form->bind('2010-06-02T03:04:00');
|
$form->bind('2010-06-02T03:04:00-10:00');
|
||||||
|
|
||||||
$outputTime->setTimezone(new \DateTimeZone('America/New_York'));
|
$outputTime->setTimezone(new \DateTimeZone('America/New_York'));
|
||||||
|
|
||||||
$this->assertDateTimeEquals($outputTime, $form->getData());
|
$this->assertDateTimeEquals($outputTime, $form->getData());
|
||||||
$this->assertEquals('2010-06-02T03:04:00', $form->getViewData());
|
$this->assertEquals('2010-06-02T03:04:00-10:00', $form->getViewData());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSubmit_stringSingleText()
|
public function testSubmit_stringSingleText()
|
||||||
@ -182,10 +182,10 @@ class DateTimeTypeTest extends LocalizedTestCase
|
|||||||
'widget' => 'single_text',
|
'widget' => 'single_text',
|
||||||
));
|
));
|
||||||
|
|
||||||
$form->bind('2010-06-02T03:04:00');
|
$form->bind('2010-06-02T03:04:00Z');
|
||||||
|
|
||||||
$this->assertEquals('2010-06-02 03:04:00', $form->getData());
|
$this->assertEquals('2010-06-02 03:04:00', $form->getData());
|
||||||
$this->assertEquals('2010-06-02T03:04:00', $form->getViewData());
|
$this->assertEquals('2010-06-02T03:04:00Z', $form->getViewData());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSubmit_stringSingleText_withSeconds()
|
public function testSubmit_stringSingleText_withSeconds()
|
||||||
@ -198,10 +198,10 @@ class DateTimeTypeTest extends LocalizedTestCase
|
|||||||
'with_seconds' => true,
|
'with_seconds' => true,
|
||||||
));
|
));
|
||||||
|
|
||||||
$form->bind('2010-06-02T03:04:05');
|
$form->bind('2010-06-02T03:04:05Z');
|
||||||
|
|
||||||
$this->assertEquals('2010-06-02 03:04:05', $form->getData());
|
$this->assertEquals('2010-06-02 03:04:05', $form->getData());
|
||||||
$this->assertEquals('2010-06-02T03:04:05', $form->getViewData());
|
$this->assertEquals('2010-06-02T03:04:05Z', $form->getViewData());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSubmit_differentPattern()
|
public function testSubmit_differentPattern()
|
||||||
@ -355,4 +355,35 @@ class DateTimeTypeTest extends LocalizedTestCase
|
|||||||
$this->assertNull($view->get('time')->get('minute')->getVar('empty_value'));
|
$this->assertNull($view->get('time')->get('minute')->getVar('empty_value'));
|
||||||
$this->assertSame('Empty second', $view->get('time')->get('second')->getVar('empty_value'));
|
$this->assertSame('Empty second', $view->get('time')->get('second')->getVar('empty_value'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testPassHtml5TypeIfSingleTextAndHtml5Format()
|
||||||
|
{
|
||||||
|
$form = $this->factory->create('datetime', null, array(
|
||||||
|
'widget' => 'single_text',
|
||||||
|
));
|
||||||
|
|
||||||
|
$view = $form->createView();
|
||||||
|
$this->assertSame('datetime', $view->getVar('type'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDontPassHtml5TypeIfNotHtml5Format()
|
||||||
|
{
|
||||||
|
$form = $this->factory->create('datetime', null, array(
|
||||||
|
'widget' => 'single_text',
|
||||||
|
'format' => 'yyyy-MM-dd HH:mm',
|
||||||
|
));
|
||||||
|
|
||||||
|
$view = $form->createView();
|
||||||
|
$this->assertNull($view->getVar('datetime'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDontPassHtml5TypeIfNotSingleText()
|
||||||
|
{
|
||||||
|
$form = $this->factory->create('datetime', null, array(
|
||||||
|
'widget' => 'text',
|
||||||
|
));
|
||||||
|
|
||||||
|
$view = $form->createView();
|
||||||
|
$this->assertNull($view->getVar('type'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -677,7 +677,7 @@ class DateTypeTest extends LocalizedTestCase
|
|||||||
$this->assertNull($view->getVar('type'));
|
$this->assertNull($view->getVar('type'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testPassHtml5TypeIfNotSingleText()
|
public function testDontPassHtml5TypeIfNotSingleText()
|
||||||
{
|
{
|
||||||
$form = $this->factory->create('date', null, array(
|
$form = $this->factory->create('date', null, array(
|
||||||
'widget' => 'text',
|
'widget' => 'text',
|
||||||
|
Reference in New Issue
Block a user