Всем привет!
В этой статье я хочу рассказать о своем опыте переезда на “отвечающую современным трендам” платформу в одном legacy проекте.
Все началось примерно год назад, когда меня перекинули в “старый” (для меня новый) отдел.
До этого я работал с Symfony/Laravel. Перейдя на проект с самописным фреймворком количество WTF просто зашкаливало, но со временем все оказалось не так и плохо.
Во-первых, проект работал. Во-вторых, применение шаблонов проектирования прослеживалось: был свой контейнер зависимостей, ActiveRecord и QueryBuilder.
Плюс, был дополнительный уровень абстракции над контейнером, логгером, работе с очередями и зачатки сервисного слоя(бизнес логика не зависела от HTTP слоя, кое-где логика была вынесена из контроллеров).
Далее я опишу те вещи, с которыми трудно было мириться:
1. Логгер log4php
Сам по себе логгер работал и хорошо. Но были жирные минусы:
- Отсутствие интерфейса
- Сложность конфигурации для задач чуть менее стандартных (например, отправлять логи уровня error в ElastickSearch).
- Подавляющее большинство компонентов мира opensource зависят от интерфейса PsrLogLoggerInterface. В проекте все равно пришлось держать оба логгера.
2-6. Контроллеры были вида:
<?php
class AwesomeController
{
public function actionUpdateCar($carId)
{
$this->checkUserIsAuthenticated();
if ($carId <= 0) {
die('Машина не найдена');
}
$car = Car::findById($carId);
$name = @$_POST['name'];
container::getCarService()->updateNameCar($car, $name);
echo json_decode([
'message' =>'Обновление выполнено'
]);
}
}
- die посреди выполнения кода приложения
- echo еще до выхода из контроллера.
- Аутентификация пользователей подключалась в каждом контроллере отдельно
- Классические $_POST и @.
- Отсутствовала возможность внедрять сервисы в контроллер.
7. Отсутствие глобального логирования ошибок приложения
Максимум, что можно было найти в логах — это текст сообщения. Более подробную информацию об ошибке можно было получить после повторения на стенде разработки. Для дополнительного логирования на боевом окружении приходилось ставить блоки try/catch в нужном методе контроллера.
8. Конфигурирование контейнера зависимостей
Контейнер зависимостей напоминал Symfony контейнер времен 2.4. Каждый сервис требовал регистрации и описания как его собрать. Имея опыт работы с контейнером laravel, где максимально используется autowiring, хотелось избавиться от рутинных действий. Так же отсутствие autowiring снижало желание программистов писать отдельные сервисы (создавать новые классы) под отдельную бизнес задачу, так как это подразумевало необходимость править конфиг контейнера. При этом всегда есть вероятность ошибиться и потерять еще больше времени.
9. Роутинг
Роутинг был логичен и прост, по мотивам в Yii1.
Адрес вида www.carexchange.ru/awesome_controller/update_car означал выполнение контроллера AwesomeController и метода actionUpdateCar.
Но, к сожалению, были вложенные поддиректории сайта и приходилось создавать url’ы вида
www.carexchange.ru/awesome_controller_for_car_insite_settings_approve/update_car
Это не напрягает, но ограничение странное
10. Хардкод url’ов
Роутинг был простым, поэтому отсутствовала возможность генерации url автоматически (зачем усложнять). Это привело к тысячам ссылок, которые были захардкожены и в php и js. Мы, конечно, редко меняем url’ы, но иногда такое случается. И искать их по проекту сложно.
Пора что то менять!
С приходом еще одного программиста стали подниматься вопросы о возможности рефакторинга, было желание сделать “по человечнее”. “По человечнее” — читай привычнее для современного разработчика. Читать и поддерживать существующий код было сложно->долго->дорого.
После нескольких обсуждений с руководством был получен зеленый флаг и началась работа над proof of concept.
С самого начала было принято решение придерживаться современных стандартов. Это и рекомендации PSR, и следование стандартному поведению других фреймворков. Новые разработчики работавшие на любом современном фреймворке должны были понять где найти контроллеры, как собираются сервисы и где писать бизнес логику.
Если внимательнее посмотреть на озвученные претензии, то мы заметим: страдает код уровня приложения (контроллеры) и инфраструктурный слой (контейнер).
Бизнес логика была написана отдельно и не зависела от уровня HTTP — ее оставляем как есть. Active Record и QueryBuilder также не трогаем, так как они работали и не сильно отличались от той же doctrine/dbal.
Выбор фреймворка
На самом деле выбор тут был не велик. Тащить весь laravel или symfony ради слоя над HTTP нет смысла. А нужные компоненты всегда можно подключить через composer.
Серьезный выбор был между двумя микро-фреймворками: Slim и Zend.
Оба этих фреймворка полностью поддерживают PSR-7 и PSR-11.
Почему не Lumen? Главная причина конечно же в том, что Lumen сложно назвать “микро” вкупе со всем этим добром. Встроить Lumen в существующий проект сложно. Контейнер зависимостей легко не подменишь (необходимо соблюдение контракта illuminate). Контракт PSR-7 фреймворк поддерживает, но все равно зависит от symfony/http-foundation.
Сначала я всерьез взялся за Zend. Но потратив 2 дня, посмотрев на реализацию приложения в идеологии "все middleware", увидев как формируется конфиг контейнера, я с ужасом представил как буду объяснять менее опытным разработчикам чем invokables отличается от factories, и когда писать aliases. Перфекционистам и академикам Zend должен прийтись по нраву. Приложение работает через pipeline и middleware. Но я испугался более высокого порога входа, в то время как переезд должен был быть легким, в идеале незаметным.
Затем я переключился на Slim. Его внедрение в проект заняло меньше дня. Выбор контроллеров (старого и новго образца) был реализовано через middleware. На Slim и остановился. В далеких планах перейти на pipeline с PSR-15 middleware.
Выбор контейнера
Здесь я просто скажу что остановился на league/container, и попытаюсь объяснить свой выбор.
- Это поддержка PSR-11.
Сейчас большинство контейнеров уже поддерживают PSR-11, но год назад лишь малая часть поддерживала container/interop интерфейс. - Autowiring.
- Синтасис довольно прост, в противопоставление тому же zend-servicemanager.
- Сервис провайдеры, позволяющие писать модули еще более изолированно.
В illuminate/container провайдеры регистрируются на уровне приложения, а в league/container провайдеры регистрируются на уровне контейнера. Таким образом приложение зависит только от контейнера, а контейнер зависит от сервис провайдеров. - Делегирование контейнеров. Эта ”фича” оказалась решающей для этапа замены контейнера, поэтому раскрою ее подробнее.
При желании внутри league/container может быть несколько PSR-11 совместимых контейнеров.
Возможный сценарий: вы решили сменить ваш старый контейнер на symfony/dependency-injection. Чтобы переходить постепенно вы можете подключить league/container и в делегаты поместить и ваш старый контейнер и контейнер symfony. При поиске сервиса ваш старый контейнер будет опрашиваться самым первыми, затем будет поиск в контейнере symfony. На следующем этапе вы сможете перенести описания всех сервисов в контейнер symfony и оставить только его. Так как код зависит от PSR-11 интерфейса — изменения минимальны.
Выбор абстракции над HTTP
Тут всего 3 варианта:
Кстати Slim движется к выделению реализации HTTP в отдельный пакет(ожидается в ветке 4.0).
Symfony bridge использовать не хотелось по причине лишнего кода и лишней зависимости. Так как Slim ни в чем нас не ограничивает, предпочтение было отдано реализации Zend. Это только увеличило независимость кода приложения от HTTP слоя.
Логированиe
Тут ничего кроме monolog в голову не приходит. Его и прикрутили. Во время разработки бывают полезны PHPConsoleHandler и ChromePHPHandler
Роутинг
Slim из коробки имеет FastRoute. На его основе появились именованные роуты. Генерация URL реализована через глобальный хелпер (Как здесь)
Ну и что изменилось?
Сейчас наш контроллер выглядит так:
<?php
namespace Controllers;
use PsrHttpMessageServerRequestInterface;
use PsrHttpMessageResponseInterface;
use ZendDiactorosResponseJsonResponse;
use DomainCarServicesCarService;
class AwesomeController
{
/**
* @var CarService
*/
private $carService;
public function __construct(CarService $carService)
{
$this->carService = $carService;
}
public function actionUpdateNameCar(ServerRequestInterface $request, $carId): ResponseInterface
{
if ($carId <= 0) {
throw new BadRequestException('Машина не найдена');
}
$car = $this->carService->getCar($carId);
$name = $request->getParsedBody()['name'];
$this->carService->updateNameCar($car, $name);
return new JsonResponse([
'message' => 'updateNameCar выполнено'
]);
}
}
Разумеется, в реальном коде вещи вроде $request->getParsedBody()['name']
и new JsonResponse
вынесены на еще один уровень абстракции с дополнительными проверками.
В качестве бонуса: в зависимости от окружения есть возможность подменять сервисы в контейнере и запускать функциональные тесты.
В заключение
Как вы видите, много практик с идеологией “так проще” позаимствовано из Laravel. Он действительно задает тренды.
Приложение получило новый фреймворк уже после того, как проработало 7 лет. Насколько я знаю, старый самописный фреймворк также появился не сразу. И никто не даст гарантий, что мы не захотим сменить фреймворк через 5 лет. Поэтому код писался максимально независимым от выбранного фреймворка. Бизнес логика и прежде не зависела от приложения, а теперь и контроллеры не зависят от фреймворка. Контроллеры зависят от PSR-7 совместимых запросов и возвращают PSR-7 ответы. А собираются контроллеры приложением, зависящим от PSR-11 совместимого контейнера.
Slim работает через middleware и добавлять общую логику стало проще (Логирование ошибок приложения, обработка ошибок пользовательского ввода). Autowiring контроллеров прекрасно работает, по сути контроллеры стали сервисами.
Кстати здесь можно подсмотреть пример включения autowiring в slim.
Конечно, приложение продолжает развиваться и уже перешло на стандартные интерфейсы кэширования и событий, но их внедрение было чуть менее кроваво:).
Автор: Fantyk