feature #32718 [Form] use a reference date to handle times during DST (xabbuh)

This PR was merged into the 4.4 branch.

Discussion
----------

[Form] use a reference date to handle times during DST

| Q             | A
| ------------- | ---
| Branch?       | 4.4
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | #18366
| License       | MIT
| Doc PR        |

Commits
-------

39c98b9a08 use a reference date to handle times during DST
This commit is contained in:
Fabien Potencier 2019-07-25 17:17:00 +02:00
commit 86440a4b77
7 changed files with 144 additions and 11 deletions

View File

@ -72,6 +72,8 @@ Filesystem
Form
----
* Using different values for the "model_timezone" and "view_timezone" options of the `TimeType` without configuring a
reference date is deprecated.
* Using `int` or `float` as data for the `NumberType` when the `input` option is set to `string` is deprecated.
FrameworkBundle

View File

@ -152,6 +152,8 @@ Finder
Form
----
* Removed support for using different values for the "model_timezone" and "view_timezone" options of the `TimeType`
without configuring a reference date.
* Removed support for using `int` or `float` as data for the `NumberType` when the `input` option is set to `string`.
* Removed support for using the `format` option of `DateType` and `DateTimeType` when the `html5` option is enabled.
* Using names for buttons that do not start with a letter, a digit, or an underscore leads to an exception.

View File

@ -4,6 +4,8 @@ CHANGELOG
4.4.0
-----
* using different values for the "model_timezone" and "view_timezone" options of the `TimeType` without configuring a
reference date is deprecated
* preferred choices are repeated in the list of all choices
* deprecated using `int` or `float` as data for the `NumberType` when the `input` option is set to `string`
* The type guesser guesses the HTML accept attribute when a mime type is configured in the File or Image constraint.

View File

@ -24,6 +24,7 @@ class DateTimeToArrayTransformer extends BaseDateTimeTransformer
private $pad;
private $fields;
private $referenceDate;
/**
* @param string $inputTimezone The input timezone
@ -31,7 +32,7 @@ class DateTimeToArrayTransformer extends BaseDateTimeTransformer
* @param array $fields The date fields
* @param bool $pad Whether to use padding
*/
public function __construct(string $inputTimezone = null, string $outputTimezone = null, array $fields = null, bool $pad = false)
public function __construct(string $inputTimezone = null, string $outputTimezone = null, array $fields = null, bool $pad = false, \DateTimeInterface $referenceDate = null)
{
parent::__construct($inputTimezone, $outputTimezone);
@ -41,6 +42,7 @@ class DateTimeToArrayTransformer extends BaseDateTimeTransformer
$this->fields = $fields;
$this->pad = $pad;
$this->referenceDate = $referenceDate ?: new \DateTimeImmutable('1970-01-01 00:00:00');
}
/**
@ -165,12 +167,12 @@ class DateTimeToArrayTransformer extends BaseDateTimeTransformer
try {
$dateTime = new \DateTime(sprintf(
'%s-%s-%s %s:%s:%s',
empty($value['year']) ? '1970' : $value['year'],
empty($value['month']) ? '1' : $value['month'],
empty($value['day']) ? '1' : $value['day'],
empty($value['hour']) ? '0' : $value['hour'],
empty($value['minute']) ? '0' : $value['minute'],
empty($value['second']) ? '0' : $value['second']
empty($value['year']) ? $this->referenceDate->format('Y') : $value['year'],
empty($value['month']) ? $this->referenceDate->format('m') : $value['month'],
empty($value['day']) ? $this->referenceDate->format('d') : $value['day'],
empty($value['hour']) ? $this->referenceDate->format('H') : $value['hour'],
empty($value['minute']) ? $this->referenceDate->format('i') : $value['minute'],
empty($value['second']) ? $this->referenceDate->format('s') : $value['second']
),
new \DateTimeZone($this->outputTimezone)
);

View File

@ -45,6 +45,10 @@ class TimeType extends AbstractType
throw new InvalidConfigurationException('You can not disable minutes if you have enabled seconds.');
}
if (null !== $options['reference_date'] && $options['reference_date']->getTimezone()->getName() !== $options['model_timezone']) {
throw new InvalidConfigurationException(sprintf('The configured "model_timezone" (%s) must match the timezone of the "reference_date" (%s).', $options['model_timezone'], $options['reference_date']->getTimezone()->getName()));
}
if ($options['with_minutes']) {
$format .= ':i';
$parts[] = 'minute';
@ -56,8 +60,6 @@ class TimeType extends AbstractType
}
if ('single_text' === $options['widget']) {
$builder->addViewTransformer(new DateTimeToStringTransformer($options['model_timezone'], $options['view_timezone'], $format));
// handle seconds ignored by user's browser when with_seconds enabled
// https://codereview.chromium.org/450533009/
if ($options['with_seconds']) {
@ -68,6 +70,20 @@ class TimeType extends AbstractType
}
});
}
if (null !== $options['reference_date']) {
$format = 'Y-m-d '.$format;
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
$data = $event->getData();
if (preg_match('/^\d{2}:\d{2}(:\d{2})?$/', $data)) {
$event->setData($options['reference_date']->format('Y-m-d ').$data);
}
});
}
$builder->addViewTransformer(new DateTimeToStringTransformer($options['model_timezone'], $options['view_timezone'], $format));
} else {
$hourOptions = $minuteOptions = $secondOptions = [
'error_bubbling' => true,
@ -157,7 +173,7 @@ class TimeType extends AbstractType
$builder->add('second', self::$widgets[$options['widget']], $secondOptions);
}
$builder->addViewTransformer(new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts, 'text' === $options['widget']));
$builder->addViewTransformer(new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts, 'text' === $options['widget'], $options['reference_date']));
}
if ('datetime_immutable' === $options['input']) {
@ -262,6 +278,7 @@ class TimeType extends AbstractType
'with_seconds' => false,
'model_timezone' => null,
'view_timezone' => null,
'reference_date' => null,
'placeholder' => $placeholderDefault,
'html5' => true,
// Don't modify \DateTime classes by reference, we treat
@ -280,6 +297,14 @@ class TimeType extends AbstractType
'choice_translation_domain' => false,
]);
$resolver->setDeprecated('model_timezone', function (Options $options, $modelTimezone): string {
if (null !== $modelTimezone && $options['view_timezone'] !== $modelTimezone && null === $options['reference_date']) {
return sprintf('Using different values for the "model_timezone" and "view_timezone" options without configuring a reference date is deprecated since Symfony 4.4.');
}
return '';
});
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
@ -300,6 +325,7 @@ class TimeType extends AbstractType
$resolver->setAllowedTypes('minutes', 'array');
$resolver->setAllowedTypes('seconds', 'array');
$resolver->setAllowedTypes('input_format', 'string');
$resolver->setAllowedTypes('reference_date', ['null', \DateTimeInterface::class]);
}
/**

View File

@ -45,7 +45,8 @@ class DebugCommandTest extends TestCase
Built-in form types (Symfony\Component\Form\Extension\Core\Type)
----------------------------------------------------------------
BirthdayType, DateTimeType, DateType, IntegerType, TimezoneType
BirthdayType, DateTimeType, DateType, IntegerType, TimeType
TimezoneType
Service form types
------------------

View File

@ -276,6 +276,57 @@ class TimeTypeTest extends BaseTypeTest
$this->assertEquals('03:04:00', $form->getViewData());
}
public function testSubmitDifferentTimezones()
{
$form = $this->factory->create(static::TESTED_TYPE, null, [
'model_timezone' => 'UTC',
'view_timezone' => 'Europe/Berlin',
'input' => 'datetime',
'with_seconds' => true,
'reference_date' => new \DateTimeImmutable('2019-01-01', new \DateTimeZone('UTC')),
]);
$form->submit([
'hour' => '16',
'minute' => '9',
'second' => '10',
]);
$this->assertSame('15:09:10', $form->getData()->format('H:i:s'));
}
public function testSubmitDifferentTimezonesDuringDaylightSavingTime()
{
$form = $this->factory->create(static::TESTED_TYPE, null, [
'model_timezone' => 'UTC',
'view_timezone' => 'Europe/Berlin',
'input' => 'datetime',
'with_seconds' => true,
'reference_date' => new \DateTimeImmutable('2019-07-12', new \DateTimeZone('UTC')),
]);
$form->submit([
'hour' => '16',
'minute' => '9',
'second' => '10',
]);
$this->assertSame('14:09:10', $form->getData()->format('H:i:s'));
}
public function testSubmitDifferentTimezonesDuringDaylightSavingTimeUsingSingleTextWidget()
{
$form = $this->factory->create(static::TESTED_TYPE, null, [
'model_timezone' => 'UTC',
'view_timezone' => 'Europe/Berlin',
'input' => 'datetime',
'with_seconds' => true,
'reference_date' => new \DateTimeImmutable('2019-07-12', new \DateTimeZone('UTC')),
'widget' => 'single_text',
]);
$form->submit('16:09:10');
$this->assertSame('14:09:10', $form->getData()->format('H:i:s'));
}
public function testSetDataWithoutMinutes()
{
$form = $this->factory->create(static::TESTED_TYPE, null, [
@ -311,6 +362,7 @@ class TimeTypeTest extends BaseTypeTest
'view_timezone' => 'Asia/Hong_Kong',
'input' => 'string',
'with_seconds' => true,
'reference_date' => new \DateTimeImmutable('2013-01-01 00:00:00', new \DateTimeZone('America/New_York')),
]);
$dateTime = new \DateTime('2013-01-01 12:04:05');
@ -337,6 +389,7 @@ class TimeTypeTest extends BaseTypeTest
'view_timezone' => 'Asia/Hong_Kong',
'input' => 'datetime',
'with_seconds' => true,
'reference_date' => new \DateTimeImmutable('now', new \DateTimeZone('America/New_York')),
]);
$dateTime = new \DateTime('12:04:05');
@ -357,6 +410,39 @@ class TimeTypeTest extends BaseTypeTest
$this->assertEquals($displayedData, $form->getViewData());
}
public function testSetDataDifferentTimezonesDuringDaylightSavingTime()
{
$form = $this->factory->create(static::TESTED_TYPE, null, [
'model_timezone' => 'UTC',
'view_timezone' => 'Europe/Berlin',
'input' => 'datetime',
'with_seconds' => true,
'reference_date' => new \DateTimeImmutable('2019-07-12', new \DateTimeZone('UTC')),
]);
$form->setData(new \DateTime('2019-07-24 14:09:10', new \DateTimeZone('UTC')));
$this->assertSame(['hour' => '16', 'minute' => '9', 'second' => '10'], $form->getViewData());
}
/**
* @group legacy
* @expectedDeprecation Using different values for the "model_timezone" and "view_timezone" options without configuring a reference date is deprecated since Symfony 4.4.
*/
public function testSetDataDifferentTimezonesWithoutReferenceDate()
{
$form = $this->factory->create(static::TESTED_TYPE, null, [
'model_timezone' => 'UTC',
'view_timezone' => 'Europe/Berlin',
'input' => 'datetime',
'with_seconds' => true,
]);
$form->setData(new \DateTime('2019-07-24 14:09:10', new \DateTimeZone('UTC')));
$this->assertSame(['hour' => '16', 'minute' => '9', 'second' => '10'], $form->getViewData());
}
public function testHoursOption()
{
$form = $this->factory->create(static::TESTED_TYPE, null, [
@ -762,6 +848,18 @@ class TimeTypeTest extends BaseTypeTest
]);
}
/**
* @expectedException \Symfony\Component\Form\Exception\InvalidConfigurationException
*/
public function testReferenceDateTimezoneMustMatchModelTimezone()
{
$this->factory->create(static::TESTED_TYPE, null, [
'model_timezone' => 'UTC',
'view_timezone' => 'Europe/Berlin',
'reference_date' => new \DateTimeImmutable('now', new \DateTimeZone('Europe/Berlin')),
]);
}
public function testPassDefaultChoiceTranslationDomain()
{
$form = $this->factory->create(static::TESTED_TYPE);