Определение и обработка значений конфигурации

Дата обновления перевода 2024-06-24

Определение и обработка значений конфигурации

Валидация значений конфигурации

После загрузки значений конфигурации из всех типов ресурсов, значения и их структура могут быть валидированы с использованием части "Определение" ("Definition") компонента Config. Значения конфигурации обычно должны отображать какую-либо иерархию. Также, значения должны быть определённого типа, быть ограничены в количестве или быть одним из заданных наборов значений. Например, следующая конфигурация (на YAML) отображает явную иерархию и некоторые правила валидации, которые должны быть применены к ней (вроде: "значение для auto_connect должно быть булевым"):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
database:
    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.

Экземпляр TreeBuilder должен быть возвращён из пользовательского класса Configuration, реализующего ConfigurationInterface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace Acme\DatabaseConfiguration;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class DatabaseConfiguration implements ConfigurationInterface
{
    public function getConfigTreeBuilder(): TreeBuilder
    {
        $treeBuilder = new TreeBuilder('database');

        // ... добавьте определения узлов в корень дерева 
        // $treeBuilder->getRootNode()->...

        return $treeBuilder;
    }
}

Добавление определений узлов в дерево

Переменные узлы

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

1
2
3
4
5
6
7
8
9
10
$rootNode
    ->children()
        ->booleanNode('auto_connect')
            ->defaultTrue()
        ->end()
        ->scalarNode('default_connection')
            ->defaultValue('default')
        ->end()
    ->end()
;

Сам корневой узел является узлом массива, имеет детей, вроде узла локации auto_connect и склаярный узел default_connection. В общем: после определения узла, вызов к end() аоднимает вас на один уровень в иерархии.

Тип узла

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

  • скаляров (общий тип, включающий в себя булевы значения, строки, числа, плавающие значения и null)
  • булевых значений
  • чисел
  • плавающих значений
  • enum (схоже со скалярами, но позволяет только ограниченный набор значений)
  • массивов
  • переменных (без валидации)

и создаются с помощью node($name, $type) или связанного с ними метода сокращения xxxxNode($name)

Ограничения числовых узлов

Числовые узлы (плавающие значения и числа) предоставляют два дополнительных ограничения - min() и max() - позволяющих валидировать значение:

1
2
3
4
5
6
7
8
9
10
11
12
13
$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

Узлы Enum предоставляют ограничение для сопоставления заданного вода с набором значений:

1
2
3
4
5
6
7
$rootNode
    ->children()
        ->enumNode('delivery')
            ->values(array('standard', 'expedited', 'priority'))
        ->end()
    ->end()
;

Это ограничит опции delivery до значений standard, expedited или priority.

Вы также можете предоставить значения исчисления для enumNode(). Давайте определим исчисление, описывающее возможные состояния примера выше:

1
2
3
4
5
6
enum Delivery: string
{
    case Standard = 'standard';
    case Expedited = 'expedited';
    case Priority = 'priority';
}

Конфигурацию теперь можно записать таким образом:

1
2
3
4
5
6
7
8
9
10
$rootNode
    ->children()
        ->enumNode('delivery')
            // Вы можете предоставить все значения исчисления...
            ->values(Delivery::cases())
            // ... или вы можете передать только некоторые значения на ряду с другими скалярными значениями
            ->values([Delivery::Priority, Delivery::Standard, 'other', false])
        ->end()
    ->end()
;

Узлы массива

Возможно добавить более глубокий уровень к иерархии, добавив узел массива. Узел массива сам по себе может имуть предопределённый набор переменных узлов:

1
2
3
4
5
6
7
8
9
10
11
12
$rootNode
    ->children()
        ->arrayNode('connection')
            ->children()
                ->scalarNode('driver')->end()
                ->scalarNode('host')->end()
                ->scalarNode('username')->end()
                ->scalarNode('password')->end()
            ->end()
        ->end()
    ->end()
;

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$rootNode
    ->children()
        ->arrayNode('connections')
            ->arrayPrototype()
                ->children()
                    ->scalarNode('driver')->end()
                    ->scalarNode('host')->end()
                    ->scalarNode('username')->end()
                    ->scalarNode('password')->end()
                ->end()
            ->end()
        ->end()
    ->end()
;

Прототип может быть использован для добавления определения, которо может быть много раз повторено в текущем узле. В соответствии с определением прототипа в примере выше, возможно иметь неколько массивов соединений (содержащих driver, host, и др.).

Иногда для улучшения опыта пользователя от вашего приложения или пакета, вы можете позволить использовать простую строку или числовое значение там, где требуется значение массива. Используйте помощника castToArray(), чтобы преобразовать эти переменные в массивы:

1
2
3
4
->arrayNode('hosts')
    ->beforeNormalization()->castToArray()->end()
    // ...
->end()

Опции узлов массива

До определения детей узла массива, вы можете предоставить опции вроде:

useAttributeAsKey()
Предоставьте название дочернего узла, значение которого должно быть использовано как ключ в полученном массиве. Этот метод также определяет то, как поступать с ключами массива конфигурации, что объясняется в следующем примере.
requiresAtLeastOneElement()
В массиве должен быть хотя бы один элемент (работает только тогда, когда также вызывается isRequired()).
addDefaultsIfNotSet()
Если какой-либо дочерний узел имеет значение по умолчанию, используйте его, если не было явно предоставлено другого значения.
normalizeKeys(false)
Если вызвана (с false), ключи с дефисами не нормализуются в нижние подчёркивания. Рекомендуется использовать с узлами прототипов, где пользователь будет определять отображение ключ-значение, чтобы избежать ненужных преобразований.
ignoreExtraKeys()
Позволяет указывать в массиве дополнительные ключи конфигурации без вызоыва исключений.

Базовая конфигурация прототипного массива можнт быть определена следующим образом:

1
2
3
4
5
6
7
8
$node
    ->fixXmlConfig('driver')
    ->children()
        ->arrayNode('drivers')
            ->scalarPrototype()->end()
        ->end()
    ->end()
;

При использовании следюущей YAML конфигурации

1
drivers: ['mysql', 'sqlite']

Или следующей XML конфигурации:

1
2
<driver>mysql</driver>
<driver>sqlite</driver>

Обработанная конфигурация:

1
2
3
4
Array(
    [0] => 'mysql'
    [1] => 'sqlite'
)

Более сложным примером будет определить прототипны массив с детьми:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$node
    ->fixXmlConfig('connection')
    ->children()
        ->arrayNode('connections')
            ->arrayPrototype()
                ->children()
                    ->scalarNode('table')->end()
                    ->scalarNode('user')->end()
                    ->scalarNode('password')->end()
                ->end()
            ->end()
        ->end()
    ->end()
;

При использовании следующей конфигурации YAML:

1
2
3
connections:
    - { table: symfony, user: root, password: ~ }
    - { table: foo, user: root, password: pa$$ }

Или следующей конфигурации XML:

1
2
<connection table="symfony" user="root" password="null" />
<connection table="foo" user="root" password="pa$$" />

Обработанная конфигурация:

1
2
3
4
5
6
7
8
9
10
11
12
Array(
    [0] => Array(
        [table] => 'symfony'
        [user] => 'root'
        [password] => null
    )
    [1] => Array(
        [table] => 'foo'
        [user] => 'root'
        [password] => 'pa$$'
    )
)

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

1
2
3
4
5
6
7
8
9
connections:
    sf_connection:
        table: symfony
        user: root
        password: ~
    default:
        table: foo
        user: root
        password: pa$$

Конфигурация вывода будет точно такой же, как и раньше. Другими словами, ключи конфигурации sf_connection и default теряются. Причиной этому является то, что компонент Symfony Config относится к массивам как к спискам по умолчанию.

Note

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

Для того, чтобы содержать ключи массива, используйте метод useAttributeAsKey():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$node
    ->fixXmlConfig('connection')
    ->children()
        ->arrayNode('connections')
            ->useAttributeAsKey('name')
            ->arrayPrototype()
                ->children()
                    ->scalarNode('table')->end()
                    ->scalarNode('user')->end()
                    ->scalarNode('password')->end()
                ->end()
            ->end()
        ->end()
    ->end()
;

Note

В YAML аргумент аргумент 'name' в useAttributeAsKey() имеет специальное значение и ссылается на ключ карты (sf_connection и default в этом примере). Если для узла connections был определен дочерний узел с ключом name, то этот ключ карты будет утерян.

Аргумент этого метода (name в примере выше) определяет название атрибута, добавляемого к каждому XML узлу для их дифференциации. Теперь вы можете использвать ту же конфигурацию YAML, что была показана ранее, или следующую конфигурацию XML:

1
2
3
4
<connection name="sf_connection"
    table="symfony" user="root" password="null" />
<connection name="default"
    table="foo" user="root" password="pa$$" />

В обоих случаях обработанная конфигурацию содержит ключи sf_connection и default keys:

1
2
3
4
5
6
7
8
9
10
11
12
Array(
    [sf_connection] => Array(
        [table] => 'symfony'
        [user] => 'root'
        [password] => null
    )
    [default] => Array(
        [table] => 'foo'
        [user] => 'root'
        [password] => 'pa$$'
    )
)

Требуемые значения и значения по умолчанию

Для всех типов узлов возможно определить значения по умолчанию и замещающие значения в случае, если узел имеет определённое значение:

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

Следующий пример отображает эти методы на практике:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$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()
;

Устаревание опции

Вы можете сделать опцию устаревшей, используя метод setDeprecated():

1
2
3
4
5
6
7
8
9
10
11
12
$rootNode
    ->children()
        ->integerNode('old_option')
            // выводит следующее общее сообщение об устарении:
            // Дочерний узел "old_option" по пути "..." устарел.
            ->setDeprecated()

            // вы также моежете передать пользовательское сообщеиие об устарении (доступны заполнители %node% и %path%):
            ->setDeprecated('Опция "%node%" устарела. Используйте "new_config_option" вместо неё.')
        ->end()
    ->end()
;

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

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

Все опции можно документировать с помощью метода info():

1
2
3
4
5
6
7
8
$rootNode
    ->children()
        ->integerNode('entries_per_page')
            ->info('Это значение используетсятолько для для страницы результатов поиска.')
            ->defaultValue(25)
        ->end()
    ->end()
;

Информация будет отображена в виде комментария при сбросе дерева конфигурации с помощью команды config:dump-reference.

В YAML у вас может быть:

1
2
# Это значение используетсятолько для для страницы результатов поиска.
entries_per_page: 25

А в XML:

1
2
<!-- entries-per-page: Это значение используетсятолько для для страницы результатов поиска. -->
<config entries-per-page="25" />

Опциональные разделы

Если у вас есть целые разделы, которые являются необязательными, и могут быть включены и отключены, вы можете воспользоваться преимуществами методов сокращения canBeEnabled() и canBeDisabled():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$arrayNode
    ->canBeEnabled()
;

// эквивалентно

$arrayNode
    ->treatFalseLike(['enabled' => false])
    ->treatTrueLike(['enabled' => true])
    ->treatNullLike(['enabled' => true])
    ->children()
        ->booleanNode('enabled')
            ->defaultFalse()
;

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

Объединение опций

Могут быть предоставлены дополнительные опции, касающиеся объединения. Для массивов:

performNoDeepMerging()
Когда значение также определено во втором массиве конфигурации, не пробуйте объединять массив, а полностью перепишите его

Для всех узлов:

cannotBeOverwritten()
не позвляйте другим массивам конфигурации перезаписывать существующее значение для этого узла

Добавление разделов

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
use Symfony\Component\Config\Definition\Builder\NodeDefinition;

public function getConfigTreeBuilder(): TreeBuilder
{
    $treeBuilder = new TreeBuilder('database');

    $treeBuilder->getRootNode()
        ->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(): NodeDefinition
{
    $treeBuilder = new TreeBuilder('parameters');

    $node = $treeBuilder->getRootNode()
        ->isRequired()
        ->requiresAtLeastOneElement()
        ->useAttributeAsKey('name')
        ->arrayPrototype()
            ->children()
                ->scalarNode('value')->isRequired()->end()
            ->end()
        ->end()
    ;

    return $node;
}

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

Пример приводит к следующему:

1
2
3
4
5
6
7
8
9
10
11
12
database:
    connection:
        driver:               ~ # Обязательно
        host:                 localhost
        username:             ~
        password:             ~
        memory:               false
        parameters:           # Обязательно

            # Прототип
            name:
                value:                ~ # Обязательно

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

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

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

Caution

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

Ещё одним различием между YAML и XML является то, как могут быть представлены значения массивов. В YAML у вас может быть:

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

А в XML:

1
2
3
4
<twig:config>
    <twig:extension>twig.extension.foo</twig:extension>
    <twig:extension>twig.extension.bar</twig:extension>
</twig:config>

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

1
2
3
4
5
6
7
8
$rootNode
    ->fixXmlConfig('extension')
    ->children()
        ->arrayNode('extensions')
            ->scalarPrototype()->end()
        ->end()
    ->end()
;

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

1
2
3
4
5
6
7
8
$rootNode
    ->fixXmlConfig('child', 'children')
    ->children()
        ->arrayNode('children')
            // ...
        ->end()
    ->end()
;

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

1
2
<connection>default</connection>
<connection>extra</connection>

А иногда только:

1
<connection>default</connection>

По умолчанию connection будет массиво в первом случае, и строкой - во втором, что приведёт к сложностям валидации. Вы можете гаранировать, чтобы он всегда был массивом, с помощью fixXmlConfig().

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

1
2
3
4
5
6
connection:
    name:     my_mysql_connection
    host:     localhost
    driver:   mysql
    username: user
    password: pass

Вы также можете позволить следующее:

1
connection: my_mysql_connection

Изменив значение строки в ассоциативный массив с name в качестве ключа:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$rootNode
    ->children()
        ->arrayNode('connection')
            ->beforeNormalization()
                ->ifString()
                ->then(function (string $v): array { return ['name' => $v]; })
            ->end()
            ->children()
                ->scalarNode('name')->isRequired()->end()
                // ...
            ->end()
        ->end()
    ->end()
;

Правила валидации

Более продвинутые правила валидации можно предоставить используя ExprBuilder. Этот конструктор реализует текущий интерфейс для широко известной структуры контроля. Конструктор используется для добавления продвинутых правил валидации к определениям узлов, вроде:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$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()
  • ifEmpty()
  • ifArray()
  • ifInArray()
  • ifNotInArray()
  • always()

Правило валидации также требует части "then" ("то"):

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

Обычно, "then" является закрывающим. Его возвратное значение будет использовано в качестве нового значения для узла, вместо исходного значения узла.

Конфигурация разделителя пути узла

Рассмотрите следующий пример конструктора конфигурации:

1
2
3
4
5
6
7
8
9
10
11
12
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('database');

$rootNode
    ->children()
        ->arrayNode('connection')
            ->children()
                ->scalarNode('driver')->end()
            ->end()
        ->end()
    ->end()
;

По умолчанию, иерархия узлов в пути конфигурации определяется с помощью символа точки (.):

1
2
3
4
5
6
7
// ...

$node = $treeBuilder->buildTree();
$children = $node->getChildren();
$childChildren = $children['connection']->getChildren();
$path = $childChildren['driver']->getPath();
// $path = 'database.connection.driver'

Используйте метод setPathSeparator()в конструкторе конфигурации, чтобы изменить разделитель пути:

1
2
3
4
5
6
7
8
// ...

$treeBuilder->setPathSeparator('/');
$node = $treeBuilder->buildTree();
$children = $node->getChildren();
$childChildren = $children['connection']->getChildren();
$path = $childChildren['driver']->getPath();
// $path = 'database/connection/driver'

Обработка значений конфигурации

Processor использует дерево, так как он был построен, используя TreeBuilder,чтобы обрабатывать несколько массивов значений конфигурации, которые должны быть объединены. Если какое-либо значение не имеет ожидаемый тип, обязательно, но не определено, или не может быть валидировано любым другим способом, будет вызвано исключение. Иначе, результатом будет чистый массив значений конфигурации:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use Acme\DatabaseConfiguration;
use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\Yaml\Yaml;

$config = Yaml::parse(
    file_get_contents(__DIR__.'/src/Matthias/config/config.yaml')
);
$extraConfig = Yaml::parse(
    file_get_contents(__DIR__.'/src/Matthias/config/config_extra.yaml')
);

$configs = [$config, $extraConfig];

$processor = new Processor();
$databaseConfiguration = new DatabaseConfiguration();
$processedConfiguration = $processor->processConfiguration(
    $databaseConfiguration,
    $configs
);

Caution

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