rzaripov.kz

Рефакторинг legacy-кода на PHP: практический опыт

В любом крупном проекте рано или поздно наступает момент, когда код перестаёт справляться с нагрузкой, новые фичи внедряются с трудом, а баги множатся. PHP-приложения, которые писались пять, десять или пятнадцать лет назад, часто представляют собой смесь процедурного стиля, устаревших функций и запутанной логики. Лично мне приходилось разбирать проекты, где один файл содержал более двух тысяч строк, а имена переменных вроде $q1, $tmp и $data2 использовались повсеместно.

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

Аудит устаревшего кода: с чего начать

Первый шаг — честная оценка состояния системы. Я обычно составляю карту модулей и отмечаю, какие из них меняются часто, какие редко, а какие вообще не трогались годами. Полезно запустить статические анализаторы вроде PHPStan или Psalm, чтобы получить объективную картину ошибок и потенциальных проблем. Дополнительно провожу ручной обзор самых крупных файлов, выписывая кодовые запахи: длинные методы, дублирование, «магические числа» и избыточные зависимости.

Отдельно стоит проговорить с командой или заказчиком приоритеты бизнеса. Не всё нужно переписывать — иногда достаточно локальных улучшений. Я разделяю код на три категории: то, что трогаем активно (требует серьёзного вмешательства); то, что работает стабильно (ограничиваемся косметикой); и то, что планируется вывести из эксплуатации (оставляем как есть). Такой подход экономит время и снижает риск поломок в критичных местах.

Пошаговая стратегия преобразований

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

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

Работа с базой данных и запросами

Один из самых болезненных участков в старых PHP-проектах — взаимодействие с базой данных. Запросы часто разбросаны по представлениям, контроллерам и вспомогательным классам, используются смешанные стили (mysql_* рядом с PDO), а индексы в схеме либо отсутствуют, либо избыточны. Первый шаг — централизация: создаю единый класс-репозиторий или шлюз, через который проходят все обращения к конкретной таблице. Затем перевожу код на подготовленные выражения и единый стиль именования.

Принципы оптимизации запросов универсальны и выходят за рамки одного языка. Полезно изучить, как аналогичные задачи решаются в смежных технологиях — в материале Оптимизация SQL-запросов в Delphi-приложениях: советы из практики разобраны подходы, во многом применимые к PHP-проектам. Общие правила остаются теми же: избегать SELECT *, следить за планами выполнения, выносить тяжёлую аналитику в фоновые задачи.

Подход в legacy-коде Современная альтернатива Эффект
Конкатенация строк в SQL Подготовленные выражения (PDO) Защита от инъекций, читаемость
Запросы внутри HTML-шаблонов Выделенный слой репозиториев Тестируемость, разделение ответственности
mysql_num_rows после SELECT Подсчёт через отдельный запрос или оконные функции Производительность на больших объёмах
Хардкод имён таблиц в коде Конфигурируемые префиксы и маппинг Простота миграций и рефакторинга схемы

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

Современные стандарты PHP в обновлённом коде

Сам по себе язык за последние версии сильно изменился, и это козырь при работе со старым кодом. Постепенно заменяю устаревшие конструкции: array() на [], длинные if/else блоки на match, анонимные функции с use на стрелочные функции. Включаю строгий режим (declare(strict_types=1)) и проверяю совместимость типов там, где это безопасно. Подобные правки не меняют поведение, но делают код короче и понятнее, что критично при командной разработке.

Отдельное внимание уделяю внедрению зависимостей. Вместо глобальных состояний и синглтонов, разбросанных по проекту, использую контейнер: PSR-11 совместимый, простой в понимании, без лишней «магии». Классы становятся слабо связанными, появляется возможность подменять реализации в тестах. Если проект использует фреймворк, подбираю DI-решение под него — например, в Laravel это стандартный механизм сервис-контейнера, а в Symfony — компонент DependencyInjection.

Тестирование после рефакторинга

Без тестов рефакторинг превращается в русскую рулетку. На этапе внедрения пишу три уровня проверок: модульные (на каждый выделенный класс), интеграционные (для критичных пользовательских сценариев) и приёмочные (сценарии от лица конечного пользователя). PHPUnit покрывает первые два уровня, а для приёмочных часто применяю Codeception или отдельные curl-скрипты, эмулирующие действия через HTTP. Это даёт уверенность, что поведение системы остаётся прежним.

Внедряю автоматический запуск тестов в CI-пайплайн — это страховка от регрессий при будущих изменениях. Дополнительно подключаю линтеры и статические анализаторы как обязательный этап перед слиянием. За годы практики я убедился: чем шире «сетка» автоматических проверок, тем спокойнее команда относится к крупным переработкам, потому что ошибки ловятся ещё до ручного тестирования.

Документация и долгосрочная поддержка

Отдельное направление работы — фиксация знаний после рефакторинга. Обновляю README, описываю архитектурные решения в отдельной папке docs/, добавляю ADR (Architecture Decision Records) для ключевых компромиссов. Это помогает новым разработчикам быстрее входить в проект и снижает вероятность повторения старых ошибок. Без такой документации через полгода придётся заново разбираться в собственных решениях.

Не обхожу вниманием и процессы: настраиваю шаблоны для тикетов, описываю code style в phpcs.xml, добавляю подробные сообщения к коммитам. Команда получает единые правила, а я через длительное время могу открыть любой файл и быстро вспомнить, почему было принято то или иное решение. Рефакторинг legacy-кода — это марафон, а не спринт, и выигрывает тот, кто думает на шаг вперёд и закладывает основу для следующих поколений разработчиков.