跳到内容

定义和处理配置值

编辑此页

验证配置值

从各种资源加载配置值后,可以使用 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 键仅在 driversqlite 时才有意义)。

使用 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 选项为 standardexpeditedpriority 之一。

你也可以为 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()
;

原型可以用于添加一个可以在当前节点内重复多次的定义。根据上面示例中的原型定义,可以有多个连接数组 (包含 driverhost 等)。

有时,为了改善你的应用程序或 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_connectiondefault 配置键丢失了。原因是 Symfony Config 组件默认将数组视为列表。

注意

在编写本文时,存在一个不一致之处:如果只有一个文件提供所讨论的配置,则键 (即 sf_connectiondefault) 不会丢失。但是,如果多个文件提供配置,则键将如上所述丢失。

为了保持数组键,请使用 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_connectiondefault)。如果为键为 nameconnections 节点定义了子节点,则映射的该键将丢失。

此方法的参数 (上面示例中的 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_connectiondefault

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
);

警告

当处理配置树时,处理器假定顶级数组键 (与扩展名匹配) 已被剥离。

这项工作,包括代码示例,根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本