定义和处理配置值
验证配置值
从各种资源加载配置值后,可以使用 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');
// ... add node definitions to the root of the tree
// $treeBuilder->getRootNode()->...
return $treeBuilder;
}
}
向树中添加节点定义
可变节点
树包含节点定义,这些定义可以以语义方式布局。这意味着,使用缩进和流式表示法,可以反映配置值的真实结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
$rootNode
->children()
->booleanNode('auto_connect')
->defaultTrue()
->end()
->scalarNode('default_connection')
->defaultValue('mysql')
->end()
->stringNode('username')
->defaultValue('root')
->end()
->stringNode('password')
->defaultValue('root')
->end()
->end()
;
7.2
stringNode()
方法在 Symfony 7.2 中引入。
根节点本身是一个数组节点,并且有子节点,例如布尔节点 auto_connect
和标量节点 default_connection
。一般来说:在定义一个节点后,调用 end()
会使你在层级结构中上升一步。
节点类型
可以通过使用适当的节点定义来验证提供的值的类型。节点类型适用于
- 标量 (通用类型,包括布尔值、字符串、整数、浮点数和
null
) - 布尔值
- 字符串
- 整数
- 浮点数
- 枚举 (类似于标量,但它只允许有限的值集)
- 数组
- 可变 (无验证)
并且使用 node($name, $type)
或其关联的快捷方式 xxxxNode($name)
方法创建。
7.2
对 string
类型的支持在 Symfony 7.2 中引入。
数值节点约束
数值节点 (浮点数和整数) 提供两个额外的约束 - 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()
;
枚举节点
枚举节点提供了一个约束,用于将给定的输入与一组值进行匹配
1 2 3 4 5 6 7
$rootNode
->children()
->enumNode('delivery')
->values(['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')
// You can provide all values of the enum...
->values(Delivery::cases())
// ... or you can pass only some values next to other scalar values
->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
等)。
有时,为了改善你的应用程序或 bundle 的用户体验,你可能允许在使用数组值是必需的地方使用简单的字符串或数值。使用 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 组件默认将数组视为列表。
注意
在编写本文时,存在一个不一致之处:如果只有一个文件提供所讨论的配置,则键 (即 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()
;
注意
在 YAML 中,useAttributeAsKey()
的 'name'
参数具有特殊含义,指的是映射的键 (在本例中为 sf_connection
和 default
)。如果为键为 name
的 connections
节点定义了子节点,则映射的该键将丢失。
此方法的参数 (上面示例中的 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 13 14 15 16
$rootNode
->children()
->integerNode('old_option')
// this outputs the following generic deprecation message:
// Since acme/package 1.2: The child node "old_option" at path "..." is deprecated.
->setDeprecated('acme/package', '1.2')
// you can also pass a custom deprecation message (%node% and %path% placeholders are available):
->setDeprecated(
'acme/package',
'1.2',
'The "%node%" option is deprecated. Use "new_config_option" instead.'
)
->end()
->end()
;
如果你使用 Web Debug Toolbar,则在重建配置时会显示这些弃用通知。
记录选项
所有选项都可以使用 info() 方法进行记录
1 2 3 4 5 6 7 8
$rootNode
->children()
->integerNode('entries_per_page')
->info('This value is only used for the search results page.')
->defaultValue(25)
->end()
->end()
;
当使用 config:dump-reference
命令转储配置树时,信息将作为注释打印出来。
在 YAML 中,你可能有
1 2
# This value is only used for the search results page.
entries_per_page: 25
在 XML 中,你可能有
1 2
<!-- entries-per-page: This value is only used for the search results page. -->
<config entries-per-page="25"/>
可选章节
如果你有整个章节是可选的并且可以启用/禁用,你可以利用快捷方式 canBeEnabled() 和 canBeDisabled() 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14
$arrayNode
->canBeEnabled()
;
// is equivalent to
$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: ~ # Required
host: localhost
username: ~
password: ~
memory: false
parameters: # Required
# Prototype
name:
value: ~ # Required
规范化
当配置的文件被处理时,它们首先被规范化,然后被合并,最后树被用来验证结果数组。规范化过程用于消除不同配置格式导致的一些差异,主要是 YAML 和 XML 之间的差异。
键中使用的分隔符在 YAML 中通常为 _
,在 XML 中通常为 -
。例如,YAML 中的 auto_connect
和 XML 中的 auto-connect
。规范化会将它们都变成 auto_connect
。
警告
如果目标键是混合的,例如 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(['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
$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
);
警告
当处理配置树时,处理器假定顶级数组键 (与扩展名匹配) 已被剥离。