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