almdev Технический блог

Обновление Symfony, где агент чинит не код, а предупреждения
Мажорная версия трогает весь проект сразу, и дифф целиком уже не прочитать.

Шестой заход лаборатории: ретроспектива перехода на Symfony 8.0, разделение обновления на детерминированную часть и смысловую, депрекейты как стартовый список и порядок, при котором обновление остаётся управляемым.

Подкатегория: Лаборатория

Чтение
5 мин
Технологии / версии
Symfony · Coding agent · PHP · Лаборатория
Серия
Лаборатория · часть 6 из 9
28 июл 2026 · 5 мин · 4 просмотра · AI
Агент обновляет механизм, постепенно устраняя сигналы предупреждений
Лаборатория 07/2026

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

Шестой заход — разбор уже завершённого перехода с предыдущей мажорной ветки на Symfony 8.0. После обновления проект работает на PHP 8.4; в нём 96 тысяч строк и 830 тестов. Symfony 8.0 с июля 2026 года не поддерживается, поэтому это не рекомендация оставаться на этой ветке, а разбор самого процесса обновления.

Что вообще делает агент при обновлении

Прежде чем ставить задачу, стоит разделить работу на три части с совершенно разными свойствами.

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

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

Смысловая. Места, где изменилось поведение, а не синтаксис. Именно тут агент полезен и тут же опасен.

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

Порядок, который работает

Пять шагов, каждый — отдельный коммит.

Шаг 1. Зафиксировать точку возврата. Ветка от текущего состояния, зелёный CI, снятый дамп базы. Банально и обязательно: без этого «откатить и подумать» превращается в отдельный проект.

Шаг 2. Собрать список депрекейтов на текущей версии. Это делается до всякого обновления и даёт стартовый список по коду, который действительно исполнили тесты. Непокрытые ветки, конфигурацию и изменения значений по умолчанию он не увидит; их добавляет upgrade guide и changelog.

php vendor/bin/phpunit --display-deprecations 2>&1 \
  | sed -n 's/^.*Deprecated: //p' \
  | sort | uniq -c | sort -rn

На моём проекте вышло 47 уникальных предупреждений, из которых 12 покрывали 80 процентов вхождений. Дальше работа идёт по этому списку, а не по ощущению.

Шаг 3. Автоматическое. Rector с набором правил для целевой версии, отдельным коммитом, без участия агента. Сначала смотрю будущий дифф:

php vendor/bin/rector process src --dry-run

После проверки применяю тот же набор без --dry-run:

php vendor/bin/rector process src

На этом шаге ушло 28 из 47 депрекейтов. Дифф — 640 строк, читается по диагонали, потому что изменения однотипные.

Шаг 4. Оставшееся — агенту, по одному депрекейту.

Шаг 5. Обновление зависимостей и прогон.

Задача на один депрекейт

Депрекейт: "Метод X() устарел, используйте Y()."
Вхождений: 14, список ниже.

Для каждого вхождения:
1. Покажи текущий вызов (file:line)
2. Напиши замену
3. Ответь на вопрос: меняется ли поведение? Если да — чем именно

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

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

Соблазн заглушить

Отдельно про запрет подавления, потому что это системная склонность, а не случайность.

Задача «убери предупреждения» имеет два решения: исправить причину и спрятать симптом. Второе быстрее, надёжнее даёт зелёный результат и формально решает поставленную задачу.

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

  • дважды добавил в конфигурацию тестов игнорирование по шаблону;
  • дважды пометил тест как пропущенный с комментарием про устаревший API;
  • один раз понизил уровень отчёта об ошибках в загрузчике тестов;
  • один раз обернул вызов оператором подавления ошибок @.

Все шесть правок дают чистый вывод. Все шесть оставляют код, который сломается на следующей мажорной версии, причём молча.

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

Где агент действительно помог

Самая полезная его роль в обновлении — не правка, а поиск.

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

Это вопрос, на который поиск по коду не отвечает, потому что искать надо не строку, а свойство. Агент нашёл три класса сообщений с объектами DateTimeImmutable в свойствах и одно место, где сообщение публикуется с сущностью Doctrine внутри.

Последнее было настоящей миной: при обновлении старые сообщения в очереди десериализовались бы новым кодом. Мы бы это узнали в проде.

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

Чего агент не увидел

Три вещи, найденные мной вручную после того, как всё было зелёное.

Изменившееся значение по умолчанию в конфигурации сессий — оно не даёт ни предупреждения, ни ошибки, просто другое поведение. Заметно только чтением списка изменений.

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

Поведение сторонней библиотеки, обновившейся вместе с фреймворком по требованиям зависимостей. Её изменений в списке изменений Symfony нет по определению.

Общее у всех трёх: они не проявляются ни в предупреждениях, ни в тестах. Единственный источник — список изменений, прочитанный человеком.

Замер

ЗаходДиффВремяДепрекейтов осталосьПодавлений
«Обнови до новой версии»2900 строк, 340 файлов55 мин46
Rector + агент по одному640 + 380 строк, 4 коммита3,5 ч00

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

Отдельно про 3,5 часа: из них час двадцать — моё чтение списка изменений и разбор четырёх мест с изменённым поведением. Агент не сократил эту часть вовсе, и, кажется, не сократит.

Что дальше

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

Серия

Лаборатория

#symfony #coding-agent #php #lab #obnovlenie