Поговорим о том, как прекратить копипастить между проектами и вынести код в переиспользуемый подключаемый бандл Symfony 5. Серия статей, обобщающих мой опыт работы с бандлами, проведет на практике от создания минимального бандла и рефакторинга демо-приложения, до тестов и релизного цикла бандла.
В предыдущей статье говорили о том, как расширять функциональность бандла в приложении-хосте с помощью тегов. В этой статье добавим бандлу гибкости: создадим конфигурационный файл и определим несколько параметров.
Часть 1. Минимальный бандл
Часть 2. Выносим код и шаблоны в бандл
Часть 3. Интеграция бандла с хостом: шаблоны, стили, JS
Часть 4. Интерфейс для расширения бандла
Часть 5. Параметры и конфигурация
Часть 6. Тестирование, микроприложение внутри бандла
Часть 7. Релизный цикл, установка и обновление
Если вы не последовательно выполняете туториал, то скачайте приложение из репозитория и переключитесь на ветку 4-extend.
Инструкции по установке и запуску проекта в файле README.md
. Финальную версию кода для этой статьи вы найдете в ветке 5-configuration.
Параметры DI-контейнера и их переопределение
Внутри бандла уже есть конфигурационный файл config/services.yaml
, где определяется конфигурация сервисов DI-контейнера. Там же можно определить и параметры.
Любые параметры и сервисы бандла, могут быть переопределены в приложении-хосте.
Мы можем воспользоваться этим для того, чтобы позволить пользователю настраивать работу бандла. Например, мы хотим сделать опциональной возможность так называемого режима soft-delete (когда при удалении записи не удаляются из БД, а помечаются «архивными»). Эта фича уже реализована, но по умолчанию отключена.
Для этого введем параметр в config/services.yaml
бандла:
parameters:
bravik.calendar.enable_soft_delete: true
Обратите внимание на формат названия параметра venodor.package.parameter
. Мы используем snake_case
добавляя через точку префикс: имя вендора и пакета. Параметры определяются в общем пространстве имен для всего приложения, и использование префикса снижает вероятность коллизии имен.
Посмотрим на конструктор контроллера EditorController
в бандле. В конструкторе есть опциональный параметр $enableSoftDelete
, по умолчанию принимающий значение false
:
public function __construct(
EventRepository $eventRepository,
bool $enableSoftDelete = false
) {
//...
}
Чтобы передать наш параметр в качестве аргумента в этот конструктор, нам нужно явно указать это в services.yaml
бандла:
bravik\CalendarBundle\Controller\EditorController:
arguments:
$enableSoftDelete: '%bravik.calendar.enable_soft_delete%'
Чтобы проверить его работу, перейдите в демо-приложении на страницу «Редактор». Вы увидите, что кнопки удаления стали желтыми, а удаление события теперь переведет его в статус «В архиве», но не удалит. Попробуйте изменить параметр обратно на false
, и кнопки снова станут красными, а удаление будет происходить «по-настоящему».
А теперь попробуем этот параметр переопределить в services.yaml
приложения-хоста. Приоритет будет отдан параметру, указанному в конфигурации хоста, а не бандла!
Такой подход работает для простых случаев, но имеет недостатки:
- Во-первых, мы грубо вмешиваемся в работу бандла. У нас нет приватных параметров, мы не можем запретить пользователю что-то переопределять, и не можем защитить его от «выстрелов себе в ногу». Мы можем просто изменить параметр конфигурации в следующем релизе, а у пользователя возникнут неожиданные проблемы. А зная это, мы не сможем спокойно работать с параметрами, не опасаясь
- Во-вторых, мы не можем валидировать корректность конфигурации.
Удалите переопределенный параметр из конфигурации приложения.
Файл конфигурации бандла
Лучшей идеей было бы выделить строго определенный интерфейс: фиксированный набор параметров, которые пользователь может менять, а бандл может парсить, проверять, в случае чего выкидывать исключение и прокидывать в свои внутренние параметры.
После этой договоренности мы уже свободно могли бы менять наш внутренний конфиг, не опасаясь проблем у пользователей. Для этого в Symfony предусмотрено решение.
Если вы откроете в хосте config/packages/
, то увидите, что для подключенных бандлов в вашем приложении создаются файлы конфигурации. Их структура и формат четко определены. Попробуйте добавить произвольный параметр в любой из конфигов, и вы получите исключение при запуске приложения.
Мы можем сделать такой же файл и для нашего приложения.
Взглянем на метод load()
в файле DependencyInjection/CalendarExtension.php
бандла:
public function load(array $configs, ContainerBuilder $container) {}
Мы видим, что помимо ContainerBuilder
первым аргументом в него передается массив $configs
. Добавим в начало метода dd($configs)
и посмотрим на его содержимое: пока что там пустой масив.
Создадим в папке config/packages/
конфиг для бандла calendar.yaml
:
calendar: # extension key
enable_soft_delete: false
Название файла значения не имеет, Symfony автоматически пропарсит все конфиг файлы в папке config/packages/
вашего приложения. Но чтобы его содержимое было передано бандлу в CalendarExtension::load()
, корневой ключ в файле должен называться так же как Extension-файл, но без слова Extension
и в snake_case
. На самом деле даже это поведение можно переопределить, но это останется за рамками статьи.
Посмотрим, что теперь попадает в массив $configs
:
^ array:1 [Ў
0 => array:1 [Ў
"enable_soft_delete" => false
]
]
Мы видим наш конфигурационный файл в виде PHP массива, но почему-то он обернут в еще один массив. Зачем?
Дело в том, что конфиг может быть определен не только в одном месте. Например в папке packages
вы можете увидеть подпапки test
, prod
и dev
для разных окружений.
И вообще, не обязательно создавать отдельные файлы для конфига. Попробуйте скопировать содержимое конфига нашего бандла, например, в конфиг framework
и посмотреть, что будет в переменной $configs
. Мы увидим там уже два массива конфигов.
Все найденные по ключу (extension key) версии конфига Symfony не сливает автоматически в один массив, а передает в виде массива конфигов в метод load()
. За слияние отвечаете вы сами.
Но нам это не нужно. Оставляем 1 конфиг, убираем dd($configs)
и обновляем страницу.
Получаем ошибку:
Если бы мы получали конфиг в виде простого массива, то в чем преимущество его создания над простым переопределением параметров? Нам нужно научить бандл понимать семантику конфига, валидировать его и сообщать пользователям человекопонятные ошибки.
Рядом с CalendarExtension
создадим класс Configuration
:
<?php
namespace bravik\CalendarBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder('calendar');
$treeBuilder->getRootNode()
->children()
->booleanNode('enable_soft_delete')->end()
->end()
;
return $treeBuilder;
}
}
Здесь мы в создаем объект TreeBuilder
, моделирующий конфигурационный файл, и объявляем в модели единственный параметр enable_soft_delete
типа boolean
.
TreeBuilder
содержит набор методов, позволяющих объявлять как параметры примитивных типов, так и массивы и вложенные объекты. Кроме этого можно добавить их описания, правила валидации, значения по умолчанию и другие свойства параметров.
Чтобы подключить модель конфигурации, в методе CalendarExtension::load()
в конец добавляем:
public function load(array $configs, ContainerBuilder $container)
{
//...
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
}
Метод processConfiguration()
на основе структуры, заданной в файле Configuration
, загрузит все доступные конфиги, сольет их в один, провалидирует и выдаст финальный массив $config
.
Теперь мы можем использовать $config
, чтобы модифицировать контейнер или его сервисы.
Например, мы можем установить нужный нам параметр:
$container->setParameter(
'bravik.calendar.enable_soft_delete',
$config['enable_soft_delete']
);
Убедитесь, что вы удалили переопределенный параметр bravik.calendar.enable_soft_delete
из конфигурации приложения config/service.yaml
.
Попробуйте поменять теперь наш параметр в конфиге бандла и убедитесь, что кнопки «Удалить» в редакторе меняют свой цвет.
Мы можем немного оптимизировать наш services.yaml
и убрать вообще параметр bravik.calendar.enable_soft_delete
. Вместо этого мы напрямую передадим параметр enable_soft_delete
из конфигурации в нужный сервис:
//$container->setParameter(
// 'bravik.calendar.enable_soft_delete',
// $config['enable_soft_delete']
//);
$definition = $container->getDefinition(EditorController::class);
$definition->setArguments([
'$enableSoftDelete' => $config['enable_soft_delete'],
]);
Работа с конфигурацией
Для начала, проверим, работает ли валидация параметров.
Попробуйте вместо true/false
ввести в качестве значения строку:
Exception: Invalid type for path "calendar.enable_soft_delete". Expected boolean, but got string.
Проверим, что будет, если пользователь вообще не добавит этот параметр в конфиг
Exception: Undefined index: enable_soft_delete
Здесь уже наша недоработка: для пользователя это совершенно непонятная и неожиданная ошибка. Нам необходимо проверять есть ли заданный параметр в конфиге или нет. Но вместо этого давайте назначим этому параметру значение по умолчанию прямо в классе Configuration
.
$treeBuilder->getRootNode()
->children()
->booleanNode('enable_soft_delete')
->defaultValue(false)
// ->defaultFalse() // Сокращенная запись для booleanNode()
->end()
->end()
;
Или еще лучше, давайте потребуем, чтобы пользователь не смог проигнорировать этот параметр и самостоятельно принял решение. Сделаем параметр обязательным:
$treeBuilder->getRootNode()
->children()
->booleanNode('enable_soft_delete')
->isRequired()
->end()
->end()
;
Наш бандл постоянно совершенствуется, параметр может быть удален в следующей версии. Чтобы обратить на это внимание пользователя, можно объявить параметр deprecated
.
Можно так же добавить немного документации. В Symfony можно использовать команду bin/console config:dump calendar
, чтобы получить информацию о конфигурации бандла.
$treeBuilder->getRootNode()
->children()
->booleanNode('enable_soft_delete')
->isRequired()
->setDeprecated()
->info('Enables soft delete mode for articles. Articles would be marked as `archived` instead of deletion')
->end()
->end()
;
Добавим вложенный элемент с числовыми параметрами и правило валидации:
calendar:
limits:
per_day: 10
per_month: 100
$treeBuilder->getRootNode()
->children()
->arrayNode('limits')
->addDefaultsIfNotSet()
->children()
->integerNode('per_day')
->defaultValue(10)
->validate()
->ifTrue(function ($v) { return $v <= 0; })
->thenInvalid('Number must be positive')
->end()
->end()
->integerNode('per_month')
->defaultValue(100)
->validate()
->ifTrue(function ($v) { return $v <= 0; })
->thenInvalid('Number must be positive')
->end()
->end()
->end()
->end()
->end()
;
Здесь мы объявляем секцию с двумя фиксированными числовыми параметрами. Каждый из них проверяем: является ли он позитивным числом.
Для валидации параметров Symfony предоставляет целый набор правил, подробнее о которых написано здесь.
Усложним пример: сделаем массив объектов.
Допустим у наc мультиязычный календарь, нам нужно передать коды локалей и их имена. Локалей может быть произвольное количество.
Тогда конфигурация может выглядеть так:
calendar:
available_locales:
locales:
- { code: 'en', label: 'English' }
- { code: 'ru', label: 'Русский' }
$treeBuilder->getRootNode()
->children()
->arrayNode('locales')
->addDefaultChildrenIfNoneSet()
->arrayPrototype()
->children()
->scalarNode('code')
->defaultValue('ru')
->end()
->scalarNode('label')
->defaultValue('Русский')
->end()
->end()
->end()
->end()
->end()
;
Здесь мы определяем поле-массив, а также определяем через прототип структуру каждого из его элементов.
Это лишь несколько примеров использования конфигурации на практике. Подробнее обо всех возможностях можно прочитать в документации.
Резюме
Мы разобрались как определять и переопределять параметры бандла, создали конфигурационный файл, научили Symfony его понимать и валидировать.
Семантический строго определенный конфигурационный файл, — это интерфейс или контракт между вашим бандлом и его пользователем. Определяя этот интерфейс, вы выносите возможные взаимодействия пользователя с бандлом в одну точку и сохраняете за собой свободу изменений в других частях внутри бандла.
Финальную версию кода для этой статьи вы найдете в ветке 5-configuration.
В следующей статье научимся тестировать бандл отдельно от хоста и создадим микроприложение Symfony для запуска тестов прямо внутри бандла.
Другие статьи серии:
Часть 1. Минимальный бандл
Часть 2. Выносим код и шаблоны в бандл
Часть 3. Интеграция бандла с хостом: шаблоны, стили, JS
Часть 4. Интерфейс для расширения бандла
Часть 5. Параметры и конфигурация
Часть 6. Тестирование, микроприложение внутри бандла
Часть 7. Релизный цикл, установка и обновление