Skip to content

Commit

Permalink
feature #32718 [Form] use a reference date to handle times during DST…
Browse files Browse the repository at this point in the history
… (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
-------

39c98b9 use a reference date to handle times during DST
  • Loading branch information
fabpot committed Jul 25, 2019
2 parents a28609c + 39c98b9 commit 86440a4
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 11 deletions.
2 changes: 2 additions & 0 deletions UPGRADE-4.4.md
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
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
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
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
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
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
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

0 comments on commit 86440a4

Please sign in to comment.