如何为Bundle创建友好的配置

3.4 版本
维护中的版本

如果你打开你的程序配置文件(通常是 app/config/config.yml),你将看到若干不同的配置区块,比如 frameworktwigdoctrine。这些配置信息中的每一个,都配置了一个特定 bundle,使你能从一个高层级(high level)来定义有关选项,进而使 bundle 基于你的设置,来实现所有低层级(low-level)、复杂的改变。

举例来说,下面的配置告诉FrameworkBundle去启用表单集成,它涉及到相当多的服务的定义以及其他相关组件的集成:

1
2
framework:
    form: true
1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:framework="http://symfony.com/schema/dic/symfony"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/symfony
        http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
 
    <framework:config>
        <framework:form />
    </framework:config>
</container>
1
2
3
$container->loadFromExtension('framework', array(
    'form' => true,
));

使用参数来配置Bundle

如果你并无计划在项目间分享你的 bundle,便没有意义去使用此种超高端的配置方式。因为你只在一个项目中使用 bundle,只需要每次修改服务配置即可。

就算你 确实 想在 config.yml 中配置一些东西,你也可以在那里创建一个参数,然后在其它地方使用该参数。

使用Bundle扩展 

基本想法是不让用户覆写个体参数,而是让他们只配置少量、特别创建的选项。作为 bundle 开发者,后面对这些配置信息进行解析,并在一个 "Extension" class 内部加载正确的服务和参数。

作为例子,假设你要创建一个社会化(social)的 bundle,提供 Twitter 等的集成。为了让你的 bundle 能够复用,你必须令 client_idclient_secret 等变量成为可配置的。你的 bundle 配置信息看起来就像是这样:

1
2
3
4
5
# app/config/config.yml
acme_social:
    twitter:
        client_id: 123
        client_secret: your_secret
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- app/config/config.xml -->
<?xml version="1.0" ?>
 
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:acme-social="http://example.org/dic/schema/acme_social"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">
 
   <acme-social:config>
       <twitter client-id="123" client-secret="your_secret" />
   </acme-social:config>
 
   <!-- ... -->
</container>
1
2
3
4
5
// app/config/config.php
$container->loadFromExtension('acme_social', array(
    'client_id'     => 123,
    'client_secret' => 'your_secret',
));

Seealso

更多关于 extension 的内容在 如何在Bundle内加载服务配置 文中。

Tip

如果一个 bundle 提供了一个 Extension 类(扩展类),那么你不应该覆写源自该 bundle 的任何服务容器参数(parameters)。思路是,如果一个扩展类存在,每一个应可配置的设置选项,都应该在那个类所提供的配置信息中出现。换句话说,扩展(extension)类定义了所有公共配置选项(public configuration settings),这些选项可以保证(你的 bundle 的)向下兼容性。

Seealso

在 dependency injection 容器中处理参数时可参考 在依赖注入类内部使用参数

处理$configs数组 

最重要的一点,你要创建一个扩展(extension)类,就像 如何在Bundle内加载服务配置 所解释的那样。

只要用户在配置文件中包含了 acme_social 键(DI 容器系统中的别名)时,它下面的配置信息就会被添加到一个配置信息的数组中,然后传入你的 extension 类的 load() 方法中(Symfony 会自动把 XML 和 YAML 数据转换为数组)。

根据上前面小节的配置示例,传给 load() 方法中的数组可能是下面这样:

1
2
3
4
5
6
7
8
array(
    array(
        'twitter' => array(
            'client_id' => 123,
            'client_secret' => 'your_secret',
        ),
    ),
)

请注意,这是一个数组的数组,而不是一个简单的扁平的配置值的数组。就这么设计的,因为它允许Symfony解析各自的配置资源。举例来说,如果acme_social 出现在其他的配置文件中,比如config_dev.yml,它们会有不同的值,里面的数组看起来应该是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
array(
    // values from config.yml / config.yml 里的配置值
    array(
        'twitter' => array(
            'client_id' => 123,
            'client_secret' => 'your_secret',
        ),
    ),
    // values from config_dev.yml / config.yml 里的配置值
    array(
        'twitter' => array(
            'client_id' => 456,
        ),
    ),
)

两个数组的顺序取决于哪一个排在前面。

但别担心!Symfony 的 Config 组件将帮助你合并这些值、提供默认值、并对用户的非法配置进行给出验证错误。以下就是其工作流程。在 DependencyInjection 目录中创建一个 Configuration 类,并构建一个树(tree),来定义你的 bundle配置 之结构。

Configuration 类处在理示例中的配置信息时可能像下面这样:

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
// src/Acme/SocialBundle/DependencyInjection/Configuration.php
namespace Acme\SocialBundle\DependencyInjection;
 
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
 
class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('acme_social');
 
        $rootNode
            ->children()
                ->arrayNode('twitter')
                    ->children()
                        ->integerNode('client_id')->end()
                        ->scalarNode('client_secret')->end()
                    ->end()
                ->end() // twitter
            ->end()
        ;
 
        return $treeBuilder;
    }
}

Seealso

Configuration 类可能远比这里演示的复杂,它可以支持“prototype”节点、高级验证、基于 XML 的标准化(normalization)以及高级合并。在 Config组件文档 中你可以了解到更多内容。通过查看一些核心的配置类,比如 FrameworkBundle ConfigurationTwigBundle Configuration,也可以看到该类的实际运作情况。

现在可以在扩展的 load() 方法中使用配置类以合并配置并强制验证(如,若有额外的选项被加入进来,则会抛出一个异常):

1
2
3
4
5
6
7
8
9
10
11
// src/Acme/SocialBundle/DependencyInjection/AcmeSocialExtension.php
 
public function load(array $configs, ContainerBuilder $container)
{
    $configuration = new Configuration();
 
    $config = $this->processConfiguration($configuration, $configs);
 
    // you now have these 2 config keys
    // $config['twitter']['client_id'] and $config['twitter']['client_secret']
}

processConfiguration() 方法使用了你已经在 Configuration 类中定义的配置树进行验证(validate)、标准化(normlize)以及把全部配置数组合并在一起。

现在,你可以使用 $config 变量来修改一个你的 bundle 所提供的某个服务了。例如,假设 bundle 中有下例的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- src/Acme/SocialBundle/Resources/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">
 
    <services>
        <service id="acme.social.twitter_client" class="Acme\SocialBundle\TwitterClient">
            <argument></argument> <!-- will be filled in with client_id dynamically -->
            <argument></argument> <!-- will be filled in with client_secret dynamically -->
        </service>
    </services>
</container>

在你的扩展中,你可以加载它,并动态设置其参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Acme/SocialBundle/DependencyInjection/AcmeSocialExtension.php
// ...
 
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\Config\FileLocator;
 
public function load(array $configs, ContainerBuilder $container)
{
    $loader = new XmlFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources/config'));
    $loader->load('services.xml');
 
    $configuration = new Configuration();
    $config = $this->processConfiguration($configuration, $configs);
 
    $def = $container->getDefinition('acme.social.twitter_client');
    $def->replaceArgument(0, $config['twitter']['client_id']);
    $def->replaceArgument(1, $config['twitter']['client_secret']);
}

Tip

当你要提供一些配置选项时,不同于在扩展中次次调用 processConfiguration(),你还可使用 ConfigurableExtension 来自动完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php
namespace Acme\HelloBundle\DependencyInjection;
 
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension;
 
class AcmeHelloExtension extends ConfigurableExtension
{
    // note that this method is called loadInternal and not load
    // 注意,此方法名为 loadInternal 而不再是 load
    protected function loadInternal(array $mergedConfig, ContainerBuilder $container)
    {
        // ...
    }
}

这个类使用了 getConfiguration() 方法来获取 Configuration 实例。

自行处理配置

使用 Config 组件完全是可选的。load() 方法会获取到一个配置值的数组。你可以直接自行解析这些数组(如,覆写配置并使用 isset 来检测某个选项值是否存在)。注意,这一过程支持XML是很难的。

1
2
3
4
5
6
7
8
9
10
11
12
public function load(array $configs, ContainerBuilder $container)
{
    $config = array();
    // let resources override the previous set value
    // 让 resources 覆写之前设定的值
    foreach ($configs as $subConfig) {
        $config = array_merge($config, $subConfig);
    }
 
    // ... now use the flat $config array
    // ... 现在可以使用 $config 纯正数组了
}

修改另一个bundle的配置 

如果你有多个相互依赖的 bundle,让一个 Extension 类去修改传入到其他 bundle 的 Extension 类中的配置信息,将是非常有用的,就好像末级开发者真的把那个配置信息放置在他们的 app/config/config.yml 文件中一样。这可以使用一个预置扩展(prepend extension)来完成。参考 如何简化多个Bundle的配置 以了解更多。

剥离出配置信息 

config:dump-reference 命令会在命令控制台中以 Yaml 格式来剥离出一个 bundle 的默认配置。

只要你的 bundle 的配置信息位于标准位置(YourBundle\DependencyInjection\Configuration)并且未使用构造器,它就会自动工作。如果你想做一些不同的事情,你的 Extension 必须要覆写 Extension::getConfiguration() 方法并返回一个你自己的 Configuration 实例。

支持XML 

Symfony允许人们使用三种不同格式的配置:Yaml, XML 和 PHP。当使用 Config 组件时,Yaml 和 PHP 使用相同的语法并且默认被支持的。要支持 XML 就需要你做更多的事情了。但是当与其他人分享你的 bundle 时,推荐你遵循下述步骤。

准备好XML配置树 

配置组件默认提供了一些方法,以令它能够正确处理 XML 配置信息。参阅组件文档的“Normalization”。然而,你也可以做一些可选的事,这将提升使用 XML 配置信息的体验。

选择一个XML命名空间 

在XML中,XML命名空间 用于确定哪些元素属于特定 bundle 的配置内容。命名空间从 Extension::getNamespace() 方法中返回。默认时,bundle 的命名空间就是一个URL(它并不必须是一个有效的链接且不一定需要存在)。默认情况下,bundle的命名空间是 http://example.org/dic/schema/DI_ALIAS,在这里 DI_ALIAS 是扩展的DI别名(译注:在容器中的名称)。你可能想要将其换成更加专业的链接:

1
2
3
4
5
6
7
8
9
10
11
12
// src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php
 
// ...
class AcmeHelloExtension extends Extension
{
    // ...
 
    public function getNamespace()
    {
        return 'http://acme_company.com/schema/dic/hello';
    }
}

提供XML Schema 

XML有一个非常有用的特性叫做 XML schema。它将允许你在 XML Schema Definition (一个 xsd 文件)中描述所有的可能的元素以及属性连同它们的值。XSD 文件被 IDE 用于代码自动完成,也被 Config 组件用来验证元素。

为了使用 schema,XML配置文件中必须提供一个 xsi:schemaLocation 属性以指向一个特定的作为 XML 命名空间的 XSD 文件。其位置总是从 XML 命名空间开始。然后,XML 命名空间会被 Extension::getXsdValidationBasePath() 方法返回的 XSD 验证的基本路径(validattion base path)所取代。接下来,此 namespace 会被“从 base path 到文件自身”的其他路径所跟随。

按照惯例,XSD 文件保存在 Resources/config/schema,但你可以把它放置在任何地方。你应该把此路径作为基本路径(base path)来返回:

1
2
3
4
5
6
7
8
9
10
11
12
// src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php
 
// ...
class AcmeHelloExtension extends Extension
{
    // ...
 
    public function getXsdValidationBasePath()
    {
        return __DIR__.'/../Resources/config/schema';
    }
}

假设 XSD 文件叫做 hello-1.0.xsd,schema 的位置就是 http://acme_company.com/schema/dic/hello/hello-1.0.xsd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- app/config/config.xml -->
<?xml version="1.0" ?>
 
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:acme-hello="http://acme_company.com/schema/dic/hello"
    xsi:schemaLocation="http://acme_company.com/schema/dic/hello
        http://acme_company.com/schema/dic/hello/hello-1.0.xsd">
 
    <acme-hello:config>
        <!-- ... -->
    </acme-hello:config>
 
    <!-- ... -->
</container>

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

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