Ловушки миграции со старого PHP: от 7.2 до безопасного перехода на 8.x
Миграция с PHP 7.2 на современные версии ломается не на крупных изменениях синтаксиса, а на мелочах: count() на не-countable, удалённых функциях, тихо изменившемся поведении each() и сравнений.

Проекты, которые пережили PHP 7.2 и до сих пор не обновились до актуальной версии, рано или поздно сталкиваются с принудительной миграцией: хостинг прекращает поддержку старого runtime, или зависимость требует PHP 8.x. Переход кажется формальностью, но именно мелкие изменения поведения, не крупные, обрушивают продакшен в первый же день.
Сложность миграции редко пропорциональна размеру изменений в официальном списке breaking changes. Один абзац changelog про изменённое правило сравнения строки и числа может означать несколько дней поиска мест, где старое поведение использовалось намеренно, а не случайно, и где новое поведение тихо меняет результат проверки без единой ошибки в логе.
Прежде чем переходить к конкретным ловушкам, полезно выделить саму идею: большинство проблем при обновлении PHP возникают не из-за намеренного использования устаревших функций, а из-за кода, который случайно оказался в серой зоне между «технически работает» и «написано правильно». Новая версия языка просто перестаёт закрывать глаза на эту серую зону.
Count() и потеря молчаливой терпимости
В PHP 7.2 функция count() на значении, которое не является массивом или объектом, реализующим Countable, выдавала предупреждение и возвращала единицу или ноль. Начиная с PHP 8.0 это уже TypeError. Код вида count($maybeArray), где переменная иногда оказывается null из-за неудачного запроса к базе, в старых версиях просто предупреждал в лог, а на 8.x останавливает выполнение запроса целиком.
// Было безопасно 'по случайности' на 7.2, упадёт на 8.x
function itemsCount($items): int
{
return count($items);
}
// Правильная защита независимо от версии
function itemsCount(?array $items): int
{
return count($items ?? []);
}Удалённые функции each() и create_function()
Поиск вызовов удалённых функций редко решается одним простым grep по кодовой базе: та же each() могла попасть в проект не напрямую, а через стороннюю зависимость, которую давно не обновляли. Перед обновлением версии PHP стоит отдельно прогнать composer-зависимости через их собственные changelog или хотя бы проверить минимально поддерживаемую версию PHP в их composer.json.
Функция each(), устаревшая ещё в 7.2, была окончательно удалена в PHP 8.0. Код, написанный в стиле ручного обхода массива через while (list($key, $value) = each($array)), встречается в легаси чаще, чем ожидается, особенно во внутренних библиотеках без активной поддержки. Замена простая: foreach справляется с той же задачей без побочных эффектов на внутренний указатель массива.
// Удалено начиная с PHP 8.0
while (list($key, $value) = each($array)) {
echo "{$key}: {$value}\n";
}
// Современная замена
foreach ($array as $key => $value) {
echo "{$key}: {$value}\n";
}Изменения в сравнении строк и чисел
PHP 8.0 изменил правило сравнения строки с числом: если строка не является числовой, число приводится к строке для сравнения, а не наоборот. Это закрывает классическую уязвимость вида 0 == 'abc', которая раньше возвращала true, а на новых версиях возвращает false. Код, который опирался на старое поведение для «мягкой» проверки входных данных, начинает вести себя иначе без единой ошибки в логе, просто с другим результатом.
| Сравнение | PHP 7.2 и ниже | PHP 8.0 и выше |
|---|---|---|
0 == 'abc' | true | false |
'1' == '01' | true | true (не изменилось) |
'10' == '1e1' | true | true (не изменилось) |
Объектный тип и слишком широкие параметры
Введённый в 7.2 тип параметра и возврата object разрешает передать экземпляр любого класса. Это удобно для действительно универсальных операций, например кеширования произвольных объектов, но в бизнес-логике превращается в источник путаницы: функция, принимающая object, ничего не говорит о том, какой именно объект ожидается, и ошибка обнаруживается только при обращении к несуществующему свойству во время выполнения, а не на этапе проверки типов.
Похожая осторожность нужна и с устаревшими директивами конфигурации php.ini: часть из них, например связанные со старым обработчиком ошибок или магическими кавычками, была удалена задолго до 8.x, но иногда до сих пор встречается в закомментированных конфигах или в скриптах развёртывания, скопированных с очень старого сервера без проверки актуальности.
Параметр типа string по умолчанию и неявные приведения
Передача null в параметр с нестрогим скалярным типом, например string $value, молча работала в PHP 7.x и ранних версиях 8.x с предупреждением об устаревании, а начиная с PHP 8.1 требует явного объявления ?string, если null действительно допустим. Код, который раньше полагался на неявное приведение null к пустой строке, нужно просматривать отдельно: часть случаев это баг, который маскировался приведением типа, а часть, действительно допустимый null, который нужно объявить честно.
Эта группа изменений особенно болезненна для кода, который вызывает функции встроенной библиотеки PHP с потенциально null-значением из базы данных: раньше интерпретатор тихо приводил null к ожидаемому скалярному типу, а начиная с восьмой версии такие вызовы при включённом strict_types завершаются ошибкой, которую проще найти заранее через статический анализатор, чем через падение в продакшене.
Порядок безопасной миграции
Миграция сразу на последнюю версию PHP с 7.2 рискованна из-за накопленного числа изменений поведения. Надёжнее двигаться по промежуточным версиям, на каждом шаге включая логирование deprecation-предупреждений в продакшене, а не только в разработке, потому что именно нетипичные входные данные из реального трафика вскрывают большинство проблем.
- Поднять локальное окружение на целевой версии PHP и прогнать полный набор тестов, если он есть
- Включить вывод deprecation-предупреждений в лог на стейджинге под реальной копией трафика
- Пройтись статическим анализатором, например PHPStan, по кодовой базе на новом уровне строгости
- Отдельно проверить места с count(), each(), object-типами и сравнением строк с числами
- Выкатывать миграцию отдельным релизом без смешивания с новыми фичами, чтобы откат был простым
Большая часть боли при переходе со старых версий PHP не в новом синтаксисе, который легко освоить за один вечер, а в коде, который раньше работал благодаря терпимым правилам языка, а не благодаря собственной корректности. Чем явнее типы и чем меньше кода опирается на неявные приведения, тем спокойнее проходит следующая миграция.
После завершения миграции полезно оставить в проекте список изменений поведения, которые реально проявились на этом переходе, с краткой пометкой, где и почему. При следующем крупном обновлении PHP, а они будут происходить регулярно, эта заметка сэкономит команде время, которое иначе снова ушло бы на самостоятельный поиск одних и тех же граблей.
Отдельно стоит зафиксировать, какие зависимости проекта оказались самым узким местом миграции: часто это не собственный код команды, а старая библиотека без активной поддержки, которую придётся либо обновить с нуля, либо форкнуть, либо заменить на современный аналог ещё до того, как основной код перейдёт на новую версию языка.
Если такой зависимости найти замену не удалось, разумный промежуточный шаг: изолировать её за собственным тонким интерфейсом внутри проекта, чтобы при следующей попытке обновления менять реализацию за этим интерфейсом, а не переписывать все места вызова библиотеки по всему приложению одновременно. Этот же интерфейс заодно упрощает написание тестов, которые раньше зависели от конкретной сторонней реализации напрямую, и позволяет подменить саму зависимость без правок в остальной кодовой базе проекта.
Частые вопросы
Можно ли мигрировать сразу с PHP 7.2 на 8.4, пропуская промежуточные версии?
Технически можно, но рискованно: накопленных изменений поведения становится больше, и сложнее понять, какое именно изменение сломало конкретное место. Надёжнее проверять код на промежуточных версиях.
Что чаще всего ломается при обновлении PHP на легаси-проектах?
Не крупный синтаксис, а мелкое поведение: count() на значениях, которые не являются массивом, удалённая функция each(), изменённые правила сравнения строки и числа.
Как заранее найти проблемные места перед обновлением версии?
Включить логирование deprecation-предупреждений на стейджинге с реальной копией трафика и прогнать статический анализатор на уровне строгости, близком к целевой версии PHP.



