定义和处理配置选项值

3.4 版本
维护中的版本

译注:光荣的PHPer,利用Packagist分发自己的程序功能,让天下Symfony开发者通过Composer安装,再经语义化Custom配置即可使用

本文上级者向,且为真枪实弹,非是“阅读+理解”型文章。初学建议绕道,精进到中段时再来读过,也完全来得及。

在此,SymfonyChina祝愿每一位框架爱好者都能真正领悟“业界标准”Symfony之强大威力。

超高难内容 本文档并非孤立,它涉及SF核心的方方面面,特别是DIC部分,精通DIC才可熟用配置。一体两面的东西,阅读时要特别注意。

Configuration隶属Symfony Extension,用于构建底层程序给其他程序员使用。SF最高奥义,完全Flexible,令你的扩展无比强大

Tip

除本文外,还应多加阅读揣摩Symfony相关源代码,比如FrameworkBundle中的若干文件。最好自己手写一个本地bundle,并对其应用语义化配置。

验证配置选项的值 

从全部类型的资源中加载配置值之后,这些值和它们的结构,可以通过Config组件的“Definition”定义部分,进行验证。配置值,通常被预期来显示某些类型的架构(hierarchy)。同时,值应当有其类型,被限制为数字,或是成为给定“值组合”(set of values)中的一员。例如,下列配置(以YAML格式)显示了一个清楚的架构,而一些验证规则应当被应用到其中(像是“ auto_connect 必须是布尔值”):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
18
namespace Acme\DatabaseConfiguration;
 
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
 
class DatabaseConfiguration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('database');
 
        // ... add node definitions to the root of the tree
        // ... 添加树根处的节点定义
 
        return $treeBuilder;
    }
}

对Tree添加节点定义 

Variable节点 

树状结构(tree),包含了“可以通过一种语义化方式展开”的节点定义。这意味着,使用缩进和箭头符号,是可以映射出配置值的真实结构的:

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() 即可令你在层级架构中前进一步。

节点类型 

通过使用合适的节点定义,就有可能验证提交的配置值类型。可用的节点类型有:

  • scalar (标量。通用类型,包括 booleans, strings, integers, floats 和 null)
  • boolean
  • integer
  • float
  • enum (类似于标量,但只允许值的有限组合)
  • array
  • variable (变量。无法验证)

它们由 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节点 

枚举节点提供了一个“针对一组值来匹配给定的输入值”的约束:

1
2
3
4
5
6
7
$rootNode
    ->children()
        ->enumNode('gender')
            ->values(array('male', 'female'))
        ->end()
    ->end()
;

这将限定 gender 选项为 malefemale 之一。

数组节点 

使用数组节点,令添加深层架构成为可能。数组节点本身,可以是一组预定义的(pre-defined)变量节点:

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

或者你也可以对数组节点中的每一个节点定义一个原型(prototype):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$rootNode
    ->children()
        ->arrayNode('connections')
            ->prototype('array')
                ->children()
                    ->scalarNode('driver')->end()
                    ->scalarNode('host')->end()
                    ->scalarNode('username')->end()
                    ->scalarNode('password')->end()
                ->end()
            ->end()
        ->end()
    ->end()
;

所谓原型,可以被用来在当前节点中“多次重复地添加一个定义”。根据上例中的原型定义,程序可以拥有多个连接数组(各自包括 driver , host 等等)。

数组节点的选项 

在定义一个数组节点的子节点之前,你可以使用如下选项:

useAttributeAsKey()
提供一个子节点的名称,其值应该作为结果数组的键来使用。本方法同时定义了配置数组的键的操作方式,在下例中会解释。
requiresAtLeastOneElement()
数组中至少应该有一个元素 (仅当 isRequired() 同时被调用时)。
addDefaultsIfNotSet()
如果任何子节点有默认值,在显式的配置值未被提供时,使用它们。
normalizeKeys(false)
如果被调用 (使用 false),键中的中杠将 被normalized为下划线。当用户定义了一个“键-值”映射关系时,为了避免不必要的转换,推荐在原型节点中使用之。

基本的prototype数组配置可以按下例进行:

1
2
3
4
5
6
7
8
$node
    ->fixXmlConfig('driver')
    ->children()
        ->arrayNode('drivers')
            ->prototype('scalar')->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'
)

更加复杂的例子应该是在ptototyped数组节点中使用子节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$node
    ->fixXmlConfig('connection')
    ->children()
        ->arrayNode('connections')
            ->prototype('array')
                ->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组件默认把数组当作清单(list)来对待。

Note

截止到本文写作为止,仍有一个前后矛盾的地方:如果只有一个文件提供考虑中的配置信息,键(如 sf_connectiondefault )并 会丢失。一旦当多个文件提供配置信息时,键就会像上面提到的那样丢失了。

为了维护数组的键,可使用 useAttributedAsKey() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$node
    ->fixXmlConfig('connection')
    ->children()
        ->arrayNode('connections')
            ->useAttributeAsKey('name')
            ->prototype('array')
                ->children()
                    ->scalarNode('table')->end()
                    ->scalarNode('user')->end()
                    ->scalarNode('password')->end()
                ->end()
            ->end()
        ->end()
    ->end()
;

这个方法的参数(上例中的 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
Array(
    [sf_connection] => Array(
        [table] => 'symfony'
        [user] => 'root'
        [password] => null
    )

默认值和必填值 

对于所有节点类型,当节点是一个特定值的时,定义默认值和取代值都是可能的:

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

选项的文档化 

所有选项可以通过 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(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
public function getConfigTreeBuilder()
{
    $treeBuilder = new TreeBuilder();
    $rootNode = $treeBuilder->root('database');
 
    $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()
                ->append($this->addParametersNode())
            ->end()
        ->end()
    ;
 
    return $treeBuilder;
}
 
public function addParametersNode()
{
    $builder = new TreeBuilder();
    $node = $builder->root('parameters');
 
    $node
        ->isRequired()
        ->requiresAtLeastOneElement()
        ->useAttributeAsKey('name')
        ->prototype('array')
            ->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 / 必须
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<database>
    <!-- driver: Required / 必须 -->
    <connection
        driver=""
        host="localhost"
        username=""
        password=""
        memory="false"
    >
 
        <!-- prototype / 原型 -->
        <!-- value: Required / 必须 -->
        <parameters
            name="parameters name"
            value=""
        />
 
    </connection>
</database>

Normalization 

当配置文件被处理时,它们首先被normalize(标准化),然后被合并为最终的树状结构,以用于验证结果数组。标准化的进程,是用于消除某些“由不同配置类型,特别是XML和YAML之间的不同”所带来的差异。

在YAML中,典型的分隔符是 typically _ ,而XML中却是 -。例如,YAML中的 auto_connect 和XML中的 auto-connect。标准化将把这些统一为 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中使用的键进行复数化(pluralizing),这种差异在标准化过程中会被消除。你可以配合 fixXmlConfig() 以这种方式来指定“希望变成复数的键”:

1
2
3
4
5
6
7
8
$rootNode
    ->fixXmlConfig('extension')
    ->children()
        ->arrayNode('extensions')
            ->prototype('scalar')->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 ($v) { return array('name' => $v); })
            ->end()
            ->children()
                ->scalarNode('name')->isRequired()
                // ...
            ->end()
        ->end()
    ->end()
;

验证规则 

更加高端的验证规则(validation rule),可以由 ExprBuilder 来提供。这个builder针对广为人知的控制结构,实现了一个流畅的接口。builder被用于对节点定义(node definition)添加高端验证规则:

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()
  • ifArray()
  • ifInArray()
  • ifNotInArray()
  • always()

同时,一个验证规则需要一个“then”部分:

  • then()
  • thenEmptyArray()
  • thenInvalid()
  • thenUnset()

通常,“then”是一个closure(闭包)。它返回的值将是作为节点的新值来使用,而不是节点原来的值。

处理配置选项的值 

Processor 会使用树状结构,因为它被构造时用到了 TreeBuilder 来处理多个配置值的数组。任何一个值没有按照预期类型提供,必须提供而未提供或者未定义,或者未通过某种方式的验证,都会抛出一个异常。反之,处理结果就是一个整齐的配置值数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Config\Definition\Processor;
use Acme\DatabaseConfiguration;
 
$config1 = Yaml::parse(
    file_get_contents(__DIR__.'/src/Matthias/config/config.yml')
);
$config2 = Yaml::parse(
    file_get_contents(__DIR__.'/src/Matthias/config/config_extra.yml')
);
 
$configs = array($config1, $config2);
 
$processor = new Processor();
$configuration = new DatabaseConfiguration();
$processedConfiguration = $processor->processConfiguration(
    $configuration,
    $configs
);

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)