yii依賴注入和依賴注入容器

依賴注入和依賴注入容器

爲了下降代碼耦合程度,提升項目的可維護性,Yii採用多許多當下最流行又相對成熟的設計模式,包括了依賴注入(Denpdency Injection, DI)和服務定位器(Service Locator)兩種模式。 關於依賴注入與服務定位器, Inversion of Control Containers and the Dependency Injection pattern 給出了很詳細的講解,這裏結合Web應用和Yii具體實現進行探討,以加深印象和理解。 這些設計模式對於提升自身的設計水平頗有幫助,這也是咱們學習Yii的一個重要出發點。php

有關概念

在瞭解Service Locator 和 Dependency Injection 以前,有必要先來了解一些高大上的概念。 別擔憂,你只須要有個大體瞭解就OK了,若是展開來講,這些東西能夠單獨寫個研究報告:html

依賴倒置原則(Dependence Inversion Principle, DIP)
DIP是一種軟件設計的指導思想。傳統軟件設計中,上層代碼依賴於下層代碼,當下層出現變更時, 上層代碼也要相應變化,維護成本較高。而DIP的核心思想是上層定義接口,下層實現這個接口, 從而使得下層依賴於上層,下降耦合度,提升整個系統的彈性。這是一種經實踐證實的有效策略。
控制反轉(Inversion of Control, IoC)
IoC就是DIP的一種具體思路,DIP只是一種理念、思想,而IoC是一種實現DIP的方法。 IoC的核心是將類(上層)所依賴的單元(下層)的實例化過程交由第三方來實現。 一個簡單的特徵,就是類中不對所依賴的單元有諸如 $component = new yii\component\SomeClass() 的實例化語句。
依賴注入(Dependence Injection, DI)
DI是IoC的一種設計模式,是一種套路,按照DI的套路,就能夠實現IoC,就能符合DIP原則。 DI的核心是把類所依賴的單元的實例化過程,放到類的外面去實現。
控制反轉容器(IoC Container)
當項目比較大時,依賴關係可能會很複雜。 而IoC Container提供了動態地建立、注入依賴單元,映射依賴關係等功能,減小了許多代碼量。 Yii 設計了一個 yii\di\Container 來實現了 DI Container。
服務定位器(Service Locator)
Service Locator是IoC的另外一種實現方式, 其核心是把全部可能用到的依賴單元交由Service Locator進行實例化和建立、配置, 把類對依賴單元的依賴,轉換成類對Service Locator的依賴。 DI 與 Service Locator並不衝突,二者能夠結合使用。 目前,Yii2.0把這DI和Service Locator這兩個東西結合起來使用,或者說經過DI容器,實現了Service Locator。

是否是雲裏霧裏的?沒錯,所謂「高大上」的玩意每每就是這樣,看着很炫,很唬人。 賣護膚品的難道會跟你說其實皮膚表層是角質層,不具吸取功能麼?這玩意又不考試,大體意會下就OK了。 萬一哪天要在妹子面前要裝一把範兒的時候,張口也能來這麼幾個「高大上」就好了。 但具體的內涵,咱們仍是要要經過下面的學習來加深理解,畢竟要把「高大上」的東西用好,發揮出做用來。mysql

依賴注入

首先講講DI。在Web應用中,很常見的是使用各類第三方Web Service實現特定的功能,好比發送郵件、推送微博等。 假設要實現當訪客在博客上發表評論後,向博文的做者發送Email的功能,一般代碼會是這樣:sql

 

// 爲郵件服務定義抽象層
interface EmailSenderInterface
{
    public function send(...);
}

// 定義Gmail郵件服務
class GmailSender implements EmailSenderInterface
{
    ...

    // 實現發送郵件的類方法
    public function send(...)
    {
        ...
    }
}

// 定義評論類
class Comment extend yii\db\ActiveRecord
{
    // 用於引用發送郵件的庫
    private $_eMailSender;

    // 初始化時,實例化 $_eMailSender
    public function init()
    {
        ...
        // 這裏假設使用Gmail的郵件服務
       $this->_eMailSender = GmailSender::getInstance();
        ...
    }

    // 當有新的評價,即 save() 方法被調用以後中,會觸發如下方法
    public function afterInsert()
    {
        ...
        //
        $this->_eMailSender->send(...);
        ...
    }
}

 

 

上面的代碼只是一個示意,大體是這麼個流程。數據庫

那麼這種常見的設計方法有什麼問題呢? 主要問題在於 Comment 對於 GmailSender 的依賴(對於EmailSenderInterface的依賴不可避免), 假設有一天忽然不使用Gmail提供的服務了,改用Yahoo或自建的郵件服務了。 那麼,你不得不修改 Comment::init() 裏面對 $_eMailSender 的實例化語句:swift

$this->_eMailSender = MyEmailSender::getInstance(); 

這個問題的本質在於,你今天寫完這個Comment,只能用於這個項目,哪天你開發別的項目要實現相似的功能, 你還要針對新項目使用的郵件服務修改這個Comment。代碼的複用性不高呀。 有什麼辦法能夠不改變Comment的代碼,就能擴展成對各類郵件服務都支持麼? 換句話說,有辦法將Comment和GmailSender解耦麼?有辦法提升Comment的普適性、複用性麼?設計模式

依賴注入就是爲了解決這個問題而生的,固然,DI也不是惟一解決問題的辦法,畢竟條條大路通羅馬。 Service Locator也是能夠實現解耦的。數組

在Yii中使用DI解耦,有2種注入方式:構造函數注入、屬性注入。緩存

構造函數注入

構造函數注入經過構造函數的形參,爲類內部的抽象單元提供實例化。 具體的構造函數調用代碼,由外部代碼決定。具體例子以下:數據結構

// 這是構造函數注入的例子
class Comment extend yii\db\ActiveRecord
{
    // 用於引用發送郵件的庫
    private $_eMailSender;

    // 構造函數注入
    public function __construct($emailSender)
    {
        ...
        $this->_eMailSender = $emailSender;
        ...
    }

    // 當有新的評價,即 save() 方法被調用以後中,會觸發如下方法
    public function afterInsert()
    {
        ...
        //
        $this->_eMailSender->send(...);
        ...
    }
}

// 實例化兩種不一樣的郵件服務,固然,他們都實現了EmailSenderInterface
sender1 = new GmailSender();
sender2 = new MyEmailSender();

// 用構造函數將GmailSender注入
$comment1 = new Comment(sender1);
// 使用Gmail發送郵件
$comment1.save();

// 用構造函數將MyEmailSender注入
$comment2 = new Comment(sender2);
// 使用MyEmailSender發送郵件
$comment2.save();

 

 

 

上面的代碼對比原來的代碼,解決了Comment類對於GmailSender等具體類的依賴,經過構造函數,將相應的實現了 EmailSenderInterface接口的類實例傳入Comment類中,使得Comment類能夠適用於不一樣的郵件服務。 今後之後,不管要使用何何種郵件服務,只需寫出新的EmailSenderInterface實現便可, Comment類的代碼再也不須要做任何更改,多爽的一件事,擴展起來、測試起來都省心省力。

屬性注入

與構造函數注入相似,屬性注入經過setter或public成員變量,將所依賴的單元注入到類內部。 具體的屬性寫入,由外部代碼決定。具體例子以下:

// 這是屬性注入的例子
class Comment extend yii\db\ActiveRecord
{
    // 用於引用發送郵件的庫
    private $_eMailSender;

    // 定義了一個 setter()
    public function setEmailSender($value)
    {
        $this->_eMailSender = $value;
    }

    // 當有新的評價,即 save() 方法被調用以後中,會觸發如下方法
    public function afterInsert()
    {
        ...
        //
        $this->_eMailSender->send(...);
        ...
    }
}

// 實例化兩種不一樣的郵件服務,固然,他們都實現了EmailSenderInterface
sender1 = new GmailSender();
sender2 = new MyEmailSender();

$comment1 = new Comment;
// 使用屬性注入
$comment1->eMailSender = sender1;
// 使用Gmail發送郵件
$comment1.save();

$comment2 = new Comment;
// 使用屬性注入
$comment2->eMailSender = sender2;
// 使用MyEmailSender發送郵件
$comment2.save();

上面的Comment若是將 private $_eMailSender 改爲 public $eMailSender 並刪除 setter函數, 也是能夠達到一樣的效果的。

與構造函數注入相似,屬性注入也是將Comment類所依賴的EmailSenderInterface的實例化過程放在Comment類之外。 這就是依賴注入的本質所在。爲何稱爲注入?從外面把東西打進去,就是注入。什麼是外,什麼是內? 要解除依賴的類內部就是內,實例化所依賴單元的地方就是外。

DI容器

從上面DI兩種注入方式來看,依賴單元的實例化代碼是一個重複、繁瑣的過程。 能夠想像,一個Web應用的某一組件會依賴於若干單元,這些單元又有可能依賴於更低層級的單元, 從而造成依賴嵌套的情形。那麼,這些依賴單元的實例化、注入過程的代碼可能會比較長,先後關係也須要特別地注意, 必須將被依賴的放在須要注入依賴的前面進行實例化。 這實在是一件既沒技術含量,又吃力不出成果的工做,這類工做是高智商(懶)人羣的天敵, 咱們是不會去作這麼無聊的事情的。

就像極其不想洗衣服的人發明了洗衣機(我臆想的,未考證)同樣,爲了解決這一無聊的問題,DI容器被設計出來了。 Yii的DI容器是 yii\di\Container ,這個容器繼承了發明人的高智商, 他知道如何對對象及對象的全部依賴,和這些依賴的依賴,進行實例化和配置。

DI容器中的內容

DI容器中實例的表示

容器顧名思義是用來裝東西的,DI容器裏面的東西是什麼呢?Yii使用 yii\di\Instance 來表示容器中的東西。 固然Yii中還將這個類用於Service Locator,這個在講Service Locator時再具體談談。

yii\di\Instance 本質上是DI容器中對於某一個類實例的引用,它的代碼看起來並不複雜:

 

class Instance
{
    // 僅有的屬性,用於保存類名、接口名或者別名
    public $id;

    // 構造函數,僅將傳入的ID賦值給 $id 屬性
    protected function __construct($id)
    {
    }

    // 靜態方法建立一個Instance實例
    public static function of($id)
    {
        return new static($id);
    }

    // 靜態方法,用於將引用解析成實際的對象,並確保這個對象的類型
    public static function ensure($reference, $type = null, $container = null)
    {
    }

    // 獲取這個實例所引用的實際對象,事實上它調用的是
    // yii\di\Container::get()來獲取實際對象
    public function get($container = null)
    {
    }
}

  

對於 yii\di\Instance ,咱們要了解:

  • 表示的是容器中的內容,表明的是對於實際對象的引用。
  • DI容器能夠經過他獲取所引用的實際對象。
  • 類僅有的一個屬性 id 通常表示的是實例的類型。

DI容器的數據結構

在DI容器中,維護了5個數組,這是DI容器功能實現的基礎:

 

// 用於保存單例Singleton對象,以對象類型爲鍵
private $_singletons = [];

// 用於保存依賴的定義,以對象類型爲鍵
private $_definitions = [];

// 用於保存構造函數的參數,以對象類型爲鍵
private $_params = [];

// 用於緩存ReflectionClass對象,以類名或接口名爲鍵
private $_reflections = [];

// 用於緩存依賴信息,以類名或接口名爲鍵
private $_dependencies = [];

  

DI容器的5個數組內容和做用如 DI容器5個數組示意圖 所示。

 

註冊依賴

使用DI容器,首先要告訴容器,類型及類型之間的依賴關係,聲明一這關係的過程稱爲註冊依賴。 使用 yii\di\Container::set()yii\di\Container::setSinglton() 能夠註冊依賴。

DI容器是怎麼管理依賴的呢?要先看看 yii\di\Container::set()yii\Container::setSinglton()

 

public function set($class, $definition = [], array $params = [])
{
    // 規範化 $definition 並寫入 $_definitions[$class]
    $this->_definitions[$class] = $this->normalizeDefinition($class,
        $definition);

    // 將構造函數參數寫入 $_params[$class]
    $this->_params[$class] = $params;

    // 刪除$_singletons[$class]
    unset($this->_singletons[$class]);
    return $this;
}

public function setSingleton($class, $definition = [], array $params = [])
{
    // 規範化 $definition 並寫入 $_definitions[$class]
    $this->_definitions[$class] = $this->normalizeDefinition($class,
        $definition);

    // 將構造函數參數寫入 $_params[$class]
    $this->_params[$class] = $params;

    // 將$_singleton[$class]置爲null,表示還未實例化
    $this->_singletons[$class] = null;
    return $this;
}

  

這兩個函數功能相似沒有太大區別,只是 set() 用於在每次請求時構造新的實例返回, 而 setSingleton() 只維護一個單例,每次請求時都返回同一對象。

表如今數據結構上,就是 set() 在註冊依賴時,會把使用 setSingleton() 註冊的依賴刪除。 不然,在解析依賴時,你讓Yii到底是依賴續絃仍是原配?所以,在DI容器中,依賴關係的定義是惟一的。 後定義的同名依賴,會覆蓋前面定義好的依賴。

從形參來看,這兩個函數的 $class 參數接受一個類名、接口名或一個別名,做爲依賴的名稱。 $definition 表示依賴的定義,能夠是一個類名、配置數組或一個PHP callable。

這兩個函數,本質上只是將依賴的有關信息寫入到容器的相應數組中去。 在 set()setSingleton() 中,首先調用 yii\di\Container::normalizeDefinition() 對依賴的定義進行規範化處理,其代碼以下:

protected function normalizeDefinition($class, $definition)
 {
     // $definition 是空的轉換成 ['class' => $class] 形式
     if (empty($definition)) {
         return ['class' => $class];

     // $definition 是字符串,轉換成 ['class' => $definition] 形式
     } elseif (is_string($definition)) {
         return ['class' => $definition];

     // $definition 是PHP callable 或對象,則直接將其做爲依賴的定義
     } elseif (is_callable($definition, true) || is_object($definition)) {
         return $definition;

     // $definition 是數組則確保該數組定義了 class 元素
     } elseif (is_array($definition)) {
         if (!isset($definition['class'])) {
             if (strpos($class, '\\') !== false) {
                 $definition['class'] = $class;
             } else {
                 throw new InvalidConfigException(
                     "A class definition requires a \"class\" member.");
             }
         }
         return $definition;
     // 這也不是,那也不是,那就拋出異常算了
     } else {
         throw new InvalidConfigException(
             "Unsupported definition type for \"$class\": "
             . gettype($definition));
     }
 }

  

規範化處理的流程以下:

  • 若是 $definition 是空的,直接返回數組 ['class' => $class]
  • 若是 $definition 是字符串,那麼認爲這個字符串就是所依賴的類名、接口名或別名, 那麼直接返回數組 ['class' => $definition]
  • 若是 $definition 是一個PHP callable,或是一個對象,那麼直接返回該 $definition
  • 若是 $definition 是一個數組,那麼其應當是一個包含了元素 $definition['class'] 的配置數組。 若是該數組未定義 $definition['class'] 那麼,將傳入的 $class 做爲該元素的值,最後返回該數組。
  • 上一步中,若是 definition['class'] 未定義,而 $class 不是一個有效的類名,那麼拋出異常。
  • 若是 $definition 不屬於上述的各類狀況,也拋出異常。

總之,對於 $_definitions 數組中的元素,它要麼是一個包含了」class」 元素的數組,要麼是一個PHP callable, 再要麼就是一個具體對象。這就是規範化後的最終結果。

在調用 normalizeDefinition() 對依賴的定義進行規範化處理後, set()setSingleton() 以傳入的 $class 爲鍵,將定義保存進 $_definition[] 中, 將傳入的 $param 保存進 $_params[] 中。

對於 set() 而言,還要刪除 $_singleton[] 中的同名依賴。 對於 setSingleton() 而言,則要將 $_singleton[] 中的同名依賴設爲 null , 表示定義了一個Singleton,可是並未實現化。

這麼講可能很差理解,舉幾個具體的依賴定義及相應數組的內容變化爲例,以加深理解:

 

$container = new \yii\di\Container;

// 直接以類名註冊一個依賴,雖然這麼作沒什麼意義。
// $_definition['yii\db\Connection'] = 'yii\db\Connetcion'
$container->set('yii\db\Connection');

// 註冊一個接口,當一個類依賴於該接口時,定義中的類會自動被實例化,並供
// 有依賴須要的類使用。
// $_definition['yii\mail\MailInterface', 'yii\swiftmailer\Mailer']
$container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer');

// 註冊一個別名,當調用$container->get('foo')時,能夠獲得一個
// yii\db\Connection 實例。
// $_definition['foo', 'yii\db\Connection']
$container->set('foo', 'yii\db\Connection');

// 用一個配置數組來註冊一個類,須要這個類的實例時,這個配置數組會發生做用。
// $_definition['yii\db\Connection'] = [...]
$container->set('yii\db\Connection', [
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

// 用一個配置數組來註冊一個別名,因爲別名的類型不詳,所以配置數組中須要
// 有 class 元素。
// $_definition['db'] = [...]
$container->set('db', [
    'class' => 'yii\db\Connection',
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

// 用一個PHP callable來註冊一個別名,每次引用這個別名時,這個callable都會被調用。
// $_definition['db'] = function(...){...}
$container->set('db', function ($container, $params, $config) {
    return new \yii\db\Connection($config);
});

// 用一個對象來註冊一個別名,每次引用這個別名時,這個對象都會被引用。
// $_definition['pageCache'] = anInstanceOfFileCache
$container->set('pageCache', new FileCache);

 

setSingleton() 對於 $_definition$_params 數組產生的影響與 set() 是同樣同樣的。 不一樣之處在於,使用 set() 會unset $_singltons 中的對應元素,Yii認爲既然你都調用 set() 了,說明你但願這個依賴再也不是單例了。 而 setSingleton() 相比較於 set() ,會額外地將 $_singletons[$class] 置爲 null 。 以此來表示這個依賴已經定義了一個單例,可是還沒有實例化。

set()setSingleton() 來看, 可能還不容易理解DI容器,好比咱們說DI容器中維護了5個數組,可是依賴註冊過程只涉及到其中3個。 剩下的 $_reflections$_dependencies 是在解析依賴的過程當中完成構建的。

從DI容器的5個數組來看也好,從容器定義了 set()setSingleton() 兩個定義依賴的方法來看也好, 不難猜出DI容器中裝了兩類實例,一種是單例,每次向容器索取單例類型的實例時,獲得的都是同一個實例; 另外一類是普通實例,每次向容器索要普通類型的實例時,容器會根據依賴信息建立一個新的實例給你。

單例類型主要用於節省構建實例的時間、節省保存實例的內存、共享數據等。而普通類型主要用於避免數據衝突。

對象的實例化

對象的實例化過程要比依賴的定義過程複雜得多。畢竟依賴的定義只是往特定的數據結構 $_singletons $_definitions$_params 3個數組寫入有關的信息。 稍複雜的東西也就是定義的規範化處理了。其它真沒什麼複雜的。像你這麼聰明的,確定以爲這太沒挑戰了。

而對象的實例化過程要相對複雜,這一過程會涉及到複雜依賴關係的解析、涉及依賴單元的實例化等過程。 且讓咱們抽絲剝繭地進行分析。

解析依賴信息

容器在獲取實例以前,必須解析依賴信息。 這一過程會涉及到DI容器中還沒有提到的另外2個數組 $_reflections$_dependenciesyii\di\Container::getDependencies() 會向這2個數組寫入信息,而這個函數又會在建立實例時,由 yii\di\Container::build() 所調用。 如它的名字所示意的, yii\di\Container::getDependencies() 方法用於獲取依賴信息,讓咱們先來看看這個函數的代碼

protected function getDependencies($class)
{
    // 若是已經緩存了其依賴信息,直接返回緩存中的依賴信息
    if (isset($this->_reflections[$class])) {
        return [$this->_reflections[$class], $this->_dependencies[$class]];
    }

    $dependencies = [];

    // 使用PHP5 的反射機制來獲取類的有關信息,主要就是爲了獲取依賴信息
    $reflection = new ReflectionClass($class);

    // 經過類的構建函數的參數來了解這個類依賴於哪些單元
    $constructor = $reflection->getConstructor();
    if ($constructor !== null) {
        foreach ($constructor->getParameters() as $param) {
            if ($param->isDefaultValueAvailable()) {

                // 構造函數若是有默認值,將默認值做爲依賴。即然是默認值了,
                // 就確定是簡單類型了。
                $dependencies[] = $param->getDefaultValue();
            } else {
                $c = $param->getClass();

                // 構造函數沒有默認值,則爲其建立一個引用。
                // 就是前面提到的 Instance 類型。
                $dependencies[] = Instance::of($c === null ? null :
                    $c->getName());
            }
        }
    }

    // 將 ReflectionClass 對象緩存起來
    $this->_reflections[$class] = $reflection;

    // 將依賴信息緩存起來
    $this->_dependencies[$class] = $dependencies;

    return [$reflection, $dependencies];
}

  

前面講了 $_reflections 數組用於緩存 ReflectionClass 實例,$_dependencies 數組用於緩存依賴信息。 這個 yii\di\Container::getDependencies() 方法實質上就是經過PHP5 的反射機制, 經過類的構造函數的參數分析他所依賴的單元。而後通通緩存起來備用。

爲何是經過構造函數來分析其依賴的單元呢? 由於這個DI容器設計出來的目的就是爲了實例化對象及該對象所依賴的一切單元。 也就是說,DI容器必然構造類的實例,必然調用構造函數,那麼必然爲構造函數準備並傳入相應的依賴單元。 這也是咱們開頭講到的構造函數依賴注入的後續延伸應用。

可能有的讀者會問,那不是還有setter注入麼,爲何不用解析setter注入函數的依賴呢? 這是由於要獲取實例不必定須要爲某屬性注入外部依賴單元,可是卻必須爲其構造函數的參數準備依賴的外部單元。 固然,有時候一個用於注入的屬性必須在實例化時指定依賴單元。 這個時候,必然在其構造函數中有一個用於接收外部依賴單元的形式參數。 使用DI容器的目的是自動實例化,只是實例化而已,就意味着只須要調用構造函數。 至於setter注入能夠在實例化後操做嘛。

另外一個與解析依賴信息相關的方法就是 yii\di\Container::resolveDependencies() 。 它也是關乎 $_reflections$_dependencies 數組的,它使用 yii\di\Container::getDependencies() 在這兩個數組中寫入的緩存信息,做進一步具體化的處理。從函數名來看,他的名字代表是用於解析依賴信息的。 下面咱們來看看它的代碼:

protected function resolveDependencies($dependencies, $reflection = null)
{
    foreach ($dependencies as $index => $dependency) {

        // 前面getDependencies() 函數往 $_dependencies[] 中
        // 寫入的是一個 Instance 數組
        if ($dependency instanceof Instance) {
            if ($dependency->id !== null) {

                // 向容器索要所依賴的實例,遞歸調用 yii\di\Container::get()
                $dependencies[$index] = $this->get($dependency->id);
            } elseif ($reflection !== null) {
                $name = $reflection->getConstructor()
                    ->getParameters()[$index]->getName();
                $class = $reflection->getName();
                throw new InvalidConfigException(
                "Missing required parameter \"$name\" when instantiating \"$class\".");
            }
        }
    }
    return $dependencies;
}

 

 

上面的代碼中能夠看到, yii\di\Container::resolveDependencies() 做用在於處理依賴信息, 將依賴信息中保存的Istance實例所引用的類或接口進行實例化。

綜合上面提到的 yii\di\Container::getDependencies()yii\di\Container::resolveDependencies() 兩個方法,咱們能夠了解到:

  • $_reflections 以類(接口、別名)名爲鍵, 緩存了這個類(接口、別名)的ReflcetionClass。一經緩存,便不會再更改。
  • $_dependencies 以類(接口、別名)名爲鍵,緩存了這個類(接口、別名)的依賴信息。
  • 這兩個緩存數組都是在 yii\di\Container::getDependencies() 中完成。這個函數只是簡單地向數組寫入數據。
  • 通過 yii\di\Container::resolveDependencies() 處理,DI容器會將依賴信息轉換成實例。 這個實例化的過程當中,是向容器索要實例。也就是說,有可能會引發遞歸。

實例的建立

解析完依賴信息,就萬事俱備了,那麼東風也該來了。實例的建立,祕密就在 yii\di\Container::build() 函數中

protected function build($class, $params, $config)
{
    // 調用上面提到的getDependencies來獲取並緩存依賴信息,留意這裏 list 的用法
    list ($reflection, $dependencies) = $this->getDependencies($class);

    // 用傳入的 $params 的內容補充、覆蓋到依賴信息中
    foreach ($params as $index => $param) {
        $dependencies[$index] = $param;
    }

    // 這個語句是兩個條件:
    // 一是要建立的類是一個 yii\base\Object 類,
    // 留意咱們在《Yii基礎》一篇中講到,這個類對於構造函數的參數是有必定要求的。
    // 二是依賴信息不爲空,也就是要麼已經註冊過依賴,
    // 要麼爲build() 傳入構造函數參數。
    if (!empty($dependencies) && is_a($class, 'yii\base\Object', true)) {
        // 按照 Object 類的要求,構造函數的最後一個參數爲 $config 數組
        $dependencies[count($dependencies) - 1] = $config;

        // 解析依賴信息,若是有依賴單元須要提早實例化,會在這一步完成
        $dependencies = $this->resolveDependencies($dependencies, $reflection);

        // 實例化這個對象
        return $reflection->newInstanceArgs($dependencies);
    } else {
        // 會出現異常的狀況有二:
        // 一是依賴信息爲空,也就是你前面又沒註冊過,
        // 如今又不提供構造函數參數,你讓Yii怎麼實例化?
        // 二是要構造的類,根本就不是 Object 類。
        $dependencies = $this->resolveDependencies($dependencies, $reflection);
        $object = $reflection->newInstanceArgs($dependencies);
        foreach ($config as $name => $value) {
            $object->$name = $value;
        }
        return $object;
    }
}

 

從這個 yii\di\Container::build() 來看:

  • DI容器只支持 yii\base\Object 類。也就是說,你只能向DI容器索要 yii\base\Object 及其子類。 再換句話說,若是你想你的類能夠放在DI容器裏,那麼必須繼承自 yii\base\Object 類。 但Yii中幾乎開發者在開發過程當中須要用到的類,都是繼承自這個類。 一個例外就是上面提到的 yii\di\Instance 類。但這個類是供Yii框架本身使用的,開發者無需操做這個類。
  • 遞歸獲取依賴單元的依賴在於 dependencies = $this->resolveDependencies($dependencies, $reflection) 中。
  • getDependencies()resolveDependencies()build() 所用。 也就是說,只有在建立實例的過程當中,DI容器纔會去解析依賴信息、緩存依賴信息。

容器內容實例化的大體過程

與註冊依賴時使用 set()setSingleton() 對應,獲取依賴實例化對象使用 yii\di\Container::get() ,其代碼以下:

public function get($class, $params = [], $config = [])
{
    // 已經有一個完成實例化的單例,直接引用這個單例
    if (isset($this->_singletons[$class])) {
        return $this->_singletons[$class];

    // 是個還沒有註冊過的依賴,說明它不依賴其餘單元,或者依賴信息不用定義,
    // 則根據傳入的參數建立一個實例
    } elseif (!isset($this->_definitions[$class])) {
        return $this->build($class, $params, $config);
    }

    // 注意這裏建立了 $_definitions[$class] 數組的副本
    $definition = $this->_definitions[$class];

    // 依賴的定義是個 PHP callable,調用之
    if (is_callable($definition, true)) {
        $params = $this->resolveDependencies($this->mergeParams($class,
            $params));
        $object = call_user_func($definition, $this, $params, $config);

    // 依賴的定義是個數組,合併相關的配置和參數,建立之
    } elseif (is_array($definition)) {
        $concrete = $definition['class'];
        unset($definition['class']);

        // 合併將依賴定義中配置數組和參數數組與傳入的配置數組和參數數組合並
        $config = array_merge($definition, $config);
        $params = $this->mergeParams($class, $params);

        if ($concrete === $class) {
            // 這是遞歸終止的重要條件
            $object = $this->build($class, $params, $config);
        } else {
            // 這裏實現了遞歸解析
            $object = $this->get($concrete, $params, $config);
        }

    // 依賴的定義是個對象則應當保存爲單例
    } elseif (is_object($definition)) {
        return $this->_singletons[$class] = $definition;
    } else {
        throw new InvalidConfigException(
            "Unexpected object definition type: " . gettype($definition));
    }

    // 依賴的定義已經定義爲單例的,應當實例化該對象
    if (array_key_exists($class, $this->_singletons)) {
        $this->_singletons[$class] = $object;
    }

    return $object;
}

get() 用於返回一個對象或一個別名所表明的對象。能夠是已經註冊好依賴的,也能夠是沒有註冊過依賴的。 不管是哪一種狀況,Yii均會自動解析將要獲取的對象對外部的依賴。

get() 接受3個參數:

  • $class 表示將要建立或者獲取的對象。能夠是一個類名、接口名、別名。
  • $params 是一個用於這個要建立的對象的構造函數的參數,其參數順序要與構造函數的定義一致。 一般用於未定義的依賴。
  • $config 是一個配置數組,用於配置獲取的對象。一般用於未定義的依賴,或覆蓋原來依賴中定義好的配置。

get() 解析依賴獲取對象是一個自動遞歸的過程,也就是說,當要獲取的對象依賴於其餘對象時, Yii會自動獲取這些對象及其所依賴的下層對象的實例。 同時,即便對於未定義的依賴,DI容器經過PHP的Reflection API,也能夠自動解析出當前對象的依賴來。

get() 不直接實例化對象,也不直接解析依賴信息。而是經過 build() 來實例化對象和解析依賴。

get() 會根據依賴定義,遞歸調用自身去獲取依賴單元。 所以,在整個實例化過程當中,一共有兩個地方會產生遞歸:一是 get() , 二是 build() 中的 resolveDependencies()

DI容器解析依賴實例化對象過程大致上是這麼一個流程:

  • 以傳入的 $class 看看容器中是否已經有實例化好的單例,若有,直接返回這一單例。
  • 若是這個 $class 根本就未定義依賴,則調用 build() 建立之。具體建立過程等下再說。
  • 對於已經定義了這個依賴,若是定義爲PHP callable,則解析依賴關係,並調用這個PHP callable。 具體依賴關係解析過程等下再說。
  • 若是依賴的定義是一個數組,首先取得定義中對於這個依賴的 class 的定義。 而後將定義中定義好的參數數組和配置數組與傳入的參數數組和配置數組進行合併, 並判斷是否達到終止遞歸的條件。從而選擇繼續遞歸解析依賴單元,或者直接建立依賴單元。

get() 的代碼能夠看出:

  • 對於已經實例化的單例,使用 get() 時只能返回已經實例化好的實例, $params 參數和 $config 參數失去做用。這點要注意,Yii不會提示你,所給出的參數不會發生做用的。 有的時候發現明明已經給定配置數組了,怎麼配置不起做用呀?就要考慮是否是由於這個緣由了。
  • 對於定義爲數組的依賴,在合併配置數組和構造函數參數數組過程當中, 定義中定義好的兩個數組會被傳入的 $config$params 的同名元素所覆蓋, 這就提供了獲取不一樣實例的可能。
  • 在定義依賴時,不管是使用 set() 仍是使用 setSingleton() 只要依賴定義爲特定對象或特定實例的, Yii均將其視爲單例。在獲取時,也將返回這一單例。

實例分析

爲了加深理解,咱們以官方文檔上的例子來講明DI容器解析依賴的過程。假設有如下代碼:

 

 

namespace app\models;

use yii\base\Object;
use yii\db\Connection;

// 定義接口
interface UserFinderInterface
{
    function findUser();
}

// 定義類,實現接口
class UserFinder extends Object implements UserFinderInterface
{
    public $db;

    // 從構造函數看,這個類依賴於 Connection
    public function __construct(Connection $db, $config = [])
    {
        $this->db = $db;
        parent::__construct($config);
    }

    public function findUser()
    {
    }
}

class UserLister extends Object
{
    public $finder;

    // 從構造函數看,這個類依賴於 UserFinderInterface接口
    public function __construct(UserFinderInterface $finder, $config = [])
    {
        $this->finder = $finder;
        parent::__construct($config);
    }
}

從依賴關係看,這裏的 UserLister 類依賴於接口 UserFinderInterface , 而接口有一個實現就是 UserFinder 類,但這類又依賴於 Connection

那麼,按照通常常規的做法,要實例化一個 UserLister 一般這麼作:

$db = new \yii\db\Connection(['dsn' => '...']);
$finder = new UserFinder($db);
$lister = new UserLister($finder);

  

就是逆着依賴關係,從最底層的 Connection 開始實例化,接着是 UserFinder 最後是 UserLister 。 在寫代碼的時候,這個先後順序是不能亂的。並且,須要用到的單元,你要本身一個一個提早準備好。 對於本身寫的可能還比較清楚,對於其餘團隊成員寫的,你還要看他的類到底是依賴了哪些,並一一實例化。 這種狀況,若是是個別的、少許的還能夠接受,若是有個10-20個的,那就麻煩了。 估計光實例化的代碼,就能夠寫滿一屏幕了。

並且,若是是團隊開發,有些單元應當是共用的,如郵件投遞服務。 不能說你寫個模塊,要用到郵件服務了,就本身實例化一個郵件服務吧?那樣豈不是有N模塊就有N個郵件服務了? 最好的方式是使郵件服務成爲一個單例,這樣任何模塊在須要郵件服務時,使用的實際上是同一個實例。 用傳統的這種實例化對象的方法來實現的話,就沒那麼直接了。

那麼改爲DI容器的話,應該是怎麼樣呢?他是這樣的:

use yii\di\Container;

// 建立一個DI容器
$container = new Container;

// 爲Connection指定一個數組做爲依賴,當須要Connection的實例時,
// 使用這個數組進行建立
$container->set('yii\db\Connection', [
    'dsn' => '...',
]);

// 在須要使用接口 UserFinderInterface 時,採用UserFinder類實現
$container->set('app\models\UserFinderInterface', [
    'class' => 'app\models\UserFinder',
]);

// 爲UserLister定義一個別名
$container->set('userLister', 'app\models\UserLister');

// 獲取這個UserList的實例
$lister = $container->get('userLister');

採用DI容器的辦法,首先各 set() 語句沒有先後關係的要求, set() 只是寫入特定的數據結構, 並未涉及具體依賴關係的解析。因此,先後關係不重要,先定義什麼依賴,後定義什麼依賴沒有關係。

其次,上面根本沒有在DI容器中定義 UserFinder 對於 Connection 的依賴。 可是DI容器經過對 UserFinder 構造函數的分析,能瞭解到這個類會對 Connection 依賴。這個過程是自動的。

最後,上面只有一個 get() 看起來好像根本沒有實例化其餘如 Connection 單元同樣,但事實上,DI容器已經安排好了一切。 在獲取 userLister 以前, ConnectionUserFinder 都會被自動實例化。 其中, Connection 是根據依賴定義中的配置數組進行實例化的。

通過上面的幾個 set() 語句以後,DI容器的 $_params 數組是空的, $_singletons 數組也是空的。 $_definintions 數組卻有了新的內容:

$_definitions = [
    'yii\db\Connection' => [
        'class' => 'yii\db\Connection',    // 注意這裏
        'dsn' => ...
    ],
    'app\models\UserFinderInterface' => ['class' => 'app\models\UserFinder'],
    'userLister' => ['class' => 'app\models\UserLister']    // 注意這裏
];

  

在調用 get('userLister') 過程當中又發生了什麼呢?說實話,這個過程不是十分複雜, 可是因爲涉及到遞歸和回溯,寫這裏的時候,我寫了改,改了寫,示意圖畫了好幾次,折騰了很久,都不滿意, 就怕說不清楚,讀者朋友們理解起來費勁。 最後畫了一個簡單的示意圖,請大家對照 DI容器解析依賴獲取實例的過程示意圖 , 以及前面關於 get() build() getDependencies() resolveDependencies() 等函數的源代碼, 瞭解大體流程。若是有任何疑問、建議,也請在底部留言。

 

 

 

 

DI容器解析依賴獲取實例的過程示意圖

DI容器解析依賴獲取實例的過程示意圖 中綠色方框表示DI容器的5個數組,淺藍色圓邊方框表示調用的函數和方法。 藍色箭頭表示讀取內存,紅色箭頭表示寫入內存,虛線箭頭表示參照的內存對象,粗線綠色箭頭表示回溯過程。 圖中3個圓柱體表示實例化過程當中,建立出來的3個實例。

對於 get() 函數:

  • 在第1步中調用 get('userLister') 表示要得到一個 userLister 實例。 這個 userLister 不是一個有效的類名,說明這是一個別名。 那麼要獲取的是這個別名所表明的類的實例。
  • 查找 $_definitions 數組,發現 $_definitions['userLister'] = ['class'=>'app\models\UserLister'] 。 這裏 userLister 不等於 app\models\UserLister , 說明要獲取的這個 userLister 實例依賴於 app\models\UserLister 。 這是查找依賴定義數組的第一種狀況。
  • 而在第2二、23步中, get(yii\db\Connection) 調用 get() 時指定要獲取的實例的類型, 與依賴定義數組 $_definitions 定義的所依賴的類型是相同的,都是 yii\db\Connection 。 也就是說,本身依賴於本身,這就基本達到了中止遞歸調用 get() 的條件,差很少能夠開始反溯了。 這是查找依賴定義數組的第二種狀況。
  • 第三種狀況是第三、4步、第1三、14步查找依賴定義數組,發現依賴不存在。 說明所要獲取的類型的依賴關係未在容器中註冊。 對於未註冊依賴關係的,DI容器認爲要麼是一個沒有外部依賴的簡單類型, 要麼是一個容器自身能夠自動解析其依賴關係的類型。
  • 對於第一種狀況,要獲取的類型依賴於其餘類型的,遞歸調用 get() 獲取所依賴的類型。
  • 對於第2、三種狀況,直接調用 build() 嘗試獲取該類型的實例。

build() 在實例化過程當中,幹了這麼幾件事:

  • 調用 getDependencies() 獲取依賴信息。
  • 調用 resolveDependencies() 解析依賴信息。
  • 將定義中的配置數組、構造函數參數與調用 get() 時傳入的配置數組和構造參數進行合併。 這一步並未在上面的示意圖中體現,請參閱 build() 的源代碼部分。
  • 根據解析回來的依賴單元,調用 newInstanceArgs() 建立實例。 請留意第3六、42步,並不是直接由 resolveDependencies() 調用 newInstanceArgs() 。 而是 resolveDependencies() 將依賴單元返回後,由 build() 來調用。就像第31步同樣。
  • 將獲取的類型實例返回給調用它的 get()

getDependencies() 函數老是被 build() 調用,他幹了這麼幾件事:

  • 建立ReflectionClass,並寫入 $_reflections 緩存數組。如第6步中, $_reflections['app\models\UserLister'] = new ReflectionClass('app\models\UserLister')
  • 利用PHP的Reflection API,經過分析構造函數的形式參數,瞭解到當前類型對於其餘單元、默認值的依賴。
  • 將上一步瞭解到的依賴,在 $_dependencies 緩存數組中寫入一個 Instance 實例。如第七、8步。
  • 當一個類型的構造函數的參數列表中,沒有默認值、參數都是簡單類型時,獲得一個 [null] 。 如第28步。

resolveDependencies() 函數老是被 build() 調用,他在實例化時,幹了這麼幾件事:

  • 根據緩存在 $_dependencies 數組中的 Instance 實例的 id , 遞歸調用容器的 get() 實例化依賴單元。並返回給 build() 接着運行。
  • 對於像第28步之類的依賴信息爲 [null] 的,則什麼都不幹。

newInstanceArgs() 函數是PHP Reflection API的函數,用於建立實例,具體請看 PHP手冊

這裏只是簡單的舉例子而已,尚未涉及到多依賴和單例的情形,可是在原理上是同樣的。 但願繼續深刻了解的讀者朋友能夠再看看上面有關函數的源代碼就好了,有疑問請隨時留言。

從上面的例子中不難發現,DI容器維護了兩個緩存數組 $_reflections$_dependencies 。這兩個數組只寫入一次,就能夠無限次使用。 所以,減小了對ReflectionClass的使用,提升了DI容器解析依賴和獲取實例的效率。

另外一方面,咱們看到,獲取一個實例,步驟其實很多。可是,對於典型的Web應用而言, 有許多模塊其實應當註冊爲單例的,好比上面的 yii\db\Connection 。 一個Web應用通常使用一個數據庫鏈接,特殊狀況下會用多幾個,因此這些數據庫鏈接通常是給定不一樣別名加以區分後, 分別以單例形式放在容器中的。所以,實際獲取實例時,步驟會簡單得。對於單例, 在第一次 get() 時,直接就返回了。並且,省去不重複構造實例的過程。

這兩個方面,都體現出Yii高效能的特色。

上面咱們分析了DI容器,這只是其中的原理部分,具體的運用,咱們將結合 服務定位器(Service Locator) 來說。

轉載自:http://www.digpage.com/di.html

相關文章
相關標籤/搜索