支付宝扫一扫付款
微信扫一扫付款
(微信为保护隐私,不显示你的昵称)
服务容器被编译,是有多个理由的,包括:检查所有潜在的问题,比如对某服务的“循环引用(circular reference)”,以及通过解析参数和移除无用服务来使得服务容器更加高效。还包括针对特定功能的——比如使用父服务(parent serivces)时——这些都需要容器被编译。
编译过程通过以下代码实现:
1 | $container->compile(); |
Compile方法使用的是被称为Compiler Passes 的编译方式(编译器传递)。DependencyInjection组件自身,拥有若干为了编译而“自动注册”的pass。例如CheckDefinitionValidityPass
类,就是用来检查容器中的服务可能存在的诸多潜在问题。在这之后,其他pass仍然要检查容器的有效性,并且,容器在被缓存之前,还有更进一步的compiler passes被用来优化容器。例如:私有服务(private services)和抽象服务(abstract services)将被移除出去,而服务假名(aliases)将被解析为服务id。
和本章之前所述(请参考The DependencyInjection组件)的“直接加载配置信息到容器中”一样,你也可以管理扩展中的配置(见下文)——
——(接上文)通过将扩展注册到容器中即可。编译过程的第一步,是将配置信息从已经注册到容器中的扩展类中进行加载。与直接加载配置文件不同,扩展的配置信息只有在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文件进来,但该参数仍然是以数组方式存在,该数组类似下面的格式:
虽然你能够手动管理、合并这些源自不同配置文件的数组,但是更好的办法却是利用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,或者需要在编译过程的其他阶段来执行一些代码。这些场景下,你可以创建一个新的类去实现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 创作共用授权。