服務容器


現代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)。