跳到内容

如何为 Bundle 创建友好的配置

编辑此页

如果你打开你的主应用程序配置目录(通常是 config/packages/),你会看到许多不同的文件,例如 framework.yamltwig.yamldoctrine.yaml。 这些文件分别配置一个特定的 bundle,允许你在高层定义选项,然后让 bundle 基于你的设置进行所有底层、复杂更改。

例如,以下配置告诉 FrameworkBundle 启用表单集成,这涉及到相当多的服务定义以及与其他相关组件的集成

1
2
3
# config/packages/framework.yaml
framework:
    form: true

有两种不同的方式为 bundle 创建友好的配置

  1. 使用主 bundle 类:推荐用于新 bundle 和遵循推荐目录结构的 bundle;
  2. 使用 Bundle 扩展类:这是传统的做法,但现在只推荐用于遵循旧目录结构的 bundle。

使用 AbstractBundle 类

在扩展 AbstractBundle 类的 bundle 中,你可以将所有与处理配置相关的逻辑添加到该类中

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
// src/AcmeSocialBundle.php
namespace Acme\SocialBundle;

use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;

class AcmeSocialBundle extends AbstractBundle
{
    public function configure(DefinitionConfigurator $definition): void
    {
        $definition->rootNode()
            ->children()
                ->arrayNode('twitter')
                    ->children()
                        ->integerNode('client_id')->end()
                        ->scalarNode('client_secret')->end()
                    ->end()
                ->end() // twitter
            ->end()
        ;
    }

    public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
    {
        // the "$config" variable is already merged and processed so you can
        // use it directly to configure the service container (when defining an
        // extension class, you also have to do this merging and processing)
        $container->services()
            ->get('acme_social.twitter_client')
            ->arg(0, $config['twitter']['client_id'])
            ->arg(1, $config['twitter']['client_secret'])
        ;
    }
}

注意

configure()loadExtension() 方法仅在编译时调用。

提示

AbstractBundle::configure() 方法还允许从一个或多个文件导入配置定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/AcmeSocialBundle.php
namespace Acme\SocialBundle;

// ...
class AcmeSocialBundle extends AbstractBundle
{
    public function configure(DefinitionConfigurator $definition): void
    {
        $definition->import('../config/definition.php');
        // you can also use glob patterns
        //$definition->import('../config/definition/*.php');
    }

    // ...
}
1
2
3
4
5
6
7
8
9
10
// config/definition.php
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;

return static function (DefinitionConfigurator $definition): void {
    $definition->rootNode()
        ->children()
            ->scalarNode('foo')->defaultValue('bar')->end()
        ->end()
    ;
};

使用 Bundle 扩展

这是为 bundle 创建友好配置的传统方式。 对于新 bundle,建议使用主 bundle 类,但创建扩展类的传统方式仍然有效。

假设你正在创建一个新的 bundle - AcmeSocialBundle - 它提供与 X/Twitter 的集成。 为了使你的 bundle 可配置给用户,你可以添加一些如下所示的配置

1
2
3
4
5
# config/packages/acme_social.yaml
acme_social:
    twitter:
        client_id: 123
        client_secret: your_secret

基本思想是,与其让用户覆盖单个参数,不如让用户仅配置一些专门创建的选项。 作为 bundle 开发人员,你随后解析该配置并在“Extension”类中加载正确的服务和参数。

注意

你的 bundle 配置的根键(前面示例中的 acme_social)是根据你的 bundle 名称自动确定的(它是 bundle 名称的蛇形命名法,不带 Bundle 后缀)。

另请参阅

阅读更多关于扩展的信息,请参阅如何在 Bundle 内加载服务配置

提示

如果 bundle 提供了 Extension 类,那么你通常不应该覆盖来自该 bundle 的任何服务容器参数。 这里的想法是,如果存在扩展类,则每个应该可配置的设置都应该存在于该类提供的配置中。 换句话说,扩展类定义了所有公共配置设置,这些设置将保持向后兼容性。

另请参阅

有关依赖注入容器中的参数处理,请参阅在依赖注入类中使用参数

处理 $configs 数组

首先,你必须创建一个扩展类,如如何在 Bundle 内加载服务配置中所述。

每当用户在配置文件中包含 acme_social 键(即 DI 别名)时,其下的配置将添加到配置数组中,并传递到你的扩展的 load() 方法(Symfony 会自动将 XML 和 YAML 转换为数组)。

对于上一节中的配置示例,传递给你的 load() 方法的数组将如下所示

1
2
3
4
5
6
7
8
[
    [
        'twitter' => [
            'client_id' => 123,
            'client_secret' => 'your_secret',
        ],
    ],
]

请注意,这是一个数组的数组,而不仅仅是配置值的单个扁平数组。 这是故意的,因为它允许 Symfony 解析多个配置资源。 例如,如果 acme_social 出现在另一个配置文件中 - 例如 config/packages/dev/acme_social.yaml - 其下具有不同的值,则传入的数组可能如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[
    // values from config/packages/acme_social.yaml
    [
        'twitter' => [
            'client_id' => 123,
            'client_secret' => 'your_secret',
        ],
    ],
    // values from config/packages/dev/acme_social.yaml
    [
        'twitter' => [
            'client_id' => 456,
        ],
    ],
]

两个数组的顺序取决于哪个数组先设置。

但别担心! Symfony 的 Config 组件将帮助你合并这些值,提供默认值,并在配置错误时向用户提供验证错误。 以下是它的工作原理。 在 DependencyInjection 目录中创建一个 Configuration 类,并构建一个树来定义 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
// src/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
    {
        $treeBuilder = new TreeBuilder('acme_social');

        $treeBuilder->getRootNode()
            ->children()
                ->arrayNode('twitter')
                    ->children()
                        ->integerNode('client_id')->end()
                        ->scalarNode('client_secret')->end()
                    ->end()
                ->end() // twitter
            ->end()
        ;

        return $treeBuilder;
    }
}

另请参阅

Configuration 类可能比此处显示的要复杂得多,它支持“原型”节点、高级验证、XML 特定的规范化和高级合并。 你可以在Config 组件文档中阅读更多关于此内容的信息。 你还可以通过查看一些核心 Configuration 类来了解它的实际应用,例如来自FrameworkBundle ConfigurationTwigBundle Configuration 的类。

这个类现在可以在你的 load() 方法中使用,以合并配置并强制验证(例如,如果传递了额外的选项,则会抛出异常)

1
2
3
4
5
6
7
8
9
10
// src/DependencyInjection/AcmeSocialExtension.php
public function load(array $configs, ContainerBuilder $container): void
{
    $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 类中定义的配置树来验证、规范化和合并所有配置数组。

现在,你可以使用 $config 变量来修改你的 bundle 提供的服务。 例如,假设你的 bundle 具有以下示例配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- src/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.ac.cn/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.ac.cn/schema/dic/services
        http://symfony.ac.cn/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/DependencyInjection/AcmeSocialExtension.php
namespace Acme\SocialBundle\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;

public function load(array $configs, ContainerBuilder $container): void
{
    $loader = new XmlFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources/config'));
    $loader->load('services.xml');

    $configuration = new Configuration();
    $config = $this->processConfiguration($configuration, $configs);

    $definition = $container->getDefinition('acme_social.twitter_client');
    $definition->replaceArgument(0, $config['twitter']['client_id']);
    $definition->replaceArgument(1, $config['twitter']['client_secret']);
}

提示

你可能希望使用 ConfigurableExtension 来自动为你执行此操作,而不是每次提供一些配置选项时都在扩展中调用 processConfiguration()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/DependencyInjection/HelloExtension.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
    protected function loadInternal(array $mergedConfig, ContainerBuilder $container): void
    {
        // ...
    }
}

此类使用 getConfiguration() 方法来获取 Configuration 实例。

使用 Config 组件是完全可选的。 load() 方法获取配置值数组。 你可以改为自己解析这些数组(例如,通过覆盖配置并使用 isset 来检查值的存在)。 请注意,这将很难支持 XML

1
2
3
4
5
6
7
8
9
10
public function load(array $configs, ContainerBuilder $container): void
{
    $config = [];
    // let resources override the previous set value
    foreach ($configs as $subConfig) {
        $config = array_merge($config, $subConfig);
    }

    // ... now use the flat $config array
}

修改另一个 Bundle 的配置

如果你有多个相互依赖的 bundle,则允许一个 Extension 类修改传递给另一个 bundle 的 Extension 类的配置可能很有用。 这可以使用前置扩展来实现。 有关更多详细信息,请参阅如何简化多个 Bundle 的配置

导出配置

config:dump-reference 命令使用 Yaml 格式在控制台中导出 bundle 的默认配置。

只要你的 bundle 的配置位于标准位置(<YourBundle>/src/DependencyInjection/Configuration)并且没有构造函数,它就会自动工作。 如果你有一些不同的东西,你的 Extension 类必须覆盖 Extension::getConfiguration() 方法,并返回你的 Configuration 的实例。

支持 XML

Symfony 允许人们以三种不同的格式提供配置:Yaml、XML 和 PHP。 当使用 Config 组件时,Yaml 和 PHP 都使用相同的语法并且默认情况下受支持。 支持 XML 需要你做更多的事情。 但是,当与他人共享你的 bundle 时,建议你遵循以下步骤。

使你的配置树为 XML 做好准备

Config 组件默认提供了一些方法,使其能够正确处理 XML 配置。 请参阅组件文档的“定义和处理配置值”。 但是,你也可以做一些可选的事情,这将改善使用 XML 配置的体验

选择 XML 命名空间

在 XML 中,XML 命名空间用于确定哪些元素属于特定 bundle 的配置。 命名空间从 Extension::getNamespace() 方法返回。 按照惯例,命名空间是一个 URL(它不必是有效的 URL,也不需要存在)。 默认情况下,bundle 的命名空间是 http://example.org/schema/dic/DI_ALIAS,其中 DI_ALIAS 是扩展的 DI 别名。 你可能希望将其更改为更专业的 URL

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/DependencyInjection/AcmeHelloExtension.php
namespace Acme\HelloBundle\DependencyInjection;

// ...
class AcmeHelloExtension extends Extension
{
    // ...

    public function getNamespace(): string
    {
        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 验证基本路径。 然后,此命名空间后跟从基本路径到文件本身的路径的其余部分。

按照惯例,XSD 文件位于 config/schema/ 目录中,但你可以将其放置在任何你喜欢的位置。 你应该将此路径作为基本路径返回

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/DependencyInjection/AcmeHelloExtension.php
namespace Acme\HelloBundle\DependencyInjection;

// ...
class AcmeHelloExtension extends Extension
{
    // ...

    public function getXsdValidationBasePath(): string
    {
        return __DIR__.'/../config/schema';
    }
}

假设 XSD 文件名为 hello-1.0.xsd,则 schema 位置将为 https://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
16
<!-- config/packages/acme_hello.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.ac.cn/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://symfony.ac.cn/schema/dic/services
        http://symfony.ac.cn/schema/dic/services/services-1.0.xsd
        http://acme_company.com/schema/dic/hello
        https://acme_company.com/schema/dic/hello/hello-1.0.xsd"
>
    <acme-hello:config>
        <!-- ... -->
    </acme-hello:config>

    <!-- ... -->
</container>
本作品,包括代码示例,根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本