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