Skip to content

Инженерная практика·8 мин чтения

Legacy PHP: обновить, вытеснять по частям или переписать? Как мы решаем

Сначала обновляйте на месте, по одной минорной версии PHP, и заменяйте по частям только то, до чего обновление не дотягивается; с нуля переписывайте лишь небольшие системы, мёртвые фреймворки или продукт, который меняется. Здесь — порядок, в котором мы всё проверяем, инструменты, которые запускаем, и то, как выглядит первый пилот.

Анатолий НовогродскийОснователь и CEO, GlanitОпубликовано 15 октября 2026

Чем плох PHP, у которого закончилась поддержка?

Тем, что его больше никто не чинит. Когда ветка PHP доходит до конца жизни, проект не выпускает для неё ни релизов, ни исправлений безопасности. PHP 8.1 получил последний релиз, 8.1.34, и 31 декабря 2025 года закончил жизнь. PHP 8.0 закончился 26 ноября 2023 года, 7.4 — 28 ноября 2022 года. Следующим идёт PHP 8.2: поддержка безопасности заканчивается 31 декабря 2026 года, через одиннадцать недель после выхода этой статьи. Уязвимость, найденную в январе, закроют в 8.3 и новее, а сервер на 8.2 останется с дырой.

Расписание хотя бы предсказуемо. Каждая ветка получает два года активной поддержки и ещё два года только исправлений безопасности, и все даты окончания приходятся на 31 декабря. PHP 8.6 уже на стадии RC3 (8 октября 2026 года).

Фреймворк часто заканчивается раньше. Laravel даёт каждой мажорной версии 18 месяцев исправлений ошибок и два года исправлений безопасности. Laravel 11 потерял поддержку безопасности 12 марта 2026 года; Laravel 12 перестал получать исправления ошибок 13 августа 2026 года, а исправления безопасности получает до 24 февраля 2027 года. Laravel 13 (17 марта 2026 года) требует PHP 8.3, так что команда, застрявшая на PHP 8.2, застряла и на ветке фреймворка, у которой осталось несколько месяцев.

Таких команд много. По данным установок Packagist на 1 июля 2026 года, на PHP 7.4 всё ещё было 5% установок, на 8.1 ещё 5%, на 8.0 — 1%. Composer никого не сдвинет принудительно: из 1000 самых популярных пакетов только 352 требуют версию PHP, которая ещё получает обновления безопасности.

Толчок приходит снаружи: строка в опроснике по безопасности от заказчика или отчёт пентеста.

Обновить, вытеснять по частям или переписать: как мы решаем?

Мы задаём пять вопросов в фиксированном порядке, и ответы обычно сами выбирают вариант. По умолчанию так: сначала обновляем, части, которые обновить нельзя, вытесняем по одной (паттерн strangler, «душитель»), а переписываем только системы, которые малы или по-настоящему мертвы. Немного тестов, живой фреймворк и приличная схема БД дают обновление, иногда с одним вытесненным модулем, потому что мёртвое расширение привязывает его к старому PHP.

  1. Есть ли тесты и запускаются ли они? Если нет, то что бы мы ни решили дальше, сначала пишем характеризационные тесты на пути, по которым бизнес выставляет счета.
  2. Поддерживается ли фреймворк и есть ли описанный путь обновления? Laravel и Symfony публикуют руководство по обновлению для каждого мажорного релиза. У самописного фреймворка 2011 года такого руководства нет.
  3. Здорова ли модель данных? Грязный код на хорошей схеме — задача на обновление. Плохая схема делает дорогим любой вариант, и переписывание от неё не спасает, потому что данные переезжают вместе с системой.
  4. Можно ли отрезать модули по границе HTTP или очереди? Если отчёты или интеграцию с поставщиком можно маршрутизировать отдельно, вытеснение обходится дёшево.
  5. Понимает ли кто-нибудь бизнес-правила? Если они живут только в коде, переписывающему придётся выкапывать их из того же кода.

Четыре варианта рядом

Начните читать с колонки «Главный риск». Каждый вариант ломается в своём месте, и сбой, который вы можете себе позволить, обычно решает больше, чем выгоды.

Четыре способа справиться с legacy PHP-приложением (качественная оценка, по нашей методике).
ВариантПодходит, когдаГлавный рискПомехи для бизнесаВремя до первой пользыНаш выбор по умолчанию, когда
Обновление на местеФреймворк поддерживается, схема здорова, есть хоть какие-то тестыТихие изменения поведения между версиями (обработка null, удалённые функции)Низкие: выходит обычными релизамиРано: каждый влитой шаг уже в продакшенеПочти всегда первый ход
Вытеснение по частям (strangler)Несколько модулей привязаны мёртвым кодом или расширениями; есть чёткие границы по HTTP или очередиДолгое соседство двух систем с общими даннымиНизкие на каждом шаге, плюс слой маршрутизации, который надо держатьРано для первого модуля, медленно для всей системыЧасти, до которых обновление не дотягивается
Полное переписываниеНебольшая система, мёртвый фреймворк, правила задокументированы или продукт меняетсяНезадокументированные правила всплывают как баги; до переключения ничего не выходитВысокие в момент переключенияПоздно: польза приходит с переключениемНебольшие системы или изменившийся продукт
Обернуть и оставить (заморозка + изоляция)Вывод из эксплуатации запланирован и датирован, изменений почти не ждутПатчей по-прежнему нет; изоляция лишь сужает рискМинимальныеНет, это выигрыш во времениДата вывода из эксплуатации уже есть
Четыре способа справиться с legacy PHP-приложением (качественная оценка, по нашей методике). — Цифр нет: ячейки описывают нашу методику, а не измеренные результаты.

Как безопасно подняться на несколько версий PHP?

По одной минорной версии, сначала страховочная сетка из тестов, а механические правки делают инструменты. Мы пишем характеризационные тесты на пути, на которых бизнес зарабатывает, запускаем PHPStan на низком уровне, даём Rector применять обновления версий небольшими пачками и держим CI зелёным и на текущей, и на целевой версии PHP, прежде чем переключать продакшен. Каждый шаг выкатывается отдельно.

Характеризационные тесты фиксируют, что код делает сегодня, включая неправильные ответы. Баг округления, найденный по дороге, получает свою задачу: если чинить его внутри обновления, дифф невозможно ревьюить.

Каждая версия ломает своё, поэтому на каждом шаге мы читаем руководство по миграции на php.net. Код времён PHP 5 до сих пор вызывает mysql_query(), а это расширение удалили в 7.0. PHP 8.0 убрал each() и create_function(). PHP 8.1 объявил устаревшей передачу null в ненулабельные параметры встроенных функций, а это встречается по всему коду, написанному под 7.x. PHP 8.4 объявил устаревшими неявно nullable типы параметров.

Большую часть этих правок делает Rector, и его собственная документация предостерегает от очевидного хода. Вызов withPhpSets() в проекте с объявленным ^7.4 применит наборы правил с 5.3 по 7.4 разом, «больше 100 правил». Документация советует withPhpLevel(), который поднимают на единицу с каждым влитым pull request, после того как PHPStan без baseline дошёл до уровня 3–4. Актуальные релизы: Rector 2.7.0 (6 октября 2026 года, работает на PHP 7.4+) и PHPStan 2.3.

<?php
// rector.php
use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPaths([__DIR__ . '/app', __DIR__ . '/tests'])
    ->withPhpLevel(1); // +1 per merged PR, then withPhpSets()

composer why-not php 8.3 выводит все пакеты, которые блокируют целевую версию. У большинства блокирующих есть новый мажор или форк. Те немногие, у кого нет ни того ни другого, и есть место, где начинается разговор о вытеснении.

До переключения CI гоняет набор тестов на обеих версиях:

strategy:
  matrix:
    php: ['8.2', '8.3']
steps:
  - uses: actions/checkout@v7
  - uses: shivammathur/setup-php@v2
    with:
      php-version: ${{ matrix.php }}
  - run: composer install --no-progress
  - run: vendor/bin/phpunit

В продакшене первым переезжает один сервер или один пул воркеров. Вопрос, нужен ли PHP в стеке вообще, отдельный; мы разбирали его в статье «Python, PHP или Node.js для бэкенда?».

Когда вытеснять по частям и как это делается в PHP?

Модуль вытесняют, когда обновить его за разумные деньги нельзя, но можно отрезать по границе: свои URL, своя очередь, в идеале свои таблицы. Фасад (nginx, балансировщик или сам front controller старого приложения) отправляет запросы этой функции в новый код, а всё остальное в старое приложение, и пользователи по-прежнему видят одну систему. Руководство Microsoft по паттерну перечисляет, где он не подходит: запросы нельзя перехватить, нет доступа к исходникам старой системы, система мала и проще заменить её целиком, или старую нужно быстро выключить.

В PHP-хозяйстве фасад часто состоит из нескольких строк nginx:

location /reports/ {
    proxy_pass http://reports-v2:8080;
}
location / {
    try_files $uri /index.php?$query_string;
}

Первая заноза — сессии: новый сервис читает сессию старого приложения или доверяет токену, который ставит фасад. Решайте это до того, как переедет первый маршрут.

База данных — то место, где проекты по вытеснению срываются, и большинство разборов о ней молчит. Два приложения, пишущие в одну таблицу с разными правилами валидации, создают строки, которых не ждёт ни то ни другое, поэтому наше правило такое: один писатель на таблицу, записанный письменно. Последовательность, которой мы следуем, описана в руководстве Microsoft. Сначала новый сервис работает со своими таблицами внутри старой базы; затем ETL-загрузка и change data capture наполняют отдельную доменную базу; проверяется согласованность; новая база становится системой учёта; таблицы старой системы убирают последними, потому что после этого откат становится дорогим. Механику CDC мы прогнали на собственном проекте: перенесли 12 ТБ PostgreSQL с простоем меньше четырёх минут (разбор).

Там, где новый код обязан вызывать старый, слой защиты от повреждения (anti-corruption layer) оставляет названия полей и коды статусов старой системы на границе. Он стоит задержки и ещё одного компонента, который надо эксплуатировать.

Когда полное переписывание действительно оправдано?

Реже, чем кажется изнутри запутанной кодовой базы. Мы рассматриваем переписывание, когда систему можно заменить за один заход, когда её фреймворк мёртв и пути миграции нет, когда бизнес-правила задокументированы вне кода или когда меняется сам продукт и старое поведение сохранять не нужно. То же исключение для небольших систем делает руководство Microsoft по strangler.

Издержки, в порядке, в котором они обычно проявляются:

  • До переключения ничего не выходит, а старая система при этом всё ещё требует исправлений.
  • Каждое правило, жившее только в коде, возвращается после запуска как баг-репорт.
  • Миграция данных — та же работа, что и в любом другом варианте, только сжатая в одни выходные.

Единственное переписывание, которое мы описали в этом блоге, перевело мобильное приложение с React Native на нативный Swift: Bluetooth-телеметрия на 100 Гц забивала JavaScript-поток, задержка превышала 1500 мс. Это был потолок рантайма. Для PHP-системы нужна причина такого же веса.

Каким должен быть первый пилот?

Один ограниченный модуль, критерии приёмки, записанные до первой строки кода, старое и новое работают рядом, затем решение об объёме. Хорошие кандидаты — отчёт, одна интеграция, экран администратора или приём документов: понятные входы и выходы, а неверный результат вылезает раньше, чем стоит кому-то денег.

Новый модуль сначала работает в теневом режиме. Он получает те же входные данные, отвечает по-прежнему старый, а скрипт сравнивает результаты на реальном трафике по критериям, согласованным с владельцем процесса (какие итоги должны совпадать, какие расхождения допустимы). Пилот оставляет в продакшене модуль и список того, из чего состоит вся работа: блокирующие зависимости, недостающие тесты, правила, которые никто не записал. Именно этот список делает оценку объёма честной.

Пилот можно вести как проект с фиксированным объёмом или как расширение команды рядом с вашими разработчиками.

Как не останавливать бизнес во время работ

Выпускать малыми шагами, держать старый путь на расстоянии одного переключателя и смотреть на продакшен, прежде чем что-то менять. Заморозка функций касается только переносимого модуля; всё остальное продолжает выходить. Трекинг ошибок и лог медленных запросов ставим до первого изменения, чтобы было с чем сравнивать. Шаги отката записаны и отрепетированы до переключения, а там, где выходы можно сравнивать, старая и новая версии какое-то время работают параллельно. Перед переключением мы проходим те же проверки, что и перед распродажей или запуском (наш чек-лист пиковой нагрузки).

Где ИИ помогает при миграции legacy, а где нет?

ИИ хорошо читает и тестирует legacy-код и заметно хуже пишет замену без присмотра. Мы используем модели, чтобы объяснить недокументированные модули, набросать характеризационные тесты и вытащить бизнес-правила из кода в списки простым языком, которые затем правит владелец предметной области. Каждый результат проверяет инженер, и ничего сгенерированного не вливается без тестов.

Недавние исследования подтверждают эту осторожность. В LegacyTranslate (март 2026 года), созданном во время миграции около 2,5 млн строк PL/SQL на Java в финансовой организации, агент первичного перевода выдавал компилируемый код в 45,6% случаев и проходил тесты в 30,9%. Привязка к внутренним API и возврат ошибок компилятора добавили ещё 8% и 3%.

Более сильный подход устроен наоборот: агент генерирует тесты, а судьёй служит старая система. В июльской статье 2026 года миграции COBOL на Java проверяли детерминированными тестами на совпадение поведения, и на внутренней программе получили покрытие ветвей 91,90%. Для PHP такой эталон уже работает на ваших серверах. Агенты получают те же правила доступа, что и все остальные: никаких учётных данных продакшена, каждое действие в журнале (семь контролей, которые мы не пропускаем).

PHP 7.x, 8.0 или 8.1 в продакшене?

Пришлите нам composer.json, версию PHP на каждом сервере и три модуля, которые меняются чаще всего. На ознакомительном звонке мы скажем, с какого из четырёх вариантов начали бы и каким был бы первый пилот. Подробнее о нашей работе — на странице «Веб-разработка», либо напишите нам через форму обратной связи.

Частые вопросы

Ещё статьи

Инженерная практика ·

Python, PHP или Node.js для бэкенда? Как мы выбираем на самом деле

Мы поставляем все три. Решение редко зависит от языка — оно зависит от команды, которая будет сопровождать систему, от библиотек, нужных задаче, и от профиля нагрузки. Простой разбор.

4 мин чтения

ИИ и машинное обучение ·

Workflow или агент? Как мы решаем, ещё не написав ни строчки кода

Если каждый шаг процесса можно записать до запуска, стройте workflow и вызывайте LLM только там, где что-то нужно прочитать и истолковать. Агент нужен, когда следующий шаг зависит от находки предыдущего, и закрывайте его жёсткими лимитами.

7 мин чтения

Безопасность ·

Отчётность по Cyber Resilience Act уже действует: что построить до того, как пойдёт первый 24-часовой отсчёт

С 11 сентября 2026 года каждый производитель, который продаёт в ЕС программное обеспечение или подключённые продукты, обязан в течение 24 часов сообщать через Единую платформу отчётности ENISA об активно эксплуатируемых уязвимостях и серьёзных инцидентах. Закон короткий. Трудно другое: построить конвейер, который за это время скажет, что именно вы поставляете и эксплуатируют ли это.

7 мин чтения