Определение и проверка параметров конфигурации для бандла в Symfony 2

Проверка параметров конфигурации

После загрузки параметров из различных источников, их значение и структуру можно проверить при помощи “Definition” части компонента Config. Чаще всего параметры устроены иерархично. Так же, как правило, на них накладываются определенные ограничение, например, параметр может принимать только числовое значение или быть одним из нескольких предопределенных значений.

В следующем примере конфигурации используется четкая иерархия и несколько правил проверки значений (значение auto_connect может иметь только булево значение).

auto_connect: true
default_connection: mysql
connections:
   mysql:
       host:     localhost
       driver:   mysql
       username: user
       password: pass
   sqlite:
       host:     localhost
       driver:   sqlite
       memory:   true
       username: user
       password: pass

При подключении дополнительных файлов конфигурации, они должны корректно друг друга дополнять и, возможно, переопределять некоторые значения. В то же время все остальные значения должны остаться нетронутыми. Так же некоторые значения должны иметь смысл только при наличии каких-то других параметров (В примере выше параметр memory имеет смысл только если значение параметра driver - sqlite).

Определение иерархии конфигурационных параметров при помощи TreeBuilder

Все правила, касающиеся параметров конфигурации задаются в TreeBuilder. Требуется создать свой класс Configuration, который реализует методы ConfigurationInterface и возвращает экземпляр TreeBuilder.

<?php
namespace Acme\DatabaseConfiguration;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
class DatabaseConfiguration implements ConfigurationInterface
{
   public function getConfigTreeBuilder()
   {
       $treeBuilder = new TreeBuilder();
       $rootNode = $treeBuilder->root('database');
       // ... add node definitions to the root of the tree
       return $treeBuilder;
   }
}

Добавление узлов в дерево конфигурации

Дерево параметров содержит в себе узлы размещенные по семантическому принципу. То есть при помощи отступов и простой системы обозначений можно с легкостью представить структуру конфигурационного дерева параметров.

<?php
$rootNode
   ->children()
       ->booleanNode('auto_connect')
           ->defaultTrue()
       ->end()
       ->scalarNode('default_connection')
           ->defaultValue('default')
       ->end()
   ->end();

Корневой узел — имеет тип массив и содержит в себе дочерние узлы, как например, узел булево значение auto_connect и скалярную величину default_connection, а вызвав метод end() после определения узла, вы переходите на один шаг вверх по иерархии.

Типы узлов

Значение параметра можно легко проверить, задав нужное определение узла. Существуют следующие значения:

  • scalar
  • boolean
  • integer
  • float
  • enum
  • array
  • variable (no validation)

Их можно создать при помощи метода node($name, $type) или при помощи соответствующего типу метода xxxxNode($name).

Правила валидации для числового узла

Числовые значения (float, integer) используют следующие ограничения — min(), max(), при помощи которых можно проверять значения.

<?php
$rootNode
   ->children()
       ->integerNode('positive_value')
           ->min(0)
       ->end()
       ->floatNode('big_value')
           ->max(5E45)
       ->end()
       ->integerNode('value_inside_a_range')
           ->min(-50)->max(50)
       ->end()
   ->end();

Тип узла для перечислений (Enum)

Значение должно быть выбрано из списка ранее заданных вариантов.

<?php
$rootNode
   ->children()
       ->enumNode('gender')
           ->values(array('male', 'female'))
       ->end()
   ->end();

Таким образом параметр gender может принимать только значения male или female.

Тип узла для массивов

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

<?php
$rootNode
   ->children()
       ->arrayNode('connection')
           ->children()
               ->scalarNode('driver')->end()
               ->scalarNode('host')->end()
               ->scalarNode('username')->end()
               ->scalarNode('password')->end()
           ->end()
       ->end()
   ->end();

Либо можно задать прототип каждого узла внутри массива:

<?php
$rootNode
   ->children()
       ->arrayNode('connections')
           ->prototype('array')
               ->children()
                   ->scalarNode('driver')->end()
                   ->scalarNode('host')->end()
                   ->scalarNode('username')->end()
                   ->scalarNode('password')->end()
               ->end()
           ->end()
       ->end()
   ->end();

Прототип можно использовать несколько раз внутри текущего узла. В соответствии с предыдущим примером, наша настройка может заключать в себе несколько массивов для соединения с БД (driver, host и т.д.). Т.е. прототип — это набор похожих конфигурация, как показано в примере несколько соединений с БД.

Параметры узла типа массив

Перед определением дочерних элементов массива, можно задать некоторые его параметры:

useAttributeAsKey() — задает имя дочернего узла, чье значение будет использовано в качестве ключа в массиве

requiresAtLeastOneElement() — массив должен содержать хотя бы один элемент.

addDefaultsIfNotSet() — если у каких-то узлов присутствует значение по-умолчанию, то именно оно будет использовано при отсутствии значения в настройке.

Пример:

<?php
$rootNode
   ->children()
       ->arrayNode('parameters')
           ->isRequired()
           ->requiresAtLeastOneElement()
           ->useAttributeAsKey('name')
           ->prototype('array')
               ->children()
                   ->scalarNode('value')->isRequired()->end()
               ->end()
           ->end()
       ->end()
   ->end();

При использовании YAML настройка будет выглядеть следующим образом:

database:
   parameters:
       param1: { value: param1val }

При XML каждый параметр будет иметь атрибут name (и value), который в последствии будет удален и использован в качестве ключа в результирующем массиве. Использование useAttributeAsKey помогает привести массивы к одному виду при использовании разных форматов (XML или YAML).

Обязательные значения и значения по-умолчанию

Для каждого узла можно задать не только значение по-умолчанию, но значение, которое будет использовано в случае, если другой параметр имеет определенное значение.

defaultValue() — задает значение по-умолчанию. isRequired() — обязательный параметр. cannotBeEmpty() — значение не должно быть пустым. default*() (null, true, false) — краткое обозначение метода defaultValue().treat*Like() — заменяет значение на одно из (null, true, false) если задано *.

<?php
$rootNode
   ->children()
       ->arrayNode('connection')
           ->children()
               ->scalarNode('driver')
                   ->isRequired()
                   ->cannotBeEmpty()
               ->end()
               ->scalarNode('host')
                   ->defaultValue('localhost')
               ->end()
               ->scalarNode('username')->end()
               ->scalarNode('password')->end()
               ->booleanNode('memory')
                   ->defaultFalse()
               ->end()
           ->end()
       ->end()
       ->arrayNode('settings')
           ->addDefaultsIfNotSet()
           ->children()
               ->scalarNode('name')
                   ->isRequired()
                   ->cannotBeEmpty()
                   ->defaultValue('value')
               ->end()
           ->end()
       ->end()
   ->end();

Документирование параметров

К каждому параметру можно задать дополнительную информацию при помощи метода [info()](http://api.symfony.com/2.5/Symfony/Component/Config/Definition/Builder/NodeDefinition.html#info()). Эта информация будет выведена в файле конфигурации в качестве комментария.

Необязательные участки конфигурации

Если в вашей настройке существует целый участок, который может быть, а может и не быть использован, то его можно либо задействовать, либо отключить при помощи методов [canBeEnabled()](http://api.symfony.com/2.5/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.html#canBeEnabled()) и [canBeDisabled()](http://api.symfony.com/2.5/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.html#canBeDisabled()):

<?php
$arrayNode
   ->canBeEnabled();
// эквивалентно
$arrayNode
   ->treatFalseLike(array('enabled' => false))
   ->treatTrueLike(array('enabled' => true))
   ->treatNullLike(array('enabled' => true))
   ->children()
       ->booleanNode('enabled')
           ->defaultFalse();

Метод canBeDisabled() выглядит абсолютно также, за исключением того, что секция будет отключена от конфигурации.

Слияние параметров

Существуют дополнительные варианты для слияния параметров. Для узлов типа массив:

performNoDeepMerging() — когда определены параметры конфигурации для второго массива, не сливает массив, а полностью переписывает его.

Для значений всех узлов:

cannotBeOverwritten() — не позволяет другому значению массива переписать уже существующие значения для этого узла.

Дополнительные секции

При сложной конфигурацию, дерево может разрастаться довольно быстро, в таком случае его следует разбить на участки. Сделать это вы можете создав отдельный узел для каждой секции, затем прикрепить его к основому дереву командой append():

<?php
public function getConfigTreeBuilder()
{
   $treeBuilder = new TreeBuilder();
   $rootNode = $treeBuilder->root('database');
   $rootNode
       ->children()
           ->arrayNode('connection')
               ->children()
                   ->scalarNode('driver')
                       ->isRequired()
                       ->cannotBeEmpty()
                   ->end()
                   ->scalarNode('host')
                       ->defaultValue('localhost')
                   ->end()
                   ->scalarNode('username')->end()
                   ->scalarNode('password')->end()
                   ->booleanNode('memory')
                       ->defaultFalse()
                   ->end()
               ->end()
               ->append($this->addParametersNode())
           ->end()
       ->end();
   return $treeBuilder;
}
public function addParametersNode()
{
   $builder = new TreeBuilder();
   $node    = $builder->root('parameters');
   $node
       ->isRequired()
       ->requiresAtLeastOneElement()
       ->useAttributeAsKey('name')
       ->prototype('array')
           ->children()
               ->scalarNode('value')->isRequired()->end()
           ->end()
       ->end();
   return $node;
}

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

Нормализация

При обработке файлов конфигурации, сначала их необходимо нормализовать, затем соединить, а после чего использовать дерево для проверки получившегося массива. Процесс нормализации применяется для удаления различий, которые могут возникнуть из-за разных форматов конфигурации, в основном между YAML и XML.

В этих форматах используются разные разделители: _ для YAML и  для XML. Например, auto_connect в YAML и auto-connect в XML. Нормализацией этих двух форматов будет auto_connect.

Целевой параметр не будет изменен, если он смешанного типа, как foo-bar_moo или если он уже существует.

Еще одно различие между YAML и XML заключается в представлении значений массива. В YAML это будет

twig:
   extensions: ['twig.extension.foo', 'twig.extension.bar'] 

а в XML

<twig:config>
   <twig:extension>twig.extension.foo</twig:extension>
   <twig:extension>twig.extension.bar</twig:extension>
</twig:config>

Это различие может быть устранено путем нормализации, которая заключается в использовании параметров, используемых в XML, во множественном числе. Вы можете указать на то, что вам необходимо использование параметра во множественном числе с помощью fixXmlConfig():

<?php
$rootNode
   ->fixXmlConfig('extension')
   ->children()
       ->arrayNode('extensions')
           ->prototype('scalar')->end()
       ->end()
   ->end()
;

Если это особая форма множественного числа, то вы можете указать тип множественного числа в качестве второго параметра:

<?php
$rootNode
   ->fixXmlConfig('child', 'children')
   ->children()
       ->arrayNode('children')
           // ...
       ->end()
   ->end();

После этого команда fixXmlConfig позволит вам перевести отдельные элементы XML в массив. Так, у вас может получиться:

<connection>default</connection>
<connection>extra</connection>

Или иногда только

<connection>default</connection>

В первом случае параметр connection должен иметь значение типа массив с самого начала, а во втором случае строковое значение, но это усложняет проверку. Вы можете убедиться в том, что это массив с помощью команды fixXmlConfig.

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

connection:
   name:     my_mysql_connection
   host:     localhost
   driver:   mysql
   username: user
   password: pass

Вы можете использовать:

connection: my_mysql_connection

Изменяя строковое значение для соответствующего массива с помощью параметра:

<?php
$rootNode
   ->children()
       ->arrayNode('connection')
           ->beforeNormalization()
               ->ifString()
               ->then(function($v) { return array('name'=> $v); })
           ->end()
           ->children()
               ->scalarNode('name')->isRequired()
               // ...
           ->end()
       ->end()
   ->end();

Правила проверки

Можно задавать и более сложные правила проверки значений при помощи ExprBuilder. Этот класс реализует методы простого интерфейса для широко известных сруктур. Он используется для определенния сложных правил проверки:

<?php
$rootNode
   ->children()
       ->arrayNode('connection')
           ->children()
               ->scalarNode('driver')
                   ->isRequired()
                   ->validate()
                   ->ifNotInArray(array('mysql', 'sqlite', 'mssql'))
                       ->thenInvalid('Invalid database driver "%s"')
                   ->end()
               ->end()
           ->end()
       ->end()
   ->end();

Такое правило проверки всегда имеет часть if, которая может принимать следующее значение:

  • ifTrue()
  • ifString()
  • ifNull()
  • ifArray()
  • ifInArray()
  • ifNotInArray()
  • always()

Также такое правило должно иметь часть then:

  • then()
  • thenEmptyArray()
  • thenInvalid()
  • thenUnset()

Значение then будет использовано в том случае, если значение параметра не прошло проверку.

Обработка параметров конфигурации

Класс Proccessor использует дерево, которое мы создали при помощи TreeBuilder и обрабатывает различные значения массивов параметров для их объединения в дальнейшем.

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

use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Config\Definition\Processor;
use Acme\DatabaseConfiguration;
$config1 = Yaml::parse(__DIR__.'/src/Matthias/config/config.yml');
$config2 = Yaml::parse(__DIR__.'/src/Matthias/config/config_extra.yml');
$configs = array($config1, $config2);
$processor = new Processor();
$configuration = new DatabaseConfiguration();
$processedConfiguration = $processor->processConfiguration(
   $configuration,
   $configs
);