跳到内容

编译容器

编辑此页

出于各种原因,可以编译服务容器。 这些原因包括检查任何潜在问题,例如循环引用,并通过解析参数和删除未使用的服务来提高容器的效率。 此外,某些功能(例如使用 父服务)需要编译容器。

它通过运行以下命令进行编译

1
$container->compile();

compile 方法使用编译器通道进行编译。 DependencyInjection 组件附带了多个通道,这些通道会自动注册以进行编译。 例如,CheckDefinitionValidityPass 检查容器中已设置的定义是否存在各种潜在问题。 在此以及检查容器有效性的其他几个通道之后,使用进一步的编译器通道来优化配置,然后再进行缓存。 例如,私有服务和抽象服务将被删除,别名将被解析。

使用扩展管理配置

除了如 DependencyInjection 组件 中所示直接将配置加载到容器中之外,您还可以通过向容器注册扩展来管理它。 编译过程的第一步是从注册到容器的任何扩展类加载配置。 与直接加载的配置不同,它们仅在编译容器时才会被处理。 如果您的应用程序是模块化的,则扩展允许每个模块注册和管理自己的服务配置。

扩展必须实现 ExtensionInterface,并且可以使用以下方式注册到容器中

1
$container->registerExtension($extension);

扩展的主要工作是在 load() 方法中完成的。 在 load() 方法中,您可以从一个或多个配置文件加载配置,并使用 如何使用服务定义对象 中所示的方法来操作容器定义。

load() 方法被传递一个全新的容器进行设置,然后合并到它注册的容器中。 这允许您拥有多个扩展独立管理容器定义。 扩展在添加时不会添加到容器的配置中,而是在调用容器的 compile() 方法时进行处理。

一个非常简单的扩展可能只是将配置文件加载到容器中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;

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

    // ...
}

与直接将文件加载到正在构建的整体容器中相比,这并没有获得太多好处。 它只是允许文件在模块/捆绑包之间拆分。 需要能够从模块/捆绑包外部的配置文件影响模块的配置,以使复杂的应用程序可配置。 这可以通过指定直接加载到容器中的配置文件部分作为特定扩展的配置文件来完成。 配置上的这些部分将不会由容器直接处理,而是由相关的扩展处理。

扩展必须指定一个 getAlias() 方法来实现接口

1
2
3
4
5
6
7
8
9
10
11
// ...

class AcmeDemoExtension implements ExtensionInterface
{
    // ...

    public function getAlias(): string
    {
        return 'acme_demo';
    }
}

对于 YAML 配置文件,将扩展的别名指定为键将意味着这些值将传递给扩展的 load() 方法

1
2
3
4
# ...
acme_demo:
    foo: fooValue
    bar: barValue

如果此文件已加载到配置中,则只有在编译容器时才会处理其中的值,此时将加载扩展

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

$container = new ContainerBuilder();
$container->registerExtension(new AcmeDemoExtension);

$loader = new YamlFileLoader($container, new FileLocator(__DIR__));
$loader->load('config.yaml');

// ...
$container->compile();

注意

当加载使用扩展别名作为键的配置文件时,扩展必须已注册到容器构建器,否则将抛出异常。

来自配置文件的这些部分的值将传递到扩展的 load() 方法的第一个参数中

1
2
3
4
5
public function load(array $configs, ContainerBuilder $container): void
{
    $foo = $configs[0]['foo']; //fooValue
    $bar = $configs[0]['bar']; //barValue
}

$configs 参数是一个数组,其中包含加载到容器中的每个不同的配置文件。 在上面的示例中,您仅加载单个配置文件,但它仍将位于数组中。 该数组将如下所示

1
2
3
4
5
6
[
    [
        'foo' => 'fooValue',
        'bar' => 'barValue',
    ],
]

虽然您可以手动管理合并不同的文件,但最好使用 Config 组件 来合并和验证配置值。 使用配置处理,您可以按以下方式访问配置值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\Component\Config\Definition\Processor;
// ...

public function load(array $configs, ContainerBuilder $container): void
{
    $configuration = new Configuration();
    $processor = new Processor();
    $config = $processor->processConfiguration($configuration, $configs);

    $foo = $config['foo']; //fooValue
    $bar = $config['bar']; //barValue

    // ...
}

您还必须实现另外两个方法。 一个是返回 XML 命名空间,以便将 XML 配置文件的相关部分传递给扩展。 另一个是指定 XSD 文件的基本路径,以验证 XML 配置

1
2
3
4
5
6
7
8
9
public function getXsdValidationBasePath(): string
{
    return __DIR__.'/../Resources/config/';
}

public function getNamespace(): string
{
    return 'http://www.example.com/symfony/schema/';
}

注意

XSD 验证是可选的,从 getXsdValidationBasePath() 方法返回 false 将禁用它。

然后,配置的 XML 版本将如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="https://symfony.ac.cn/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:acme-demo="http://www.example.com/schema/dic/acme_demo"
    xsi:schemaLocation="https://symfony.ac.cn/schema/dic/services
        https://symfony.ac.cn/schema/dic/services/services-1.0.xsd
        http://www.example.com/schema/dic/acme_demo
        https://www.example.com/schema/dic/acme_demo/acme_demo-1.0.xsd"
>
    <acme-demo:config>
        <acme_demo:foo>fooValue</acme_demo:foo>
        <acme_demo:bar>barValue</acme_demo:bar>
    </acme-demo:config>
</container>

注意

在 Symfony 全栈框架中,有一个基本扩展类实现了这些方法以及用于处理配置的快捷方法。 有关更多详细信息,请参见 如何在捆绑包内加载服务配置

处理后的配置值现在可以作为容器参数添加,就像它列在配置文件的 parameters 部分中一样,但具有合并多个文件和验证配置的额外好处

1
2
3
4
5
6
7
8
9
10
public function load(array $configs, ContainerBuilder $container): void
{
    $configuration = new Configuration();
    $processor = new Processor();
    $config = $processor->processConfiguration($configuration, $configs);

    $container->setParameter('acme_demo.FOO', $config['foo']);

    // ...
}

更复杂的配置要求可以在扩展类中得到满足。 例如,您可以选择加载主服务配置文件,但也仅在设置了某个参数时才加载辅助文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function load(array $configs, ContainerBuilder $container): void
{
    $configuration = new Configuration();
    $processor = new Processor();
    $config = $processor->processConfiguration($configuration, $configs);

    $loader = new XmlFileLoader(
        $container,
        new FileLocator(__DIR__.'/../Resources/config')
    );
    $loader->load('services.xml');

    if ($config['advanced']) {
        $loader->load('advanced.xml');
    }
}

您还可以在扩展中弃用容器参数,以警告用户不要再使用它们。 这有助于跨扩展的主要版本进行迁移。

只有在使用 PHP 配置扩展时才可能进行弃用,而不是在使用 XML 或 YAML 时。 使用 ContainerBuilder::deprecateParameter() 方法来提供弃用详细信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function load(array $configs, ContainerBuilder $containerBuilder)
{
    // ...

    $containerBuilder->setParameter('acme_demo.database_user', $configs['db_user']);

    $containerBuilder->deprecateParameter(
        'acme_demo.database_user',
        'acme/database-package',
        '1.3',
        // optionally you can set a custom deprecation message
        '"acme_demo.database_user" is deprecated, you should configure database credentials with the "acme_demo.database_dsn" parameter instead.'
    );
}

被弃用的参数必须在声明为已弃用之前设置。 否则,将抛出 ParameterNotFoundException 异常。

注意

仅向容器注册扩展不足以使其包含在编译容器时处理的扩展中。 加载使用扩展别名作为键的配置(如以上示例中所示)将确保加载它。 也可以使用容器构建器的 loadFromExtension() 方法告知容器构建器加载它

1
2
3
4
5
6
7
use Symfony\Component\DependencyInjection\ContainerBuilder;

$container = new ContainerBuilder();
$extension = new AcmeDemoExtension();
$container->registerExtension($extension);
$container->loadFromExtension($extension->getAlias());
$container->compile();

注意

如果您需要操作扩展加载的配置,则无法从另一个扩展中执行此操作,因为它使用全新的容器。 您应该改为使用编译器通道,该通道在扩展处理完成后与完整容器一起工作。

预先准备传递给扩展的配置

扩展可以通过实现 PrependExtensionInterface,在调用 load() 方法之前预先准备任何捆绑包的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
// ...

class AcmeDemoExtension implements ExtensionInterface, PrependExtensionInterface
{
    // ...

    public function prepend(ContainerBuilder $container): void
    {
        // ...

        $container->prependExtensionConfig($name, $config);

        // ...
    }
}

有关更多详细信息,请参见 如何简化多个捆绑包的配置,该文档专门针对 Symfony 框架,但包含有关此功能的更多详细信息。

在编译期间执行代码

您还可以在编译期间通过编写自己的编译器通道来执行自定义代码。 通过在您的扩展中实现 CompilerPassInterface,在编译期间将调用添加的 process() 方法

1
2
3
4
5
6
7
8
9
10
11
12
// ...
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;

class AcmeDemoExtension implements ExtensionInterface, CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        // ... do something during the compilation
    }

    // ...
}

由于在加载所有扩展之后调用 process(),因此它允许您编辑其他扩展的服务定义以及检索有关服务定义的信息。

可以使用 如何使用服务定义对象 中描述的方法来操作容器的参数和定义。

注意

请注意,扩展类中的 process() 方法在 PassConfig::TYPE_BEFORE_OPTIMIZATION 步骤中调用。 如果您需要在另一个步骤中编辑容器,则可以阅读 下一节

注意

作为规则,仅在编译器通道中使用服务定义,而不要创建服务实例。 实际上,这意味着使用方法 has()findDefinition()getDefinition()setDefinition() 等,而不是 get()set() 等。

提示

确保您的编译器通道不需要服务存在。 如果某些必需的服务不可用,请中止方法调用。

编译器通道的常见用例是搜索所有具有特定标记的服务定义,以便将每个服务动态地插入到其他服务中。 有关示例,请参见有关 服务标签 的部分。

创建单独的编译器通道

有时,您需要在编译期间执行多项操作,希望在没有扩展的情况下使用编译器通道,或者需要在编译过程的另一个步骤中执行某些代码。 在这些情况下,您可以创建一个实现 CompilerPassInterface 的新类

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class CustomPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        // ... do something during the compilation
    }
}

然后,您需要向容器注册您的自定义通道

1
2
3
4
use Symfony\Component\DependencyInjection\ContainerBuilder;

$container = new ContainerBuilder();
$container->addCompilerPass(new CustomPass());

注意

如果您使用的是全栈框架,则编译器通道的注册方式有所不同,有关更多详细信息,请参见 如何使用编译器通道

控制通道排序

默认编译器通道分为优化通道和删除通道。 优化通道首先运行,包括解析定义中的引用等任务。 删除通道执行诸如删除私有别名和未使用的服务之类的任务。 使用 addCompilerPass() 注册编译器通道时,您可以配置何时运行编译器通道。 默认情况下,它们在优化通道之前运行。

您可以使用以下常量来确定何时执行通道

  • PassConfig::TYPE_BEFORE_OPTIMIZATION
  • PassConfig::TYPE_OPTIMIZE
  • PassConfig::TYPE_BEFORE_REMOVING
  • PassConfig::TYPE_REMOVE
  • PassConfig::TYPE_AFTER_REMOVING

例如,要在默认删除通道运行后运行自定义通道,请使用

1
2
3
4
5
// ...
$container->addCompilerPass(
    new CustomPass(),
    PassConfig::TYPE_AFTER_REMOVING
);

您还可以控制在每个编译阶段运行编译器通道的顺序。 使用 addCompilerPass() 的可选第三个参数将优先级设置为整数。 默认优先级为 0,值越高,执行时间越早

1
2
3
4
5
6
7
8
// ...
// FirstPass is executed after SecondPass because its priority is lower
$container->addCompilerPass(
    new FirstPass(), PassConfig::TYPE_AFTER_REMOVING, 10
);
$container->addCompilerPass(
    new SecondPass(), PassConfig::TYPE_AFTER_REMOVING, 30
);

转储配置以提高性能

一旦有很多服务,使用配置文件来管理服务容器可能比使用 PHP 更容易理解。 但是,这种易用性以性能为代价,因为配置文件需要解析,并且需要从中构建 PHP 配置。 编译过程使容器更高效,但是运行需要时间。 但是,您可以通过使用配置文件,然后转储和缓存结果配置来获得两全其美的效果。 PhpDumper 用于转储已编译的容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;

$file = __DIR__ .'/cache/container.php';

if (file_exists($file)) {
    require_once $file;
    $container = new ProjectServiceContainer();
} else {
    $container = new ContainerBuilder();
    // ...
    $container->compile();

    $dumper = new PhpDumper($container);
    file_put_contents($file, $dumper->dump());
}

提示

file_put_contents() 函数不是原子的。 这可能会在生产环境中造成多个并发请求的问题。 而是使用 Symfony Filesystem 组件中的 dumpFile() 方法 或 Symfony 提供的其他方法(例如 $containerConfigCache->write()),这些方法是原子的。

ProjectServiceContainer 是转储的容器类的默认名称。 但是,您可以使用转储容器时的 class 选项更改此名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...
$file = __DIR__ .'/cache/container.php';

if (file_exists($file)) {
    require_once $file;
    $container = new MyCachedContainer();
} else {
    $container = new ContainerBuilder();
    // ...
    $container->compile();

    $dumper = new PhpDumper($container);
    file_put_contents(
        $file,
        $dumper->dump(['class' => 'MyCachedContainer'])
    );
}

现在,您将获得 PHP 配置容器的速度以及使用配置文件的便利性。 此外,以这种方式转储容器进一步优化了容器创建服务的方式。

在上面的示例中,每当进行任何更改时,您都需要删除缓存的容器文件。 添加对变量的检查以确定您是否处于调试模式,这使您可以在生产环境中保持缓存容器的速度,但在开发应用程序时获得最新的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ...

// based on something in your project
$isDebug = ...;

$file = __DIR__ .'/cache/container.php';

if (!$isDebug && file_exists($file)) {
    require_once $file;
    $container = new MyCachedContainer();
} else {
    $container = new ContainerBuilder();
    // ...
    $container->compile();

    if (!$isDebug) {
        $dumper = new PhpDumper($container);
        file_put_contents(
            $file,
            $dumper->dump(['class' => 'MyCachedContainer'])
        );
    }
}

可以通过仅在调试模式下,在对配置进行更改而不是在每次请求时重新编译容器来进一步改进这一点。 这可以通过以 config 组件文档中的“基于资源进行缓存”中所述的方式缓存用于配置容器的资源文件来完成。

您无需计算出要缓存哪些文件,因为容器构建器会跟踪用于配置它的所有资源,不仅是配置文件,还包括扩展类和编译器通道。 这意味着对任何这些文件的任何更改都将使缓存无效,并触发容器重建。 您需要向容器询问这些资源,并将它们用作缓存的元数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ...

// based on something in your project
$isDebug = ...;

$file = __DIR__ .'/cache/container.php';
$containerConfigCache = new ConfigCache($file, $isDebug);

if (!$containerConfigCache->isFresh()) {
    $container = new ContainerBuilder();
    // ...
    $container->compile();

    $dumper = new PhpDumper($container);
    $containerConfigCache->write(
        $dumper->dump(['class' => 'MyCachedContainer']),
        $container->getResources()
    );
}

require_once $file;
$container = new MyCachedContainer();

现在,无论调试模式是否开启,都将使用缓存的转储容器。 区别在于 ConfigCache 在其第二个构造函数参数中设置为调试模式。 当缓存未处于调试模式时,如果缓存存在,则将始终使用缓存的容器。 在调试模式下,会写入一个额外的元数据文件,其中包含所有涉及的资源文件。 然后检查这些文件以查看其时间戳是否已更改,如果已更改,则将缓存视为过时。

注意

在全栈框架中,容器的编译和缓存由您负责。

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