Как переиспользовать код с бандлами Symfony 5? Часть 5. Конфигурация

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


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


    • Параметры DI-контейнера и их переопределение
    • Файл конфигурации бандла
    • Работа с конфигурацией


    Если вы не последовательно выполняете туториал, то скачайте приложение из репозитория и переключитесь на ветку 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 приложения-хоста. Приоритет будет отдан параметру, указанному в конфигурации хоста, а не бандла!


    Такой подход работает для простых случаев, но имеет недостатки:


    1. Во-первых, мы грубо вмешиваемся в работу бандла. У нас нет приватных параметров, мы не можем запретить пользователю что-то переопределять, и не можем защитить его от «выстрелов себе в ногу». Мы можем просто изменить параметр конфигурации в следующем релизе, а у пользователя возникнут неожиданные проблемы. А зная это, мы не сможем спокойно работать с параметрами, не опасаясь
    2. Во-вторых, мы не можем валидировать корректность конфигурации.

    Удалите переопределенный параметр из конфигурации приложения.


    Файл конфигурации бандла


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

    После этой договоренности мы уже свободно могли бы менять наш внутренний конфиг, не опасаясь проблем у пользователей. Для этого в 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. Релизный цикл, установка и обновление

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 0

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

    Самое читаемое