Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions UPGRADE-4.4.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions UPGRADE-5.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Component/Form/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ class DateTimeToArrayTransformer extends BaseDateTimeTransformer
private $pad;

private $fields;
private $referenceDate;

/**
* @param string $inputTimezone The input timezone
* @param string $outputTimezone The output timezone
* @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);

Expand All @@ -41,6 +42,7 @@ public function __construct(string $inputTimezone = null, string $outputTimezone

$this->fields = $fields;
$this->pad = $pad;
$this->referenceDate = $referenceDate ?: new \DateTimeImmutable('1970-01-01 00:00:00');
}

/**
Expand Down Expand Up @@ -165,12 +167,12 @@ public function reverseTransform($value)
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)
);
Expand Down
32 changes: 29 additions & 3 deletions src/Symfony/Component/Form/Extension/Core/Type/TimeType.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ public function buildForm(FormBuilderInterface $builder, array $options)
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';
Expand All @@ -56,8 +60,6 @@ public function buildForm(FormBuilderInterface $builder, array $options)
}

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']) {
Expand All @@ -68,6 +70,20 @@ public function buildForm(FormBuilderInterface $builder, array $options)
}
});
}

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,
Expand Down Expand Up @@ -157,7 +173,7 @@ public function buildForm(FormBuilderInterface $builder, array $options)
$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']) {
Expand Down Expand Up @@ -262,6 +278,7 @@ public function configureOptions(OptionsResolver $resolver)
'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
Expand All @@ -280,6 +297,14 @@ public function configureOptions(OptionsResolver $resolver)
'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);

Expand All @@ -300,6 +325,7 @@ public function configureOptions(OptionsResolver $resolver)
$resolver->setAllowedTypes('minutes', 'array');
$resolver->setAllowedTypes('seconds', 'array');
$resolver->setAllowedTypes('input_format', 'string');
$resolver->setAllowedTypes('reference_date', ['null', \DateTimeInterface::class]);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ public function testDebugDeprecatedDefaults()
Built-in form types (Symfony\Component\Form\Extension\Core\Type)
----------------------------------------------------------------

BirthdayType, DateTimeType, DateType, IntegerType, TimezoneType
BirthdayType, DateTimeType, DateType, IntegerType, TimeType
TimezoneType

Service form types
------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,57 @@ public function testSubmitWithSecondsAndBrowserOmissionSeconds()
$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, [
Expand Down Expand Up @@ -311,6 +362,7 @@ public function testSetDataDifferentTimezones()
'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');
Expand All @@ -337,6 +389,7 @@ public function testSetDataDifferentTimezonesDateTime()
'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');
Expand All @@ -357,6 +410,39 @@ public function testSetDataDifferentTimezonesDateTime()
$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, [
Expand Down Expand Up @@ -762,6 +848,18 @@ public function testThrowExceptionIfSecondsIsInvalid()
]);
}

/**
* @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);
Expand Down