Визначення та обробка значень конфігурації

Дата оновлення перекладу 2024-04-30

Визначення та обробка значень конфігурації

Валідація значень конфігурації

Після завантаження значень конфігурації з усіх типів джерел, значення та їх структура можуть бути валідовані з використанням частини "Визначення" компонента Конфігурація. Значення конфігурації зазвичай мають відображати якусь ієрархію. Також, значення мають бути печного типу, бути обмежені у кіькості або бути одним з заданих наборів значень. Наприклад, наступна конфігурація (на 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 Конфігурація відноситься до масивів як до списків за замовчуванням.

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:

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(array('enabled' => false))
    ->treatTrueLike(array('enabled' => true))
    ->treatNullLike(array('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
13
14
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('database');

$treeBuilder = new TreeBuilder('database');

$treeBuilder->getRootNode()
    ->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

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