服务容器
现代php程序全部是对象。一个对象可以负责电子邮件的发送,另一个对象能让你把信息持久化到数据库中。在程序中你可以建立一个对象,用来管理产品库存,或者用另一个对象处理第三方API中的数据。结论是,现代程序可以做许多事,而程序是由许多组织在一起的处理各自任务的对象构成。
本章讲的是Symfony中一个特殊的PHP对象,它帮助你实例化、组织和取出程序中的这许多对象。这个对象,被称为“服务容器”,它允许你在程序中把对象的组织方式变得标准化与集中化。容器令事情变得简单,它非常快,而且强调从架构上提升代码的复用性同时降低藕合性。由于Symfony所有的类都要使用容器,你将学习到如何扩展、配置与使用Symfony中的对象。从大的方面讲,服务容器是Symfony的速度与扩展性的最大功臣。
最后,配置与使用服务容器很简单。学完本章,你能通过服务容器轻松创建自己的对象,也能自定义第三方bundle中的任何对象。你将能够开始书写可复用、可测试、松藕合的代码,其原因就在于服务容器令编写良好代码变得容易。
如果你在读完本章还想了解更多,请参考 依赖注入组件。
什么是服务 ¶
简单地说,服务(Service)可以是任何执行“全局”任务的对象。它在计算机科学中是一个专有名词,用来形容一个“为完成某种使命(比如发送邮件)”而被创建的对象。在程序中,当你需要某个服务所提供的特定功能时,可以随时取用该服务。创建服务时你不需要做任何特殊的事:只要写一个PHP类,令其完成某个任务即可。恭喜,你已经创建了一个服务!
作为原则,一个PHP对象若要成为服务,必须能在程序的全局范围使用。一个独立的 Mailer
服务被“全局”用于发送邮件信息,然而它所传输的这些 Message
信息对象并不是服务。类似的,一个 Product
对象,并不是服务,但是一个能够把产品持久化到数据库中的对象,就是服务。
这说明了什么?以“服务”角度思考问题的好处在于,你已经开始想要把程序中的每一个功能给分离出来,形成一系列服务。由于每个服务只做一件事,你可以在任何需要的时候轻松访问到这个服务。对每个服务的测试和配置变得更加容易,因为它们已经从你程序中的其他功能性中分离出来。这个理念被称为 面向服务架构,并非Symfony或PHP专有。把你的程序通过一组独立存在的服务类进行“结构化”,是久经考验且广为人知的面向对象编程之最佳实践。这个技巧可谓是成为任何一门语言的优秀开发者的关键。
什么是服务容器 ¶
服务容器(Service Container/dependency injection container)就是一个PHP对象,它管理服务(即对象)的实例化。
举例来说,假设你有一个简单的类,用于发送邮件信息。如果没有服务容器,你必须在需要的时候手动创建对象。
use Acme\HelloBundle\Mailer; $mailer = new Mailer('sendmail'); $mailer->send('ryan@example.com', ...);
这很简单。一个虚构的 Mailer
邮件服务类,允许你配置邮件的发送方法(比如 sendmail
,或 smtp
,等等)。但如果你想在其他地方使用邮件服务怎么办?你当然不希望每次都重新配置 Mailer
对象。如果你想改变邮件的传输方式,把整个程序中所有的 sendmail
改成 smtp
怎么办?你不得不找到所有创建了 Mailer
的地方,手动去更新。
在容器中创建和配置服务 ¶
上面问题的最佳答案,是让服务容器来为你创建Mailer对象。为了让容器正常工作,你先要教会它如何创建Mailer服务。这是通过配置来实现的,配置方式有YAML,XML或PHP:
PHP:// app/config/services.phpuse Symfony\Component\DependencyInjection\Definition; $container->setDefinition('app.mailer', new Definition( 'AppBundle\Mailer', array('sendmail')));
XML:<!-- app/config/services.xml --><?xml version="1.0" encoding="UTF-8" ?><container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <services> <service id="app.mailer" class="AppBundle\Mailer"> <argument>sendmail</argument> </service> </services></container>
YAML:# app/config/services.ymlservices: app.mailer: class: AppBundle\Mailer arguments: [sendmail]
当Symfony初始化时,它要根据配置信息来建立服务容器(默认配置文件是 app/config/config.yml
)。被加载的正确配置文件是由 AppKernel::registerContainerConfiguration()
方法指示,该方法加载了一个“特定环境”的配置文件(如config_dev.yml是dev开发环境的,而 config_prod.yml
则是生产环境的)。
一个 Acme\HelloBundle\Mailer
对象的实例已经可以通过服务容器来使用了。容器在任何一个标准的Symfony控制器中可以通过get()快捷方法直接获得。
class HelloController extends Controller{ // ... public function sendEmailAction() { // ... $mailer = $this->get('app.mailer'); $mailer->send('ryan@foobar.net', ...); }}
当你从容器中请求 app.mailer
服务时,容器构造了该对象并返回(实例化之后的)它。这是使用服务容器的又一个好处。即,一个服务不会被构造(constructed),除非在需要时。如果你定义了一个服务,但在请求(request)过程中从未用到,该服务不会被创建。这可节省内存并提高程序运行速度。这也意味着在定义大量服务时,很少会对性能有冲击。从不使用的服务绝对不会被构造。
附带一点,Mailer服务只被创建一次,每次你请求它时返回的是同一实例。这可满足你的大多数需求(该行为灵活而强大),但是后面你要了解怎样才能配置一个拥有多个实例的服务,参考 如何定义非共享服务 一文。
本例中,控制器继承了Symfony的Controller基类,给了你一个使用服务容器的机会,通过get()方法就可从容器中找到并取出 app.mailer
服务。
服务参数 ¶
通过容器建立新服务的过程十分简单明了。参数(Parameter)可以令服务的定义更加灵活、有序:
PHP:// app/config/services.phpuse Symfony\Component\DependencyInjection\Definition; $container->setParameter('app.mailer.transport', 'sendmail'); $container->setDefinition('app.mailer', new Definition( 'AppBundle\Mailer', array('%app.mailer.transport%')));
XML:<!-- app/config/services.xml --><?xml version="1.0" encoding="UTF-8" ?><container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <parameters> <parameter key="app.mailer.transport">sendmail</parameter> </parameters> <services> <service id="app.mailer" class="AppBundle\Mailer"> <argument>%app.mailer.transport%</argument> </service> </services></container>
YAML:# app/config/services.ymlparameters: app.mailer.transport: sendmailservices: app.mailer: class: AppBundle\Mailer arguments: ['%app.mailer.transport%']
结果就和之前一样。区别在于,你是如何定义的服务。通过把 app.mailer.transport
用 %
百分号给括起来,容器就知道应该去找对应这个名字的参数。容器自身生成时,会把每个参数的值,还原到服务定义中。
如果你要把一个由 @
开头的字符串,在YAML文件中用做参数值的话(例如一个非常安全的邮件密码),你需要添加另一个 @
符号进行转义(这种情况只在YAML格式的配置文件中适用)
# app/config/parameters.ymlparameters: # This will be parsed as string '@securepass' mailer_password: '@@securepass'
配置参数(parameter)或方法参数(argument)中的百分号,也必须用另一个%进行转义:
1 | <argument type="string">http://symfony.com/?foo=%%s&bar=%%d</argument> |
参数的目的,是要把信息传给服务。当然,不用参数的话,也不会有什么问题。但使用参数有以下几个好处:
分离并组织所有服务“选项”到一个统一的“参数”键下
参数值可以被用到多个服务定义中
在bundle中创建服务时,使用参数可以令服务在全局程序中的定制变得容易
是否使用参数,选择权在你。高质量第三方bundles,始终使用参数,因为他们要让存于容器中的服务具备更强的“可配置性”。当然,你可能并不需要参数带来的灵活性。
数组参数 ¶
参数可以包含数组,参见 数组参数(Array Parameters)。
引用(注入)服务 ¶
至此,原来的 app.mailer
服务是简单的:它在构造器中只有一个参数,因此很容易配置。你可以预见到,在你创建一个需要依赖一个或多个容器中的其他服务时,容器的真正威力开始体现出来。
例如,你有一个新的服务, NewsletterManager
,它帮助你管理和发送邮件信息到地址集。 app.mailer
服务已经可以发邮件了,因此你可以把它用在 NewsletterManager
中来负责信息传送的部分。这个类看上去像下面这样:
// src/Acme/HelloBundle/Newsletter/NewsletterManager.phpnamespace Acme\HelloBundle\Newsletter; use Acme\HelloBundle\Mailer; class NewsletterManager{ protected $mailer; public function __construct(Mailer $mailer) { $this->mailer = $mailer; } // ...}
如果不使用服务容器,你可以在controller中很容易地创建一个NewsletterManager:
use Acme\HelloBundle\Newsletter\NewsletterManager; // ... public function sendNewsletterAction(){ $mailer = $this->get('app.mailer'); $newsletter = new NewsletterManager($mailer); // ...}
这样去实现是可以的,但当你以后要对 NewsletterManager
类增加第二或第三个构造器参数时怎么办?如果你决定重构代码并且重命名这个类时怎么办?这两种情况,你都需要找到每一个 NewsletterManager
类被实例化的地方,然后手动个性它。毫无疑问,服务容器提供了一个更加吸引人的处理方式:
PHP:// app/config/services.phpuse Symfony\Component\DependencyInjection\Definition;use Symfony\Component\DependencyInjection\Reference; $container->setDefinition('app.mailer', ...); $container->setDefinition('app.newsletter_manager', new Definition( 'AppBundle\Newsletter\NewsletterManager', array(new Reference('app.mailer'))));
XML:<!-- app/config/services.xml --><?xml version="1.0" encoding="UTF-8" ?><container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <services> <service id="app.mailer"> <!-- ... --> </service> <service id="app.newsletter_manager" class="AppBundle\Newsletter\NewsletterManager"> <argument type="service" id="app.mailer"/> </service> </services></container>
YAML:# app/config/services.ymlservices: app.mailer: # ... app.newsletter_manager: class: AppBundle\Newsletter\NewsletterManager arguments: ['@app.mailer']
在YAML中,特殊的 @app.mailer
语法,告诉容器去寻找一个名为 app.mailer
的服务,然后把这个对象传给 NewsletterManager
的构造器参数。本例中,指定的 app.mailer
服务是确实存在的。如果它不存在,则异常会抛出。不过你可以标记依赖可选 – 这个话题将在下一小节中讨论。
(对服务的)引用是个强大的工具,它允许你创建独立的服务,却拥有准确定义的依赖关系。在这个例子中, app.newsletter_manager
服务为了实现功能,需要依赖 app.mailer
服务。当你在服务容器中定义了这个依赖时,容器托管了对这个类进行实例化的全部工作。
可选的依赖:Setter注入 ¶
将依赖对象注入到构造器中是一个办法,这可确保依赖可以利用(否则构造函数无法执行)。但是对一个类来说,如果它有一个可选的依赖,那么“setter注入”是一个更好的方案。这意味着用一个类方法来注入依赖,而不是构造器。这个类看上去可能是这样的:
namespace AppBundle\Newsletter; use AppBundle\Mailer; class NewsletterManager{ protected $mailer; public function setMailer(Mailer $mailer) { $this->mailer = $mailer; } // ...}
服务定义需要对setter注入做出相应调整:
PHP:// app/config/services.phpuse Symfony\Component\DependencyInjection\Definition;use Symfony\Component\DependencyInjection\Reference; $container->setDefinition('app.mailer', ...); $container->setDefinition('app.newsletter_manager', new Definition( 'AppBundle\Newsletter\NewsletterManager'))->addMethodCall('setMailer', array( new Reference('app.mailer'),));
XML:<!-- app/config/services.xml --><?xml version="1.0" encoding="UTF-8" ?><container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <services> <service id="app.mailer"> <!-- ... --> </service> <service id="app.newsletter_manager" class="AppBundle\Newsletter\NewsletterManager"> <call method="setMailer"> <argument type="service" id="app.mailer" /> </call> </service> </services></container>
YAML:# app/config/services.ymlservices: app.mailer: # ... app.newsletter_manager: class: AppBundle\Newsletter\NewsletterManager calls: - [setMailer, ['@app.mailer']]
本节所实现的过程被称为“构造器注入”和“setter注入”。此外Symfony的容器体系还支持属性注入(property injection)。