8000 [Validator] Allow intl timezones · symfony/symfony@7294b59 · GitHub
[go: up one dir, main page]

Skip to content

Commit 7294b59

Browse files
committed
[Validator] Allow intl timezones
1 parent 1c110fa commit 7294b59

File tree

3 files changed

+99
-13
lines changed

3 files changed

+99
-13
lines changed

src/Symfony/Component/Validator/Constraints/Timezone.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,18 @@ class Timezone extends Constraint
2626
public const TIMEZONE_IDENTIFIER_ERROR = '5ce113e6-5e64-4ea2-90fe-d2233956db13';
2727
public const TIMEZONE_IDENTIFIER_IN_ZONE_ERROR = 'b57767b1-36c0-40ac-a3d7-629420c775b8';
2828
public const TIMEZONE_IDENTIFIER_IN_COUNTRY_ERROR = 'c4a22222-dc92-4fc0-abb0-d95b268c7d0b';
29+
public const TIMEZONE_IDENTIFIER_INTL_ERROR = '45863c26-88dc-41ba-bf53-c73bd1f7e90d';
2930

3031
public $zone = \DateTimeZone::ALL;
3132
public $countryCode;
33+
public $intlCompatible = 10000 false;
3234
public $message = 'This value is not a valid timezone.';
3335

3436
protected static $errorNames = [
3537
self::TIMEZONE_IDENTIFIER_ERROR => 'TIMEZONE_IDENTIFIER_ERROR',
3638
self::TIMEZONE_IDENTIFIER_IN_ZONE_ERROR => 'TIMEZONE_IDENTIFIER_IN_ZONE_ERROR',
3739
self::TIMEZONE_IDENTIFIER_IN_COUNTRY_ERROR => 'TIMEZONE_IDENTIFIER_IN_COUNTRY_ERROR',
40+
self::TIMEZONE_IDENTIFIER_INTL_ERROR => 'TIMEZONE_IDENTIFIER_INTL_ERROR',
3841
];
3942

4043
/**
@@ -51,5 +54,8 @@ public function __construct(array $options = null)
5154
} elseif (\DateTimeZone::PER_COUNTRY !== (\DateTimeZone::PER_COUNTRY & $this->zone)) {
5255
throw new ConstraintDefinitionException('The option "countryCode" can only be used when the "zone" option is configured with "\DateTimeZone::PER_COUNTRY".');
5356
}
57+
if ($this->intlCompatible && !class_exists(\IntlTimeZone::class)) {
58+
throw new ConstraintDefinitionException('The option "intlCompatible" can only be used when the PHP intl extension is available.');
59+
}
5460
}
5561
}

src/Symfony/Component/Validator/Constraints/TimezoneValidator.php

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\Validator\Constraints;
1313

14+
use Symfony\Component\Intl\Exception\MissingResourceException;
15+
use Symfony\Component\Intl\Timezones;
1416
use Symfony\Component\Validator\Constraint;
1517
use Symfony\Component\Validator\ConstraintValidator;
1618
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
@@ -43,14 +45,28 @@ public function validate($value, Constraint $constraint)
4345

4446
$value = (string) $value;
4547

46-
// @see: https://bugs.php.net/bug.php?id=75928
48+
if ($constraint->intlCompatible && 'Etc/Unknown' === \IntlTimeZone::createTimeZone($value)->getID()) {
49+
$this->context->buildViolation($constraint->message)
50+
->setParameter('{{ value }}', $this->formatValue($value))
51+
->setCode(Timezone::TIMEZONE_IDENTIFIER_INTL_ERROR)
52+
->addViolation();
53+
54+
return;
55+
}
56+
4757
if ($constraint->countryCode) {
48-
$timezoneIds = @\DateTimeZone::listIdentifiers($constraint->zone, $constraint->countryCode) ?: [];
58+
$phpTimezoneIds = @\DateTimeZone::listIdentifiers($constraint->zone, $constraint->countryCode) ?: [];
59+
try {
60+
$intlTimezoneIds = Timezones::forCountryCode($constraint->countryCode);
61+
} catch (MissingResourceException $e) {
62+
$intlTimezoneIds = [];
63+
}
4964
} else {
50-
$timezoneIds = \DateTimeZone::listIdentifiers($constraint->zone);
65+
$phpTimezoneIds = \DateTimeZone::listIdentifiers($constraint->zone);
66+
$intlTimezoneIds = self::getIntlTimezones($constraint->zone);
5167
}
5268

53-
if (\in_array($value, $timezoneIds, true)) {
69+
if (\in_array($value, $phpTimezoneIds, true) || \in_array($value, $intlTimezoneIds, true)) {
5470
return;
5571
}
5672

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

6581
$this->context->buildViolation($constraint->message)
66-
->setParameter('{{ value }}', $this->formatValue($value))
67-
->setCode($code)
68-
->addViolation();
82+
->setParameter('{{ value }}', $this->formatValue($value))
83+
->setCode($code)
84+
->addViolation();
6985
}
7086

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

90106
return array_search($value, (new \ReflectionClass(\DateTimeZone::class))->getConstants(), true) ?: $value;
91107
}
108+
109+
private static function getIntlTimezones(int $zone): array
110+
{
111+
$timezones = Timezones::getIds();
112+
113+
if (\DateTimeZone::ALL === (\DateTimeZone::ALL & $zone)) {
114+
return $timezones;
115+
}
116+
117+
$filtered = [];
118+
foreach ((new \ReflectionClass(\DateTimeZone::class))->getConstants() as $const => $flag) {
119+
if ($flag !== ($flag & $zone)) {
120+
continue;
121+
}
122+
123+
$filtered[] = array_filter($timezones, static function ($id) use ($const) {
124+
return 0 === stripos($id, $const.'/');
125+
});
126+
}
127+
128+
return $filtered ? array_merge(...$filtered) : [];
129+
}
92130
}

src/Symfony/Component/Validator/Tests/Constraints/TimezoneValidatorTest.php

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,26 @@ public function testValidTimezones(string $timezone)
6060

6161
public function getValidTimezones(): iterable
6262
{
63+
// ICU standard (alias/BC in PHP)
64+
yield ['Etc/UTC'];
65+
yield ['Etc/GMT'];
66+
yield ['America/Buenos_Aires'];
67+
68+
// PHP standard (alias in ICU)
69+
yield ['UTC'];
6370
yield ['America/Argentina/Buenos_Aires'];
71+
72+
// not deprecated in ICU
73+
yield ['CST6CDT'];
74+
yield ['EST5EDT'];
75+
yield ['MST7MDT'];
76+
yield ['PST8PDT'];
77+
yield ['America/Montreal'];
78+
79+
// expired in ICU
80+
yield ['Europe/Saratov'];
81+
82+
// standard
6483
yield ['America/Barbados'];
6584
yield ['America/Toronto'];
6685
yield ['Antarctica/Syowa'];
@@ -71,7 +90,6 @@ public function getValidTimezones(): iterable
7190
yield ['Europe/Copenhagen'];
7291
yield ['Europe/Paris'];
7392
yield ['Pacific/Noumea'];
74-
yield ['UTC'];
7593
}
7694

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

91109
public function getValidGroupedTimezones(): iterable
92110
{
111+
yield ['America/Buenos_Aires', \DateTimeZone::AMERICA | \DateTimeZone::AUSTRALIA]; // icu
112+
yield ['America/Argentina/Buenos_Aires', \DateTimeZone::AMERICA]; // php
93113
yield ['America/Argentina/Cordoba', \DateTimeZone::AMERICA];
94114
yield ['America/Barbados', \DateTimeZone::AMERICA];
95115
yield ['Africa/Cairo', \DateTimeZone::AFRICA];
@@ -124,6 +144,7 @@ public function testInvalidTimezoneWithoutZone(string $timezone)
124144

125145
public function getInvalidTimezones(): iterable
126146
{
147+
yield ['Buenos_Aires/America'];
127148
yield ['Buenos_Aires/Argentina/America'];
128149
yield ['Mayotte/Indian'];
129150
yield ['foobar'];
@@ -149,11 +170,15 @@ public function testInvalidGroupedTimezones(string $timezone, int $zone)
149170

150171
public function getInvalidGroupedTimezones(): iterable
151172
{
173+
yield ['America/Buenos_Aires', \DateTimeZone::ASIA | \DateTimeZone::AUSTRALIA]; // icu
174+
yield ['America/Argentina/Buenos_Aires', \DateTimeZone::EUROPE]; // php
152175
yield ['Antarctica/McMurdo', \DateTimeZone::AMERICA];
153176
yield ['America/Barbados', \DateTimeZone::ANTARCTICA];
154177
yield ['Europe/Kiev', \DateTimeZone::ARCTIC];
155178
yield ['Asia/Ho_Chi_Minh', \DateTimeZone::INDIAN];
156179
yield ['Asia/Ho_Chi_Minh', \DateTimeZone::INDIAN | \DateTimeZone::ANTARCTICA];
180+
yield ['UTC', \DateTimeZone::EUROPE];
181+
yield ['Etc/UTC', \DateTimeZone::EUROPE];
157182
}
158183

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

174199
public function getValidGroupedTimezonesByCountry(): iterable
175200
{
201+
yield ['America/Buenos_Aires', 'AR']; // icu
202+
yield ['America/Argentina/Buenos_Aires', 'AR']; // php
176203
yield ['America/Argentina/Cordoba', 'AR'];
177204
yield ['America/Barbados', 'BB'];
178205
yield ['Africa/Cairo', 'EG'];
@@ -215,6 +242,7 @@ public function getInvalidGroupedTimezonesByCountry(): iterable
215242
yield ['America/Argentina/Cordoba', 'FR'];
216243
yield ['America/Barbados', 'PT'];
217244
yield ['Europe/Bern', 'FR'];
245+
yield ['Etc/UTC', 'NL'];
218246
yield ['Europe/Amsterdam', 'AC']; // "AC" has no timezones, but is a valid country code
219247
}
220248

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

268296
public function getDeprecatedTimezones(): iterable
269297
{
270-
yield ['America/Buenos_Aires'];
271-
yield ['America/Montreal'];
272298
yield ['Australia/ACT'];
273299
yield ['Australia/LHI'];
274300
yield ['Australia/Queensland'];
@@ -277,13 +303,29 @@ public function getDeprecatedTimezones(): iterable
277303
yield ['Canada/Mountain'];
278304
yield ['Canada/Pacific'];
279305
yield ['CET'];
280-
yield ['CST6CDT'];
281-
yield ['Etc/GMT'];
306+
yield ['GMT'];
282307
yield ['Etc/Greenwich'];
283308
yield ['Etc/UCT'];
284309
yield ['Etc/Universal'];
285-
yield ['Etc/UTC'];
286310
yield ['Etc/Zulu'];
287311
yield ['US/Pacific'];
288312
}
313+
314+
/**
315+
* @requires extension intl
316+
*/
317+
public function testIntlCompatibility()
318+
{
319+
$constraint = new Timezone([
320+
'message' => 'myMessage',
321+
'intlCompatible' => true,
322+
]);
323+
324+
$this->validator->validate('Europe/Saratov', $constraint);
325+
326+
$this->buildViolation('myMessage')
327+
->setParameter('{{ value }}', '"Europe/Saratov"')
328+
->setCode(Timezone::TIMEZONE_IDENTIFIER_INTL_ERROR)
329+
->assertRaised();
330+
}
289331
}

0 commit comments

Comments
 (0)
0