По локоть в легаси: пошагово перезапускаем устаревший портал на PHP

в 4:34, , рубрики: Без рубрики

PHP — один из самых популярных языков веб-разработки уже около 20 лет, а самому языку скоро стукнет 30. За это время на нем написали огромное количество больших и маленьких проектов. Некоторые сайты, созданные в 90-х, 00-х и 10-х, хранят код еще с тех давних времен. И чем больше времени проходит с начала разработки, тем меньше на рынке специалистов, готовых разбираться в легаси и не самых современных технологиях.

В похожей ситуации оказался портал fishingsib.ru — один из крупнейших в рунете сайтов о рыбалке, который посещают больше 10 000 человек ежедневно. Он создавался в начале 2000-х как форум для рыбаков-любителей и пережил несколько довольно серьезных обновлений кодовой базы. Последнее из них — переезд на CakePHP 2 в 2012 году. На этом фреймворке и PHP 5 сайт жил до 2017 года.

Владелец fishingsib.ru планировал поддерживать и развивать сайт, внедрять новую функциональность, однако столкнулся с техническими проблемами. Любые доработки были очень долгими из-за неудачных архитектурных решений и сильной зависимости от устаревающего и не особенно популярного CakePHP 2. После каждого обновления появлялось множество багов. В то же время не удавалось найти новых разработчиков, потому что большинство специалистов не хотели работать в проекте с неактуальным стеком. Развитие проекта сильно замедлилось и стало понятно, что с технической частью нужно что-то делать.

Выбираем путь

Очевидное решение — выкинуть старый код и написать новый сайт с нуля. Подобный опыт у команды уже был, поэтому мы примерно понимали, сколько потребуется времени и средств. При хорошем раскладе команда из трех человек потратила бы на разработку минимум год. Причем такая оценка получилась при условии, что мы «заморозим» старый сайт: не будем вносить значительных изменений, пока не стартует новая платформа.

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

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

Второй вариант — постепенная модернизация кодовой базы. Код хоть и был устаревшим, но все же не выглядел безнадежно. В основе лежал какой-никакой MVC фреймворк, работающий на PHP 5.6. 

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

Изучаем стартовые условия

В 2017 году сайту fishingsib.ru было 17 лет, а действующей кодовой базе — около восьми. Приложение было написано с использованием CakePHP 2. Это классический PHP-фреймворк тех времен, продвигающий философию RAD (Rapid Application Development): с высокой связанностью (coupling) компонентов и жесткими гайдлайнами по написанию кода, которые завязывали всю бизнес-логику на фреймворк.

В нашем приложении именно так и было: большая часть бизнес-логики была размазана по контроллерам, моделям, компонентам (Components), поведениям (Behaviors) и помощникам (Helpers). Components, Behaviors и Helpers — это логические пакеты, которые можно было переиспользовать внутри CakePHP, но вне фреймворка они не работали.

Архитектура приложения на CakePHP

Архитектура приложения на CakePHP

В приложении накопились практически все проблемы, присущие легаси:

  • высокая связанность кода;

  • зависимость бизнес-логики от фреймворка;

  • отсутствие каких-либо автоматических тестов;

  • неявные дублирования кода;

  • избыточное наследование;

  • магические константы и методы, 

и многое другое, что свойственно устаревшим приложениям. Из плюсов проекта отмечу хорошо структурированную и нормализованную базу данных с понятными и корректными названиями столбцов и таблиц. Она сильно облегчила нам жизнь, потому что новый код мог обращаться напрямую в старую базу и работать с накопленным за годы работы сайта огромным массивом данных.

Разрабатываем план действий

Первым этапом стал выбор и согласование нового фреймворка. Для владельца сайта это была больная тема, потому что он не хотел через 3–5 лет снова начинать масштабную переработку всей кодовой базы для дальнейшего развития проекта.

Наш выбор выпал на Symfony, как максимально гибкую платформу, которая, с одной стороны, может обеспечить плавный переход с устаревшего кода. А с другой стороны, является одним из самых популярных PHP-фреймворков со стабильной поддержкой и регулярными релизами новых версий на протяжении многих лет.

Все компоненты Symfony можно использовать отдельно друг от друга и в любой комбинации. Именно так и написано на главной странице их сайта: "Symfony is a set of reusable PHP components... and a PHP framework for web projects". Эта особенность Symfony позволила нам двигаться небольшими инкрементальными улучшениями — сначала без больших сложностей совместить два приложения, а затем окончательно перенести весь сайт на новый фреймворк без остановки.

Мы составили примерный план действий, в котором каждый шаг был небольшим улучшением существующего проекта, но в то же время двигал нас в сторону новой архитектуры и фреймворка:

  1. пишем тесты для старого приложения на CakePHP;

  2. корректно логируем ошибки и оперативно уведомляем о них команду;

  3. обновляем PHP до версии 7.1 (актуальной на то время);

  4. внедряем менеджер Composer (в старом приложении он не использовался);

  5. внедряем первые пакеты Symfony (Config и Container) в приложение на CakePHP;

  6. создаем новое приложение на полноценном фреймворке Symfony;

  7. создаем единую точку входа для старого и нового приложений;

  8. выносим общую функциональность из двух приложений;

  9. поэтапно переносим в Symfony весь код из старого приложения в новое, а затем делаем уборку — удаляем ненужный код и вспомогательные инструменты.

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

Так выглядела задача, в которой детально описаны все шаги подготовки к внедрению первого компонента Symfony

Так выглядела задача, в которой детально описаны все шаги подготовки к внедрению первого компонента Symfony

Пишем тесты

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

Чтобы покрыть все базовые сценарии работы приложения, пришлось создать больше 150 acceptance (end-2-end) тестов. В качестве инструмента мы выбрали Codeception. На тот момент это был, наверное, единственный инструмент на PHP, который позволял быстро и удобно писать Acceptance-тесты, эмулирующие работу браузера.

Сейчас Codeception все еще остается достойным решением для тестирования, хотя появилось большое количество аналогичных инструментов. А если говорить про разработку на Symfony, то советую обратить внимание на средства тестирования от создателей этого фреймворка и проект Symfony Panther.

К старому приложению на CakePHP мы добавили уведомления об ошибках в логах. Для общения в команде мы использовали Slack, поэтому все сообщения об ошибках решили отправлять туда. Это помогло разработчикам быстро замечать и исправлять баги.

Только после этого можно было спокойно приступить к модернизации кода.

Обновляем PHP

Следующим большим шагом был переход на новую версию языка. В 2017 уже был доступен релиз PHP 7.1, и оставаться на версии 5.6, вышедшей еще в 2014 году, не было никакого смысла ни для бизнеса, ни для разработчиков.

Для подсказок об устаревших вызовах и возможностях использовали IDE PHPStorm. А тесты, логи и уведомления действительно помогли практически мгновенно замечать сбои.

В современных реалиях помимо IDE разумно использовать статические анализаторы кода, которые еще лучше помогают в обновлении версий PHP и рефакторинге устаревшего кода:

  • Rector — инструмент №1, автоматически заменяет несовместимые с новой версией языка части кода;

  • Exakat — анализирует совместимость кода по версиям PHP, дает список проблемных участков и используемых расширений;

  • Phan — подсвечивает лексические конструкции, которых нет в новой версии.

Подключаем компоненты Symfony

Мы начали всего с двух компонентов: Container и Config. С их помощью проверили, что две версии приложения будут работать вместе, а мы без проблем сможем внедрять в старое приложение другие компоненты Symfony и новый код.

В Symfony Config мы перенесли конфигурацию приложения: подключение к базе данных и некоторые переменные окружения. Так мы смогли использовать конфигурацию одновременно в CakePHP и Symfony без дублирования кода. А затем, через Symfony Container, внедренный в легаси-приложение, считали данные конфигурации.

На этом подготовительный этап можно было считать успешно завершенным. Архитектура приложения пока изменилась незначительно, но мы заложили фундамент для Symfony-приложения, которое со временем должно появиться рядом со старым кодом, а в итоге окончательно его заменить.

Архитектура приложения на этом этапе: мы используем приложение на CakePHP и два компонента Symfony

Архитектура приложения на этом этапе: мы используем приложение на CakePHP и два компонента Symfony

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

Создаем новое приложение рядом со старым

Дальше мы подключили все основные компоненты Symfony, которые требуются для полноценного фреймворка, и начали переносить приложение с самой простой части — админки сайта. Мы создали интерфейс на Symfony, дали админке новый адрес и получили два практически независимых приложения, которые работают с общей базой данных.

Теперь архитектура проекта значительно изменилась, но связь между старым и новым кодом все еще оставалась минимальной.

На этом этапе работали два почти независимых приложения

На этом этапе работали два почти независимых приложения

Администраторы портала могли свободно переключаться между двумя разными версиями админки, просто переходя по старому или новому адресу. Но с публичными разделами сайта, к сожалению, нельзя было поступить так же просто. Все внутренние переделки должны были проходить незаметно для десятков тысяч посетителей сайта. Нам необходимо было найти способ перенаправлять запросы в нужную часть приложения: в старый код на CakePHP, либо в новый на Symfony.

Создаем общую точку входа 

Решение — создать общую точку входа, Front Controller, который будет решать, куда отправить запрос: в старое или новое приложение. Для этого данной надстройке нужен список адресов, которые может обработать новое (или старое) приложение.

Простое решение для этого — прописать список поддерживаемых адресов прямо в коде, но тогда его нужно обновлять всякий раз при изменении структуры приложения.

Мы сделали чуть сложнее: наша надстройка обращалась к роутингу Symfony и определяла, существует ли в нем обработчик для запрашиваемой страницы. Если да, то запрос отправлялся в новое приложение, если нет — в старое. На схеме этот блок называется Multi Application Router. А общая архитектура приложения приняла такой вид:

Роутер объединил два независимых приложения в систему

Роутер объединил два независимых приложения в систему

Выносим общую функциональность из двух приложений

Часть функциональности приложений была общей для CakePHP и Symfony. Например, аутентификация, проверка прав доступа, переадресация и так далее. Поэтому разумным было вынести такой функционал на тот же уровень, где находился Multi Application Router. В этом случае идеальным решением оказалась концепция Middleware, которую многие PHP-разработчики могут знать благодаря ее реализации в Laravel.

Но у нас были Symfony и CakePHP, каждый из которых имел свои Request/Response-объекты, несовместимые друг с другом. Выйти из ситуации помогла библиотека zend-diactoros (ныне laminas-diactoros), которая реализует объекты Request/Response по стандарту PSR-7, а также поддерживает концепцию Middleware по PSR-15.

Благодаря популярности Symfony и его компонентов, перевести объекты симфони в PSR-7-совместимые оказалось довольно легко — для этого есть готовая библиотека. Для CakePHP аналогичный конвертер пришлось написать самим. Кстати, начиная с версии 3.4.0 CakePHP поддерживает стандарт PSR-7, за что им плюсик в карму. Но у нас тогда была лишь вторая версия этого фреймворка.

В Middleware мы вынесли значительную часть общей функциональности, которую было удобно реализовать на уровне Request/Response-объектов. Начиная с этого момента, легаси-приложение стало ощутимо уменьшаться, а любую новую функциональность мы могли создавать уже без оглядки на старый код. Так у нас появился совершенно новый SEO-модуль, который позволял гибко настраивать значимые для поисковой оптимизации теги и генерировать разметку Open Graph для одной или группы связанных страниц.

Архитектура нашего приложения приняла следующую структуру и дальше уже не менялась до полного избавления от CakePHP.

Несколько лет сайт существовал в таком виде: два приложения были довольно плотно связаны. База данных здесь та же самая, но осталась за кадром картинки, чтобы не усложнять

Несколько лет сайт существовал в таком виде: два приложения были довольно плотно связаны. База данных здесь та же самая, но осталась за кадром картинки, чтобы не усложнять

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

Это было связано с еще одной переделкой, которая активно шла параллельно с обновлением кода — на портале меняли дизайн и внедряли адаптивную верстку. Этот процесс также шел эволюционно, чтобы не напугать пользователей кардинальными изменениями внешнего вида сайта.

Методично переносим код и завершаем обновление

Последняя часть нашей работы была самой понятной — нужно было планомерно переписать весь старый код на новый фреймворк. Этот процесс завершился в 2020 году. Приоритет мы всегда отдавали задачам по добавлению новой или изменению старой функциональности. Перенос кодовой базы был побочным процессом, им мы занимались, если новые функции затрагивали старый код, либо не было срочных задач от бизнеса.

Когда от старого кода практически ничего не осталось, мы провели окончательный рефакторинг и избавились от CakePHP. Больше не было нужды в надстройках — роутере и Middleware, поэтому их мы тоже удалили, а функциональность перенесли в Symfony. С этого момента остался только Symfony.

В новом приложении мы постарались учесть одну из ключевых ошибок устаревшего приложения — жесткую привязку бизнес-логики к фреймворку разработки. Конечно, где-то такие зависимости все еще остались, но их гораздо меньше, и такой выбор был сделан осознанно. Если когда-то мы вновь столкнемся со сменой фреймворка, то решим эту задачу гораздо быстрее и со значительно меньшими усилиями. Но будем надеяться, что такой необходимости не возникнет. Пожалуй, сейчас Symfony лучший PHP-фреймворк для больших и сложных приложений, и думаю, что он таковым и останется еще долгое время.

Заключение

После переноса сайта на новые рельсы, бизнес получил качественное приложение, которое можно дорабатывать, и актуальный стек. Это решило главную проблему: открыло возможности для роста и улучшения проекта.

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

Разработчикам стало проще поддерживать сайт. Мы создали удобную среду разработки, настроили Docker, CI/CD, внедрили статические анализаторы кода, отладили командные процессы разработки. Приложение тестируется на всех уровнях: от unit до приемочных тестов.

Современный фреймворк Symfony регулярно обновляется, сейчас команда использует версию 6. Она поддерживает все необходимые для комфортной работы инструменты. Версия PHP также поддерживается в актуальном состоянии.

Думаю, такой подход к переработке и переносу архитектуры целого приложения окажется полезным для многих. Его можно применять и в таких случаях, как наш — с большими, сложными проектами и многолетним легаси, и в более простых, когда нужно обновить легковесный, но устаревший сайт. Напишите в комментариях, приходилось ли вам сталкиваться с поэтапным обновлением проектов? Расскажите, к какому решению вы пришли и что получили в итоге?

Сейчас в 9 из 10 проектов нашей команды — это развитие и поддержка ИТ-продуктов, в разработке которых возникли какие-либо сложности. Мы присоединяемся к команде клиента или берем всю работу в свои руки, а иногда буквально спасаем продукты, которые, кажется, проще переписать с нуля. Если вы узнали в этом описании свой проект — обращайтесь 🙂 Мы поможем найти решение, которое сэкономит вам время, деньги и нервы.

Автор: Дмитрий Панькин

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js