Поговорим о том, как прекратить копипастить между проектами и вынести код в переиспользуемый подключаемый бандл Symfony 5. Серия статей, обобщающих мой опыт работы с бандлами, проведет на практике от создания минимального бандла и рефакторинга демо-приложения, до тестов и релизного цикла бандла.
В предыдущих статьях серии мы вынесли переиспользуемый код в полноценный бандл, настроили и протестировали. В завершающей статье поговорим о жизненном цикле бандла: от установки до релизного цикла.
В этой статье:
- README.md
- Установка: через composer, рецепты Flex, консольные команды
- Релизный цикл, выпуск новых версий
- Семантическое версионирование
- Фиксация изменений в CHANGELOG.md
Если вы не последовательно выполняете туториал, то скачайте приложение из репозитория и переключитесь на ветку 6-testing.
Инструкции по установке и запуску проекта в файле README.md
. Финальную версию кода для этой статьи вы найдете в ветке 7-support.
Дисклеймер
Эта статья про бандлы будет наиболее «зыбкая», так как на некоторые вопросы, которые здесь поднимаются у меня пока что нет четкого ответа. Если вы знаете как сделать лучше, пожалуйста поделитесь в комментариях.
Установка бандла
Чтобы установить готовый бандл в проект, потребуется всем знакомая команда composer:
composer require bravik/calendar-bundle
Эта команда выполнит необходимый минимум:
- Загрузит в папку
vendors
код бандла - С помощью composer-плагина Symfony Flex, автоматически подключит бандл к вашему приложению в
config/bundles.php
.
Однако более сложные бандлы могут требовать дополнительный действий по инициализации бандла. Вот только несколько вопросов, возникающих с уже знакомым нам демо-бандлом CalendarBundle
:
- В демо-бандле мы используем файл конфигурации с обязательными параметрами. Приложение не запустится до тех пор, пока мы не создадим в
config/packages/
конфиг-файл и не пропишем туда обязательные параметры. Можно ли автоматизировать его создание при установке? - В этом же бандле мы используем сущности, размеченные аннотациями Doctrine. А значит, после подключения в проект нам потребуется сгенерировать миграции, а в некоторых случая может понадобиться занести при установке первоначальные данные в БД. Как это делать и как управлять изменениями схемы в дальнейшем?
- Еще одна проблема — ассеты, сборка скриптов и стилей. Наверное, правильнее будет поставлять с бандлом уже собранную версию скриптов и стилей, и именно их подключать в проект. Но у меня в практике пока что всегда использовались исходники. И в этом случае возникает проблема: если зависимости composer бандла Symfony умеет автоматически находить, добавлять в общее дерево зависимостей и управлять ими, то с npm и зависимостями, определенными в package.json бандла этот фокус не пройдет. Приходится вручную добавлять зависимости бандла в проект.
Что можно сделать, чтобы облегчить жизнь пользователя и максимально автоматизировать процесс?
Пишите понятный и подробный README.md
Не все действия можно автоматизировать. Очевидная, но обязательная рекомендация: описывайте подробно инструкции по установке в README.md файле.
Указывайте конкретные команды, добавляйте примеры конфигов, фиксируйте подводные камни и проблемы с которыми сталкиваетесь в процессе разработки. Можно добавить в документацию бандла образцы файлов конфигурации или других файлов.
Документация незаменима при работе в команде, но пишите её даже если вы работаете над проектом один. Все это сэкономит ваше время и нервы, когда вы решите вернуться к разработке проекта после даже небольшого перерыва.
Пример из Best Practices for Reusable Bundles
Автоматизация с Symfony Flex
При установке официальных бандлов Symfony можно заметить, что они практически сразу готовы к использованию. Для них создаются все нужные конфиги, выполняются какие-то скрипты для настройки, добавляются ENV-переменные, .gitignore-правила и так далее.
Все это делает плагин composer Symfony Flex. Он позволяет определить так-называемый рецепт для установки бандла, в котором задаются необходимые для его инициализации действия. Для бандлов, у которых нет рецепта, используется авто-генерируемый рецепт. Он просто подключает бандл в проект и ничего более.
Рецепты позволяют декларативно описать действия, которые должны выполняться при установке и удаления бандла. Инструмент дает удобный инструментарий, чтобы копировать произвольные файлы, конфиги, добавлять записи в .gitignore, ENV-параметры, запускать произвольные скрипты.
Но можем ли мы тоже воспользоваться магией Symfony Flex? В то время как для официальных и серьезных open-source проектов Flex прекрасно работает, для приватных проектов все не так гладко.
Дело в том, что рецепты по задумке должны храниться в отдельном от бандла репозитории. Их всего два: репозиторий для официальных бандлов Symfony и Contribution-репозиторий для всех остальных бандлов от сообщества Symfony. Contrib репозиторий публичный, принимает рецепты по заявкам и после модерации, что делает его бесполезными для частных проектов.
Для крупных компаний в бете фича, позволяющая создать приватный сервер рецептов. Вы можете попробовать создать свой по этим материалам:
Symfony Flex Private Repositories — Fabien Potencier, 2017
Symfony 4 Using Private Recipes with Flex — Sebastian Sellmeier, 2018
Symfony Flex Private Recipes: создание, настройка и использование — Юрий Богданов, 2017
Однако, есть несколько недостатков:
- Фича в бете уже 1.5-2 года
- Flex Server бесплатный только в бете, а после беты 249 EUR в месяц.
- Flex Server требует регистрации на packagist, а приватные пакеты там только платные.
- Flex Server заставляет установить для своего бандла открытую лицензию MIT или BSD
- Есть баги, затрудняющие с ним работу. Этому багу 1.5 года.
Все это делает Symfony Flex хорошим инструментом для официальной экосистемы Symfony и крупных open-source проектов Symfony-сообщества, но бесполезным для приватных бандлов на практике.
Использование CLI-команд Symfony
Итак, большую часть действий для запуска бандла нам придется выполнить вручную по инструкции из README.md
Но некоторые действия все же можно автоматизировать: мы можем создать обычную консольную команду внутри бандла.
mybundle/src/Command/InstallCommand.php
:
<?php
namespace bravik\CalendarBundle\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\Filesystem;
class InstallCommand extends Command
{
protected static $defaultName = 'bravik:calendar:install';
/** @var Filesystem */
private $filesystem;
private $projectDir;
public function __construct(Filesystem $filesystem, string $projectDir)
{
parent::__construct();
$this->filesystem = $filesystem;
$this->projectDir = $projectDir;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('Installing bravik/CalendarBundle...');
$this->initConfig($output);
// Прочие действия для инициализации
// ...
return 0;
}
private function initConfig(OutputInterface $output): void
{
// Create default config if not exists
$bundleConfigFilename = $this->projectDir
. DIRECTORY_SEPARATOR . 'config'
. DIRECTORY_SEPARATOR . 'packages'
. DIRECTORY_SEPARATOR . 'calendar.yaml'
;
if ($this->filesystem->exists($bundleConfigFilename)) {
$output->writeln('Config file already exists');
return;
}
// Конечно лучше скопировать из готового файла
$config = <<<YAML
calendar:
enable_soft_delete: true
YAML;
$this->filesystem->appendToFile($bundleConfigFilename, $config);
$output->writeln('Config created: "config/packages/calendar.yaml"');
}
}
Здесь мы проверяем, была ли выполнена инициализация ранее и определяем команду, которая создаст конфиг, если он еще не создан.
Такой подход имеет свои ограничения. Например конфиг не получиться создать, если бандл требует обязательных параметров в конфигурации. При инициализации команды бандл уже будет требовать наличия конфига и выдаст исключение.
Как вариант, можно написать не команду Symfony, а простой shell-script.
Обновление бандла
Как и любой программный продукт, бандл будет жить и развиваться. Поэтому о том, как будет организован процесс обновления нужно думать заранее.
Если у вас простой бандл, то достаточно вести семантическое версионирование и учет изменений в CHANGELOG.md
. Более сложные бандлы, столкнутся со специфическими проблемами: например как управлять изменениями в структурах данных и их представлением в БД
Семантическое версионирование
Идея семантического версионирования в том, что вы определяете некий публичный контракт, гарантирующий неизменность интерфейса бандла. Далее с помощью специального формата версии вы указываете, есть ли в новой версии изменения, ломающие обратную совместимость и безопасно ли обновлять ваш пакет.
Формат версии:
<major>.<minor>.<patch>
где каждая из переменных — неотрицательное натуральное число.
Выпуская новую версию увеличивают:
- МАЖОРНУЮ версию, когда сделаны существенные изменения, ломающие обратную совместимость.
- МИНОРНУЮ версию, когда вы добавляете новую функциональность, не нарушая обратной совместимости.
- ПАТЧ-версию, когда вы вносите мелкие исправления или доработки, не нарушая обратной совместимости.
Подробнее о семантическом версионировании
Менеджер зависимостей Composer работает с семантическим версионированием и позволяет гибко организовать процесс учета зависимостей, отслеживания их изменений и автоматического обновления пакетов, в которых нет обратно-несовместимых изменений.
Разберем на примере CalendarBundle
.
Как выпустить новую версию бандла?
Версия бандла указывается в его composer.json
:
{
"name": "bravik/calendar-bundle",
"version": "0.1.0",
...
}
Для локальной разработки этого будет достаточно, но обычно Composer загружает пакет из удаленного git-репозитория. Чтобы понять, где искать нужную версию каждый релиз должен быть помечен тегом в формате соответствующей версии. Например:
git tag v0.1.0
После того как вы пушнете обновленный composer.json и новый тег в удаленный репозиторий — composer сможет обнаружить новую версию.
Какую версию указывать для нового бандла?
В composer.json
приложения-хоста мы добавили:
"require": {
"bravik/calendar-bundle": "^0.1.0",
...
},
Знак ^
перед версией разрешает composer загружать все версии бандла, не ломающие обратную совместимость:
- Для версий >
1.0.0
такими будут считаться все патч и минорные версии вплоть до2.0.0
. - До версии
1.0.0
все релизы считаются находящимися в фазе «активной разработки», поэтому безопасными будут считаться только патч-версии.
Для нашего пакета безопасными будут считаться версии > 0.1.0
< 0.2.0
.
Как проверить доступность новых обновлений?
Увеличим в bundles/CalendarBundle/composer.json
патч версию:
{
"version": "0.1.1",
}
Выполним команду:
composer outdated
Команда выведет список бандлов для которых есть обновления. Левая версия — текущая, правая — последняя доступная. Композер выделяет красным цветом новую версию, которую можно безопасно загрузить без нарушений обратной совместимости.
Теперь увеличим минорную версию бандла:
{
"version": "0.2.1",
}
Теперь Composer подсветил новую версию желтым. Это означает, что возможны изменения обратной совместимости. При обновлении composer не загрузит эту версию до тех пор, пока вы вручную не поднимите её в секции require
composer.json
приложения-хоста.
Как обновлять?
composer update
Команда загрузит все безопасные обновления.
Подробнее о том, как задавать ограничения версий Composer
Учет изменений в CHANGELOG.md
С развитием проекта, в вашем бандле неминуемо будут появляться изменения, ломающие обратную совместимость. Такие изменения необходимо отслеживать и документировать. Еще лучше отслеживать все основные изменения. Для этого используется файл CHANGELOG.md
Зачем это нужно?
Представьте, что ваш бандл используется в двух проектах. Вы активно работаете над первым, а второй пока простаивает. Во время работы вы расширили функциональность своего бандла, — стали появляться изменения, ломающие обратную совместимость.
Спустя несколько мажорных версий (или минорных если вы «в стадии активной разработки» и еще версионируете с 0.X.X
) вам необходимо продолжить работу над вторым проектом. Чтобы обновиться придется пофиксить все изменения, ломающие обратную совместимость. Но без CHANGELOG.md — эта задача если и не невозможна, то крайне затруднительна. Проще будет удалить и начать сначала!
Если же вы ведете CHANGElOG.md, то в нем вы можете указать основные изменения и необходимые для миграции действия.
Я веду простой лог примерно в таком формате:
# CHANGELOG
Все значимые изменения документируются в этом файле
[BC] - Пометка для breaking changes
[BugFix] - Пометка для багов
## Unreleased
Здесь копятся изменения до следующего релиза
* CAL-5 [BC] Важное изменение
* CAL-5 Добавлена крутая новая фича
## [0.2.0] (2020-04-06)
* CAL-5 [BC] Экспортеры теперь подключаются по тегу
- Неоходимо пометить экспортеры вне бандла тегами `calendar_bundle.exporter`
* CAL-6 [BC] Добавлены новые поля в Мероприятие
- Создайте новую миграцию, обратите внимание, что при миграции необходимо перенести данные из X в Y. Для этого добавьте в сгенерированную миграцию дополнительную SQL команду "XXX"
* CAL-7 Код покрыт тестами
## [0.1.1] (2020-03-30)
* CAL-4 [BugFix] Календарь не должен показывать удаленные мероприятия
## [0.1.0] (2020-02-15)
* CAL-1 Добавлены основные функции
* CAL-2 Добавлен шаблон календаря
* CAL-3 Добавлен экспорт мероприятия
* [BugFix] Editor should initialize blocks only when they change
Посмотрите пример CHANGELOG.md symfony/framework-bundle
. Symfony масштабный проект: для мажорных версий делается целая подборка критичных изменений. Например вот UPGRADE.md с Symfony 4 до Symfony 5.
Проблема миграций
Использование размеченных Doctrine сущностей создает проблемы с миграциями.
Миграции специфичны для конкретного окружения. Они должны генерироваться и храниться в конкретном проекте, но не в бандле. Если изменения в базе достаточно просты — проблем может не возникнуть.
Но если при миграции вам нужно сохранить и каким-то особым образом преобразовать уже имеющиеся данные возникнет проблема: где-то нужно хранить логику этих преобразований.
Если в обычном приложении можно модифицировать миграцию и добавить туда нужные SQL запросы для преобразования, то что делать с бандлом, миграции для которого генерирует пользователь?
Этот вопрос пока остается открытым.
Как возможный вариант можно использовать команды, для того, чтобы хранить логику преобразований между отдельными версиями и указывать на необходимость запуска этих команд в инструкциях по обновлению.
Резюме
В заключительной статье серии мы поговорили о некоторых практических советах как организовать первичную установку, дальнейшую работу над бандлом и выпуск новых релизов. Многие вопросы в разработке бандлов остаются открытыми — предлагаю вам поделиться своим опытом в комментариях.
Финальную версию кода для этой статьи вы найдете в ветке 7-support.
Другие статьи серии
Часть 1. Минимальный бандл
Часть 2. Выносим код и шаблоны в бандл
Часть 3. Интеграция бандла с хостом: шаблоны, стили, JS
Часть 4. Интерфейс для расширения бандла
Часть 5. Параметры и конфигурация
Часть 6. Тестирование, микроприложение внутри бандла
Часть 7. Релизный цикл, установка и обновление