Хабрахабр

DevConf: из шаурмы в Symfony или миграция legacy

Под занавес прошлогоднего DevConf Артем Дегтярь и Павел Степанец рассказали как они мигрировали ERP-систему написанную на «голом» PHP5.3, работающую на винде, в Symfony + PHP7, и построили на его основе облачный сервис в сфере b2b. Видео доступно по ссылке доклада. А я представлю текстовый, немного сжатый, вариант.

Мы работали над большой системой, которая позволяла создавать заявки и менять статусы, плюс биллинг, учет ТМЦ и много всего. Сегодня мы расскажем как рефакторили эту систему, мигрировали ее в Symfony. Первоначально система была написана на чистом PHP, и имела много «особенностей». Например, этот пятиуровневый тернарник на слайде весьма оригинально работал с датой, пришедшей от юзера.

Не самый оптимальный способ залогировать $_GET & $_POST. Еще один пример затейливости. PhpMetrics показала, что кода много, а файлов мало, и «поддерживаемость» кода была очень низкой.
Предыдущий программист покинул проект и нам он достался в наследство в таком состоянии: Перейдем к более объективным метрикам.

Мы начали с того, что с помощью PhpMetrics построили граф зависимостей и нашли ключевые узлы системы. Большой виндовый сервер, 400 пользователей, огромные контроллеры. Вычистили «оригинальности» и по тестам мы видели, что ничего не сломалось.
С базой хотелось работать удобнее, чем с помощью чистого SQL. Покрыли их юнит-тестами и начали их переделывать. Она довольно легко настроилась. Включили в проект Doctrine ORM. Но не все было гладко. Мы сгенерили XML-конфиг по существующей базе данных, а по нему и классы сущностей с аннотациями. Когда мы добавляли связи между сущностями, то доктрина пыталась эти связи создать. В базе не было ни одного foreign-ключа. Но данные на тот момент в базе были неконсистентные и любая попытка создать ключи вызывала ошибки базы.

Мы использовали DoctrineMigrationBunde. Не используйте доктрину без миграций! Неконсистентность убирали беспощадным delete from… left join(по foreign связи) where foreign field is null в миграциях.
Был один момент, когда код доктрины хорошо работал на локалке, но отказывался работать в продакшене. Он позволяет просчитать разницу схем между базой данный и конфигом доктрины и сгенерировать миграцию. Не используйте их!(я бы посоветовал вообще избегать использования кириллицы где-либо кроме файлов локализации. Оказалось что лексер аннотаций доктрины падал, когда встречал кириллические комментарии там. Adelf) прим.

Небольшая задача по переделке формы с POST на GET, что не самая приятная задача, если используются глобальные массивы $_GET & $_POST. Следующим этапом стало внедрение HttpFoundation. И этот процесс прошел почти безболезненно. Я решил интегрировать HttpFoundation из Symfony. В код, который вызывал контроллер, просто стал передаваться симфониевский реквест-объект.

Раньше это был огромный файл, который делал все подряд. Логичным продолжение стала полная переработка фронт-контроллера. Результатом стала интеграция HttpKernel, компонента симфони, который позволяет полностью контролировать процесс выполнения HTTP-запроса. Подключал файлы зависимостей, инициализировал кучу глобальных(да-да) переменных, типа $DB, $USER, аутентификация, поиск, роутинг, логирование, проверка ошибок и вызов контроллеров. Фронт-контроллер сильно упростился. У него в зависимостях есть EventDispatcher и он вызывает там кучу полезных событий.

Вызов HttpKernel для получения ответа(response), который и отправляем, после чего завершаем работу. Создание объекта запроса. Шаурма-контроллеры проекта могли вернуть строку, а могли ничего не вернуть(null или false) и сделать echo сами. Но тут обнаружилась проблема.

Все это происходило без feature freeze стадии. Пришлось расширить стандартный HttpKernel, добавив в него то, что выделено на слайде. Но внедрение этих компонентов без ломания Bacward compatibility позволило имплементить фичи одновременно с рефакторингом. Задачи приходили постоянно.

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

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

В отдельной ветке гита мы выделили всю нашу шаурму в отдельный бандл. Настал тот день, когда мы решили полностью перейти на Symfony. Как я уже описывал мы переписывали HttpKernel, однако на этом этапе решили сделать без воздействия на ядро. Однако сохранили всю физическую структуру файлов, чтобы избежать кучу конфликтов при merge/rebase с основной веткой. Однако под этот паттерн попадают все роуты, поэтому этот роут должен идти самым последним. У нас был добавлен так называемый DefaultController, который и включил в себя всю ту схему с обработкой старых контроллеров.

У нее был собственный авто-лоадер. Шаурма упорно сопротивлялась. Для этого в AppKernel::initializeContainer был добавлен вызов старого автолоадера: spl_register_autoload('oldAutoload');

Ее вынесли в listener onKernelRequest. Инициализация глобальных переменных никуда не пропала.

Причем мы делали это без feature freeze, поэтому бизнес был доволен. В итоге мы на данный момент имеем проект, в котором все еще много legacy, но его уже можно назвать Symfony-based и все новые фичи имплементить в Symfony-стиле.

Напоследок небольшой план рефакторинга для перевода проекта на Symfony.

Я посчитал это хорошим материалом для вечерне-пятничного поста. Приходите 18 мая на DevConf. Думаю, там можно будет услышать много похожих историй. Например, Андрей Брюханов хочет выступить с докладом "Переписать проект и выжить". А для читателей Хабра предусмотрена специальная регистрация со скидкой.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть