验证配置的值
从各种资源加载配置值之后,这些值和他们的结构都会通过配置组件的“Definition”部分被验证。配置的这些值会表现出一定的层次结构。此外,这个值可能应该有一个特定的类型,数量上的限制或者是一组给定的值中的一个。例如,下面的配置(在yaml中)显示出一个清晰的结构并且一些验证规则,应该适用于它(像:“auto_connect
值一定是boolean”):
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 |
当加载多个配置文件,他应该有能力做到合并和覆盖一些值。有些值不能合并并且我们要第一时间保住他们。此外,一些键当输入一个特定的值之后才有效(例如在上面的实例中:当driver
键是sqlite时memory键才有意义)。
使用TreeBuilder定义配置值的层次结构
所有的涉及配置值的规则都能够使用TreeBuilder定义。
从一个自定义并实现了ConfigurationInterface
的Configuration类中返回
一个TreeBuilder实例:
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\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; } } |
在树状中添加节点
节点变量
一个树状以语义的方式定义包含的节点。这意味着要使用缩进和流利的符号,能够反映出配置值的实际结构:
1 2 3 4 5 6 7 8 9 10 |
$rootNode ->children() ->booleanNode('auto_connect') ->defaultTrue() ->end() ->scalarNode('default_connection') ->defaultValue('default') ->end() ->end() ; |
这个root节点本身是一个数组节点,并且有子节点,像boolean节点auto_connect和这个scalar节点default_connection。总之:在定义节点之后,要调用end()再进一步设置层次结构。
Node类型
使用适当的节点定义去验证提供值的类型。
节点类型可用于:
- scalar (generic type that includes booleans, strings, integers, floats and
null
) 通用类型包含booleans,strings,integers,floats或者null - boolean
- integer
- float
- enum (similar to scalar, but it only allows a finite set of values) 类似scalar,但它仅允许有限的一组值
- array
- variable (no validation) 没有验证
并用node($name, $type)创建或者用快捷方式xxxxNode($name)方法。
数值型节点的约束
数值型节点(float和integer)提供了两个额外的约束 – 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('gender') ->values(array('male', 'female')) ->end() ->end() ; |
这将限制性别只能从男性或女性中选择。
Array节点
可以添加一个更深层次的层次结构,通过array节点。array节点本身可以具有一个预定义的一组变量节点:
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() ; |
一个prototype可以用于添加一个定义,这个定义可能在当前结点使用多次。在上面的例子中按照prototype的定义,他可能有多个connection数组(包含driver,host)。
Array节点选项
定义一个数组子节点之前,你可以提供像这样的选项:
useAttributeAsKey()
提供一个子节点名称,其值应作为所得数组中的键的名称。这种方法还限定被处理配置数组键,下面的例子说明了这种方式。
requiresAtLeastOneElement()
在这个数组里至少应该有一个元素(只能在isRequired()被调用后)
addDefaultsIfNotSet()
如果任何子节点有默认值,如果没有明确的规定值,可以使用他们。
normalizeKeys(false)
如果调用(false),键就用破折号不是标准的下划线。建议用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' ) |
一个更复杂的例子是定义一个有子原型数组:
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_connection和default配置键丢失。
原因是Symfony的配置组件将数组作为默认列表。
在写这篇文章,有一个不一致:如果仅仅一个文件提供所讨论的配置,这个键(
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') ->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_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() ; |
记录配置
所有的选项都可以使用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()
不要让其他配置数组去覆盖现有的节点值
附加部分
如果你有一个复杂的配置需要验证,这个树(tree)可能长得很大,我估计你可能想要把它分成各个部分。你能够做的是让一个单独的节点作为一个部分并且然后使用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 |
标准化
当他们第一次规范化处理配置文件,然后合并,最后树用于验证得到的结果数组。这个规范化的处理,习惯从不同的配置格式中一些差异的结果,主要是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') ->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就可以确保始终是一个数组。
如果你需要,你可以进一步控制标准化进程。例如,你可能想允许一个字符串去设置或者被使用,让它作为一个特定的键或者几个键明确被设定。因此,如果在这个配置中除了名字的都是可选的:
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到关联数组中作为键(key):
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() ; |
验证规则
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“是一个闭包。他的返回值将作为节点一个新的值,替换节点原始的值。
处理配置值
The Processor使用tree,因为他使用内置的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 ); |