В реальной разработке почти никто не начинает с идеально чистого репозитория, свежего Laravel и аккуратной архитектуры. Намного чаще в работу попадает уже давно живущий проект: Laravel 5.7, PHP 7.2, контроллеры по 800 строк, бизнес-правила вперемешку с SQL, шаблонами и побочными эффектами, а тестов либо нет вовсе, либо они остались как артефакт прошлой попытки «навести порядок». При этом продукт работает, у него есть пользователи, интеграции, накопленная доменная логика, и именно поэтому его нельзя бездумно ломать.

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


Почему старые Laravel-проекты становятся проблемой

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

Типичные симптомы «устаревшего» Laravel

  • Laravel 5.x, иногда 4.x, без обновлений до 8+/9+/10+.
  • PHP 7.2 или ниже, без плана обновления.
  • Минимум тестов (часто 0), ручное тестирование — основной способ проверки.
  • Структура приложения: всё в App\Http\Controllers, бизнес-логика в контроллерах и вьюхах.
  • Копипаста кода, дублирующие методы, магия вида new \DateTime('now') по всему проекту.
  • Отсутствие понимания, что именно делает каждая часть системы.
  • Сложности с деплоем, нет CI/CD, деплой «руками» или полуавтоматическими скриптами.

На практике к этому списку почти всегда добавляются ещё несколько неприятных признаков: нестабильные cron-задачи, «тихие» падения очередей, использование фасадов и статических вызовов в местах, где уже трудно отследить поток данных, а также сильная зависимость от конкретных людей в команде. Если один разработчик знает, почему здесь стоит этот if, а в документации этого нет, — это тоже форма технического долга.

Особенно тяжело сопровождать код, где бизнес-правила размазаны по нескольким слоям сразу: часть условий живёт в контроллере, часть — в Blade-шаблоне, часть — в observer, а ещё часть прячется в SQL-запросе. Такой код не просто сложно читать — его трудно безопасно менять. Любая новая задача превращается в исследовательскую работу с риском сломать соседний сценарий.

Почему просто «переписать» — плохая идея

Полная перепись старого Laravel-проекта почти всегда:

  • Стоит дороже, чем ожидается.
  • Занимает больше времени, чем казалось.
  • Несёт огромный риск ошибок и регрессий.
  • Уничтожает уже накопленный опыт, логику и интеграции.

С инженерной точки зрения rewrite часто выглядит привлекательно: хочется выбросить весь старый код, перейти на актуальный стек и начать «как надо». Но в реальности полная перепись редко переносит всю накопленную доменную логику с первого раза. Старый проект, каким бы неаккуратным он ни был, обычно содержит десятки исключений, обходных сценариев и интеграционных особенностей, о которых никто не вспоминает до тех пор, пока они не перестают работать.

Кроме того, перепись почти всегда конкурирует с текущей разработкой. Бизнес продолжает просить новые функции, баги нужно чинить сегодня, а не после завершения идеального нового продукта через полгода. В результате команда либо пытается поддерживать две кодовые базы сразу, либо затягивает rewrite до состояния, когда он съедает бюджет и всё равно не доходит до продакшена.

Гораздо разумнее поддерживать старый проект, постепенно улучшая его архитектуру, а не пытаться снести и переписать с нуля.


Шаг 1. Оценить состояние проекта

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

1.1. Зафиксировать текущее состояние

  • Зафиксируйте версии:
    • PHP
    • Laravel
    • базовых пакетов (composer.json)
  • Проверьте, есть ли уже CI/CD, Docker, скрипты деплоя.
  • Сделайте скриншоты ключевых экранов и процессов (если есть доступ).
  • Запишите, какие интеграции используются: API, платежные системы, аналитика, CRM.

Это не формальность и не бюрократия. Когда вы начнёте что-то менять, вам потребуется базовая точка сравнения: что было до изменений, какие сценарии существовали, какие зависимости влияют на поведение приложения. Хорошая практика — собрать короткий технический паспорт проекта: версия PHP, Laravel, список критичных пакетов, способ деплоя, очереди, cron, кэш, очередность запуска сервисов, внешние API и чувствительные бизнес-процессы.

Отдельно стоит посмотреть на composer.lock, конфигурации окружений, очереди, планировщик задач и логи последних ошибок. Очень часто именно там видно реальное состояние системы: например, проект «работает», но queue worker давно падает, часть писем не отправляется, а nightly-job завершался с ошибкой последние три недели. Такие вещи редко видны из интерфейса, но именно они потом становятся источником инцидентов.

1.2. Проверить минимальные требования

  • Поддерживает ли текущий PHP-версия обновления Laravel (хотя бы до 8.x).
  • Есть ли у хостинга/сервера возможность обновить PHP.
  • Есть ли доступ к базе данных, логам, окружению.

Если обновить PHP нельзя, это уже серьёзный ограничитель, и план будет строиться с учётом этого.

Здесь важно мыслить не только в категориях «запустится / не запустится», но и в категориях управляемости. Если у команды нет доступа к логам, невозможно выполнить резервное копирование базы, нет staging-окружения и никто точно не знает, как устроен деплой, то проблема не только в коде. Это уже ограничение инфраструктуры и процесса поставки. В таком случае любые обновления нужно планировать особенно осторожно: сначала восстановить базовую наблюдаемость и контроль, а потом двигаться к крупным изменениям.


Шаг 2. Внедрить минимальный базовый уровень качества

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

2.1. Начать писать тесты

Даже если проект старый, начинать можно с малого:

  • Пишите тесты для новых фич, которые вы добавляете.
  • Постепенно покрывайте критические части: авторизация, оплата, заказы, профиль пользователя.

Пример: вы добавляете новую кнопку «Отменить заказ». Перед этим пишете тест, который проверяет:

  • что заказ можно отменить;
  • что статус меняется;
  • что деньги не списываются дважды.

Это уже защита от регрессий.

В legacy-проекте тесты нужны не ради красивой метрики покрытия, а как страховка на самые дорогие сценарии. Если у вас нет тестов вообще, не пытайтесь покрыть всё сразу — это почти всегда заканчивается усталостью команды и пачкой нестабильных тестов. Намного полезнее действовать по принципу «touch it, test it»: трогаете участок кода — добавьте хотя бы один осмысленный тест вокруг него.

На практике лучше всего работают два типа тестов:

  • интеграционные или feature-тесты для критичных пользовательских сценариев;
  • точечные unit-тесты для новой вынесенной бизнес-логики, например сервисов и валидаторов.

Если код слишком связан и unit-тесты писать тяжело, это уже сигнал о проблеме дизайна. В таких местах сначала полезно сделать небольшой рефакторинг на выделение зависимостей, а уже потом покрывать тестами. С инженерной точки зрения это важнее, чем формально «добить» coverage.

2.2. Внедрить базовый CI/CD

Если его нет, добавьте минимальный конвейер:

  • composer install
  • php artisan test
  • php artisan lint (если есть)
  • деплой на staging (если есть)

Это не обязательно должно быть сложным: даже простой GitHub Actions или GitLab CI уже даёт огромный прирост стабильности.

Главный смысл CI/CD на старом проекте — не в модности инструмента, а в повторяемости. Пока сборка, проверка и деплой зависят от того, кто именно сегодня дежурит и не забыл ли он нужную команду, проект остаётся уязвимым. Даже минимальный pipeline уже убирает часть человеческого фактора.

Если php artisan lint в проекте не настроен, можно начать хотя бы с PHP_CodeSniffer, PHP CS Fixer или Laravel Pint — в зависимости от версии и возможностей кодовой базы. Полноценный статический анализ через PHPStan или Psalm тоже полезен, но на сильно старом проекте его лучше внедрять постепенно, с разумным baseline, иначе команда просто утонет в сотнях предупреждений и перестанет обращать на них внимание.


Шаг 3. Постепенно обновлять Laravel и PHP

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

3.1. Составить план обновления

Laravel 5.x → 8+/9+/10+ — это не один шаг. Нужен план:

  • 5.7 → 5.8 → 6.x → 7.x → 8.x → 9.x → 10.x (если актуально на момент чтения).
  • Каждый шаг — отдельный релиз, с тестированием.

Почему это важно:

  • Laravel 5.x уже давно не поддерживается.
  • Нет обновлений безопасности, что делает проект уязвимым.
  • Современные пакеты не поддерживают старые версии.

На практике полезно открыть официальные upgrade guides для каждой версии и пройтись по ним как по чек-листу. Не стоит надеяться, что composer update сам всё разрулит. Обычно проблемы возникают в нескольких типичных местах: middleware, аутентификация, очереди, mail, кастомные service provider’ы, старые хелперы и поведение сторонних пакетов.

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

3.2. Обновлять PHP параллельно

  • PHP 7.2 → 7.4 → 8.0 → 8.1 → 8.2 (в зависимости от дистрибуции).
  • После каждого обновления — проверка тестов, ручное тестирование.

Если хостинг не позволяет обновить PHP, стоит обсудить с заказчиком миграцию на более современный хостинг.

Обновление PHP — не только про совместимость с Laravel. Это ещё и про производительность, безопасность и предсказуемость рантайма. Переход на новые версии часто выявляет старые неявные допущения в коде: предупреждения становятся ошибками, меняется поведение некоторых встроенных функций, всплывают проблемы с типами. Именно поэтому обновление PHP нужно проверять отдельно, а не воспринимать как «техническую мелочь».

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


Шаг 4. Разделить бизнес-логику от контроллеров

Это один из самых заметных и практичных шагов в старом Laravel-проекте. Если кодовая база годами развивалась без архитектурной дисциплины, контроллеры почти наверняка стали точкой концентрации всего подряд: валидации, ветвлений, запросов к базе, вызовов API, преобразования данных и отправки уведомлений.

4.1. Вынести логику в сервисы

В старых Laravel-проектах бизнес-логика часто живёт в контроллерах. Это делает код нечитаемым и трудноподдерживаемым.

Решение: вынести логику в сервисы (App\Services):

Это уже делает код чище, тестировать легче.

С практической точки зрения сервисный слой полезен не потому, что это «архитектурно красиво», а потому что он снижает связность. Контроллер должен принимать запрос, делегировать работу и возвращать ответ. Как только в нём появляются сложные правила отмены заказа, расчёта скидок, работы со статусами и интеграции с платёжной системой, вы теряете возможность нормально тестировать и переиспользовать эту логику.

При этом не стоит превращать App\Services в новую свалку из сотен классов без ясной ответственности. Хороший сервис отвечает за один понятный сценарий или группу близких операций. Например, OrderCancellationService обычно лучше, чем расплывчатый OrderService, который делает всё сразу. Чем точнее названа ответственность, тем легче поддерживать код и проводить code review.

4.2. Использовать Repository Pattern

Для работы с базой данных можно использовать репозитории:

Это упрощает миграцию на новые ORM или архитектурные изменения в будущем.

Здесь важно сделать практическую оговорку: Repository Pattern полезен не сам по себе и не в каждом месте подряд. Если обернуть каждый вызов Eloquent в бессмысленный прокси, поддерживаемость не улучшится — появится только лишний слой абстракции. Репозитории действительно помогают там, где нужно централизовать сложные запросы, скрыть детали выборки, унифицировать доступ к данным или изолировать доменную логику от конкретного способа хранения.

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


Шаг 5. Внедрить минимальную архитектуру

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

5.1. Добавить модульность

Если проект растёт, можно начать с модульности:

  • App\Modules\Orders
  • App\Modules\Users
  • App\Modules\Payments

Каждый модуль — отдельная директория с контроллерами, сервисами, репозиториями, тестами.

Это не обязательно Laravel Packages, но уже даёт структуру.

Для старого проекта это особенно полезно: модульность позволяет не переделывать весь репозиторий сразу, а локально наводить порядок в тех областях, которые команда реально трогает. Например, если сейчас активно развивается платёжный контур, можно начать с Payments, а не пытаться одномоментно реструктурировать всё приложение.

С точки зрения сопровождения модульный подход выигрывает тем, что снижает когнитивную нагрузку. Новому разработчику проще понять систему, когда код по заказам, пользователям и оплатам разделён по предметным областям, а не разбросан по общим каталогам. Это напрямую влияет на скорость онбординга, качество ревью и риск случайных регрессий в соседних частях системы.

5.2. Использовать Events и Listeners

Laravel уже поддерживает события. Начните с простого:

  • OrderCreated, OrderCancelled
  • UserRegistered, UserUpdated

Это упрощает добавление новой логики без изменения существующего кода.

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

Но здесь тоже важна дисциплина. Не стоит превращать events в магию, когда уже невозможно понять, что реально происходит после создания заказа. Если событие запускает критичный для бизнеса процесс, это должно быть явно документировано и покрыто тестами. В противном случае вы получите не архитектуру, а сложно отслеживаемый каскад побочных эффектов.


Шаг 6. Улучшить безопасность

Старые Laravel-проекты часто уязвимы не потому, что в них есть один очевидный критический баг, а потому что накопилось много мелких инженерных упущений: устаревшие зависимости, слабая конфигурация окружения, ошибки в обработке входных данных, лишняя информация в логах и ответах сервера.

6.1. Обновить зависимости

  • composer update --dry-run — посмотреть, что можно обновить.
  • Проверить зависимости на уязвимости (composer audit или сторонние инструменты).

При этом обновлять зависимости лучше не массово и бездумно, а с пониманием критичности. Сначала смотрят на пакеты, связанные с безопасностью, авторизацией, HTTP-клиентами, сериализацией, загрузкой файлов и внешними интеграциями. После каждого набора обновлений проект нужно прогонять через тесты и хотя бы минимальный smoke-test на staging. Иначе попытка повысить безопасность легко превращается в новый источник инцидентов.

6.2. Внедрить базовые практики

  • Использовать php artisan key:generate для ключа приложения.
  • Не хранить пароли в коде (.env).
  • Использовать HTTPS.
  • Проверять входящие данные (валидация).
  • Не выводить детали ошибок в продакшене.

Это базовые вещи, но именно их часто не хватает в проектах, которые «как-то выросли сами собой». Я бы также добавил сюда ревизию прав доступа к административным маршрутам, проверку хранения резервных копий, аудит логирования чувствительных данных и контроль за тем, какие значения попадают в исключения и внешние системы мониторинга.

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


Шаг 7. Внедрить мониторинг и логирование

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

7.1. Логирование ошибок

  • Использовать Sentry, Bugsnag или просто логи.
  • Логировать критические ошибки (Log::error).

Важно не просто складывать всё подряд в лог-файлы, а сделать сообщения полезными для диагностики. Хороший лог содержит контекст: идентификатор пользователя или заказа, имя сценария, внешний сервис, correlation id, входные параметры без чувствительных данных. Бесполезный лог — это строка «Something went wrong» без намёка на то, что именно произошло.

На старом проекте полезно также проверить, не логируются ли пароли, токены, персональные данные и другие чувствительные значения. Это частая проблема в системах, где отладка когда-то делалась «на скорую руку», а потом осталась в продакшене на годы.

7.2. Мониторинг производительности

  • Использовать Laravel Telescope (если подходит).
  • Или сторонние инструменты (New Relic, Datadog).

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

Если Telescope слишком тяжёлый или не подходит для конкретного окружения, можно начать с внешнего APM и метрик инфраструктуры. Главное — добиться, чтобы поведение приложения стало наблюдаемым, а не оставалось догадкой.


Шаг 8. Обновлять документацию

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

8.1. Документировать архитектуру

  • Какие модули есть.
  • Какие сервисы используются.
  • Какие события и слушатели.

Это особенно важно, если в команде новые разработчики.

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

8.2. Документировать процессы

  • Как деплоить.
  • Как запускать тесты.
  • Как обновлять зависимости.

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

Хорошо написанная процессная документация снижает bus factor и делает сопровождение менее зависимым от конкретных участников команды. Для legacy-систем это одна из самых практичных инвестиций.


Заключение

Поддерживать старый Laravel-проект без полной переписи — реально, если подходить к этому системно. Начните с оценки состояния, внедрите минимальный уровень качества (тесты, CI/CD), постепенно обновляйте Laravel и PHP, выносите бизнес-логику в сервисы, внедряйте базовую архитектуру и улучшайте безопасность.

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

Это не «волшебная таблетка», но постепенный подход позволит вам поддерживать проект, не разрушая уже работающее приложение.


FAQ

Вопрос: Сколько времени займёт обновление Laravel с 5.7 до 10.x?

Ответ: Зависит от проекта. Для небольшого проекта — несколько дней. Для большого — несколько недель. Главное — делать это пошагово, с тестами.

На практике срок больше всего зависит не от числа файлов, а от количества интеграций, кастомных пакетов, уровня связности кода и наличия автоматических проверок. Если проект хорошо покрыт хотя бы базовыми feature-тестами, обновление идёт заметно спокойнее.

Вопрос: Можно ли обновить Laravel без обновления PHP?

Ответ: Теоретически можно, но это не рекомендуется. Laravel 10.x требует PHP 8.1+. Без обновления PHP вы теряете безопасность и производительность.

Кроме того, разрыв между версией фреймворка и версией рантайма обычно быстро приводит к новым ограничениям по пакетам и инфраструктуре. Поэтому обновление PHP лучше рассматривать как часть общего плана, а не как необязательное приложение к Laravel.

Вопрос: Как начать писать тесты, если их вообще нет?

Ответ: Начните с критических частей: авторизация, оплата, заказы. Пишите тесты для новых фич, а старые части покрывайте по мере возможности.

Если совсем неясно, с чего стартовать, начните с одного smoke-сценария на самый важный бизнес-поток. Даже один надёжный тест лучше, чем абстрактный план «покрыть всё потом».

Вопрос: Нужно ли сразу внедрять модульность?

Ответ: Не обязательно. Сначала вынесите логику в сервисы, затем, когда проект растёт, добавьте модульность.

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

Вопрос: Как убедить заказчика, что нужно обновлять Laravel?

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

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


Если вы хотите, чтобы ваш старый Laravel-проект не превратился в бесконечный источник технического долга, начните с малого: тесты, CI/CD, обновление Laravel и PHP. Даже эти базовые шаги уже дают заметный прирост стабильности, безопасности и управляемости разработки. А дальше — шаг за шагом — можно выстроить кодовую базу, которую не страшно развивать и сопровождать.