Под занавес прошлогоднего 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. Думаю, там можно будет услышать много похожих историй. Например, Андрей Брюханов хочет выступить с докладом "Переписать проект и выжить". А для читателей Хабра предусмотрена специальная регистрация со скидкой.

Комментарии (16)


  1. VolCh
    27.04.2018 17:21
    +1

    symfony (со строчной буквы) — это древнее legacy под PHP 5.3 (хотя есть неофициальный форк до 7.2 включительно точно), современный скелет, фреймворк и набор компонентов — это Symfony. Думал пост о переходе на него :)

    А использовались сессии на проекте где-то в глубине? Чаще всего с ними больше всего проблем при переходе из всех суперглобалов, поскольку они активно модифицируются.


    1. VolCh
      27.04.2018 17:22

      *о переходе на symfony, aka symfony1


    1. Adelf Автор
      27.04.2018 17:40

      Исправил. Спасибо. Мы, ларавельщики, такой тонкой разницы не чувствуем.


      1. porn
        27.04.2018 18:10

        Вы серьёзно не особо чувствуете разницу между Symfony разных мажорных версий?!


        1. VolCh
          27.04.2018 19:49

          По опыту перехода по всем версиям, начиная с 1.2 до 4.0 с разным шагом на разных проектах, только с 1.* на 2.0 проблемы были серьёзные, что на годы затягивалось, собственно полная смена фреймворка и ActiveRecord на DataMapper.


        1. Adelf Автор
          27.04.2018 20:09

          Я про case sensitive названий :)


    1. stepps
      28.04.2018 13:59

      Для аутентификации и для сохранения форм фильтров. Не припомню, каких-то особых проблем с сессией. Сложнее security было натянуть на существующую логику авторизации.


  1. PerlPower
    27.04.2018 18:16

    Сколько времени было на это все потрачено, и сколько человек работало на какой загрузке? И каков размер проекта, например в строках?


    1. stepps
      28.04.2018 13:37
      +1

      Двое нас, полгода примерно. Без фичефриза.


      1. PerlPower
        28.04.2018 19:07

        И все же какой размер у проекта, если это не секрет?

        Можете рассказать как вы подошли к тестированию такого кода? Покрывали методы как через обычный phpunit, или тестировали чисто по функционалу, через http запросы? Сделали какие-то выводы для себя в процессе создания тестов, что вы бы стали делать по-другому?


  1. Dreyk
    28.04.2018 08:33

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


  1. evgwed
    28.04.2018 11:07
    +1

    Наличие в первоначальной версии фронт контроллера очень сильно упрощает переписывание кода на современные технологии. А вот если бы его не было, то было бы очень больно. Все же у вас не самый хардкорный legacy был.

    А что было с проектом, пока вы его переписывали? Бизнес же не может стоять. А если делать постепенно, то есть шанс превратить проект в некоторый франкенштейн, который также сложно поддерживать.


    1. vtvz_ru
      28.04.2018 13:34

      У нас такая же ситуация была. Но фронт контроллера не было, а было распихано по файлам. Проект был в настолько плохом состоянии, что написание тестов только навредило бы. Постелено внедрил в проект Yii2. Теперь от старого кода осталась только БД, которую я постепенно мигрирую на новый манер. Удивительно, но прям сложно это не было.
      Сейчас понимаю, что Yii2 — не самый подходящий для нашего проекта инструмент и хочу как-то переползти на Symfony. Только Yii2 не очень-то учит хорошим практикам программирования, и из-за нехватки опыта в свое я успел научиться плохому и жёстко привязать проект к фреймворку. И вот теперь я понимаю, что мигрировать с одного фреймворка на другой будет сложнее, чем этого хотелось


    1. stepps
      28.04.2018 13:47

      Закастомизировали ControllerResolver, он подцеплял легаси контроллеры. Новый функционал писали уже в стиле Symfony, чтобы потом безболезненно к фреймфорку зацепить. Попутно внедряя компоненты типа DI, Security, Twig. Как переехали на Symfony, стали в бандлах писать. А вот совсем недавно весь легась был полностью выпилен! ))


  1. Fantyk
    28.04.2018 15:15

    На сколько у вас деградировала производительность при переходе на Doctrine? В текущем проекте пытались переползти на Doctrine, но разница с AR существенна.(
    Чтобы заставить Doctrine быть производительной нужно писать много кода.


    1. stepps
      28.04.2018 16:10

      Если сравнивать с легаси, то производительность даже выросла. Наш предшественник не много уделял внимания производительности. Например, был класс проверяющий права пользователя, который лазил в базу, кеширования никакого не было, соответственно, на каком-нибудь хитром ui, где нужно было проверять показывать или нет кнопки пользователю, выходило несколько сотен запросов. Самая жесть была на странице, где выводился график всех пользователей с задачами (примерно как в гугл-календаре), там было около 30к запросов.
      Так что провести сравнение до/после доктрины возможности не было.