编译服务容器

3.4 版本
维护中的版本

服务容器被编译,是有多个理由的,包括:检查所有潜在的问题,比如对某服务的“循环引用(circular reference)”,以及通过解析参数和移除无用服务来使得服务容器更加高效。还包括针对特定功能的——比如使用父服务(parent serivces)时——这些都需要容器被编译。

编译过程通过以下代码实现:

1
$container->compile();

Compile方法使用的是被称为Compiler Passes 的编译方式(编译器传递)。DependencyInjection组件自身,拥有若干为了编译而“自动注册”的pass。例如CheckDefinitionValidityPass类,就是用来检查容器中的服务可能存在的诸多潜在问题。在这之后,其他pass仍然要检查容器的有效性,并且,容器在被缓存之前,还有更进一步的compiler passes被用来优化容器。例如:私有服务(private services)和抽象服务(abstract services)将被移除出去,而服务假名(aliases)将被解析为服务id。

管理扩展的配置信息 

和本章之前所述(请参考The DependencyInjection组件)的“直接加载配置信息到容器中”一样,你也可以管理扩展中的配置(见下文)——

译注

扩展是bundle中的Extension类的别称,这个类作用极大,是实现Symfony模块化、语义配置以及松耦合的最关键部分。扩展中的配置,是指bunlde相关目录下的config信息连同app/config.yml中的语义化配置之总称。而不是单指app/config.yml中的直接被读到容器中的service配置信息(虽然你可以把自己bundle的服务定义写在app/config.yml中,但当你要写真正的扩展时,就必须把这些定义写在自己bundle下面的文件夹中)。

对于真正意义的扩展来说,由于必须要分发,因此早晚要进入vendor/文件夹。扩展并非指你在src/中建立的bundle。因此,app/config.yml是存放你的扩展的语义化配置的位置。本小节标题中的“扩展的配置信息”,是这些语义化配置,是你的bunlde在以扩展身分“分发”之后,通过composer下载该扩展的用户,需要在app/config.yml进行配置的内容——这些由你的用户修改过的配置内容,就是你必须要进行管理的“扩展的配置信息”。

本篇文章(乃至DI组件大章)术语很多,但表达的是一个具有特别重大意义的过程,Symfony之所以无敌,一言以蔽之,就是因为扩展/extension。希望大家多加理解,并加以实践。

因为容器编译和扩展的编写过程是非常高难的(在体系里来说应该仅次于ACL),而官方文档并没有完整覆盖,大家应该多读Symfony核心bundle的相关代码,否则不能掌握真谛。

——(接上文)通过将扩展注册到容器中即可。编译过程的第一步,是将配置信息从已经注册到容器中的扩展类中进行加载。与直接加载配置文件不同,扩展的配置信息只有在container被编译时,才能被处理。如果你的程序是模块化的,那么扩展是允许每个模块注册和管理它们各自的服务配置的。

扩展要实现ExtensionInterface接口,并且通过以下方式注册到容器中:

1
$container->registerExtension($extension);

扩展的主要工作已经在load方法里完成了。通过load方法,你可以从一个或多个配置文件中加载配置信息,同时你还可以尽情操作容器中的服务定义——通过在后面的如何操作“服务定义”对象小节中所展示的方法。

load方法被传递了一个用来完成后续配置的、新鲜的container,此处的container将在后面被合并到该extension所注册到的容器之中。这样一来,你就可以拥有多个扩展,进而分别管理容器中“属于不同扩展”的服务定义(container definitions)。扩展在添加进来时,并不被加载到服务容器的配置信息中,而是当服务容器的compile方法被调用时,扩展的配置信息才被处理。

一个超简单的扩展可能只是将配置信息读到服务容器中,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\Config\FileLocator;
 
class AcmeDemoExtension implements ExtensionInterface
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new XmlFileLoader(
            $container,
            new FileLocator(__DIR__.'/../Resources/config')
        );
        $loader->load('services.xml');
    }
 
    // ...
}

相对于“直接读取配置信息”到即将被编译的总容器,上述方法并没有得到更多,它只是允许配置文件被分割到多个模块(译注:bundle/extension)之中。为了能够在模块之外的配置文件中,去影响该模块的配置本身,这是复杂程序的配置过程所必须的。symfony2能够加载核心配置文件(译注:config.yml)中的“指定区块”到服务容器中,令得这部分配置能够被指定的扩展所使用。这些“指定区块”并不会被服务容器直接处理,而是被其所属的扩展(Extension)所驱动。

symfony2中每一个扩展(extension),必须指定一个getAlias方法,以便实现接口:

1
2
3
4
5
6
7
8
9
10
11
// ...
 
class AcmeDemoExtension implements ExtensionInterface
{
    // ...
 
    public function getAlias()
    {
        return 'acme_demo';
    }
}

对于YAML格式配置文件来说(config.yml),为扩展指定一个假名,意味着所有相关的配置信息,将被传递到该扩展的load方法中:

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

如果该yml文件被加载到扩展的配置信息中,则上述配置中的值将在服务容器被编译时,才被处理,此时也即该扩展被加载之时:

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
 
$container = new ContainerBuilder();
$container->registerExtension(new AcmeDemoExtension);
 
$loader = new YamlFileLoader($container, new FileLocator(__DIR__));
$loader->load('config.yml');
 
// ...
$container->compile();

当加载一个“使用了扩展假名”的配置文件时,该扩展必须已经注册到container builder中,否则Symfony会抛出一个异常。

配置文件中那些“指定区块”中的值,将被传递到该扩展的load方法中的第一个参数中:

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

$config参数是一个数组,包含了“被加载到容器中”的每一个不同配置文件。上述例程只是加载了一个config文件进来,但该参数仍然是以数组方式存在,该数组类似下面的格式:

1
2
3
4
5
6
array(
    array(
        'foo' => 'fooValue',
        'bar' => 'barValue',
    ),
)

虽然你能够手动管理、合并这些源自不同配置文件的数组,但是更好的办法却是利用Config组件来实现对相关配置值(config value)进行合并和验证。利用“配置处理”(configuration processing)操作,你可以访问到任何一个配置值,如下例第8行:

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)
{
    $configuration = new Configuration();
    $processor = new Processor();
    $config = $processor->processConfiguration($configuration, $configs);
 
    $foo = $config['foo']; //fooValue
    $bar = $config['bar']; //barValue
 
    // ...
}

此时,有两个方法你必须加以实现。一个是用来返回xml的命名空间,以便使某个xml配置文件“相关部分”之内容,能够被传递到扩展处。另外一个则是要指定“用来验证xml配置文件”的XSD文件之根目录:

1
2
3
4
5
6
7
8
9
public function getXsdValidationBasePath()
{
    return __DIR__.'/../Resources/config/';
}
 
public function getNamespace()
{
    return 'http://www.example.com/symfony/schema/';
}

XSD验证是可选的,从getXsdValidationBasePath方法中返回false将关闭它。

一个xml格式的配置文件看上去像下面这样:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:acme_demo="http://www.example.com/symfony/schema/"
    xsi:schemaLocation="http://www.example.com/symfony/schema/ http://www.example.com/symfony/schema/hello-1.0.xsd">
 
    <acme_demo:config>
        <acme_demo:foo>fooValue</acme_hello:foo>
        <acme_demo:bar>barValue</acme_demo:bar>
    </acme_demo:config>
</container>

在Symfony完整版框架中,有一个Extension基类,以快捷方式来实现上述方法,用于处理配置信息。请查阅cookbook如何加载bundle中的服务配置信息了解更多细节。

处理过的配置值,现在已经可以作为“容器的参数(container parameter)”被添加,就像前面 ~配置文件的参数(parameters)~ 小节中所列出的那样,只是此处的处理过程,还具有“合并多个配置文件”与“验证配置文件”的好处:

1
2
3
4
5
6
7
8
9
10
public function load(array $configs, ContainerBuilder $container)
{
    $configuration = new Configuration();
    $processor = new Processor();
    $config = $processor->processConfiguration($configuration, $configs);
 
    $container->setParameter('acme_demo.FOO', $config['foo']);
 
    // ...
}

在bundle的Extension类中,更加复杂的配置需求也可以被满足。例如,你可以选择加载一个主要的服务配置文件,同时在这文件的某个参数“满足特定条件时”去加载第二个服务配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function load(array $configs, ContainerBuilder $container)
{
    $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');
    }
}

当容器被编译时,仅仅将扩展注册到容器中是不够的,这无法使其进入“处理扩展的配置信息”相关流程。在配置文件中拥有该扩展的“假名(alias)”,方能确保它被加载。服务容器的构造器(container builder)可以被告之加载某个扩展的配置信息,利用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();

当你要操作某个扩展的“配置文件信息”时,你不能从另外一个扩展中进行,因为这里使用的是新鲜的容器(fresh container)。你只能用compiler pass来替代,“编译时的传递”使用的是总容器(full container),它可以在一个扩展的配置信息被处理之后,完成针对这些信息的其他操作。

传递给扩展的“预配置信息” 

load()方法被调用之前,任何一个bundle的扩展都可以进行预先配置。这是通过PrependExtensionInterface接口来实现的:

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()
    {
        // ...
 
        $container->prependExtensionConfig($name, $config);
 
        // ...
    }
}

更多细节,参考cookbook如何简化多个bundle的配置信息,这部分对symfony框架来说属于特定内容,但却含有“预先配置”这一功能的相关信息。

在编译过程中执行代码 

你也可以在编译过程中通过编写你自己的compiler passes来执行自定义代码。只要在扩展类中实现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)
    {
       // ... do something during the compilation 在编译过程中做一些事
    }
 
    // ...
}

由于process()是在全部扩展被加载之后才调用,它允许你去编辑其他扩展的服务定义,也可以从服务定义中取出相关信息。

container的参数和服务定义,都可以被操作,通过在前面的玩转容器的服务定义章节中描述的方法实现。

请注意扩展类中的process()方法是在“容器的优化阶段”被调用的。如果你需要在其他阶段来编辑容器,参考本文下一小节的内容。

作为一个原则,在compiler pass过程中,只能操作服务定义,不要创建服务实例。实践中,这意味着要经常使用以下方法:has()findDefinition()getDefinition()setDefinition()等等,来代替get()set()之类。

确保你的compiler pass不要包容已经存在的服务。如果某些必须的服务不可利用,放弃相关的方法调用。

在compiler pass中最常做的一件事,就是检索出所有“具有特定标签(tag)”的服务定义,然后以某种方式处理这些服务,或是动态地将它们注入其他的服务定义中。请参考后续章节玩转Tagged Service

创建一个独立的Compiler Pass 

有时,你需要在编译过程中做一件以上的事情,想要抛开扩展来使用compiler pass,或者需要在编译过程的其他阶段来执行一些代码。这些场景下,你可以创建一个新的类去实现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)
    {
       // ... do something during the compilation 在编译过程中做一些事
    }
}

你可以注册你的自定义pass到container中:

1
2
3
4
use Symfony\Component\DependencyInjection\ContainerBuilder;
 
$container = new ContainerBuilder();
$container->addCompilerPass(new CustomPass());

compiler passes有不同的注册方式。如果你使用的是完整版框架,请参阅cookbook中的如何玩转bundle中的compiler passes章节。

compiler pass给了我们一个机会,来操作“已编译完成”的服务定义(service definitions)。这是非常有威力的过程,但却不是每个人在日常开发中都需要的。compiler pass必须有process()方法,用来传递正在编译的container。译注:根据DX开发体验的要求,从symfony2.8起,compiler pass已不需要在bundle类中手动注册,这个过程经过已经被Symfony自动完成。

控制传递的顺序 

默认的compiler passes被划分为:优化传递(optimization passes)、移除传递(removal passes)。优化传递先运行,它包括诸如“解析服务定义中的引用”等任务。移除传递执行的任务是“去除私有假名(private alias)和无用服务”等等。当使用addCompilerPass()方法来注册一个compiler pass时,你可以选择该传递运行于何时——默认顺序是:它们全都在“优化传递”之前运行。

你可以通过下列常量作为“注册compiler pass到容器中”的第二参数,用于控制pass从哪里切入“传递次序”:

  • PassConfig::TYPE_BEFORE_OPTIMIZATION

  • PassConfig::TYPE_OPTIMIZE

  • PassConfig::TYPE_BEFORE_REMOVING

  • PassConfig::TYPE_REMOVE

  • PassConfig::TYPE_AFTER_REMOVING

例如,运行一个自定义的pass,在默认的“移除传递”运行之后:

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

出于性能考虑剥离配置信息 

相对于使用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());
}

ProjectServiceContainer是用在剥离出来的container类上的默认名称,但你可以在剥离container时通过参数中的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(array('class' => 'MyCachedContainer'))
    );
}

现在你已经得到经过PHP方式编译的、被配置过了的服务容器——而配置信息是来自于“更加容易使用的配置文件”。另外,以此种方式剥离container还将进一步优化“container是如何创建服务的”这一过程。

上例中,当你(在配置文件上)有任何改变时,你需要删除缓存文件。通过对一个“决定你是否处于dubug模式”的变量进行校验,可令你保持生产环境下“被缓存了的服务容器”之运行速度。但如果在开发环境下发现配置文件中有某个“更新”信息,则会重新编译容器(以体现该更新):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ...
 
// 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(array('class' => 'MyCachedContainer'))
        );
    }
}

上述代码还有进一步改进的可能,即,在debug模式下“针对配置信息所做的改变”发生之后,只重新编译容器本身,而不是针对每一次请求(request)。这可以通过“只缓存【作用于配置服务容器的】resource文件”来实现。这部分的详细信息,请参阅config组件的文档基于Resource进行缓存

你不必算出具体哪个文件要被缓存,因为container builder是跟踪全部“能够影响到它”的resource资源的,不光是配置文件,也包括扩展类(extension class)和编译器传递(compiler pass)。这意味着,上面几种文件中的“任何改变”都将使缓存失效,并且触发容器重建。你需要向容器请求这些资源,并将这些资源用作metadata来生成缓存。

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()) {
    $containerBuilder = new ContainerBuilder();
    // ...
    $containerBuilder->compile();
 
    $dumper = new PhpDumper($containerBuilder);
    $containerConfigCache->write(
        $dumper->dump(array('class' => 'MyCachedContainer')),
        $containerBuilder->getResources()
    );
}
 
require_once $file;
$container = new MyCachedContainer();

现在,无论debug模式是否开启,已缓存的被剥离容器都将被使用。区别在于ConfigCache的第二个构造参数是设置debug模式的。当缓存不在debug模式中时,被缓存的container将始终被使用(如果它存在的话)。如果缓存处在debug模式中,一个附加的metadata文件将被生成,其内容是所有资源文件的“时间戳”。该文件中的所有元数据都将被检查,以明确是否有文件被改动过,若其有缓存,将被认为是过期的。

在完整版Symfony框架中,所有关于“服务容器”的编译和缓存毋须人为干涉。(译注:这简直是太好了^_^;)

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

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