
Рефакторинг PHP-кода: пошаговое руководство
Рефакторинг PHP-кода — это улучшение структуры без смены поведения. Разбираем признаки, безопасный порядок, приёмы и инструменты: PHPStan, Rector, PHPUnit.
Рефакторинг PHP-кода: как навести порядок и не сломать проект
Рефакторинг PHP-кода — это последовательное улучшение его структуры без изменения поведения: вы разбиваете длинные методы, убираете дублирование, вводите понятные имена и типы, а тесты подтверждают, что всё работает как прежде.
Почти в любом PHP-проекте, которому больше двух-трёх лет, накапливается технический долг: контроллеры на тысячу строк, массивы вместо объектов, глобальные состояния и условия на пять уровней вложенности. Ниже — практическое руководство: как распознать проблемные места, в каком порядке действовать, какие приёмы использовать и чем автоматизировать рутину.
Что такое рефакторинг и чем он отличается от переписывания
Рефакторинг — это не «переписать всё красиво» и не добавление новых функций. Это серия небольших преобразований, каждое из которых сохраняет внешнее поведение системы. Именно поэтому он безопасен при правильном подходе: вы никогда не оказываетесь в ситуации, когда проект «разобран» на несколько недель и не запускается.
Переписывание с нуля устроено иначе: вы строите новую систему рядом со старой, а потом пытаетесь воспроизвести всю накопленную бизнес-логику, включая неочевидные исключения и «костыли», которые на самом деле были реакцией на реальные случаи. Такие проекты чаще всего затягиваются, а старый код продолжает жить параллельно. Поэтому для большинства PHP-приложений эволюционный рефакторинг выгоднее и надёжнее.
Отдельно стоит отделять рефакторинг от смены версии языка. Миграция с PHP 7.4 на 8.x — это обновление, но во многом оно решается теми же приёмами: типизация, readonly-свойства, enum, match, именованные аргументы. Многие из этих изменений можно автоматизировать, и об этом поговорим в разделе про инструменты.
Шесть «запахов» в PHP-коде
Длинный метод
Метод не помещается на экран, а в имени есть «и»: validateAndSaveAndNotify. Ориентир — до 20–30 строк.
God-класс
Один класс знает про базу, почту, оплату и шаблоны. Любая правка рискует сломать соседнее.
Глубокая вложенность
Больше трёх уровней if и foreach. Условия читаются как лабиринт, а не как логика.
Магические значения
Числа и строки вроде status == 3 без объяснений. Смысл знает только автор, и то не всегда.
Дублирование
Один и тот же расчёт скопирован в пяти местах. Исправили в одном — забыли в четырёх.
Массивы вместо объектов
Данные передаются как $data['x']['y']. Нет типов, нет подсказок IDE, любая опечатка видна только в продакшене.
Когда рефакторить, а когда оставить как есть
Хороший ориентир — «правило бойскаута»: оставляйте код чище, чем он был до вас. Если вы пришли в модуль, чтобы добавить функцию, потратьте немного времени на то, чтобы сначала упростить место, куда эта функция встанет. Так порядок наводится постепенно, без отдельных многомесячных проектов.
Второй полезный принцип — «правило трёх»: первый раз вы пишете как получилось, второй раз морщитесь, но дублируете, а на третий раз выносите общую логику. Это защищает от преждевременных абстракций, которые бывают не менее вредны, чем копипаст.
Не стоит трогать код, который стабильно работает, не меняется годами и не влияет на сроки задач. Рефакторинг ради рефакторинга съедает бюджет и создаёт риски без отдачи. Приоритет получают участки, которые часто меняются, часто ломаются и тормозят новых разработчиков: именно там улучшения окупаются быстрее всего.
Безопасный порядок рефакторинга PHP-проекта
- 01
Зафиксируйте текущее поведение. Напишите тесты на участок, который собираетесь менять: юнит-тесты на PHPUnit или Pest, а для запутанного кода — характеризующие тесты, которые просто записывают то, что код делает сейчас.
- 02
Подключите статический анализ и единый стиль. PHPStan или Psalm найдут несоответствия типов и мёртвый код, PHP CS Fixer приведёт форматирование к одному виду, чтобы в диффах не было шума.
- 03
Выберите одну небольшую цель. Например, «разбить метод processOrder» или «убрать массивы из слоя оплаты». Формулировка должна помещаться в одну строку.
- 04
Делайте мелкие шаги: один приём — один коммит. Переименовали, вынесли метод, заменили условие на ранний возврат — каждое действие отдельно, чтобы при ошибке было легко откатиться.
- 05
Запускайте тесты и анализатор после каждого шага. Если что-то покраснело, причина почти всегда в последнем изменении — найти её дёшево.
- 06
Не смешивайте рефакторинг с новыми функциями. Отдельный pull request на улучшения проще проверить на ревью и проще откатить, если что-то пойдёт не так.
- 07
Измерьте результат. Сравните цикломатическую сложность, число ошибок анализатора, время на типовую правку. Без цифр трудно доказать пользу и выбрать следующую цель.
Тесты как страховка: что делать, если их нет
Классическая ловушка legacy-проектов: код нужно улучшить, но он не покрыт тестами, потому что его невозможно протестировать без улучшения. Выход — начинать не с идеальных юнит-тестов, а с грубых, но полезных проверок: интеграционный тест, который вызывает эндпоинт и сравнивает ответ с эталоном, либо snapshot-тест на вывод функции для набора реальных входных данных.
Такие тесты не красивы, зато они дают главное — сигнал «поведение изменилось». Когда вокруг проблемного места появилась сетка, можно аккуратно выделять зависимости, внедрять интерфейсы и постепенно заменять грубые тесты нормальными юнит-тестами.
Если тестов нет совсем, а менять код нужно срочно, опирайтесь на строгие типы, статический анализ и ручную проверку критичных сценариев. Но фиксируйте это как долг: следующим шагом в плане должны стоять тесты.
Тот же расчёт скидки, но читаемый и типизированный
function calc($o) {
if ($o['status'] == 1) {
if ($o['total'] > 5000) {
return $o['total'] * 0.9;
} else {
return $o['total'];
}
} else {
return 0;
}
}- магические числа 1, 5000, 0.9
- вложенность в три уровня
- массив без типов и подсказок IDE
enum OrderStatus: int
{
case Paid = 1;
}
final class DiscountCalculator
{
private const THRESHOLD = 5000;
private const RATE = 0.9;
public function total(Order $order): float
{
if ($order->status !== OrderStatus::Paid) {
return 0.0;
}
return $order->total > self::THRESHOLD
? $order->total * self::RATE
: $order->total;
}
}- имена вместо магических значений
- ранний возврат, один уровень вложенности
- типы и объект Order, работает автодополнение
Какие приёмы дают максимальный эффект
В PHP-проектах большую часть пользы приносят несколько простых техник. Извлечение метода превращает «простыню» в набор именованных шагов. Ранний возврат (guard clauses) убирает лишние вложенные if и делает основной сценарий заметным. Замена магических значений константами или enum даёт коду язык предметной области.
Отдельная история — отказ от «массивов-мешков». Value Object и DTO дают типы, валидацию на входе и автодополнение в редакторе. В современном PHP это почти бесплатно: constructor promotion и readonly-свойства позволяют описать объект буквально в несколько строк.
Наконец, внедрение зависимостей. Вместо вызовов new внутри методов и статических обращений к глобальному состоянию класс получает нужные объекты через конструктор. Это основа тестируемости: подменить мок проще, чем перепаивать работающий код. Ниже сведены основные приёмы в одной таблице.
| Приём | Что решает | Результат |
|---|---|---|
| Извлечение метода | Длинные методы, смешение задач | Код читается как план действий |
| Ранний возврат | Глубокая вложенность условий | Основной сценарий виден сразу |
| Enum и константы | Магические числа и строки | Смысл значений ясен из имён |
| Value Object / DTO | Массивы вместо структур данных | Типы, автодополнение, меньше ошибок |
| Constructor promotion + readonly | Шаблонный код в конструкторах | Меньше строк, неизменяемость данных |
| Внедрение зависимостей | Жёсткие связи, статика, new внутри логики | Код легко покрывается тестами |
Автоматизация: пусть рутину делают инструменты
Значительную часть механической работы можно доверить инструментам. Они не заменяют инженерное мышление, но экономят часы и снижают риск человеческих ошибок, особенно на больших кодовых базах.
Статический анализ стоит подключать с первого дня: начните с невысокого уровня строгости и постепенно повышайте его, фиксируя «базовую линию» существующих ошибок, чтобы новые не появлялись. Так вы не утонете в тысячах замечаний, а качество будет расти только вверх.
Инструменты, которые стоит добавить в PHP-проект
- 01
PHPStan и Psalm — статический анализ типов, поиск мёртвого кода и потенциальных ошибок без запуска программы.
- 02
Rector — автоматические преобразования кода: обновление до новых версий PHP, замена устаревших конструкций, добавление типов.
- 03
PHP CS Fixer и PHP_CodeSniffer — единый стиль кода и чистые диффы без споров о форматировании.
- 04
PHPUnit и Pest — регрессионные тесты, которые подтверждают, что поведение не изменилось.
- 05
PHPMD и метрики сложности — подсветка слишком длинных методов и перегруженных классов.
- 06
Infection — мутационное тестирование, показывающее, насколько тесты действительно ловят ошибки.
- 07
PhpStorm — безопасные встроенные рефакторинги: переименование, извлечение метода, перенос класса с обновлением ссылок.
Хорошая практика — встроить эти инструменты в CI: каждый pull request проходит тесты, анализатор и проверку стиля. Тогда качество кода перестаёт зависеть от внимательности отдельных людей и становится свойством процесса. А рефакторинг превращается из героического подвига в повседневную привычку.
Самое время посмотреть на типичные ошибки, из-за которых рефакторинг получает дурную славу.
Типичные ошибки при рефакторинге PHP-кода
Как делают неправильно
- Начинают менять код без тестов и проверки поведения.
- Смешивают в одном коммите рефакторинг, багфикс и новую функцию.
- Пытаются «переписать всё за один спринт» и замораживают разработку.
- Вводят сложные паттерны там, где хватило бы простой функции.
- Не согласуют изменения с командой и бизнесом, а потом удивляются конфликтам.
Как делать правильно
- 01
Сначала тесты и статический анализ, затем изменения.
- 02
Один коммит — одна цель, рефакторинг отдельно от функциональности.
- 03
Дробить работу на маленькие шаги и выкатывать их постепенно.
- 04
Выбирать самое простое решение, которое устраняет конкретную проблему.
- 05
Заранее договориться о приоритетах и доле времени на технический долг.
Как встроить рефакторинг в рабочий процесс
Самый устойчивый подход — не выделять рефакторинг в отдельный «проект», а заложить его в ритм команды. Например, 10–20% времени спринта отдаётся на улучшение кода, а все задачи по долгу фиксируются в трекере с понятной оценкой выгоды: «сократит время правок в модуле заказов» или «уберёт класс ошибок с неверными типами».
Для бизнеса аргументы лучше формулировать в деньгах и рисках. Сколько часов уходит на типовую доработку в проблемном модуле? Сколько багов в месяц приходит именно оттуда? За сколько новый разработчик входит в проект? Ответы на эти вопросы обычно убедительнее любых рассуждений о «чистоте кода».
Ниже — пример плана на месяц для среднего PHP-проекта. Цифры условные, но последовательность хорошо работает на практике.
Рефакторинг по неделям: от страховки к результату
Страховка
Подключить PHPStan с базовой линией, PHP CS Fixer и CI. Написать тесты на самый рискованный модуль.
Быстрые победы
Убрать дублирование, заменить магические значения на enum и константы, ввести ранние возвраты.
Структура
Выделить сервисы из God-класса, заменить массивы на DTO, внедрить зависимости через конструктор.
Закрепление
Поднять уровень анализатора, замерить метрики и выбрать следующий модуль по данным.
Итоги: как подойти к рефакторингу PHP-кода
- 01
Определите проблемные места по признакам: длинные методы, God-классы, вложенность, дублирование, массивы вместо объектов.
- 02
Начните со страховки — тестов, статического анализа и единого стиля.
- 03
Двигайтесь мелкими шагами: один приём — один коммит, проверка после каждого.
- 04
Используйте Rector, PHPStan, PHP CS Fixer и встроенные инструменты IDE, чтобы автоматизировать рутину.
- 05
Измеряйте результат и закладывайте долю времени на технический долг в каждом спринте.
Чем раньше команда превращает рефакторинг в привычку, тем дешевле обходится развитие продукта: меньше багов, быстрее релизы, проще вход новых разработчиков. Если проект уже разросся и непонятно, с чего начинать, помогает независимый аудит кода: он покажет приоритеты и реалистичный план работ.
Часто задаваемые вопросы
Это изменение внутренней структуры кода без изменения его внешнего поведения. Программа делает то же самое, но читается проще, ломается реже и легче расширяется.
Рефакторить стоит, когда вы собираетесь менять участок кода, находите в нём дублирование, глубокую вложенность или непонятные имена. Не стоит трогать код, который работает, не меняется и не мешает развитию продукта, особенно если нет тестов.
Можно, но рискованно. Минимум — написать характеризующие тесты на текущее поведение, включить статический анализ (PHPStan или Psalm) и двигаться очень мелкими шагами с проверкой после каждого.
Rector для автоматической миграции на новые версии PHP и правила обновления кода, PHPStan и Psalm для статического анализа, PHP CS Fixer для единого стиля, PHPUnit или Pest для тестов и встроенные рефакторинги PhpStorm.
Говорите на языке сроков и рисков: сколько времени уходит на правки в проблемном модуле, как часто там возникают баги и сколько стоит онбординг нового разработчика. Предложите закладывать на рефакторинг фиксированную долю спринта.
Нужен рефакторинг PHP-проекта без остановки разработки и потери функций?
Проведём аудит кода, составим план по приоритетам и покажем, что исправлять в первую очередь.
Спасибо за заявку!
Мы свяжемся с вами в ближайшее время.
Более 50 PHP-проектов приведено в порядок



