Skip to content

Commit 39c98b9

Browse files
committed
use a reference date to handle times during DST
1 parent 5db58f6 commit 39c98b9

File tree

7 files changed

+144
-11
lines changed

7 files changed

+144
-11
lines changed

UPGRADE-4.4.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ Filesystem
7272
Form
7373
----
7474

75+
* Using different values for the "model_timezone" and "view_timezone" options of the `TimeType` without configuring a
76+
reference date is deprecated.
7577
* Using `int` or `float` as data for the `NumberType` when the `input` option is set to `string` is deprecated.
7678

7779
FrameworkBundle

UPGRADE-5.0.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ Finder
152152
Form
153153
----
154154

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

src/Symfony/Component/Form/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ CHANGELOG
44
4.4.0
55
-----
66

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

src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@ class DateTimeToArrayTransformer extends BaseDateTimeTransformer
2424
private $pad;
2525

2626
private $fields;
27+
private $referenceDate;
2728

2829
/**
2930
* @param string $inputTimezone The input timezone
3031
* @param string $outputTimezone The output timezone
3132
* @param array $fields The date fields
3233
* @param bool $pad Whether to use padding
3334
*/
34-
public function __construct(string $inputTimezone = null, string $outputTimezone = null, array $fields = null, bool $pad = false)
35+
public function __construct(string $inputTimezone = null, string $outputTimezone = null, array $fields = null, bool $pad = false, \DateTimeInterface $referenceDate = null)
3536
{
3637
parent::__construct($inputTimezone, $outputTimezone);
3738

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

4243
$this->fields = $fields;
4344
$this->pad = $pad;
45+
$this->referenceDate = $referenceDate ?: new \DateTimeImmutable('1970-01-01 00:00:00');
4446
}
4547

4648
/**
@@ -165,12 +167,12 @@ public function reverseTransform($value)
165167
try {
166168
$dateTime = new \DateTime(sprintf(
167169
'%s-%s-%s %s:%s:%s',
168-
empty($value['year']) ? '1970' : $value['year'],
169-
empty($value['month']) ? '1' : $value['month'],
170-
empty($value['day']) ? '1' : $value['day'],
171-
empty($value['hour']) ? '0' : $value['hour'],
172-
empty($value['minute']) ? '0' : $value['minute'],
173-
empty($value['second']) ? '0' : $value['second']
170+
empty($value['year']) ? $this->referenceDate->format('Y') : $value['year'],
171+
empty($value['month']) ? $this->referenceDate->format('m') : $value['month'],
172+
empty($value['day']) ? $this->referenceDate->format('d') : $value['day'],
173+
empty($value['hour']) ? $this->referenceDate->format('H') : $value['hour'],
174+
empty($value['minute']) ? $this->referenceDate->format('i') : $value['minute'],
175+
empty($value['second']) ? $this->referenceDate->format('s') : $value['second']
174176
),
175177
new \DateTimeZone($this->outputTimezone)
176178
);

src/Symfony/Component/Form/Extension/Core/Type/TimeType.php

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ public function buildForm(FormBuilderInterface $builder, array $options)
4545
throw new InvalidConfigurationException('You can not disable minutes if you have enabled seconds.');
4646
}
4747

48+
if (null !== $options['reference_date'] && $options['reference_date']->getTimezone()->getName() !== $options['model_timezone']) {
49+
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()));
50+
}
51+
4852
if ($options['with_minutes']) {
4953
$format .= ':i';
5054
$parts[] = 'minute';
@@ -56,8 +60,6 @@ public function buildForm(FormBuilderInterface $builder, array $options)
5660
}
5761

5862
if ('single_text' === $options['widget']) {
59-
$builder->addViewTransformer(new DateTimeToStringTransformer($options['model_timezone'], $options['view_timezone'], $format));
60-
6163
// handle seconds ignored by user's browser when with_seconds enabled
6264
// https://codereview.chromium.org/450533009/
6365
if ($options['with_seconds']) {
@@ -68,6 +70,20 @@ public function buildForm(FormBuilderInterface $builder, array $options)
6870
}
6971
});
7072
}
73+
74+
if (null !== $options['reference_date']) {
75+
$format = 'Y-m-d '.$format;
76+
77+
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
78+
$data = $event->getData();
79+
80+
if (preg_match('/^\d{2}:\d{2}(:\d{2})?$/', $data)) {
81+
$event->setData($options['reference_date']->format('Y-m-d ').$data);
82+
}
83+
});
84+
}
85+
86+
$builder->addViewTransformer(new DateTimeToStringTransformer($options['model_timezone'], $options['view_timezone'], $format));
7187
} else {
7288
$hourOptions = $minuteOptions = $secondOptions = [
7389
'error_bubbling' => true,
@@ -157,7 +173,7 @@ public function buildForm(FormBuilderInterface $builder, array $options)
157173
$builder->add('second', self::$widgets[$options['widget']], $secondOptions);
158174
}
159175

160-
$builder->addViewTransformer(new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts, 'text' === $options['widget']));
176+
$builder->addViewTransformer(new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts, 'text' === $options['widget'], $options['reference_date']));
161177
}
162178

163179
if ('datetime_immutable' === $options['input']) {
@@ -262,6 +278,7 @@ public function configureOptions(OptionsResolver $resolver)
262278
'with_seconds' => false,
263279
'model_timezone' => null,
264280
'view_timezone' => null,
281+
'reference_date' => null,
265282
'placeholder' => $placeholderDefault,
266283
'html5' => true,
267284
// Don't modify \DateTime classes by reference, we treat
@@ -280,6 +297,14 @@ public function configureOptions(OptionsResolver $resolver)
280297
'choice_translation_domain' => false,
281298
]);
282299

300+
$resolver->setDeprecated('model_timezone', function (Options $options, $modelTimezone): string {
301+
if (null !== $modelTimezone && $options['view_timezone'] !== $modelTimezone && null === $options['reference_date']) {
302+
return sprintf('Using different values for the "model_timezone" and "view_timezone" options without configuring a reference date is deprecated since Symfony 4.4.');
303+
}
304+
305+
return '';
306+
});
307+
283308
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
284309
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
285310

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

305331
/**

src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ public function testDebugDeprecatedDefaults()
4545
Built-in form types (Symfony\Component\Form\Extension\Core\Type)
4646
----------------------------------------------------------------
4747
48-
BirthdayType, DateTimeType, DateType, IntegerType, TimezoneType
48+
BirthdayType, DateTimeType, DateType, IntegerType, TimeType
49+
TimezoneType
4950
5051
Service form types
5152
------------------

src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,57 @@ public function testSubmitWithSecondsAndBrowserOmissionSeconds()
276276
$this->assertEquals('03:04:00', $form->getViewData());
277277
}
278278

279+
public function testSubmitDifferentTimezones()
280+
{
281+
$form = $this->factory->create(static::TESTED_TYPE, null, [
282+
'model_timezone' => 'UTC',
283+
'view_timezone' => 'Europe/Berlin',
284+
'input' => 'datetime',
285+
'with_seconds' => true,
286+
'reference_date' => new \DateTimeImmutable('2019-01-01', new \DateTimeZone('UTC')),
287+
]);
288+
$form->submit([
289+
'hour' => '16',
290+
'minute' => '9',
291+
'second' => '10',
292+
]);
293+
294+
$this->assertSame('15:09:10', $form->getData()->format('H:i:s'));
295+
}
296+
297+
public function testSubmitDifferentTimezonesDuringDaylightSavingTime()
298+
{
299+
$form = $this->factory->create(static::TESTED_TYPE, null, [
300+
'model_timezone' => 'UTC',
301+
'view_timezone' => 'Europe/Berlin',
302+
'input' => 'datetime',
303+
'with_seconds' => true,
304+
'reference_date' => new \DateTimeImmutable('2019-07-12', new \DateTimeZone('UTC')),
305+
]);
306+
$form->submit([
307+
'hour' => '16',
308+
'minute' => '9',
309+
'second' => '10',
310+
]);
311+
312+
$this->assertSame('14:09:10', $form->getData()->format('H:i:s'));
313+
}
314+
315+
public function testSubmitDifferentTimezonesDuringDaylightSavingTimeUsingSingleTextWidget()
316+
{
317+
$form = $this->factory->create(static::TESTED_TYPE, null, [
318+
'model_timezone' => 'UTC',
319+
'view_timezone' => 'Europe/Berlin',
320+
'input' => 'datetime',
321+
'with_seconds' => true,
322+
'reference_date' => new \DateTimeImmutable('2019-07-12', new \DateTimeZone('UTC')),
323+
'widget' => 'single_text',
324+
]);
325+
$form->submit('16:09:10');
326+
327+
$this->assertSame('14:09:10', $form->getData()->format('H:i:s'));
328+
}
329+
279330
public function testSetDataWithoutMinutes()
280331
{
281332
$form = $this->factory->create(static::TESTED_TYPE, null, [
@@ -311,6 +362,7 @@ public function testSetDataDifferentTimezones()
311362
'view_timezone' => 'Asia/Hong_Kong',
312363
'input' => 'string',
313364
'with_seconds' => true,
365+
'reference_date' => new \DateTimeImmutable('2013-01-01 00:00:00', new \DateTimeZone('America/New_York')),
314366
]);
315367

316368
$dateTime = new \DateTime('2013-01-01 12:04:05');
@@ -337,6 +389,7 @@ public function testSetDataDifferentTimezonesDateTime()
337389
'view_timezone' => 'Asia/Hong_Kong',
338390
'input' => 'datetime',
339391
'with_seconds' => true,
392+
'reference_date' => new \DateTimeImmutable('now', new \DateTimeZone('America/New_York')),
340393
]);
341394

342395
$dateTime = new \DateTime('12:04:05');
@@ -357,6 +410,39 @@ public function testSetDataDifferentTimezonesDateTime()
357410
$this->assertEquals($displayedData, $form->getViewData());
358411
}
359412

413+
public function testSetDataDifferentTimezonesDuringDaylightSavingTime()
414+
{
415+
$form = $this->factory->create(static::TESTED_TYPE, null, [
416+
'model_timezone' => 'UTC',
417+
'view_timezone' => 'Europe/Berlin',
418+
'input' => 'datetime',
419+
'with_seconds' => true,
420+
'reference_date' => new \DateTimeImmutable('2019-07-12', new \DateTimeZone('UTC')),
421+
]);
422+
423+
$form->setData(new \DateTime('2019-07-24 14:09:10', new \DateTimeZone('UTC')));
424+
425+
$this->assertSame(['hour' => '16', 'minute' => '9', 'second' => '10'], $form->getViewData());
426+
}
427+
428+
/**
429+
* @group legacy
430+
* @expectedDeprecation Using different values for the "model_timezone" and "view_timezone" options without configuring a reference date is deprecated since Symfony 4.4.
431+
*/
432+
public function testSetDataDifferentTimezonesWithoutReferenceDate()
433+
{
434+
$form = $this->factory->create(static::TESTED_TYPE, null, [
435+
'model_timezone' => 'UTC',
436+
'view_timezone' => 'Europe/Berlin',
437+
'input' => 'datetime',
438+
'with_seconds' => true,
439+
]);
440+
441+
$form->setData(new \DateTime('2019-07-24 14:09:10', new \DateTimeZone('UTC')));
442+
443+
$this->assertSame(['hour' => '16', 'minute' => '9', 'second' => '10'], $form->getViewData());
444+
}
445+
360446
public function testHoursOption()
361447
{
362448
$form = $this->factory->create(static::TESTED_TYPE, null, [
@@ -762,6 +848,18 @@ public function testThrowExceptionIfSecondsIsInvalid()
762848
]);
763849
}
764850

851+
/**
852+
* @expectedException \Symfony\Component\Form\Exception\InvalidConfigurationException
853+
*/
854+
public function testReferenceDateTimezoneMustMatchModelTimezone()
855+
{
856+
$this->factory->create(static::TESTED_TYPE, null, [
857+
'model_timezone' => 'UTC',
858+
'view_timezone' => 'Europe/Berlin',
859+
'reference_date' => new \DateTimeImmutable('now', new \DateTimeZone('Europe/Berlin')),
860+
]);
861+
}
862+
765863
public function testPassDefaultChoiceTranslationDomain()
766864
{
767865
$form = $this->factory->create(static::TESTED_TYPE);

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy