為了降低代碼耦合程度,提高項目的可維護性,Yii采用多許多當下最流行又相對成熟的設計模式,包括了依賴注入(Denpdency Injection, DI)和服務定位器(Service Locator)兩種模式。 關于依賴注入與服務定位器,?[Inversion of Control Containers and the Dependency Injection pattern](http://martinfowler.com/articles/injection.html)?給出了很詳細的講解,這里結合Web應用和Yii具體實現進行探討,以加深印象和理解。 這些設計模式對于提高自身的設計水平很有幫助,這也是我們學習Yii的一個重要出發點。
## 有關概念[](http://www.digpage.com/di.html#id2 "Permalink to this headline")
在了解Service Locator 和 Dependency Injection 之前,有必要先來了解一些高大上的概念。 別擔心,你只需要有個大致了解就OK了,如果展開來說,這些東西可以單獨寫個研究報告:
**依賴倒置原則(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了。 萬一哪天要在妹子面前要裝一把范兒的時候,張口也能來這么幾個“高大上”就行了。 但具體的內涵,我們還是要要通過下面的學習來加深理解,畢竟要把“高大上”的東西用好,發揮出作用來。
## 依賴注入[](http://www.digpage.com/di.html#id3 "Permalink to this headline")
首先講講DI。在Web應用中,很常見的是使用各種第三方Web Service實現特定的功能,比如發送郵件、推送微博等。 假設要實現當訪客在博客上發表評論后,向博文的作者發送Email的功能,通常代碼會是這樣:
~~~
// 為郵件服務定義抽象層
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?的實例化語句:
~~~
$this->_eMailSender = MyEmailSender::getInstance();
~~~
這個問題的本質在于,你今天寫完這個Comment,只能用于這個項目,哪天你開發別的項目要實現類似的功能, 你還要針對新項目使用的郵件服務修改這個Comment。代碼的復用性不高呀。 有什么辦法可以不改變Comment的代碼,就能擴展成對各種郵件服務都支持么? 換句話說,有辦法將Comment和GmailSender解耦么?有辦法提高Comment的普適性、復用性么?
依賴注入就是為了解決這個問題而生的,當然,DI也不是唯一解決問題的辦法,畢竟條條大路通羅馬。 Service Locator也是可以實現解耦的。
在Yii中使用DI解耦,有2種注入方式:構造函數注入、屬性注入。
### 構造函數注入[](http://www.digpage.com/di.html#id4 "Permalink to this headline")
構造函數注入通過構造函數的形參,為類內部的抽象單元提供實例化。 具體的構造函數調用代碼,由外部代碼決定。具體例子如下:
~~~
// 這是構造函數注入的例子
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類的代碼不再需要作任何更改,多爽的一件事,擴展起來、測試起來都省心省力。
### 屬性注入[](http://www.digpage.com/di.html#id5 "Permalink to this headline")
與構造函數注入類似,屬性注入通過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容器[](http://www.digpage.com/di.html#id6 "Permalink to this headline")
從上面DI兩種注入方式來看,依賴單元的實例化代碼是一個重復、繁瑣的過程。 可以想像,一個Web應用的某一組件會依賴于若干單元,這些單元又有可能依賴于更低層級的單元, 從而形成依賴嵌套的情形。那么,這些依賴單元的實例化、注入過程的代碼可能會比較長,前后關系也需要特別地注意, 必須將被依賴的放在需要注入依賴的前面進行實例化。 這實在是一件既沒技術含量,又吃力不出成果的工作,這類工作是高智商(懶)人群的天敵, 我們是不會去做這么無聊的事情的。
就像極其不想洗衣服的人發明了洗衣機(我臆想的,未考證)一樣,為了解決這一無聊的問題,DI容器被設計出來了。 Yii的DI容器是?yii\di\Container?,這個容器繼承了發明人的高智商, 他知道如何對對象及對象的所有依賴,和這些依賴的依賴,進行實例化和配置。
### DI容器中的內容[](http://www.digpage.com/di.html#id7 "Permalink to this headline")
#### DI容器中實例的表示[](http://www.digpage.com/di.html#id8 "Permalink to this headline")
容器顧名思義是用來裝東西的,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容器的數據結構[](http://www.digpage.com/di.html#id9 "Permalink to this headline")
在DI容器中,維護了5個數組,這是DI容器功能實現的基礎:
~~~
// 用于保存單例Singleton對象,以對象類型為鍵
private $_singletons = [];
// 用于保存依賴的定義,以對象類型為鍵
private $_definitions = [];
// 用于保存構造函數的參數,以對象類型為鍵
private $_params = [];
// 用于緩存ReflectionClass對象,以類名或接口名為鍵
private $_reflections = [];
// 用于緩存依賴信息,以類名或接口名為鍵
private $_dependencies = [];
~~~
DI容器的5個數組內容和作用如?[_DI容器5個數組示意圖_](http://www.digpage.com/di.html#img-di-data)?所示。

DI容器5個數組示意圖
### 注冊依賴[](http://www.digpage.com/di.html#id10 "Permalink to this headline")
使用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容器中裝了兩類實例,一種是單例,每次向容器索取單例類型的實例時,得到的都是同一個實例; 另一類是普通實例,每次向容器索要普通類型的實例時,容器會根據依賴信息創建一個新的實例給你。
單例類型主要用于節省構建實例的時間、節省保存實例的內存、共享數據等。而普通類型主要用于避免數據沖突。
### 對象的實例化[](http://www.digpage.com/di.html#id11 "Permalink to this headline")
對象的實例化過程要比依賴的定義過程復雜得多。畢竟依賴的定義只是往特定的數據結構$_singletons?$_definitions?和?$_params?3個數組寫入有關的信息。 稍復雜的東西也就是定義的規范化處理了。其它真沒什么復雜的。像你這么聰明的,肯定覺得這太沒挑戰了。
而對象的實例化過程要相對復雜,這一過程會涉及到復雜依賴關系的解析、涉及依賴單元的實例化等過程。 且讓我們抽絲剝繭地進行分析。
#### 解析依賴信息[](http://www.digpage.com/di.html#id12 "Permalink to this headline")
容器在獲取實例之前,必須解析依賴信息。 這一過程會涉及到DI容器中尚未提到的另外2個數組$_reflections?和?$_dependencies?。?yii\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容器會將依賴信息轉換成實例。 這個實例化的過程中,是向容器索要實例。也就是說,有可能會引起遞歸。
#### 實例的創建[](http://www.digpage.com/di.html#id13 "Permalink to this headline")
解析完依賴信息,就萬事俱備了,那么東風也該來了。實例的創建,秘密就在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容器才會去解析依賴信息、緩存依賴信息。
#### 容器內容實例化的大致過程[](http://www.digpage.com/di.html#id14 "Permalink to this headline")
與注冊依賴時使用?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均將其視為單例。在獲取時,也將返回這一單例。
### 實例分析[](http://www.digpage.com/di.html#id15 "Permalink to this headline")
為了加深理解,我們以官方文檔上的例子來說明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?之前,?Connection?和?UserFinder?都會被自動實例化。 其中,?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容器解析依賴獲取實例的過程示意圖_](http://www.digpage.com/di.html#img-di)?, 以及前面關于?get()?build()?getDependencies()?resolveDependencies()等函數的源代碼, 了解大致流程。如果有任何疑問、建議,也請在底部留言。
[](https://box.kancloud.cn/2015-08-12_55cb0c500315e.png)
DI容器解析依賴獲取實例的過程示意圖
在?[_DI容器解析依賴獲取實例的過程示意圖_](http://www.digpage.com/di.html#img-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?。 這是查找依賴定義數組的第一種情況。
* 而在第22、23步中,?get(yii\db\Connection)?調用?get()?時指定要獲取的實例的類型, 與依賴定義數組?$_definitions?定義的所依賴的類型是相同的,都是?yii\db\Connection?。 也就是說,自己依賴于自己,這就基本達到了停止遞歸調用?get()?的條件,差不多可以開始反溯了。 這是查找依賴定義數組的第二種情況。
* 第三種情況是第3、4步、第13、14步查找依賴定義數組,發現依賴不存在。 說明所要獲取的類型的依賴關系未在容器中注冊。 對于未注冊依賴關系的,DI容器認為要么是一個沒有外部依賴的簡單類型, 要么是一個容器自身可以自動解析其依賴關系的類型。
* 對于第一種情況,要獲取的類型依賴于其他類型的,遞歸調用?get()?獲取所依賴的類型。
* 對于第二、三種情況,直接調用?build()?嘗試獲取該類型的實例。
build()?在實例化過程中,干了這么幾件事:
* 調用?getDependencies()?獲取依賴信息。
* 調用?resolveDependencies()?解析依賴信息。
* 將定義中的配置數組、構造函數參數與調用?get()?時傳入的配置數組和構造參數進行合并。 這一步并未在上面的示意圖中體現,請參閱?build()?的源代碼部分。
* 根據解析回來的依賴單元,調用?newInstanceArgs()?創建實例。 請留意第36、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?實例。如第7、8步。
* 當一個類型的構造函數的參數列表中,沒有默認值、參數都是簡單類型時,得到一個?[null]?。 如第28步。
resolveDependencies()?函數總是被?build()?調用,他在實例化時,干了這么幾件事:
* 根據緩存在?$_dependencies?數組中的?Instance?實例的?id?, 遞歸調用容器的?get()?實例化依賴單元。并返回給?build()?接著運行。
* 對于像第28步之類的依賴信息為?[null]?的,則什么都不干。
newInstanceArgs()?函數是PHP Reflection API的函數,用于創建實例,具體請看?[PHP手冊](http://php.net/manual/zh/reflectionclass.newinstanceargs.php)?。
這里只是簡單的舉例子而已,還沒有涉及到多依賴和單例的情形,但是在原理上是一樣的。 希望繼續深入了解的讀者朋友可以再看看上面有關函數的源代碼就行了,有疑問請隨時留言。
從上面的例子中不難發現,DI容器維護了兩個緩存數組?$_reflections?和?$_dependencies?。這兩個數組只寫入一次,就可以無限次使用。 因此,減少了對ReflectionClass的使用,提高了DI容器解析依賴和獲取實例的效率。
另一方面,我們看到,獲取一個實例,步驟其實不少。但是,對于典型的Web應用而言, 有許多模塊其實應當注冊為單例的,比如上面的?yii\db\Connection?。 一個Web應用一般使用一個數據庫連接,特殊情況下會用多幾個,所以這些數據庫連接一般是給定不同別名加以區分后, 分別以單例形式放在容器中的。因此,實際獲取實例時,步驟會簡單得。對于單例, 在第一次?get()?時,直接就返回了。而且,省去不重復構造實例的過程。
這兩個方面,都體現出Yii高效能的特點。
上面我們分析了DI容器,這只是其中的原理部分,具體的運用,我們將結合?[_服務定位器(Service Locator)_](http://www.digpage.com/service_locator.html#service-locator)?來講。
如果覺得《深入理解Yii2.0》對您有所幫助,也請[幫助《深入理解Yii2.0》](http://www.digpage.com/donate.html#donate)。 謝謝!
- 更新記錄
- 導讀
- Yii是什么
- Yii2.0的亮點
- 背景知識
- 如何閱讀本書
- Yii基礎
- 屬性(Property)
- 事件(Event)
- 行為(Behavior)
- Yii約定
- Yii應用的目錄結構和入口腳本
- 別名(Alias)
- Yii的類自動加載機制
- 環境和配置文件
- 配置項(Configuration)
- Yii模式
- MVC
- 依賴注入和依賴注入容器
- 服務定位器(Service Locator)
- 請求與響應(TBD)
- 路由(Route)
- Url管理
- 請求(Reqeust)
- Web應用Request
- Yii與數據庫(TBD)
- 數據類型
- 事務(Transaction)
- AcitveReocrd事件和關聯操作
- 樂觀鎖與悲觀鎖
- 《深入理解Yii2.0》視頻教程
- 第一講:基礎配置
- 第二講:用戶登錄
- 第三講:文章及評論的模型
- 附錄
- 附錄1:Yii2.0 對比 Yii1.1 的重大改進
- 附錄2:Yii的安裝
- 熱心讀者