Мы работали над большой системой, которая позволяла создавать заявки и менять статусы, плюс биллинг, учет ТМЦ и много всего. Сегодня мы расскажем как рефакторили эту систему, мигрировали ее в 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. Думаю, там можно будет услышать много похожих историй. Например, Андрей Брюханов хочет выступить с докладом "Переписать проект и выжить". А для читателей Хабра предусмотрена специальная регистрация со скидкой.
Комментарии (16)
PerlPower
27.04.2018 18:16Сколько времени было на это все потрачено, и сколько человек работало на какой загрузке? И каков размер проекта, например в строках?
stepps
28.04.2018 13:37+1Двое нас, полгода примерно. Без фичефриза.
PerlPower
28.04.2018 19:07И все же какой размер у проекта, если это не секрет?
Можете рассказать как вы подошли к тестированию такого кода? Покрывали методы как через обычный phpunit, или тестировали чисто по функционалу, через http запросы? Сделали какие-то выводы для себя в процессе создания тестов, что вы бы стали делать по-другому?
Dreyk
28.04.2018 08:33очень знакомо :) вам повезло, что в команде остался хоть кто-то (не обязательно программист), который знает, как должна работать система и почему.
а то внезапно оказывается, что шаурма обеспечивала функционал
evgwed
28.04.2018 11:07+1Наличие в первоначальной версии фронт контроллера очень сильно упрощает переписывание кода на современные технологии. А вот если бы его не было, то было бы очень больно. Все же у вас не самый хардкорный legacy был.
А что было с проектом, пока вы его переписывали? Бизнес же не может стоять. А если делать постепенно, то есть шанс превратить проект в некоторый франкенштейн, который также сложно поддерживать.vtvz_ru
28.04.2018 13:34У нас такая же ситуация была. Но фронт контроллера не было, а было распихано по файлам. Проект был в настолько плохом состоянии, что написание тестов только навредило бы. Постелено внедрил в проект Yii2. Теперь от старого кода осталась только БД, которую я постепенно мигрирую на новый манер. Удивительно, но прям сложно это не было.
Сейчас понимаю, что Yii2 — не самый подходящий для нашего проекта инструмент и хочу как-то переползти на Symfony. Только Yii2 не очень-то учит хорошим практикам программирования, и из-за нехватки опыта в свое я успел научиться плохому и жёстко привязать проект к фреймворку. И вот теперь я понимаю, что мигрировать с одного фреймворка на другой будет сложнее, чем этого хотелось
stepps
28.04.2018 13:47Закастомизировали ControllerResolver, он подцеплял легаси контроллеры. Новый функционал писали уже в стиле Symfony, чтобы потом безболезненно к фреймфорку зацепить. Попутно внедряя компоненты типа DI, Security, Twig. Как переехали на Symfony, стали в бандлах писать. А вот совсем недавно весь легась был полностью выпилен! ))
Fantyk
28.04.2018 15:15На сколько у вас деградировала производительность при переходе на Doctrine? В текущем проекте пытались переползти на Doctrine, но разница с AR существенна.(
Чтобы заставить Doctrine быть производительной нужно писать много кода.stepps
28.04.2018 16:10Если сравнивать с легаси, то производительность даже выросла. Наш предшественник не много уделял внимания производительности. Например, был класс проверяющий права пользователя, который лазил в базу, кеширования никакого не было, соответственно, на каком-нибудь хитром ui, где нужно было проверять показывать или нет кнопки пользователю, выходило несколько сотен запросов. Самая жесть была на странице, где выводился график всех пользователей с задачами (примерно как в гугл-календаре), там было около 30к запросов.
Так что провести сравнение до/после доктрины возможности не было.
VolCh
symfony (со строчной буквы) — это древнее legacy под PHP 5.3 (хотя есть неофициальный форк до 7.2 включительно точно), современный скелет, фреймворк и набор компонентов — это Symfony. Думал пост о переходе на него :)
А использовались сессии на проекте где-то в глубине? Чаще всего с ними больше всего проблем при переходе из всех суперглобалов, поскольку они активно модифицируются.
VolCh
*о переходе на symfony, aka symfony1
Adelf Автор
Исправил. Спасибо. Мы, ларавельщики, такой тонкой разницы не чувствуем.
porn
Вы серьёзно не особо чувствуете разницу между Symfony разных мажорных версий?!
VolCh
По опыту перехода по всем версиям, начиная с 1.2 до 4.0 с разным шагом на разных проектах, только с 1.* на 2.0 проблемы были серьёзные, что на годы затягивалось, собственно полная смена фреймворка и ActiveRecord на DataMapper.
Adelf Автор
Я про case sensitive названий :)
stepps
Для аутентификации и для сохранения форм фильтров. Не припомню, каких-то особых проблем с сессией. Сложнее security было натянуть на существующую логику авторизации.