Поговорим о том, как прекратить копипастить между проектами и вынести код в переиспользуемый подключаемый бандл 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. Релизный цикл, установка и обновление
ссылка на оригинал статьи https://habr.com/ru/post/499076/
Добавить комментарий