Skip to content

Commit

Permalink
bug #31354 [Intl][Validator] Handle alias locales/timezones (ro0NL)
Browse files Browse the repository at this point in the history
This PR was squashed before being merged into the 4.3-dev branch (closes #31354).

Discussion
----------

[Intl][Validator] Handle alias locales/timezones

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no     <!-- see https://symfony.com/bc -->
| Deprecations? | no
| Tests pass?   | yes (including intl-data group)
| Fixed tickets | #31022
| License       | MIT
| Doc PR        | symfony/symfony-docs#... <!-- required for new features -->

both timezones and locales have aliases (either thru deprecation/migration/etc.)

for locales we compile a mapping, for timezones we dont. yet we can benefit partial alias support thru DateTimeZone, which knows about most timezone IDs already.

both the timezone + locale validator already support aliases. Connsequently, we should support aliases in  `Timezones::exists()`  + `Locales::exists()` as well IMHO.

so far so good; the catch is; with this PR `Locales::getName()` supports aliases, whereas `Timezones::getName()` doesnt. I think it's reasonable for now, until we compile the timezone mapping so we can widen the timezone ID conversion here.

Commits
-------

0a9be0d [Intl][Validator] Handle alias locales/timezones
  • Loading branch information
fabpot committed May 6, 2019
2 parents 26e1d89 + 0a9be0d commit fec95e0
Show file tree
Hide file tree
Showing 7 changed files with 74 additions and 24 deletions.
12 changes: 10 additions & 2 deletions src/Symfony/Component/Intl/Locales.php
Expand Up @@ -44,7 +44,7 @@ public static function exists(string $locale): bool

return true;
} catch (MissingResourceException $e) {
return false;
return \in_array($locale, self::getAliases(), true);
}
}

Expand All @@ -53,7 +53,15 @@ public static function exists(string $locale): bool
*/
public static function getName(string $locale, string $displayLocale = null): string
{
return self::readEntry(['Names', $locale], $displayLocale);
try {
return self::readEntry(['Names', $locale], $displayLocale);
} catch (MissingResourceException $e) {
if (false === $aliased = array_search($locale, self::getAliases(), true)) {
throw $e;
}

return self::readEntry(['Names', $aliased], $displayLocale);
}
}

/**
Expand Down
7 changes: 7 additions & 0 deletions src/Symfony/Component/Intl/Tests/LocalesTest.php
Expand Up @@ -92,9 +92,16 @@ public function testGetNameWithInvalidLocale()
Locales::getName('foo');
}

public function testGetNameWithAliasLocale()
{
$this->assertSame(Locales::getName('tl_PH'), Locales::getName('fil_PH'));
}

public function testExists()
{
$this->assertTrue(Locales::exists('nl_NL'));
$this->assertTrue(Locales::exists('tl_PH'));
$this->assertTrue(Locales::exists('fil_PH')); // alias for "tl_PH"
$this->assertFalse(Locales::exists('zxx_ZZ'));
}
}
14 changes: 13 additions & 1 deletion src/Symfony/Component/Intl/Tests/TimezonesTest.php
Expand Up @@ -530,14 +530,23 @@ public function testGetNameDefaultLocale()
/**
* @expectedException \Symfony\Component\Intl\Exception\MissingResourceException
*/
public function testGetNameWithInvalidTimezoneId()
public function testGetNameWithInvalidTimezone()
{
Timezones::getName('foo');
}

/**
* @expectedException \Symfony\Component\Intl\Exception\MissingResourceException
*/
public function testGetNameWithAliasTimezone()
{
Timezones::getName('US/Pacific'); // alias in icu (not compiled), name unavailable in php
}

public function testExists()
{
$this->assertTrue(Timezones::exists('Europe/Amsterdam'));
$this->assertTrue(Timezones::exists('US/Pacific')); // alias in icu (not compiled), identifier available in php
$this->assertFalse(Timezones::exists('Etc/Unknown'));
}

Expand All @@ -547,6 +556,9 @@ public function testGetRawOffset()
$this->assertSame(0, Timezones::getRawOffset('Etc/UTC'));
$this->assertSame(-10800, Timezones::getRawOffset('America/Buenos_Aires'));
$this->assertSame(20700, Timezones::getRawOffset('Asia/Katmandu'));

// ensure we support identifiers available in php (not compiled from icu)
Timezones::getRawOffset('US/Pacific');
}

/**
Expand Down
14 changes: 10 additions & 4 deletions src/Symfony/Component/Intl/Timezones.php
Expand Up @@ -36,12 +36,18 @@ public static function exists(string $timezone): bool

return true;
} catch (MissingResourceException $e) {
return false;
try {
new \DateTimeZone($timezone);

return true;
} catch (\Exception $e) {
return false;
}
}
}

/**
* @throws MissingResourceException if the timezone identifier does not exists
* @throws MissingResourceException if the timezone identifier does not exist or is an alias
*/
public static function getName(string $timezone, string $displayLocale = null): string
{
Expand All @@ -57,7 +63,7 @@ public static function getNames(string $displayLocale = null): array
}

/**
* @throws \Exception if the timezone identifier does not exists
* @throws \Exception if the timezone identifier does not exist
* @throws RuntimeException if there's no timezone DST transition information available
*/
public static function getRawOffset(string $timezone, int $timestamp = null): int
Expand Down Expand Up @@ -92,7 +98,7 @@ public static function getCountryCode(string $timezone): string
}

/**
* @throws MissingResourceException if the country code does not exists
* @throws MissingResourceException if the country code does not exist
*/
public static function forCountryCode(string $country): array
{
Expand Down
Expand Up @@ -11,10 +11,10 @@

namespace Symfony\Component\Validator\Constraints;

use Symfony\Component\Intl\Intl;
use Symfony\Component\Intl\Locales;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\LogicException;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;

Expand Down Expand Up @@ -42,13 +42,17 @@ public function validate($value, Constraint $constraint)
throw new UnexpectedValueException($value, 'string');
}

if (!class_exists(Locales::class)) {
throw new LogicException('The "symfony/intl" component is required to use the Locale constraint.');
}

$inputValue = (string) $value;
$value = $inputValue;
if ($constraint->canonicalize) {
$value = \Locale::canonicalize($value);
}

if (!Locales::exists($value) && !\in_array($value, Locales::getAliases(), true)) {
if (!Locales::exists($value)) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $this->formatValue($inputValue))
->setCode(Locale::NO_SUCH_LOCALE_ERROR)
Expand Down
40 changes: 26 additions & 14 deletions src/Symfony/Component/Validator/Constraints/TimezoneValidator.php
Expand Up @@ -54,19 +54,10 @@ public function validate($value, Constraint $constraint)
return;
}

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)) {
if (
\in_array($value, self::getPhpTimezones($constraint->zone, $constraint->countryCode), true) ||
\in_array($value, self::getIntlTimezones($constraint->zone, $constraint->countryCode), true)
) {
return;
}

Expand Down Expand Up @@ -106,8 +97,29 @@ protected function formatValue($value, $format = 0)
return array_search($value, (new \ReflectionClass(\DateTimeZone::class))->getConstants(), true) ?: $value;
}

private static function getIntlTimezones(int $zone): array
private static function getPhpTimezones(int $zone, string $countryCode = null): array
{
if (null !== $countryCode) {
return @\DateTimeZone::listIdentifiers($zone, $countryCode) ?: [];
}

return \DateTimeZone::listIdentifiers($zone);
}

private static function getIntlTimezones(int $zone, string $countryCode = null): array
{
if (!class_exists(Timezones::class)) {
return [];
}

if (null !== $countryCode) {
try {
return Timezones::forCountryCode($countryCode);
} catch (MissingResourceException $e) {
return [];
}
}

$timezones = Timezones::getIds();

if (\DateTimeZone::ALL === (\DateTimeZone::ALL & $zone)) {
Expand Down
Expand Up @@ -111,7 +111,8 @@ public function getValidLocales()
['pt', ['canonicalize' => true]],
['pt_PT', ['canonicalize' => true]],
['zh_Hans', ['canonicalize' => true]],
['fil_PH', ['canonicalize' => true]],
['tl_PH', ['canonicalize' => true]],
['fil_PH', ['canonicalize' => true]], // alias for "tl_PH"
];
}

Expand Down

0 comments on commit fec95e0

Please sign in to comment.