Skip to content

Commit ce0eac4

Browse files
committed
[Intl] Better DX when intl package is missing
1 parent e7be30e commit ce0eac4

File tree

2 files changed

+112
-104
lines changed

2 files changed

+112
-104
lines changed

Locale.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111

1212
namespace Symfony\Component\Intl;
1313

14+
if (!class_exists(\Locale::class)) {
15+
throw new \LogicException(sprintf('You cannot use the "%s\Locale" class as the "intl" extension is not installed. See https://php.net/intl.', __NAMESPACE__));
16+
}
17+
1418
/**
1519
* Provides access to locale-related data.
1620
*

Transliterator/EmojiTransliterator.php

Lines changed: 108 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -11,144 +11,148 @@
1111

1212
namespace Symfony\Component\Intl\Transliterator;
1313

14-
if (\PHP_VERSION_ID >= 80200) {
15-
final class EmojiTransliterator extends \Transliterator
16-
{
17-
use EmojiTransliteratorTrait;
18-
19-
private const QUICK_CHECK = "\xA9\xAE\xE2\xE3\xF0";
20-
private const REVERSEABLE_IDS = [
21-
'emoji-github' => 'github-emoji',
22-
'emoji-slack' => 'slack-emoji',
23-
'github-emoji' => 'emoji-github',
24-
'slack-emoji' => 'emoji-slack',
25-
];
26-
27-
public readonly string $id;
28-
}
14+
if (!class_exists(\Transliterator::class)) {
15+
throw new \LogicException(sprintf('You cannot use the "%s\EmojiTransliterator" class as the "intl" extension is not installed. See https://php.net/intl.', __NAMESPACE__));
2916
} else {
30-
final class EmojiTransliterator extends \Transliterator
17+
/**
18+
* @internal
19+
*/
20+
trait EmojiTransliteratorTrait
3121
{
32-
use EmojiTransliteratorTrait;
22+
private array $map;
23+
private \Transliterator $transliterator;
3324

34-
private const QUICK_CHECK = "\xA9\xAE\xE2\xE3\xF0";
35-
private const REVERSEABLE_IDS = [
36-
'emoji-github' => 'github-emoji',
37-
'emoji-slack' => 'slack-emoji',
38-
'github-emoji' => 'emoji-github',
39-
'slack-emoji' => 'emoji-slack',
40-
];
41-
}
42-
}
25+
public static function create(string $id, int $direction = self::FORWARD): self
26+
{
27+
$id = strtolower($id);
4328

44-
/**
45-
* @internal
46-
*/
47-
trait EmojiTransliteratorTrait
48-
{
49-
private array $map;
50-
private \Transliterator $transliterator;
29+
if (!isset(self::REVERSEABLE_IDS[$id]) && !str_starts_with($id, 'emoji-')) {
30+
$id = 'emoji-'.$id;
31+
}
5132

52-
public static function create(string $id, int $direction = self::FORWARD): self
53-
{
54-
$id = strtolower($id);
33+
if (self::REVERSE === $direction) {
34+
if (!isset(self::REVERSEABLE_IDS[$id])) {
35+
// Create a failing reverse-transliterator to populate intl_get_error_*()
36+
\Transliterator::createFromRules('A > B')->createInverse();
5537

56-
if (!isset(self::REVERSEABLE_IDS[$id]) && !str_starts_with($id, 'emoji-')) {
57-
$id = 'emoji-'.$id;
58-
}
38+
throw new \IntlException(intl_get_error_message(), intl_get_error_code());
39+
}
40+
$id = self::REVERSEABLE_IDS[$id];
41+
}
5942

60-
if (self::REVERSE === $direction) {
61-
if (!isset(self::REVERSEABLE_IDS[$id])) {
62-
// Create a failing reverse-transliterator to populate intl_get_error_*()
63-
\Transliterator::createFromRules('A > B')->createInverse();
43+
if (!preg_match('/^[a-z0-9@_\\.\\-]*$/', $id) || !is_file(\dirname(__DIR__)."/Resources/data/transliterator/emoji/{$id}.php")) {
44+
\Transliterator::create($id); // Populate intl_get_error_*()
6445

6546
throw new \IntlException(intl_get_error_message(), intl_get_error_code());
6647
}
67-
$id = self::REVERSEABLE_IDS[$id];
68-
}
6948

70-
if (!preg_match('/^[a-z0-9@_\\.\\-]*$/', $id) || !is_file(\dirname(__DIR__)."/Resources/data/transliterator/emoji/{$id}.php")) {
71-
\Transliterator::create($id); // Populate intl_get_error_*()
49+
static $maps;
7250

73-
throw new \IntlException(intl_get_error_message(), intl_get_error_code());
74-
}
51+
// Create an instance of \Transliterator with a custom id; that's the only way
52+
if (\PHP_VERSION_ID >= 80200) {
53+
static $newInstance;
54+
$instance = ($newInstance ??= (new \ReflectionClass(self::class))->newInstanceWithoutConstructor(...))();
55+
$instance->id = $id;
56+
} else {
57+
$instance = unserialize(sprintf('O:%d:"%s":1:{s:2:"id";s:%d:"%s";}', \strlen(self::class), self::class, \strlen($id), $id));
58+
}
7559

76-
static $maps;
60+
$instance->map = $maps[$id] ??= require \dirname(__DIR__)."/Resources/data/transliterator/emoji/{$id}.php";
7761

78-
// Create an instance of \Transliterator with a custom id; that's the only way
79-
if (\PHP_VERSION_ID >= 80200) {
80-
static $newInstance;
81-
$instance = ($newInstance ??= (new \ReflectionClass(self::class))->newInstanceWithoutConstructor(...))();
82-
$instance->id = $id;
83-
} else {
84-
$instance = unserialize(sprintf('O:%d:"%s":1:{s:2:"id";s:%d:"%s";}', \strlen(self::class), self::class, \strlen($id), $id));
62+
return $instance;
8563
}
8664

87-
$instance->map = $maps[$id] ??= require \dirname(__DIR__)."/Resources/data/transliterator/emoji/{$id}.php";
65+
public function createInverse(): self
66+
{
67+
return self::create($this->id, self::REVERSE);
68+
}
8869

89-
return $instance;
90-
}
70+
public function getErrorCode(): int|false
71+
{
72+
return $this->transliterator?->getErrorCode() ?? 0;
73+
}
9174

92-
public function createInverse(): self
93-
{
94-
return self::create($this->id, self::REVERSE);
95-
}
75+
public function getErrorMessage(): string|false
76+
{
77+
return $this->transliterator?->getErrorMessage() ?? false;
78+
}
9679

97-
public function getErrorCode(): int|false
98-
{
99-
return $this->transliterator?->getErrorCode() ?? 0;
100-
}
80+
public static function listIDs(): array
81+
{
82+
static $ids = [];
10183

102-
public function getErrorMessage(): string|false
103-
{
104-
return $this->transliterator?->getErrorMessage() ?? false;
105-
}
84+
if ($ids) {
85+
return $ids;
86+
}
10687

107-
public static function listIDs(): array
108-
{
109-
static $ids = [];
88+
foreach (scandir(\dirname(__DIR__).'/Resources/data/transliterator/emoji/') as $file) {
89+
if (str_ends_with($file, '.php')) {
90+
$ids[] = substr($file, 0, -4);
91+
}
92+
}
11093

111-
if ($ids) {
11294
return $ids;
11395
}
11496

115-
foreach (scandir(\dirname(__DIR__).'/Resources/data/transliterator/emoji/') as $file) {
116-
if (str_ends_with($file, '.php')) {
117-
$ids[] = substr($file, 0, -4);
97+
public function transliterate(string $string, int $start = 0, int $end = -1): string|false
98+
{
99+
$quickCheck = ':' === array_key_first($this->map)[0] ? ':' : self::QUICK_CHECK;
100+
101+
if (0 === $start && -1 === $end && preg_match('//u', $string)) {
102+
return \strlen($string) === strcspn($string, $quickCheck) ? $string : strtr($string, $this->map);
118103
}
119-
}
120104

121-
return $ids;
122-
}
105+
// Here we rely on intl to validate the $string, $start and $end arguments
106+
// and to slice the string. Slicing is done by replacing the part if $string
107+
// between $start and $end by a unique cookie that can be reliably used to
108+
// identify which part of $string should be transliterated.
123109

124-
public function transliterate(string $string, int $start = 0, int $end = -1): string|false
125-
{
126-
$quickCheck = ':' === array_key_first($this->map)[0] ? ':' : self::QUICK_CHECK;
110+
static $cookie;
111+
static $transliterator;
127112

128-
if (0 === $start && -1 === $end && preg_match('//u', $string)) {
129-
return \strlen($string) === strcspn($string, $quickCheck) ? $string : strtr($string, $this->map);
130-
}
113+
$cookie ??= md5(random_bytes(8));
114+
$this->transliterator ??= clone $transliterator ??= \Transliterator::createFromRules('[:any:]* > '.$cookie);
131115

132-
// Here we rely on intl to validate the $string, $start and $end arguments
133-
// and to slice the string. Slicing is done by replacing the part if $string
134-
// between $start and $end by a unique cookie that can be reliably used to
135-
// identify which part of $string should be transliterated.
136-
137-
static $cookie;
138-
static $transliterator;
116+
if (false === $result = $this->transliterator->transliterate($string, $start, $end)) {
117+
return false;
118+
}
139119

140-
$cookie ??= md5(random_bytes(8));
141-
$this->transliterator ??= clone $transliterator ??= \Transliterator::createFromRules('[:any:]* > '.$cookie);
120+
$parts = explode($cookie, $result);
121+
$start = \strlen($parts[0]);
122+
$length = -\strlen($parts[1]) ?: null;
123+
$string = substr($string, $start, $length);
142124

143-
if (false === $result = $this->transliterator->transliterate($string, $start, $end)) {
144-
return false;
125+
return $parts[0].(\strlen($string) === strcspn($string, $quickCheck) ? $string : strtr($string, $this->map)).$parts[1];
145126
}
127+
}
128+
}
146129

147-
$parts = explode($cookie, $result);
148-
$start = \strlen($parts[0]);
149-
$length = -\strlen($parts[1]) ?: null;
150-
$string = substr($string, $start, $length);
130+
if (\PHP_VERSION_ID >= 80200) {
131+
final class EmojiTransliterator extends \Transliterator
132+
{
133+
use EmojiTransliteratorTrait;
151134

152-
return $parts[0].(\strlen($string) === strcspn($string, $quickCheck) ? $string : strtr($string, $this->map)).$parts[1];
135+
private const QUICK_CHECK = "\xA9\xAE\xE2\xE3\xF0";
136+
private const REVERSEABLE_IDS = [
137+
'emoji-github' => 'github-emoji',
138+
'emoji-slack' => 'slack-emoji',
139+
'github-emoji' => 'emoji-github',
140+
'slack-emoji' => 'emoji-slack',
141+
];
142+
143+
public readonly string $id;
144+
}
145+
} else {
146+
final class EmojiTransliterator extends \Transliterator
147+
{
148+
use EmojiTransliteratorTrait;
149+
150+
private const QUICK_CHECK = "\xA9\xAE\xE2\xE3\xF0";
151+
private const REVERSEABLE_IDS = [
152+
'emoji-github' => 'github-emoji',
153+
'emoji-slack' => 'slack-emoji',
154+
'github-emoji' => 'emoji-github',
155+
'slack-emoji' => 'emoji-slack',
156+
];
153157
}
154158
}

0 commit comments

Comments
 (0)