From 39c98b9a087edec23717d32b1ba158886cf71c92 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 24 Jul 2019 16:46:11 +0200 Subject: [PATCH] use a reference date to handle times during DST --- UPGRADE-4.4.md | 2 + UPGRADE-5.0.md | 2 + src/Symfony/Component/Form/CHANGELOG.md | 2 + .../DateTimeToArrayTransformer.php | 16 +-- .../Form/Extension/Core/Type/TimeType.php | 32 +++++- .../Form/Tests/Command/DebugCommandTest.php | 3 +- .../Extension/Core/Type/TimeTypeTest.php | 98 +++++++++++++++++++ 7 files changed, 144 insertions(+), 11 deletions(-) diff --git a/UPGRADE-4.4.md b/UPGRADE-4.4.md index 416603f3df84b..93854c80739e9 100644 --- a/UPGRADE-4.4.md +++ b/UPGRADE-4.4.md @@ -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 diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index a02d2e302e06b..7ae6f5d7f578f 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -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. diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index f6bd9c35d5a16..122a37e89fc83 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -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. diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php index 00600f8487b1c..51efcb43a9509 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php @@ -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 @@ 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'); } /** @@ -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) ); diff --git a/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php b/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php index fb46274e31ab3..8d4ef4181b2da 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php @@ -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'; @@ -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']) { @@ -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, @@ -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']) { @@ -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 @@ -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); @@ -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]); } /** diff --git a/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php b/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php index 16434073a548f..7be042fbb8849 100644 --- a/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php +++ b/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php @@ -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 ------------------ diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php index 308207f2b32c5..e5d7fa9a7c263 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php @@ -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, [ @@ -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'); @@ -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'); @@ -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, [ @@ -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); 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