Конфигурация Symfony2 проекта, используя TreeBuilder

В этой статье хотелось бы немного пролить свет на один из этапов создания Symfony2 бандла - система конфигураций на основе TreeBuilder.

Создание расширения

Сперва нужно создать расширение, которое будет лежать в папке DependencyInjection и выполнять 2 основных задачи:

  • Проверка конфигурации
  • Подключение сервисов

Далее я опишу этап проверки конфигурации. Для этого воспользуемся демо бандлом Acme\DemoBundle, которые можно использовать при создании нового Symfony2 проекта.

Новое расширение должно иметь путь /DependencyInjection/[BundleNameWithoutBundle]Extension.php (в нашем случае AcmeDemoExtension) и иметь похожее содержимое:

namespace Acme\DemoBundle\DependencyInjection;
 
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Definition\Processor;
 
class AcmeDemoExtension extends Extension
{
     public function load(array $configs, ContainerBuilder $container)
     {
         $processor = new Processor();
         $configuration = new Configuration();
         $config = $processor->processConfiguration($configuration, $configs);
 
         $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
         $loader->load('services.yml');
     }
 
     public function getAlias()
     {
         return 'acme_demo';
     }
}

Для примеря создадим возможность включать / выключать наш бандл. Пример конфигурации /app/config.yml:

acme_demo:
    enabled: true

Далее нужно создать файл конфигурации.

Класс Configuration

Создадим класс Configuration в том же каталоге: /DependencyInjection/Configuration.php и в нем метод getConfigTreeBuilder, который должен вернуть нам объект TreeBuilder:

namespace Acme\DemoBundle\DependencyInjection;
 
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
 
class Configuration implements ConfigurationInterface
{
     public function getConfigTreeBuilder()
     {
         $treeBuilder = new TreeBuilder();
         $rootNode = $treeBuilder->root('acme_demo');
 
         return $treeBuilder;
     }
}

Наш новый класс содержит метод создания пустого объекта TreeBuilder, что позволяет конфигурировать бандл. Но пока мы не проверяем вложенные параметры конфигурации. Для примера мы решили проверять параметр, включен ли бандл или нет. Для этого подойдет BooleanNode, проверяющий boolean значения.

BooleanNode

Существует много способов для указания правил првоерки значений конфигурации, но сначала давайте создадим новый узел в дереве для нашего пока еще недоступоного значения "enabled". Значение всегда должно быть типа "boolean", поэтому, прежде чем вернуть $rootNode добавим:

$rootNode->
     children()
         ->booleanNode('enabled')->end()
     end()
;

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

Вызов метода end() означает, что мы хотим вернуться к родительскому узлу текущего узла.

ScalarNode

Давайте добавим узел для скалярных значений (строки, числа...):

$rootNode->
     children()
         ->booleanNode('enabled')->end()
         ->scalarNode('default_user')
             ->isRequired()
             ->cannotBeEmpty()
         ->end()
     end()
;

Новый узел определяет значение конфигурации "default_user", который является обязательным и не может быть пустым.

ArrayNode

Далее разберемся с массивами. Создадим узел массива, который позволяет группе пользователей быть указанными в конфигурационном файле:

$rootNode->
     children()
         ->booleanNode('enabled')->end()
         ->scalarNode('default_user')
             ->isRequired()
             ->cannotBeEmpty()
         ->end()
         ->arrayNode('users')
             ->requiresAtLeastOneElement()
             ->prototype('array')
                 ->children()
                     ->scalarNode('full_name')
                         ->isRequired(true)
                     ->end()
                     ->booleanNode('is_active')
                         ->defaultValue(true)
                     ->end()
                 ->end()
             ->end()
         ->end()
     ->end()
;

Здесь у нас есть родительский элемент "user" и определен массив элементов, которые будут определять каждого отдельного пользователя со своими скалярными значениями (full_name, is_active). Наш узел подразумевает начилие хотябы одного пользователя.

Предварительная нормализация

Последний момент, который нужно знать - есть возможность изменять данные перед их проверкой.

$rootNode->
     ->beforeNormalization()
         ->ifTrue(function($v) {
             // $v contains the raw configuration values
             return !isset($v['enabled']) || false === $v['enabled'];
         })
         ->then(function($v) {
             unset($v['users']);
             return $v;
         })
         ->end()
     ->children()
          // ...
     ->end()
;

Здесь мы в методе ifTrue() проверяем значение "enabled" каждого пользователя. И удаляем пользователя из списка, если он неактивен.

Похожий принцип можно использовать при подключении модулей или сервисов. Для примера, можно проверять свой services.xml, расположенный в /DependencyInjection/AcmeDemoBundleExtension.php

class AcmeDemoExtension extends Extension
{
     public function load(array $configs, ContainerBuilder $container)
     {
         $processor = new Processor();
         $configuration = new Configuration();
         $config = $processor->processConfiguration($configuration, $configs);
 
         if (isset($config['enabled']) && $config['enabled']) {
             $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
             $loader->load('services.xml');
         }
     }
}

Теги: Symfony2, Treebuilder, Configuration, Dependencyinjection, Проверка конфигурации