Под занавес прошлогоднего DevConf Артем Дегтярь и Павел Степанец рассказали как они мигрировали ERP-систему написанную на «голом» PHP5.3, работающую на винде, в Symfony + PHP7, и построили на его основе облачный сервис в сфере b2b. Видео доступно по ссылке доклада. А я представлю текстовый, немного сжатый, вариант.
Мы работали над большой системой, которая позволяла создавать заявки и менять статусы, плюс биллинг, учет ТМЦ и много всего. Сегодня мы расскажем как рефакторили эту систему, мигрировали ее в Symfony. Первоначально система была написана на чистом PHP, и имела много «особенностей». Например, этот пятиуровневый тернарник на слайде весьма оригинально работал с датой, пришедшей от юзера.
Еще один пример затейливости. Не самый оптимальный способ залогировать $_GET & $_POST. Перейдем к более объективным метрикам. PhpMetrics показала, что кода много, а файлов мало, и «поддерживаемость» кода была очень низкой.
Предыдущий программист покинул проект и нам он достался в наследство в таком состоянии:
Большой виндовый сервер, 400 пользователей, огромные контроллеры. Мы начали с того, что с помощью PhpMetrics построили граф зависимостей и нашли ключевые узлы системы. Покрыли их юнит-тестами и начали их переделывать. Вычистили «оригинальности» и по тестам мы видели, что ничего не сломалось.
С базой хотелось работать удобнее, чем с помощью чистого SQL. Включили в проект Doctrine ORM. Она довольно легко настроилась. Мы сгенерили XML-конфиг по существующей базе данных, а по нему и классы сущностей с аннотациями. Но не все было гладко. В базе не было ни одного foreign-ключа. Когда мы добавляли связи между сущностями, то доктрина пыталась эти связи создать. Но данные на тот момент в базе были неконсистентные и любая попытка создать ключи вызывала ошибки базы.
Не используйте доктрину без миграций! Мы использовали DoctrineMigrationBunde. Он позволяет просчитать разницу схем между базой данный и конфигом доктрины и сгенерировать миграцию. Неконсистентность убирали беспощадным delete from… left join(по foreign связи) where foreign field is null в миграциях.
Был один момент, когда код доктрины хорошо работал на локалке, но отказывался работать в продакшене. Оказалось что лексер аннотаций доктрины падал, когда встречал кириллические комментарии там. Не используйте их!(я бы посоветовал вообще избегать использования кириллицы где-либо кроме файлов локализации. прим. Adelf)
Следующим этапом стало внедрение HttpFoundation. Небольшая задача по переделке формы с POST на GET, что не самая приятная задача, если используются глобальные массивы $_GET & $_POST. Я решил интегрировать HttpFoundation из Symfony. И этот процесс прошел почти безболезненно. В код, который вызывал контроллер, просто стал передаваться симфониевский реквест-объект.
Логичным продолжение стала полная переработка фронт-контроллера. Раньше это был огромный файл, который делал все подряд. Подключал файлы зависимостей, инициализировал кучу глобальных(да-да) переменных, типа $DB, $USER, аутентификация, поиск, роутинг, логирование, проверка ошибок и вызов контроллеров. Результатом стала интеграция HttpKernel, компонента симфони, который позволяет полностью контролировать процесс выполнения HTTP-запроса. У него в зависимостях есть EventDispatcher и он вызывает там кучу полезных событий. Фронт-контроллер сильно упростился.
Создание объекта запроса. Вызов HttpKernel для получения ответа(response), который и отправляем, после чего завершаем работу. Но тут обнаружилась проблема. Шаурма-контроллеры проекта могли вернуть строку, а могли ничего не вернуть(null или false) и сделать echo сами.
Пришлось расширить стандартный HttpKernel, добавив в него то, что выделено на слайде. Все это происходило без feature freeze стадии. Задачи приходили постоянно. Но внедрение этих компонентов без ломания Bacward compatibility позволило имплементить фичи одновременно с рефакторингом.
Следующим этапом стало внедрение DependencyInjection. Система содержала большое количество сервисных классов, которые было сложно бутстрапить. Компонент DependencyInjection позволил гибко сконфигурировать все эти сервисы, предоставив единый механизм доступа к ним.
В старой системе была некая система плагинов, держащаяся на глобальных переменных и роутинг зависел от них в том числе. Мы решили перейти на стандартный роутинг Symfony. Внутри него, использовали старый резолвер, таким образом позволяя legacy-роутам тоже работать.
Настал тот день, когда мы решили полностью перейти на Symfony. В отдельной ветке гита мы выделили всю нашу шаурму в отдельный бандл. Однако сохранили всю физическую структуру файлов, чтобы избежать кучу конфликтов при merge/rebase с основной веткой. Как я уже описывал мы переписывали HttpKernel, однако на этом этапе решили сделать без воздействия на ядро. У нас был добавлен так называемый DefaultController, который и включил в себя всю ту схему с обработкой старых контроллеров. Однако под этот паттерн попадают все роуты, поэтому этот роут должен идти самым последним.
Шаурма упорно сопротивлялась. У нее был собственный авто-лоадер. Для этого в AppKernel::initializeContainer был добавлен вызов старого автолоадера: spl_register_autoload('oldAutoload');
Инициализация глобальных переменных никуда не пропала. Ее вынесли в listener onKernelRequest.
В итоге мы на данный момент имеем проект, в котором все еще много legacy, но его уже можно назвать Symfony-based и все новые фичи имплементить в Symfony-стиле. Причем мы делали это без feature freeze, поэтому бизнес был доволен.
Напоследок небольшой план рефакторинга для перевода проекта на Symfony.
Я посчитал это хорошим материалом для вечерне-пятничного поста. Приходите 18 мая на DevConf. Думаю, там можно будет услышать много похожих историй. Например, Андрей Брюханов хочет выступить с докладом "Переписать проект и выжить". А для читателей Хабра предусмотрена специальная регистрация со скидкой.
Автор: Adelf