服務容器
現代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)。