
Надеюсь, этот материал вам никогда не понадобится. А если понадобится, то вы уже преисполнились проблемой и полны решимости ее исправить!
Вкратце, новые таймзоны не распознаются старыми библиотеками, а это чревато ошибками и неожиданностями.
Проблема поширше
В целом, это касается не только таймзон, но и других интернациональных особенностей: локалей, форматов чисел, валют, и т.д.
Причем не только в старых версиях библиотек, но и новых, просто не в такой степени.
Поэтому, если вы хотите соответствовать последним веяниям разных народов и стран (либо просто не можете контролировать список значений, поступающих с клиентской стороны), то нужно быть начеку.
Примеры новых таймзон (https://launchpad.net/debian/+source/tzdata/+changelog):
2022: Europe/Kyiv
2024: America/Nuuk
2025: America/Coyhaique
Код, приводящий к ошибкам, либо неопределенным значениям (еще хуже):
new DateTimeZone('America/Nuuk');
# Uncaught Exception: DateTimeZone::__construct(): Unknown or bad timezone (America/Nuuk)
new IntlDateFormatter('en', 0, 0, 'America/Coyhaique');
# Uncaught IntlException: datefmt_create: No such time zone: 'America/Coyhaique': U_ILLEGAL_ARGUMENT_ERROR
var_dump(IntlTimeZone::createTimeZone("Europe/Kyiv"));
# object(IntlTimeZone)#1 (4) {
# ["valid"]=> bool(true)
# ["id"]=> string(11) "Etc/Unknown"
# ["rawOffset"]=> int(0)
# ["currentOffset"]=> int(0)
# }
Чтобы исправить работу DateTimeZone, достаточно обновить tzdata в самой ОС:
Ubuntu: apt update tzdata
CentOS: yum update tzdata
А вот если проблема связана с IntlTimeZone (а именно так и будет), тогда.. Собственно, вся остальная часть статьи посвящена именно этому.

Почему с intl все сложно
Расширение intl опирается на библиотеку ICU, в которую зашивается tzdata на этапе сборки.
Те, у кого свежий стек технологий (что вы здесь забыли?) могут обновить расширение стандартным способом. Но и тут есть важный нюанс: имеет значение не дата выпуска intl, а версия ICU, с которой оно было собрано.
Например, для CentOS есть пакет php 7.3 intl из remi репозитория, собранный в 2024 году (вот). Но ICU в нем 69.1 (tzdata 2021a). Потому что пакет только патчился от уязвимостей, без обновления зависимостей.
И даже если у вас php 8.5 (ого!), внутри может оказаться не самая последняя ICU. Например, ICU 77, а последняя на момент выхода статьи - 78 (специально дождался, ога).
Узнать, какой у вас ICU можно командой
php -i | grep ICU
А посмотреть, с какой версией ICU сегодня не стыдно выходить на мировую арену, можно здесь: https://unicode-org.github.io/icu/download/
Офтоп
Посмотрите, какой красивый у них сайт
https://icu.unicode.org/download/75

Но, начиная с версии ICU 76, они пересмотрели свои внутренние процессы и переехали на github.io.
https://unicode-org.github.io/icu/download/78.html

В дальнейшем, я попытаюсь подать в черном свете еще пару "рефакторингов", которые обнаружу за ними (без негатива).
Какие-то обходные пути в духе перехвата вызовов к intl через boostrap / autoload / classmap и прочие извращения (типа runkit7) - не работают.
Нужно пересобирать intl.
Путь для Ubuntu
Для тех, у кого Ubuntu, есть отличнейшая новость! Существует волшебный репозиторий, в котором собрано целое море различных intl + icu комбинаций!
Вот это сокровище - https://github.com/shivammathur/icu-intl !!1
Например, у вас php 8.4, и нужно установить новенькую, хрустящую ICU 78.1
Подготавливаем ICU
Скачиваем архив 78.1 из раздела icu4c Builds и распаковываем в /opt/icu78_1. Должно получиться /opt/icu78_1/(bin, include, lib, sbin, share).
Можно использовать и другое расположение - главное, чтобы путь к lib был добавлен в конфигурацию ОС. Это делается так:
echo "/opt/icu78_1/lib" | tee /etc/ld.so.conf.d/icu78.conf
ldconfig
Подготавливаем Intl
Скачиваем php8.4-intl-78.1.so и проверяем, что расширение видит нужную библиотеку ICU
ldd /tmp/php8.4-intl-78.1.so | grep icu78_1
Если пусто, значит конфиг не подхватился (может забыли выполнить ldconfig ?).
Если все в порядке, результат будет таким:
libicuuc.so.78 => /opt/icu78_1/lib/libicuuc.so.78 (0x000070b59b8a8000)
libicuio.so.78 => /opt/icu78_1/lib/libicuio.so.78 (0x000070b59b893000)
libicui18n.so.78 => /opt/icu78_1/lib/libicui18n.so.78 (0x000070b59b380000)
libicudata.so.78 => /opt/icu78_1/lib/libicudata.so.78 (0x000070b598f60000)
Отлично, теперь смотрим, где лежат модули php
php -i | grep extension_dir
На всякий случай делаем резервную копию оригинального intl.so из этой директории, и заменяем на скаченный файл.
cp /path/to/your/modules/intl.so /path/to/your/modules/intl.so.bak
cp /tmp/php8.4-intl-78.1.so /path/to/your/modules/intl.so
Проверим что все прижилось:
php -i | grep ICU
Видим нужные номерки и благодарим Shivam за то, что он есть!
Путь для CentOS
В интернете есть разные скрипты по сборке intl с нужным ICU. Проблема в том, что из-за почтенного возраста многие из них ссылаются на ресурсы, которые уже недоступны. А главное - их писали настоящие гангстерсы, привыкшие стрелять от бедра!
Оригинальный intl.so в них сносится еще до начала сборки, без малейших сомнений в успехе последующих шагов. В некоторых вариантах следом летит и сам php.
Хорошая новость: чтобы собрать intl, ничего удалять не надо.
Отличные скрипты для сборки есть в уже упомянутом репозитории для Ubuntu. Однако с CentOS они у меня не прижились. Возможно, потому что часть зависимостей указывает на debian места. А возможно, потому что я не так уж хорошо во всем этом разбираюсь (и, вероятно, с этого и стоило начинать статью).
Как бы то ни было, вот ссылка на мой вариант скриптов - https://github.com/vodevel/php-intl-with-specific-icu.
Их там два:
build.sh - для сборки ICU и Intl
apply.sh - для быстрой замены intl.so и, в случае провала, еще более быстрого отката.
Оба скрипта нужно предварительно открыть и настроить.
А теперь - чуть подробней о том, что и как.

Этап 1. Установка необходимых инструментов
Саму сборку достаточно выполнить всего один раз (и желательно на локальном или тестовом сервере, а не проде). После этого можно перенести полученные файлы куда надо, главное, чтобы ОС и версия php совпадали.
Итак, понадобятся:
1. Инструмент для сборки php (может он у вас уже установлен?)
which phpize
which php-config
Если нет, то устанавливается командой
sudo yum install php-devel
2. Инструмент для сборки ICU (g++)
g++ --version
В CentOS скорее всего это будет 4.8.5, что не годится. Нужно добавить еще один, поновее. Именно добавить, а не обновить текущий, иначе будет бо-бо для всей ОС.
Посмотрим, какие версии доступны и установим самую последнюю (ну а чего стеснятся)
sudo yum install centos-release-scl -y
yum list available | grep devtoolset
sudo yum install devtoolset-11-gcc devtoolset-11-gcc-c++ devtoolset-11-binutils
О номерках g++
В примере установлен g++ 11 (максимально доступный для CentOS). Для компиляции ICU 74 и ниже подойдет g++ 5 (в нем C++ 11), а для ICU 75 и выше нужен минимум g++ 7 (в нем C++ 17). Но лучше брать g++ с запасом, т.к. в версиях, где поддержка только появляется, обычно есть ограничения и больше багов.
Подводные камни
Возможны проблемы из-за того, что оригинальные репозитории (mirrorlist.centos.org) больше не работают:
Loaded plugins: copr, fastestmirror
Loading mirror speeds from cached hostfile
Could not retrieve mirrorlist http://mirrorlist.centos.org?arch=x86\_64&release=7&repo=sclo-rh error was 14: HTTPS Error 403 - Forbidden
...
Cannot find a valid baseurl for repo: centos-sclo-rh/x86_64
Либо вообще не покажет ни одного варианта devtoolset, если репозитории CentOS-SCLo-scl.repo отсутствуют или отключены.
В обоих случаях нужно заменить ссылки на актуальные (на vault.centos.org) и включить репозитории.
В примере ниже показано, как полностью заменить конфигурацию репозиториев на рабочую. Но сначала обязательно сделайте бэкап текущих файлов, мало ли.
cp /etc/yum.repos.d/CentOS-SCLo-scl.repo /backup/path/CentOS-SCLo-scl.repo
cp /etc/yum.repos.d/CentOS-SCLo-scl-rh.repo /backup/path/CentOS-SCLo-scl-rh.repo
sudo bash -c 'cat > /etc/yum.repos.d/CentOS-SCLo-scl.repo << "EOF"
[centos-sclo-sclo]
name=CentOS-7 - SCLo sclo
baseurl=http://vault.centos.org/7.9.2009/sclo/x86_64/sclo/
enabled=1
gpgcheck=0
EOF'
sudo bash -c 'cat > /etc/yum.repos.d/CentOS-SCLo-scl-rh.repo << "EOF"
[centos-sclo-rh]
name=CentOS-7 - SCLo rh
baseurl=http://vault.centos.org/7.9.2009/sclo/x86_64/rh/
enabled=1
gpgcheck=0
EOF'
После этого обновляем кэш и проверяем еще раз:
sudo yum clean all
sudo yum makecache
yum list available | grep devtoolset
Этап 2. Сборка ICU
Исходный код ICU можно скачать здесь https://github.com/unicode-org/icu/releases. Нужен архив вида Source code.tar.gz.
Для тех, кто пишет универсальный скрипт
Обратите внимание: в новых версиях номер релиза записывается через точку (release-78.1.tar.gz), а в старых - через дефис (release-73-1.tar.gz). Очередной рефакторинг, спасибо (нет).
Распаковываем архив куда-нибудь, заходим внутрь icu4c/source и выполняем сборку.
Пример для 78.1
ICU_VERSION="78_1"
ICU_PREFIX="/opt/icu${ICU_VERSION}"
source /opt/rh/devtoolset-11/enable
cd /tmp/build78/release-release-78.1/icu4c/source
mkdir -p build && cd build
../runConfigureICU Linux --prefix="$ICU_PREFIX"
make -j4
make install
popd >/dev/null
Подводные камни
Если появится ошибка вида
...
../../common/uarrsort.cpp:246:36: error: request for member ‘getAlias’ in ‘xw’, which is of non-class type ‘int’
xw.getAlias(), xw.getAlias() + sizeInMaxAlignTs(itemSize)); ^ *** Failed compilation command follows: ---------------------------------------------------------- g++ -D_REENTRANT -DU_HAVE_ELF_H=1 -DU_HAVE_STRTOD_L=1 -DU_HAVE_XLOCALE_H=1 -I../../common -DDEFAULT_ICU_PLUGINS="/opt/icu74/lib/icu" -DU_ATTRIBUTE_DEPRECATED= -DU_COMMON_IMPLEMENTATION -O3 -W -Wall -pedantic -Wpointer-arith -Wwrite-strings -Wno-long-long -std=c++11 -c -DPIC -fPIC -o uarrsort.o ../../common/uarrsort.cpp --- ( rebuild with "make VERBOSE=1 all" to show all parameters ) --------make[1]: *** [uarrsort.o] Error 1
make[1]: *** Waiting for unfinished jobs....
make[1]: Leaving directory/tmp/intl_build/icu/source/build/common'make: *** [all-recursive] Error 2
Это значит, что сборка выполняется недостаточно новым gc++. Скорее всего, просто не подхватился новый компилятор.
Перед началом сборки обязательно переключитесь на нужный devtoolset — либо внутри скрипта (как в примере), либо прямо в текущей сессии терминала перед вызовом:
scl enable devtoolset-11 bash
# or
source /opt/rh/devtoolset-11/enable
Другая возможная ошибка возникает, если экспериментировать с разными версиями ICU, распаковывая их в одну и ту же директорию (а кто из нас так не делал?)
...
/opt/rh/devtoolset-11/root/usr/libexec/gcc/x86_64-redhat-linux/11/ld: ../../lib/libicuuc.so: undefined reference to icu_73::CharacterIterator::CharacterIterator(icu_73::CharacterIterator const&)' /opt/rh/devtoolset-11/root/usr/libexec/gcc/x86_64-redhat-linux/11/ld: ../../lib/libicuuc.so: undefined reference to icu_74::StringEnumeration::StringEnumeration()' collect2: error: ld returned 1 exit status
make[2]: *** [../../bin/makeconv] Error 1
...
Может показаться, что это уже через чур надуманный сценарий. Соглашусь. Но теперь вы точно знаете об этом всё, что знаю я)
Если все пройдет гладко, терминал огласит Build complete, а в условной директории /opt/icu78_1 будут лежать папка lib и другие.
Этап 3. Сборка php intl
Первым делом нужно скачать и распаковать исходники php. Конечно, нам не нужно всё, но модуль intl именно там, а не отдельно.
PHP_VERSION="7.3.33"
wget -O php-"$PHP_VERSION".tar.gz https://www.php.net/distributions/php-"$PHP_VERSION".tar.gz
tar xzf php-"$PHP_VERSION".tar.gz
cd php-"$PHP_VERSION"
А дальше.. Знаете, до этого момента статья носила вполне легальный характер. Стандартные шаги, официальные пакеты, чинно и благородно мы переходили речку вброд.
Но буря всё равно грядёт!
Дело в том, что мы можем запустить процесс сборки прямо здесь и сейчас! У нас есть для этого всё: и инструменты, и исходники, и нужный icu маринуется в ожидании. Однако, уверяю, попытка запуска обернется провалом:
P::CodePointBreakIterator::operator==(const icu_74::BreakIterator&) const'
42 | virtual UBool operator==(const BreakIterator& that) const;
| ^~~~~~~~
In file included from /tmp/intl_build/php-7.3.33/ext/intl/breakiterator/breakiterator_class.cpp:21:
/opt/icu74/include/unicode/brkiter.h:127:18: note: overridden function is 'virtual bool icu_74::BreakIterator::operator==(const icu_74::BreakIterator&) const'
127 | virtual bool operator==(const BreakIterator&) const = 0;
| ^~~~~~~~
make: *** [Makefile:334: breakiterator/breakiterator_class.lo] Error 1
make: *** Waiting for unfinished jobs....
Здесь нам сообщается о том, что что-то в новом коде ICU несовместимо с чем-то в старом коде php intl. Невероятно!

Но подождите, ведь автор репозитория со сборками для Убунты как-то же их делает?!
Если присмотреться к его скриптам, можно заметить, что перед сборкой выполняется патчинг intl.
Хм.. а мы точно хотим сборку с новым ICU? Ну, хотим-то хотим, но какой ценой? Нет ли там эксплойтов? Не получится ли результат слишком... ненадежным?
Ладно, давайте хотя бы посмотрим, что это за патчи. Их, к счастью, не так уж много, и по сути они делают всего два изменения:
1. Добавляется заботливая проверка на то, что используется подходящий компилятор:
+
+ AC_MSG_CHECKING([if intl requires -std=gnu++17])
+ AS_IF([test "$PKG_CONFIG icu-uc --atleast-version=74"],[
+ AC_MSG_RESULT([yes])
+ PHP_CXX_COMPILE_STDCXX(17, mandatory, PHP_INTL_STDCXX)
+ ],[
+ AC_MSG_RESULT([no])
+ PHP_CXX_COMPILE_STDCXX(11, mandatory, PHP_INTL_STDCXX)
+ ])
+
2. UBool заменяется на bool, чтобы сигнатуры совпали:
+#if U_ICU_VERSION_MAJOR_NUM >= 70
+bool CodePointBreakIterator::operator==(const BreakIterator& that) const
+#else
UBool CodePointBreakIterator::operator==(const BreakIterator& that) const
+#endif
...
+#if U_ICU_VERSION_MAJOR_NUM >= 70
+ virtual bool operator==(const BreakIterator& that) const;
+#else
virtual UBool operator==(const BreakIterator& that) const;
+#endif
Об особенностях чужеродного языка
Заметили, патчинг сделан не через "удалить одно, добавить другое", а именно обрамлением:
#if U_ICU_VERSION_MAJOR_NUM >= 70
virtual bool operator==(const BreakIterator& that) const;
#else
virtual UBool operator==(const BreakIterator& that) const;
#endif
Во прикольные возможности синтаксиса! (а бочки все равно на пэхапэ катят).
По поводу UBool - это был собственный тип ICU тянущийся с 90-х годов, когда солнце было желтее, а C++ зеленее. Начиная с ICU 70 от него решили отказаться в пользу стандартного bool (ох уж эти рефакторщики).
Если всё действительно так прозрачно и просто, то почему бы и да! Чтобы применить патчи, нужно запустить их из корня исходников php.
cd /path/to/root/of/downloaded/php/source
PHP_VERSION_SHORT="7.3"
PATCH_DIR="/path/to/repo/shivammathur/icu-intl"
PATCH_LIST="{$PATCH_DIR}/patches/intl/series-php${PHP_VERSION_SHORT}"
while IFS= read -r patch || [[ -n "$patch" ]]; do
echo "Applying patch: $patch"
if [[ "$patch" =~ $ICU ]]; then
patch -d "$PHP_SOURCE_DIR" -N -p1 -s < "${PATCH_DIR}/patches/intl/${patch}"
fi
done < "$PATCH_LIST"
На экране должны отобразиться сообщения о применении всех патчей из файл series-phpVERSION (ни один, ни два, а все). Иначе что-то не так (может не тот путь к патчам, или место запуска скрипта, или список патчей взят не для той версии пхп, или не та версия пхп скачана).
После успешного патчинга из корня исходников заходим еще глубже, в ext/intl - и наконец запускаем сборку:
cd /path/to/root/of/downloaded/php/source/ext/intl
phpize
./configure \
--with-php-config="$(command -v php-config)" \
--enable-intl \
--with-icu-dir="$ICU_PREFIX"
{
echo "#define FALSE 0"
echo "#define TRUE 1"
} >> config.h
CXX_STANDARD=$([[ "${ICU%.*}" -ge 75 ]] && echo 17 || echo 11)
make CXXFLAGS="-O2 -std=c++${CXX_STANDARD} -DU_USING_ICU_NAMESPACE=1 -DTRUE=1 -DFALSE=0 ${CXXFLAGS:-}"
Долгожданное расширение будет ждать нас в ext/intl/modules/intl.so. Но прежде чем хватать его и бежать менять, стоит проверить одну важную вещь.
Важная вещь. Расширение должно использовать именно ту версию ICU, которую указали. Путь к ней передается через --with-icu-dir. Но несмотря на это (и на ободряющие надписи с этой версией во время сборки), в итоге intl.so все равно может собраться не с ней, а с первой попавшейся ICU из конфигурации PHP. Для CentOS 7 это будет ICU 50.
Такое расширение даже не получится использовать. Ошибка будет примерно такая:
PHP Warning: PHP Startup: Unable to load dynamic library 'intl' (tried: /usr/lib64/php/modules/intl (/usr/lib64/php/modules/intl: cannot open shared object file: No such file or directory), /usr/lib64/php/modules/intl.so (/usr/lib64/php/modules/intl.so: undefined symbol: ZTIN6icu7317StringEnumerationE)) in Unknown on line 0 PHP Fatal error: Uncaught Error: Class 'IntlTimeZone' not found in Command line code:1
Чтобы понять, что что-то пошло не по плану, не обязательно подключать intl.so к PHP. Достаточно проверить зависимости у получившегося файла:
ldd /path/with/php/source/ext/intl/modules/intl.so | grep icu
На выходе должны отображаться номерки собранной ICU.
Если не совпадают
Если это не так, нужно во чтобы то ни стало заставить скрипт обнаруживать наш ICU самым первым. Можно попробовать добавить
export ICU_PREFIX=/opt/icu78_1
export CFLAGS="-I$ICU_PREFIX/include"
export CXXFLAGS="-I$ICU_PREFIX/include -std=c++11 -DU_USING_ICU_NAMESPACE=1 -DTRUE=1 -DFALSE=0"
export LDFLAGS="-L$ICU_PREFIX/lib -Wl,-rpath,$ICU_PREFIX/lib"
export PKG_CONFIG_PATH="$ICU_PREFIX/lib/pkgconfig"
Но скажу как есть, я уже не помню, что именно в итоге помогло. Долгое время сборка упорно линковалась с ICU 50, а потом вдруг начала корректно подхватывать нужную версию - без каких-либо явных изменений во внешних конфигах.
Возможно, эти экспорты и не стали решающим фактором, но теплое воспоминание об успехе с ними сохранилось. Так что оставлю их здесь - на всякий случай.
Также, перед повторными попытками запуска сборки, настоятельно рекомендую очищать предыдущие результаты (может это и помогло).
cd /path/to/php/source/ext/intl/
make clean
rm -f config.cache config.h
Ну а если все ок (ура!), можно отшлифовать результат командой, уменьшающей размер итоговой библиотеки:
strip --strip-unneeded ./modules/intl.so
Этап 4. Замена одного intl на другой
Пришло время воспользоваться плодами наших трудов! Это одновременно самый простой и самый опасный этап.
Сначала нужно определить, где лежат рабочие модули php
php-config --extension-dir
# or
php -i | grep extension_dir
Допустим, это /usr/lib64/php/modules/
Для надежности, скопируем оригинальную intl.so для возможности отката. После этого поместим в эту директорию нашу новую intl.
cp /usr/lib64/php/modules/intl.so /usr/lib64/php/modules/intl.so.original.bak
cp ./modules/intl.so /usr/lib64/php/modules/intl.so
Проверим, что все работает:
php -r 'var_dump(IntlTimeZone::createTimeZone("Europe/Kyiv"));
Если что-то пошло не так - возвращаем оригинальный intl.so обратно.
cp /usr/lib64/php/modules/intl.so.original.bak /usr/lib64/php/modules/intl.so
Если все ок, то перегружаем еще и php-fpm.service (ну или что там у вас модного стоит)
systemctl reload php-pfm.service
Как закинуть результат на прод
Как уже говорилось, на проде не нужно выполнять все эти шаги со сборкой и настройками. (Но если вы собирали по пути Ubuntu, то все-таки не забудьте обновить конфиг).
Нужно лишь залить и положить в аналогичные места папку с ICU (/opt/icu78_1) и файл intl.so. После чего проверить работу и перезапустит php-pfm.
Если по какой-то причине нет возможности залить файлы через scp или UI-инструмент, то всегда можно выложить архив на другой свой сайт и скачать его оттуда командой:
curl -LO https://your-site.com/public/source/intl781.zip
Удачи!


pda0
Это конечно мощно. Сам подобным развлекался. Если нужна именно последняя ICU, то remi, наверное, не поможет. Но не проще ли бы нужный проект в контейнер упаковать, где и нужный pho и нужная ICU готовая может быть? cli в контейнере запускать проще, чем вот это всё.