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


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


В этой статье:


  • README.md
  • Установка: через composer, рецепты Flex, консольные команды
  • Релизный цикл, выпуск новых версий
  • Семантическое версионирование
  • Фиксация изменений в CHANGELOG.md


Если вы не последовательно выполняете туториал, то скачайте приложение из репозитория и переключитесь на ветку 6-testing.


Инструкции по установке и запуску проекта в файле README.md. Финальную версию кода для этой статьи вы найдете в ветке 7-support.


Дисклеймер


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


Установка бандла


Чтобы установить готовый бандл в проект, потребуется всем знакомая команда composer:


composer require bravik/calendar-bundle

Эта команда выполнит необходимый минимум:


  1. Загрузит в папку vendors код бандла
  2. С помощью 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. Фича в бете уже 1.5-2 года
  2. Flex Server бесплатный только в бете, а после беты 249 EUR в месяц.
  3. Flex Server требует регистрации на packagist, а приватные пакеты там только платные.
  4. Flex Server заставляет установить для своего бандла открытую лицензию MIT или BSD
  5. Есть баги, затрудняющие с ним работу. Этому багу 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. Релизный цикл, установка и обновление