diff --git a/src/Symfony/Component/Form/Extension/Core/Type/TimezoneType.php b/src/Symfony/Component/Form/Extension/Core/Type/TimezoneType.php index 54968e6799..ccf51d1fb5 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/TimezoneType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/TimezoneType.php @@ -13,10 +13,12 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader; +use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader; use Symfony\Component\Form\Exception\LogicException; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeZoneToStringTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\IntlTimeZoneToStringTransformer; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Intl\Timezones; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -40,19 +42,41 @@ class TimezoneType extends AbstractType public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ + 'intl' => false, 'choice_loader' => function (Options $options) { - $regions = $options->offsetGet('regions', false); $input = $options['input']; + if ($options['intl']) { + $choiceTranslationLocale = $options['choice_translation_locale']; + + return new IntlCallbackChoiceLoader(function () use ($input, $choiceTranslationLocale) { + return self::getIntlTimezones($input, $choiceTranslationLocale); + }); + } + + $regions = $options->offsetGet('regions', false); + return new CallbackChoiceLoader(function () use ($regions, $input) { - return self::getTimezones($regions, $input); + return self::getPhpTimezones($regions, $input); }); }, 'choice_translation_domain' => false, + 'choice_translation_locale' => null, 'input' => 'string', 'regions' => \DateTimeZone::ALL, ]); + $resolver->setAllowedTypes('intl', ['bool']); + + $resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']); + $resolver->setNormalizer('choice_translation_locale', function (Options $options, $value) { + if (null !== $value && !$options['intl']) { + throw new LogicException('The "choice_translation_locale" option can only be used if the "intl" option is set to true.'); + } + + return $value; + }); + $resolver->setAllowedValues('input', ['string', 'datetimezone', 'intltimezone']); $resolver->setNormalizer('input', function (Options $options, $value) { if ('intltimezone' === $value && !class_exists(\IntlTimeZone::class)) { @@ -64,6 +88,13 @@ class TimezoneType extends AbstractType $resolver->setAllowedTypes('regions', 'int'); $resolver->setDeprecated('regions', 'The option "%name%" is deprecated since Symfony 4.2.'); + $resolver->setNormalizer('regions', function (Options $options, $value) { + if ($options['intl'] && \DateTimeZone::ALL !== (\DateTimeZone::ALL & $value)) { + throw new LogicException('The "regions" option can only be used if the "intl" option is set to false.'); + } + + return $value; + }); } /** @@ -82,10 +113,7 @@ class TimezoneType extends AbstractType return 'timezone'; } - /** - * Returns a normalized array of timezone choices. - */ - private static function getTimezones(int $regions, string $input): array + private static function getPhpTimezones(int $regions, string $input): array { $timezones = []; @@ -99,4 +127,19 @@ class TimezoneType extends AbstractType return $timezones; } + + private static function getIntlTimezones(string $input, string $locale = null): array + { + $timezones = array_flip(Timezones::getNames($locale)); + + if ('intltimezone' === $input) { + foreach ($timezones as $name => $timezone) { + if ('Etc/Unknown' === \IntlTimeZone::createTimeZone($timezone)->getID()) { + unset($timezones[$name]); + } + } + } + + return $timezones; + } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimezoneTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimezoneTypeTest.php index 8e1fee8d36..bded60a7de 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimezoneTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimezoneTypeTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; use Symfony\Component\Form\ChoiceList\View\ChoiceView; +use Symfony\Component\Intl\Util\IntlTestHelper; class TimezoneTypeTest extends BaseTypeTest { @@ -83,6 +84,17 @@ class TimezoneTypeTest extends BaseTypeTest $this->assertContains(new ChoiceView('Europe/Amsterdam', 'Europe/Amsterdam', 'Europe / Amsterdam'), $choices, '', false, false); } + /** + * @group legacy + * @expectedDeprecation The option "regions" is deprecated since Symfony 4.2. + * @expectedException \Symfony\Component\Form\Exception\LogicException + * @expectedExceptionMessage The "regions" option can only be used if the "intl" option is set to false. + */ + public function testFilterByRegionsWithIntl() + { + $this->factory->create(static::TESTED_TYPE, null, ['regions' => \DateTimeZone::EUROPE, 'intl' => true]); + } + /** * @requires extension intl */ @@ -116,4 +128,54 @@ class TimezoneTypeTest extends BaseTypeTest $this->assertNull($form->getData()); $this->assertNotContains('Europe/Saratov', $form->getConfig()->getAttribute('choice_list')->getValues()); } + + /** + * @requires extension intl + */ + public function testIntlTimeZoneInputWithBcAndIntl() + { + $form = $this->factory->create(static::TESTED_TYPE, null, ['input' => 'intltimezone', 'intl' => true]); + $form->submit('Europe/Saratov'); + + $this->assertNull($form->getData()); + $this->assertNotContains('Europe/Saratov', $form->getConfig()->getAttribute('choice_list')->getValues()); + } + + public function testTimezonesAreSelectableWithIntl() + { + IntlTestHelper::requireIntl($this, false); + + $choices = $this->factory->create(static::TESTED_TYPE, null, ['intl' => true]) + ->createView()->vars['choices']; + + $this->assertContains(new ChoiceView('Europe/Amsterdam', 'Europe/Amsterdam', 'Central European Time (Amsterdam)'), $choices, '', false, false); + $this->assertContains(new ChoiceView('Etc/UTC', 'Etc/UTC', 'Coordinated Universal Time'), $choices, '', false, false); + } + + /** + * @requires extension intl + */ + public function testChoiceTranslationLocaleOptionWithIntl() + { + $choices = $this->factory + ->create(static::TESTED_TYPE, null, [ + 'intl' => true, + 'choice_translation_locale' => 'uk', + ]) + ->createView()->vars['choices']; + + $this->assertContains(new ChoiceView('Europe/Amsterdam', 'Europe/Amsterdam', 'за центральноєвропейським часом (Амстердам)'), $choices, '', false, false); + $this->assertContains(new ChoiceView('Etc/UTC', 'Etc/UTC', 'за всесвітнім координованим часом'), $choices, '', false, false); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\LogicException + * @expectedExceptionMessage The "choice_translation_locale" option can only be used if the "intl" option is set to true. + */ + public function testChoiceTranslationLocaleOptionWithoutIntl() + { + $this->factory->create(static::TESTED_TYPE, null, [ + 'choice_translation_locale' => 'uk', + ]); + } }