[Validator] Allow intl timezones
This commit is contained in:
parent
1c110fa1f7
commit
7294b59c65
@ -26,15 +26,18 @@ class Timezone extends Constraint
|
||||
public const TIMEZONE_IDENTIFIER_ERROR = '5ce113e6-5e64-4ea2-90fe-d2233956db13';
|
||||
public const TIMEZONE_IDENTIFIER_IN_ZONE_ERROR = 'b57767b1-36c0-40ac-a3d7-629420c775b8';
|
||||
public const TIMEZONE_IDENTIFIER_IN_COUNTRY_ERROR = 'c4a22222-dc92-4fc0-abb0-d95b268c7d0b';
|
||||
public const TIMEZONE_IDENTIFIER_INTL_ERROR = '45863c26-88dc-41ba-bf53-c73bd1f7e90d';
|
||||
|
||||
public $zone = \DateTimeZone::ALL;
|
||||
public $countryCode;
|
||||
public $intlCompatible = false;
|
||||
public $message = 'This value is not a valid timezone.';
|
||||
|
||||
protected static $errorNames = [
|
||||
self::TIMEZONE_IDENTIFIER_ERROR => 'TIMEZONE_IDENTIFIER_ERROR',
|
||||
self::TIMEZONE_IDENTIFIER_IN_ZONE_ERROR => 'TIMEZONE_IDENTIFIER_IN_ZONE_ERROR',
|
||||
self::TIMEZONE_IDENTIFIER_IN_COUNTRY_ERROR => 'TIMEZONE_IDENTIFIER_IN_COUNTRY_ERROR',
|
||||
self::TIMEZONE_IDENTIFIER_INTL_ERROR => 'TIMEZONE_IDENTIFIER_INTL_ERROR',
|
||||
];
|
||||
|
||||
/**
|
||||
@ -51,5 +54,8 @@ class Timezone extends Constraint
|
||||
} elseif (\DateTimeZone::PER_COUNTRY !== (\DateTimeZone::PER_COUNTRY & $this->zone)) {
|
||||
throw new ConstraintDefinitionException('The option "countryCode" can only be used when the "zone" option is configured with "\DateTimeZone::PER_COUNTRY".');
|
||||
}
|
||||
if ($this->intlCompatible && !class_exists(\IntlTimeZone::class)) {
|
||||
throw new ConstraintDefinitionException('The option "intlCompatible" can only be used when the PHP intl extension is available.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,8 @@
|
||||
|
||||
namespace Symfony\Component\Validator\Constraints;
|
||||
|
||||
use Symfony\Component\Intl\Exception\MissingResourceException;
|
||||
use Symfony\Component\Intl\Timezones;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
@ -43,14 +45,28 @@ class TimezoneValidator extends ConstraintValidator
|
||||
|
||||
$value = (string) $value;
|
||||
|
||||
// @see: https://bugs.php.net/bug.php?id=75928
|
||||
if ($constraint->countryCode) {
|
||||
$timezoneIds = @\DateTimeZone::listIdentifiers($constraint->zone, $constraint->countryCode) ?: [];
|
||||
} else {
|
||||
$timezoneIds = \DateTimeZone::listIdentifiers($constraint->zone);
|
||||
if ($constraint->intlCompatible && 'Etc/Unknown' === \IntlTimeZone::createTimeZone($value)->getID()) {
|
||||
$this->context->buildViolation($constraint->message)
|
||||
->setParameter('{{ value }}', $this->formatValue($value))
|
||||
->setCode(Timezone::TIMEZONE_IDENTIFIER_INTL_ERROR)
|
||||
->addViolation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (\in_array($value, $timezoneIds, true)) {
|
||||
if ($constraint->countryCode) {
|
||||
$phpTimezoneIds = @\DateTimeZone::listIdentifiers($constraint->zone, $constraint->countryCode) ?: [];
|
||||
try {
|
||||
$intlTimezoneIds = Timezones::forCountryCode($constraint->countryCode);
|
||||
} catch (MissingResourceException $e) {
|
||||
$intlTimezoneIds = [];
|
||||
}
|
||||
} else {
|
||||
$phpTimezoneIds = \DateTimeZone::listIdentifiers($constraint->zone);
|
||||
$intlTimezoneIds = self::getIntlTimezones($constraint->zone);
|
||||
}
|
||||
|
||||
if (\in_array($value, $phpTimezoneIds, true) || \in_array($value, $intlTimezoneIds, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -63,9 +79,9 @@ class TimezoneValidator extends ConstraintValidator
|
||||
}
|
||||
|
||||
$this->context->buildViolation($constraint->message)
|
||||
->setParameter('{{ value }}', $this->formatValue($value))
|
||||
->setCode($code)
|
||||
->addViolation();
|
||||
->setParameter('{{ value }}', $this->formatValue($value))
|
||||
->setCode($code)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -89,4 +105,26 @@ class TimezoneValidator extends ConstraintValidator
|
||||
|
||||
return array_search($value, (new \ReflectionClass(\DateTimeZone::class))->getConstants(), true) ?: $value;
|
||||
}
|
||||
|
||||
private static function getIntlTimezones(int $zone): array
|
||||
{
|
||||
$timezones = Timezones::getIds();
|
||||
|
||||
if (\DateTimeZone::ALL === (\DateTimeZone::ALL & $zone)) {
|
||||
return $timezones;
|
||||
}
|
||||
|
||||
$filtered = [];
|
||||
foreach ((new \ReflectionClass(\DateTimeZone::class))->getConstants() as $const => $flag) {
|
||||
if ($flag !== ($flag & $zone)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filtered[] = array_filter($timezones, static function ($id) use ($const) {
|
||||
return 0 === stripos($id, $const.'/');
|
||||
});
|
||||
}
|
||||
|
||||
return $filtered ? array_merge(...$filtered) : [];
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,26 @@ class TimezoneValidatorTest extends ConstraintValidatorTestCase
|
||||
|
||||
public function getValidTimezones(): iterable
|
||||
{
|
||||
// ICU standard (alias/BC in PHP)
|
||||
yield ['Etc/UTC'];
|
||||
yield ['Etc/GMT'];
|
||||
yield ['America/Buenos_Aires'];
|
||||
|
||||
// PHP standard (alias in ICU)
|
||||
yield ['UTC'];
|
||||
yield ['America/Argentina/Buenos_Aires'];
|
||||
|
||||
// not deprecated in ICU
|
||||
yield ['CST6CDT'];
|
||||
yield ['EST5EDT'];
|
||||
yield ['MST7MDT'];
|
||||
yield ['PST8PDT'];
|
||||
yield ['America/Montreal'];
|
||||
|
||||
// expired in ICU
|
||||
yield ['Europe/Saratov'];
|
||||
|
||||
// standard
|
||||
yield ['America/Barbados'];
|
||||
yield ['America/Toronto'];
|
||||
yield ['Antarctica/Syowa'];
|
||||
@ -71,7 +90,6 @@ class TimezoneValidatorTest extends ConstraintValidatorTestCase
|
||||
yield ['Europe/Copenhagen'];
|
||||
yield ['Europe/Paris'];
|
||||
yield ['Pacific/Noumea'];
|
||||
yield ['UTC'];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -90,6 +108,8 @@ class TimezoneValidatorTest extends ConstraintValidatorTestCase
|
||||
|
||||
public function getValidGroupedTimezones(): iterable
|
||||
{
|
||||
yield ['America/Buenos_Aires', \DateTimeZone::AMERICA | \DateTimeZone::AUSTRALIA]; // icu
|
||||
yield ['America/Argentina/Buenos_Aires', \DateTimeZone::AMERICA]; // php
|
||||
yield ['America/Argentina/Cordoba', \DateTimeZone::AMERICA];
|
||||
yield ['America/Barbados', \DateTimeZone::AMERICA];
|
||||
yield ['Africa/Cairo', \DateTimeZone::AFRICA];
|
||||
@ -124,6 +144,7 @@ class TimezoneValidatorTest extends ConstraintValidatorTestCase
|
||||
|
||||
public function getInvalidTimezones(): iterable
|
||||
{
|
||||
yield ['Buenos_Aires/America'];
|
||||
yield ['Buenos_Aires/Argentina/America'];
|
||||
yield ['Mayotte/Indian'];
|
||||
yield ['foobar'];
|
||||
@ -149,11 +170,15 @@ class TimezoneValidatorTest extends ConstraintValidatorTestCase
|
||||
|
||||
public function getInvalidGroupedTimezones(): iterable
|
||||
{
|
||||
yield ['America/Buenos_Aires', \DateTimeZone::ASIA | \DateTimeZone::AUSTRALIA]; // icu
|
||||
yield ['America/Argentina/Buenos_Aires', \DateTimeZone::EUROPE]; // php
|
||||
yield ['Antarctica/McMurdo', \DateTimeZone::AMERICA];
|
||||
yield ['America/Barbados', \DateTimeZone::ANTARCTICA];
|
||||
yield ['Europe/Kiev', \DateTimeZone::ARCTIC];
|
||||
yield ['Asia/Ho_Chi_Minh', \DateTimeZone::INDIAN];
|
||||
yield ['Asia/Ho_Chi_Minh', \DateTimeZone::INDIAN | \DateTimeZone::ANTARCTICA];
|
||||
yield ['UTC', \DateTimeZone::EUROPE];
|
||||
yield ['Etc/UTC', \DateTimeZone::EUROPE];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -173,6 +198,8 @@ class TimezoneValidatorTest extends ConstraintValidatorTestCase
|
||||
|
||||
public function getValidGroupedTimezonesByCountry(): iterable
|
||||
{
|
||||
yield ['America/Buenos_Aires', 'AR']; // icu
|
||||
yield ['America/Argentina/Buenos_Aires', 'AR']; // php
|
||||
yield ['America/Argentina/Cordoba', 'AR'];
|
||||
yield ['America/Barbados', 'BB'];
|
||||
yield ['Africa/Cairo', 'EG'];
|
||||
@ -215,6 +242,7 @@ class TimezoneValidatorTest extends ConstraintValidatorTestCase
|
||||
yield ['America/Argentina/Cordoba', 'FR'];
|
||||
yield ['America/Barbados', 'PT'];
|
||||
yield ['Europe/Bern', 'FR'];
|
||||
yield ['Etc/UTC', 'NL'];
|
||||
yield ['Europe/Amsterdam', 'AC']; // "AC" has no timezones, but is a valid country code
|
||||
}
|
||||
|
||||
@ -267,8 +295,6 @@ class TimezoneValidatorTest extends ConstraintValidatorTestCase
|
||||
|
||||
public function getDeprecatedTimezones(): iterable
|
||||
{
|
||||
yield ['America/Buenos_Aires'];
|
||||
yield ['America/Montreal'];
|
||||
yield ['Australia/ACT'];
|
||||
yield ['Australia/LHI'];
|
||||
yield ['Australia/Queensland'];
|
||||
@ -277,13 +303,29 @@ class TimezoneValidatorTest extends ConstraintValidatorTestCase
|
||||
yield ['Canada/Mountain'];
|
||||
yield ['Canada/Pacific'];
|
||||
yield ['CET'];
|
||||
yield ['CST6CDT'];
|
||||
yield ['Etc/GMT'];
|
||||
yield ['GMT'];
|
||||
yield ['Etc/Greenwich'];
|
||||
yield ['Etc/UCT'];
|
||||
yield ['Etc/Universal'];
|
||||
yield ['Etc/UTC'];
|
||||
yield ['Etc/Zulu'];
|
||||
yield ['US/Pacific'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires extension intl
|
||||
*/
|
||||
public function testIntlCompatibility()
|
||||
{
|
||||
$constraint = new Timezone([
|
||||
'message' => 'myMessage',
|
||||
'intlCompatible' => true,
|
||||
]);
|
||||
|
||||
$this->validator->validate('Europe/Saratov', $constraint);
|
||||
|
||||
$this->buildViolation('myMessage')
|
||||
->setParameter('{{ value }}', '"Europe/Saratov"')
|
||||
->setCode(Timezone::TIMEZONE_IDENTIFIER_INTL_ERROR)
|
||||
->assertRaised();
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user