“中国要复兴、富强,必须在开源软件领域起到主导作用,为了国家安全和人类发展,责无旁贷,我们须为此而奋斗”——By:云客
5. 服务容器及Symfony依赖注入组件
迟迟未写这个主题是因为它太重要,以至于是drupal8系统运行的一个阶段性标识,它贯穿整个系统,服务容器及Symfony依赖注入组件是drupal8系统的中枢,学习的重中之重
很多新同学可能对“服务容器”、“依赖注入”这样的词感觉陌生,其实非常简单,只是名字玄乎而已,下面解释一下:
何为依赖注入?当一个对象的运行要依靠另外一个对象的帮助,那么就是依赖,把这个依赖的对象保存到本对象的属性中以便随时调用,那么就是注入了,加起来就是“依赖注入”这个词的来历,举个详细的例子:比如一个字符串格式验证器对象Validator,可以验证电子邮件、身份证号码、电话号码等等,但它自身不实现任何验证方法,每一类验证由一个专门的对象负责,比如电子邮件格式验证由mailer对象完成,通过Validator对象的set方法或addValidator方法将mailer或其他验证器对象保存到Validator的属性里面,在Validator对象中就可以调用不同的验证器来完成工作了,这就是将依赖注入到内部属性里面,它就是依赖注入的全部了,是不是很简单?其实这就是一种软件设计模式而已。
在一个大型系统中会有许许多多的常用对象,更进一步的,我们将这些常用对象统统保存到一个超级对象中,要用的时候通过对应的标识符取出使用即可,这样是不是很简洁方便呢!在drupal8中有五百多个常用对象,它们就被保存到了一个超级对象中,这个超级对象就叫做容器,这些常用对象按计算机科学术语就叫做服务(就像计算机操作系统中最常用的程序被一直后台执行,在window中就叫做服务,比如在win7中是右击我的电脑-管理-服务和应用程序-服务,就列出了系统所有的服务),所以这个容器又可以叫做服务容器!
到这里你应该明白了“服务容器”、“依赖注入”,名字玄乎而已,先有依赖注入这个设计模式,然后就产生了服务容器这样的结果,在drupal8中大部分程序都是以服务对象的方式保存在容器中,统计了一下默认下载的初始安装有五百多个服务,下面详细看看服务容器。
一个容器(超级对象)里面保存的对象(又叫服务,面向开发而言叙述方便下文有时也称为对象)有两大类,一类是外部实例化好的对象,然后注入到容器里面,另外一类是容器根据对象的定义数据自己实例化出来的对象。在drupal8中大部分是后者,容器对象根据定义数据在需要时才实例化能提高性能及资源消耗。
下面来看一看drupal如何实现容器的:
容器的形成是在\core\lib\Drupal\Core\DrupalKernel中,它称之为drupal核心类,从名字就可以看出,这个类的主要工作就是创建容器,可见容器之重要。
在实际代码过程中:
先是形成一个引导容器,这个容器包含了数据库对象、缓存对象等基本的服务,以供后续容器形成使用,采用这样的设计是简洁起见
然后是构建Symfony容器,这个是容器形成的主要工作,形成后将容器定义数据导出,并保存到缓存中,下次访问将直接从缓存获取定义数据,节省了大量工作
最后是用第二步形成的容器定义数据创建一个最终使用的drupal容器,这个容器又叫运行时容器Drupal 8 run-time container,它是经过简化的Symfony容器,去掉了一些功能,并改进了一些代码提高运行速度
以上就是容器形成的宏观步骤,第二步是容器的本质性形成,它使用了Symfony依赖注入组件(基本是完全使用,drupal的 ContainerBuilder继承了Symfony的ContainerBuilder类,只这些差别:仅允许服务标识符和参数名为小写;不允许@=方式的扩展;抑制弃用警告;为服务设置_serviceId属性;允许冻结容器时使用set方法注入外部服务;添加__sleep方法;详见Drupal\Core\DependencyInjection\ContainerBuilder),除这些不同外是完全兼容Symfony容器,因此我们在drupal8里面定义服务也是兼容Symfony服务定义的,这个过程也是工作量最大,最复杂的过程,理解了它后其他两步就很容易理解了,下面我们就来看看Symfony容器的形成。
先说说服务容器中对象的定义数据,想想在php中要实例化一个对象并让它可用是怎么一个过程呢?
你可以通过一个类定义直接实例化一个对象,也可以调用工厂方法产生一个对象;然后要可用,可能还需要通过类似set或add这样的方法为对象设置一些必要的属性,也可能是在构造函数里面传递一些参数;容器里面有很多个对象,如果都全部实例化后再保存在容器里面,那么很不利于性能,所以基本是保存对象相关信息,在获取的时候才实例化;有些对象实在太巨大非常消耗资源,对于这样的对象在获取时也不实例化,而是在真正使用时才实例化,获取时得到的只是一个代理而已,这样的技术又叫做延迟加载,或懒加载;有些服务只提供给容器内部使用,不对外,因此服务又有公有私有这样的属性;在每次获取服务的时候可以选择是同一个对象还是重新实例化一个,因此又有共享和不共享的属性;有些服务做类似的工作或具备共同特征,比如上文介绍的验证器,虽然是不同的验证器,但都是对字符串做验证使用,那么需要对这些服务进行组织标记,因此就出现了标签的概念;在容器形成时可能需要对服务对象的参数调整或处理具备某标签的服务,就有了容器编译的概念;服务是外部注入的呢还是容器自己实例化的呢也需要进行标记;服务可能需要参数也可能和其他服务共享参数,那么需要定义参数机制;如果服务需要函数库则需要加载函数库文件;
容器对象的定义数据就是围绕这些内容进行的,先有个印象对后面的学习就轻松许多了。
在Symfony中有个称为Definition的类代表了服务对象定义数据,它位于\vendor\symfony\dependency-injection\Definition.php,提供了操作服务对象元数据的所有方法,上文说了大部分对象都是容器自己实例化的,它就是依据这个对象保存的信息来实例化服务,如果是外部注入的对象,Symfony称为合成物,这样的对象在Definition对象里面没有多余的数据,仅有标识符,和合成物标识。
下面来看一看Symfony容器源代码:
它们都位于\vendor\symfony\dependency-injection下,提供了几个接口来规范容器:
ContainerInterface:基本容器接口
IntrospectableContainerInterface:内省容器接口,继承自基本接口,所谓内省就是看服务是否已经由定义元数据实例化成了对象,该接口在Symfony3.0并入基本接口
ResettableContainerInterface:可重置容器接口,继承自基本接口,提供重置容器功能
TaggedContainerInterface:带标签容器接口,继承自基本接口,为容器增加标签功能
Symfony定义了一个默认容器实现类:
Container:是一个简单的容器,提供了接口要求的基本功能。
我们在创建容器的时候往往需要更多的功能,所以Symfony提供了一个ContainerBuilder,它继承自Container容器,但提供了丰富的功能来帮助构建容器,可以看做是一个融合了容器工厂的容器,所以它的名字是ContainerBuilder容器构建,这个容器也是主要使用的容器。
在上文提到的宏观步骤中构建容器,就是用ContainerBuilder构建一个空容器,然后将服务定义数据添加进去,再设置各种参数、合成物最终形成一个Symfony容器。
Symfony容器构建成功后,将导出(dumper)容器定义数据,这个导出工作是由\core\lib\Drupal\Component\DependencyInjection\Dumper完成的,导出保存到缓存,并用这个定义数据建立drupal容器,也叫作运行时容器,系统后续就是用该容器了,说到这里你可能有一个疑问,为什么不直接使用Symfony容器呢?还要导出再建立那么麻烦,答案是:性能!首先导出的定义数据被整理,并使用php数组形式,再者drupal容器根据自己需要去掉了许多不必要的功能,这样能大大提高速度和节省资源,这里说一个题外话:许多中间供应商都有一个冲动,就是把产品做的尽量强大,满足尽可能多的需要,而在最终产品的使用上许多功能却是用不到的,因此最终产品设计者经常需要对半成品进行瘦身定制,不只是软件设计领域,在生活中其他领域也经常看到,想想是不是这样?
下面说说Symfony容器和drupal容器的区别:
Symfony容器使用Definition对象做服务的定义元数据,而drupal容器使用php数组,想var_dump这个数组满足下好奇心?看最后的容器补充
Symfony容器中的以下功能在drupal容器中都没有了:
addCompilerPass、getCompilerPassConfig、getCompiler、compile:添加编译器,所以drupal容器无法再编译,它是一个已经编译的容器,关于编译下面再讲
merge:合并容器
addAliases、setAliases、setAlias、removeAlias、hasAlias、getAliases、getAlias:操作服务别名
register:注册服务对象
addDefinitions、removeDefinition、getDefinitions、setDefinition、hasDefinition、getDefinition、findDefinition:定义数据操作
findTaggedServiceIds、findTags、findUnusedTags:标签操作
以上这些功能均不能在drupal容器(运行时容器)中使用了
drupal容器不支持的Symfony容器语法:
容器定义数据dumper的时候不允许decorated定义,drupal容器不允许这个,该属性必须在Symfony容器构建阶段定义编译pass解决;
yaml文件不允许@=扩展,这样的语法在drupal中禁用
下面说说什么是容器编译:
我们知道容器中有些服务对象是功能相似的,他们被用标签tags分成一组,举个具体的列子:在对缓存进行访问的时候,需要策略控制,一条策略就是一个服务,drupal模块可以定义自己的策略,只要对这个策略服务打上策略标签即可,这样在访问缓存的时候就会调用这个策略,那么问题来了,是在执行访问检查的时候才去查找这些策略吗?其实是在容器形成的时候就已经查找出了这些策略,并把他们当做参数传给了访问控制策略表对象,这个工作就是容器编译,准确的说容器编译就是根据标签信息建立服务对象之间的关系,或者调整一些容器对象的参数,总之就是对容器里面的服务对象做些补充工作。
在构建容器时,drupal如何给出服务对象的定义数据:
在构建容器的时候drupal通过服务静态定义文件services.yml文件和服务提供器ServiceProvider来为容器添加服务定义文件,相比services.yml而言服务提供器有更多的自由度,它实现ServiceProviderInterface接口,可以完成services.yml的所有工作外,还可以操作容器编译。而services.yml和ServiceProvider是怎么来的呢?是通过Drupal\Core\Extension\ExtensionDiscovery类扫描所有的核心及扩展模块找出来的,这里澄清一个让很多初学者模糊的概念,关于模块的名字,很多人混淆模块所在文件夹名、.info.yml文件名、.info.yml文件里面的name值,看完ExtensionDiscovery类的实现后就会明白文件夹名和模块名无关,文件名才是模块名,里面的name值仅仅用于后台显示。
关于服务容器的补充:
1:在站点设置文件中可以设置运行时容器类,默认是\Drupal\Core\DependencyInjection\Container,格式:$settings['container_base_class']=“\Drupal\模块名\Container”;当你需要特殊功能时可以继承默认的类,定义一些新功能,通过这个设置运行时容器就可以自定义了。
2:要想看一看运行时容器的定义数据是什么样子?drupal8都提供了哪些常用服务?请用phpMyAdmin打开数据库,找到cache_container数据表,下载里面的data字段内容,得到一个扩展名为bin的文件,它就是容器定义数据的缓存文件,内容是经过序列化的,unserialize并print_r即可看到:print_r(unserialize(file_get_contents("cache_container-data.bin")));
3:在容器编译的时候有个特殊的标签:{ name: service_collector, tag: tags_name, call: add},意思是查找所有标签为tags_name的服务,并把它们依次作为回调add的参数,在drupal中有许多这样的用法,非常灵活方便
4:容器这么好,服务对象都在容器里面,在代码里面怎么取得容器?\Drupal::getContainer();即可,在drupal核心DrupalKernel建立完运行时容器后,就将他注入到了全局类drupal的静态属性里面:\Drupal::setContainer($this->container);,因此你可以在任意地方取到容器,并使用里面的服务,比如要操作数据库,取到数据库对象,代码如下:\Drupal::getContainer()->get('database');全局类Drupal也提供了许多常用方法,比如获取数据库又可以这样写:\Drupal::database();,这样的写法在内部就是通过前面的方法实现的,只不过做了包装,更加方便使用
5:可以在站点配置文件中添加services.yml文件,这里添加的services.yml文件优先级比系统提供的高,比如/sites/default/services.yml就是在配置文件中添加的,它可以覆盖系统提供的值,因此希望替换系统提供的服务时可以使用此方法,格式为:$settings['container_yamls'][] = '/services.yml';
反馈互动