Skip to content

[Validator] Allow intl timezones #31292

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/Symfony/Component/Validator/Constraints/Timezone.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
];

/**
Expand All @@ -51,5 +54,8 @@ public function __construct(array $options = null)
} 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.');
}
}
}
52 changes: 45 additions & 7 deletions src/Symfony/Component/Validator/Constraints/TimezoneValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -43,14 +45,28 @@ public function validate($value, Constraint $constraint)

$value = (string) $value;

// @see: https://bugs.php.net/bug.php?id=75928
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 ($constraint->countryCode) {
$timezoneIds = @\DateTimeZone::listIdentifiers($constraint->zone, $constraint->countryCode) ?: [];
$phpTimezoneIds = @\DateTimeZone::listIdentifiers($constraint->zone, $constraint->countryCode) ?: [];
try {
$intlTimezoneIds = Timezones::forCountryCode($constraint->countryCode);
} catch (MissingResourceException $e) {
$intlTimezoneIds = [];
}
} else {
$timezoneIds = \DateTimeZone::listIdentifiers($constraint->zone);
$phpTimezoneIds = \DateTimeZone::listIdentifiers($constraint->zone);
$intlTimezoneIds = self::getIntlTimezones($constraint->zone);
}

if (\in_array($value, $timezoneIds, true)) {
if (\in_array($value, $phpTimezoneIds, true) || \in_array($value, $intlTimezoneIds, true)) {
return;
}

Expand All @@ -63,9 +79,9 @@ public function validate($value, Constraint $constraint)
}

$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $this->formatValue($value))
->setCode($code)
->addViolation();
->setParameter('{{ value }}', $this->formatValue($value))
->setCode($code)
->addViolation();
}

/**
Expand All @@ -89,4 +105,26 @@ protected function formatValue($value, $format = 0)

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) : [];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,26 @@ public function testValidTimezones(string $timezone)

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'];
Expand All @@ -71,7 +90,6 @@ public function getValidTimezones(): iterable
yield ['Europe/Copenhagen'];
yield ['Europe/Paris'];
yield ['Pacific/Noumea'];
yield ['UTC'];
}

/**
Expand All @@ -90,6 +108,8 @@ public function testValidGroupedTimezones(string $timezone, int $zone)

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];
Expand Down Expand Up @@ -124,6 +144,7 @@ public function testInvalidTimezoneWithoutZone(string $timezone)

public function getInvalidTimezones(): iterable
{
yield ['Buenos_Aires/America'];
yield ['Buenos_Aires/Argentina/America'];
yield ['Mayotte/Indian'];
yield ['foobar'];
Expand All @@ -149,11 +170,15 @@ public function testInvalidGroupedTimezones(string $timezone, int $zone)

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];
}

/**
Expand All @@ -173,6 +198,8 @@ public function testValidGroupedTimezonesByCountry(string $timezone, string $cou

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'];
Expand Down Expand Up @@ -215,6 +242,7 @@ public function getInvalidGroupedTimezonesByCountry(): iterable
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
}

Expand Down Expand Up @@ -267,8 +295,6 @@ public function testDeprecatedTimezonesAreInvalidWithoutBC(string $timezone)

public function getDeprecatedTimezones(): iterable
{
yield ['America/Buenos_Aires'];
yield ['America/Montreal'];
yield ['Australia/ACT'];
yield ['Australia/LHI'];
yield ['Australia/Queensland'];
Expand All @@ -277,13 +303,29 @@ public function getDeprecatedTimezones(): iterable
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();
}
}