服務(wù)容器
現(xiàn)代php程序全部是對(duì)象。一個(gè)對(duì)象可以負(fù)責(zé)電子郵件的發(fā)送,另一個(gè)對(duì)象能讓你把信息持久化到數(shù)據(jù)庫(kù)中。在程序中你可以建立一個(gè)對(duì)象,用來(lái)管理產(chǎn)品庫(kù)存,或者用另一個(gè)對(duì)象處理第三方API中的數(shù)據(jù)。結(jié)論是,現(xiàn)代程序可以做許多事,而程序是由許多組織在一起的處理各自任務(wù)的對(duì)象構(gòu)成。
本章講的是Symfony中一個(gè)特殊的PHP對(duì)象,它幫助你實(shí)例化、組織和取出程序中的這許多對(duì)象。這個(gè)對(duì)象,被稱為“服務(wù)容器”,它允許你在程序中把對(duì)象的組織方式變得標(biāo)準(zhǔn)化與集中化。容器令事情變得簡(jiǎn)單,它非???,而且強(qiáng)調(diào)從架構(gòu)上提升代碼的復(fù)用性同時(shí)降低藕合性。由于Symfony所有的類都要使用容器,你將學(xué)習(xí)到如何擴(kuò)展、配置與使用Symfony中的對(duì)象。從大的方面講,服務(wù)容器是Symfony的速度與擴(kuò)展性的最大功臣。
最后,配置與使用服務(wù)容器很簡(jiǎn)單。學(xué)完本章,你能通過(guò)服務(wù)容器輕松創(chuàng)建自己的對(duì)象,也能自定義第三方bundle中的任何對(duì)象。你將能夠開(kāi)始書寫可復(fù)用、可測(cè)試、松藕合的代碼,其原因就在于服務(wù)容器令編寫良好代碼變得容易。
如果你在讀完本章還想了解更多,請(qǐng)參考 依賴注入組件。
什么是服務(wù) ?
簡(jiǎn)單地說(shuō),服務(wù)(Service)可以是任何執(zhí)行“全局”任務(wù)的對(duì)象。它在計(jì)算機(jī)科學(xué)中是一個(gè)專有名詞,用來(lái)形容一個(gè)“為完成某種使命(比如發(fā)送郵件)”而被創(chuàng)建的對(duì)象。在程序中,當(dāng)你需要某個(gè)服務(wù)所提供的特定功能時(shí),可以隨時(shí)取用該服務(wù)。創(chuàng)建服務(wù)時(shí)你不需要做任何特殊的事:只要寫一個(gè)PHP類,令其完成某個(gè)任務(wù)即可。恭喜,你已經(jīng)創(chuàng)建了一個(gè)服務(wù)!
作為原則,一個(gè)PHP對(duì)象若要成為服務(wù),必須能在程序的全局范圍使用。一個(gè)獨(dú)立的 Mailer
服務(wù)被“全局”用于發(fā)送郵件信息,然而它所傳輸?shù)倪@些 Message
信息對(duì)象并不是服務(wù)。類似的,一個(gè) Product
對(duì)象,并不是服務(wù),但是一個(gè)能夠把產(chǎn)品持久化到數(shù)據(jù)庫(kù)中的對(duì)象,就是服務(wù)。
這說(shuō)明了什么?以“服務(wù)”角度思考問(wèn)題的好處在于,你已經(jīng)開(kāi)始想要把程序中的每一個(gè)功能給分離出來(lái),形成一系列服務(wù)。由于每個(gè)服務(wù)只做一件事,你可以在任何需要的時(shí)候輕松訪問(wèn)到這個(gè)服務(wù)。對(duì)每個(gè)服務(wù)的測(cè)試和配置變得更加容易,因?yàn)樗鼈円呀?jīng)從你程序中的其他功能性中分離出來(lái)。這個(gè)理念被稱為 面向服務(wù)架構(gòu),并非Symfony或PHP專有。把你的程序通過(guò)一組獨(dú)立存在的服務(wù)類進(jìn)行“結(jié)構(gòu)化”,是久經(jīng)考驗(yàn)且廣為人知的面向?qū)ο缶幊讨罴褜?shí)踐。這個(gè)技巧可謂是成為任何一門語(yǔ)言的優(yōu)秀開(kāi)發(fā)者的關(guān)鍵。
什么是服務(wù)容器 ?
服務(wù)容器(Service Container/dependency injection container)就是一個(gè)PHP對(duì)象,它管理服務(wù)(即對(duì)象)的實(shí)例化。
舉例來(lái)說(shuō),假設(shè)你有一個(gè)簡(jiǎn)單的類,用于發(fā)送郵件信息。如果沒(méi)有服務(wù)容器,你必須在需要的時(shí)候手動(dòng)創(chuàng)建對(duì)象。
use Acme\HelloBundle\Mailer; $mailer = new Mailer('sendmail'); $mailer->send('ryan@example.com', ...);
這很簡(jiǎn)單。一個(gè)虛構(gòu)的 Mailer
郵件服務(wù)類,允許你配置郵件的發(fā)送方法(比如 sendmail
,或 smtp
,等等)。但如果你想在其他地方使用郵件服務(wù)怎么辦?你當(dāng)然不希望每次都重新配置 Mailer
對(duì)象。如果你想改變郵件的傳輸方式,把整個(gè)程序中所有的 sendmail
改成 smtp
怎么辦?你不得不找到所有創(chuàng)建了 Mailer
的地方,手動(dòng)去更新。
在容器中創(chuàng)建和配置服務(wù) ?
上面問(wèn)題的最佳答案,是讓服務(wù)容器來(lái)為你創(chuàng)建Mailer對(duì)象。為了讓容器正常工作,你先要教會(huì)它如何創(chuàng)建Mailer服務(wù)。這是通過(guò)配置來(lái)實(shí)現(xiàn)的,配置方式有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]
當(dāng)Symfony初始化時(shí),它要根據(jù)配置信息來(lái)建立服務(wù)容器(默認(rèn)配置文件是 app/config/config.yml
)。被加載的正確配置文件是由 AppKernel::registerContainerConfiguration()
方法指示,該方法加載了一個(gè)“特定環(huán)境”的配置文件(如config_dev.yml是dev開(kāi)發(fā)環(huán)境的,而 config_prod.yml
則是生產(chǎn)環(huán)境的)。
一個(gè) Acme\HelloBundle\Mailer
對(duì)象的實(shí)例已經(jīng)可以通過(guò)服務(wù)容器來(lái)使用了。容器在任何一個(gè)標(biāo)準(zhǔn)的Symfony控制器中可以通過(guò)get()快捷方法直接獲得。
class HelloController extends Controller{ // ... public function sendEmailAction() { // ... $mailer = $this->get('app.mailer'); $mailer->send('ryan@foobar.net', ...); }}
當(dāng)你從容器中請(qǐng)求 app.mailer
服務(wù)時(shí),容器構(gòu)造了該對(duì)象并返回(實(shí)例化之后的)它。這是使用服務(wù)容器的又一個(gè)好處。即,一個(gè)服務(wù)不會(huì)被構(gòu)造(constructed),除非在需要時(shí)。如果你定義了一個(gè)服務(wù),但在請(qǐng)求(request)過(guò)程中從未用到,該服務(wù)不會(huì)被創(chuàng)建。這可節(jié)省內(nèi)存并提高程序運(yùn)行速度。這也意味著在定義大量服務(wù)時(shí),很少會(huì)對(duì)性能有沖擊。從不使用的服務(wù)絕對(duì)不會(huì)被構(gòu)造。
附帶一點(diǎn),Mailer服務(wù)只被創(chuàng)建一次,每次你請(qǐng)求它時(shí)返回的是同一實(shí)例。這可滿足你的大多數(shù)需求(該行為靈活而強(qiáng)大),但是后面你要了解怎樣才能配置一個(gè)擁有多個(gè)實(shí)例的服務(wù),參考 如何定義非共享服務(wù) 一文。
本例中,控制器繼承了Symfony的Controller基類,給了你一個(gè)使用服務(wù)容器的機(jī)會(huì),通過(guò)get()方法就可從容器中找到并取出 app.mailer
服務(wù)。
服務(wù)參數(shù) ?
通過(guò)容器建立新服務(wù)的過(guò)程十分簡(jiǎn)單明了。參數(shù)(Parameter)可以令服務(wù)的定義更加靈活、有序:
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%']
結(jié)果就和之前一樣。區(qū)別在于,你是如何定義的服務(wù)。通過(guò)把 app.mailer.transport
用 %
百分號(hào)給括起來(lái),容器就知道應(yīng)該去找對(duì)應(yīng)這個(gè)名字的參數(shù)。容器自身生成時(shí),會(huì)把每個(gè)參數(shù)的值,還原到服務(wù)定義中。
如果你要把一個(gè)由 @
開(kāi)頭的字符串,在YAML文件中用做參數(shù)值的話(例如一個(gè)非常安全的郵件密碼),你需要添加另一個(gè) @
符號(hào)進(jìn)行轉(zhuǎn)義(這種情況只在YAML格式的配置文件中適用)
# app/config/parameters.ymlparameters: # This will be parsed as string '@securepass' mailer_password: '@@securepass'
配置參數(shù)(parameter)或方法參數(shù)(argument)中的百分號(hào),也必須用另一個(gè)%進(jìn)行轉(zhuǎn)義:
1 | <argument type="string">http://symfony.com/?foo=%%s&bar=%%d</argument> |
參數(shù)的目的,是要把信息傳給服務(wù)。當(dāng)然,不用參數(shù)的話,也不會(huì)有什么問(wèn)題。但使用參數(shù)有以下幾個(gè)好處:
分離并組織所有服務(wù)“選項(xiàng)”到一個(gè)統(tǒng)一的“參數(shù)”鍵下
參數(shù)值可以被用到多個(gè)服務(wù)定義中
在bundle中創(chuàng)建服務(wù)時(shí),使用參數(shù)可以令服務(wù)在全局程序中的定制變得容易
是否使用參數(shù),選擇權(quán)在你。高質(zhì)量第三方bundles,始終使用參數(shù),因?yàn)樗麄円尨嬗谌萜髦械姆?wù)具備更強(qiáng)的“可配置性”。當(dāng)然,你可能并不需要參數(shù)帶來(lái)的靈活性。
數(shù)組參數(shù) ?
參數(shù)可以包含數(shù)組,參見(jiàn) 數(shù)組參數(shù)(Array Parameters)。
引用(注入)服務(wù) ?
至此,原來(lái)的 app.mailer
服務(wù)是簡(jiǎn)單的:它在構(gòu)造器中只有一個(gè)參數(shù),因此很容易配置。你可以預(yù)見(jiàn)到,在你創(chuàng)建一個(gè)需要依賴一個(gè)或多個(gè)容器中的其他服務(wù)時(shí),容器的真正威力開(kāi)始體現(xiàn)出來(lái)。
例如,你有一個(gè)新的服務(wù), NewsletterManager
,它幫助你管理和發(fā)送郵件信息到地址集。 app.mailer
服務(wù)已經(jīng)可以發(fā)郵件了,因此你可以把它用在 NewsletterManager
中來(lái)負(fù)責(zé)信息傳送的部分。這個(gè)類看上去像下面這樣:
// 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; } // ...}
如果不使用服務(wù)容器,你可以在controller中很容易地創(chuàng)建一個(gè)NewsletterManager:
use Acme\HelloBundle\Newsletter\NewsletterManager; // ... public function sendNewsletterAction(){ $mailer = $this->get('app.mailer'); $newsletter = new NewsletterManager($mailer); // ...}
這樣去實(shí)現(xiàn)是可以的,但當(dāng)你以后要對(duì) NewsletterManager
類增加第二或第三個(gè)構(gòu)造器參數(shù)時(shí)怎么辦?如果你決定重構(gòu)代碼并且重命名這個(gè)類時(shí)怎么辦?這兩種情況,你都需要找到每一個(gè) NewsletterManager
類被實(shí)例化的地方,然后手動(dòng)個(gè)性它。毫無(wú)疑問(wèn),服務(wù)容器提供了一個(gè)更加吸引人的處理方式:
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
語(yǔ)法,告訴容器去尋找一個(gè)名為 app.mailer
的服務(wù),然后把這個(gè)對(duì)象傳給 NewsletterManager
的構(gòu)造器參數(shù)。本例中,指定的 app.mailer
服務(wù)是確實(shí)存在的。如果它不存在,則異常會(huì)拋出。不過(guò)你可以標(biāo)記依賴可選 – 這個(gè)話題將在下一小節(jié)中討論。
(對(duì)服務(wù)的)引用是個(gè)強(qiáng)大的工具,它允許你創(chuàng)建獨(dú)立的服務(wù),卻擁有準(zhǔn)確定義的依賴關(guān)系。在這個(gè)例子中, app.newsletter_manager
服務(wù)為了實(shí)現(xiàn)功能,需要依賴 app.mailer
服務(wù)。當(dāng)你在服務(wù)容器中定義了這個(gè)依賴時(shí),容器托管了對(duì)這個(gè)類進(jìn)行實(shí)例化的全部工作。
可選的依賴:Setter注入 ?
將依賴對(duì)象注入到構(gòu)造器中是一個(gè)辦法,這可確保依賴可以利用(否則構(gòu)造函數(shù)無(wú)法執(zhí)行)。但是對(duì)一個(gè)類來(lái)說(shuō),如果它有一個(gè)可選的依賴,那么“setter注入”是一個(gè)更好的方案。這意味著用一個(gè)類方法來(lái)注入依賴,而不是構(gòu)造器。這個(gè)類看上去可能是這樣的:
namespace AppBundle\Newsletter; use AppBundle\Mailer; class NewsletterManager{ protected $mailer; public function setMailer(Mailer $mailer) { $this->mailer = $mailer; } // ...}
服務(wù)定義需要對(duì)setter注入做出相應(yīng)調(diào)整:
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']]
本節(jié)所實(shí)現(xiàn)的過(guò)程被稱為“構(gòu)造器注入”和“setter注入”。此外Symfony的容器體系還支持屬性注入(property injection)。