ThinkPHP6源碼:從Http類的實例化看依賴注入是如何實現(xiàn)的

ThinkPHP6源碼:從Http類的實例化看依賴注入是如何實現(xiàn)的

thinkphp 6 從原先的 app 類中分離出 http 類,負責應用的初始化和調(diào)度等功能,而 app 類則專注于容器的管理,符合單一職責原則。

以下源碼分析,我們可以從 App,Http 類的實例化過程,了解類是如何實現(xiàn)自動實例化的,依賴注入是怎么實現(xiàn)的。

從入口文件出發(fā)


當訪問一個 ThinkPHP 搭建的站點,框架最先是從入口文件開始的,然后才是應用初始化、路由解析、控制器調(diào)用和響應輸出等操作。

入口文件主要代碼如下:

立即學習PHP免費學習筆記(深入)”;

//?引入自動加載器,實現(xiàn)類的自動加載功能(PSR4標準) //?對比Laravel、Yii2、Thinkphp的自動加載實現(xiàn),它們基本就都一樣 //?具體實現(xiàn)可參考我之前寫的Laravel的自動加載實現(xiàn): //?@link:?https://learnku.com/articles/20816 require?__DIR__?.?'/../vendor/autoload.php'; //?這一句和分為兩部分分析,App的實例化和調(diào)用「http」,具體見下文分析 $http?=?(new?App())->http; $response?=?$http->run(); $response->send(); $http->end($response);

App 實例化


執(zhí)行 new App() 實例化時,首先會調(diào)用它的構(gòu)造函數(shù)。

public?function?__construct(string?$rootPath?=?'') { ????//?thinkPath目錄:如,D:devtp6vendortopthinkframeworksrc ????$this->thinkPath???=?dirname(__DIR__)?.?DIRECTORY_SEPARATOR; ????//?項目根目錄,如:D:devtp6 ????$this->rootPath????=?$rootPath???rtrim($rootPath,?DIRECTORY_SEPARATOR)?.?DIRECTORY_SEPARATOR?:?$this->getDefaultRootPath(); ????$this->appPath?????=?$this->rootPath?.?'app'?.?DIRECTORY_SEPARATOR; ????$this->runtimePath?=?$this->rootPath?.?'runtime'?.?DIRECTORY_SEPARATOR; ????//?如果存在「綁定類庫到容器」文件 ????if?(is_file($this->appPath?.?'provider.php'))?{ ????????//將文件里的所有映射合并到容器的「$bind」成員變量中 ????????$this->bind(include?$this->appPath?.?'provider.php'); ????} ????//將當前容器實例保存到成員變量「$instance」中,也就是容器自己保存自己的一個實例 ????static::setInstance($this); ????//?保存綁定的實例到「$instances」數(shù)組中,見對應分析 ????$this->instance('app',?$this); ????$this->instance('thinkContainer',?$this); }

構(gòu)造函數(shù)實現(xiàn)了項目各種基礎路徑的初始化,并讀取了 provider.php 文件,將其類的綁定并入 $bind 成員變量,provider.php 文件默認內(nèi)容如下:

return?[ ????'thinkRequest'??????????=>?Request::class, ????'thinkexceptionHandle'?=>?ExceptionHandle::class, ];

合并后,$bind 成員變量的值如下:

ThinkPHP6源碼:從Http類的實例化看依賴注入是如何實現(xiàn)的

$bind 的值是一組類的標識到類的映射。從這個實現(xiàn)也可以看出,我們不僅可以在 provider.php 文件中添加標識到類的映射,而且可以覆蓋其原有的映射,也就是將某些核心類替換成自己定義的類。

static::setInstance($this) 實現(xiàn)的作用,如圖:

ThinkPHP6源碼:從Http類的實例化看依賴注入是如何實現(xiàn)的

thinkApp 類的 $instance 成員變量指向 thinkApp 類的一個實例,也就是類自己保存自己的一個實例。

instance() 方法的實現(xiàn):

public?function?instance(string?$abstract,?$instance) { ????//檢查「$bind」中是否保存了名稱到實際類的映射,如?'app'=>?'thinkApp' ????//也就是說,只要綁定了這種對應關系,通過傳入名稱,就可以找到實際的類 ????if?(isset($this->bind[$abstract]))?{ ????????//$abstract?=?'app',?$bind?=?"thinkApp" ????????$bind?=?$this->bind[$abstract]; ????????//如果「$bind」是字符串,重走上面的流程 ????????if?(is_string($bind))?{ ????????????return?$this->instance($bind,?$instance); ????????} ????} ????//保存綁定的實例到「$instances」數(shù)組中 ????//比如,$this->instances["thinkApp"]?=?$instance; ????$this->instances[$abstract]?=?$instance; ????return?$this; }

執(zhí)行結(jié)果大概是這樣的:

ThinkPHP6源碼:從Http類的實例化看依賴注入是如何實現(xiàn)的

Http 類的實例化以及依賴注入原理


這里,$http = (new App())->http,前半部分好理解,后半部分乍一看有點讓人摸不著頭腦,App 類并不存在 http 成員變量,這里何以大膽調(diào)用了一個不存在的東東呢?

原來,App 類繼承自 Container 類,而 Container 類實現(xiàn)了__get() 魔術方法,在 PHP 中,當訪問到的變量不存在,就會觸發(fā)__get() 魔術方法。該方法的實現(xiàn)如下:

public?function?__get($name) { ????return?$this->get($name); }

實際上是調(diào)用 get() 方法:

public?function?get($abstract) { ????//先檢查是否有綁定實際的類或者是否實例已存在 ????//比如,$abstract?=?'http' ????if?($this->has($abstract))?{ ????????return?$this->make($abstract); ????} ????//?找不到類則拋出類找不到的錯誤 ????throw?new?ClassNotFoundException('class?not?exists:?'?.?$abstract,?$abstract); }

然而,實際上,主要是 make() 方法:

public?function?make(string?$abstract,?array?$vars?=?[],?bool?$newInstance?=?false) ????{ ????????//如果已經(jīng)存在實例,且不強制創(chuàng)建新的實例,直接返回已存在的實例 ????????if?(isset($this->instances[$abstract])?&&?!$newInstance)?{ ????????????return?$this->instances[$abstract]; ????????} ????????//如果有綁定,比如?'http'=>?'thinkHttp',則?$concrete?=?'thinkHttp' ????????if?(isset($this->bind[$abstract]))?{ ????????????$concrete?=?$this->bind[$abstract]; ????????????if?($concrete?instanceof?Closure)?{ ????????????????$object?=?$this->invokeFunction($concrete,?$vars); ????????????}?else?{ ????????????????//重走一遍make函數(shù),比如上面http的例子,則會調(diào)到后面「invokeClass()」處 ????????????????return?$this->make($concrete,?$vars,?$newInstance); ????????????} ????????}?else?{ ????????????//實例化需要的類,比如'thinkHttp' ????????????$object?=?$this->invokeClass($abstract,?$vars); ????????} ????????if?(!$newInstance)?{ ????????????$this->instances[$abstract]?=?$object; ????????} ????????return?$object; ????}

然而,然而,make() 方法主要靠 invokeClass() 來實現(xiàn)類的實例化。該方法具體分析:

public?function?invokeClass(string?$class,?array?$vars?=?[]) ????{ ????????try?{ ????????????//通過反射實例化類 ????????????$reflect?=?new?ReflectionClass($class); ????????????//檢查是否有「__make」方法 ????????????if?($reflect->hasMethod('__make'))?{ ????????????????//返回的$method包含'__make'的各種信息,如公有/私有 ????????????????$method?=?new?ReflectionMethod($class,?'__make'); ????????????????//檢查是否是公有方法且是靜態(tài)方法 ????????????????if?($method->isPublic()?&&?$method->isStatic())?{ ????????????????????//綁定參數(shù) ????????????????????$args?=?$this->bindParams($method,?$vars); ????????????????????//調(diào)用該方法(__make),因為是靜態(tài)的,所以第一個參數(shù)是null ????????????????????//因此,可得知,一個類中,如果有__make方法,在類實例化之前會首先被調(diào)用 ????????????????????return?$method->invokeArgs(null,?$args); ????????????????} ????????????} ????????????//獲取類的構(gòu)造函數(shù) ????????????$constructor?=?$reflect->getConstructor(); ????????????//有構(gòu)造函數(shù)則綁定其參數(shù) ????????????$args?=?$constructor???$this->bindParams($constructor,?$vars)?:?[]; ????????????//根據(jù)傳入的參數(shù),通過反射,實例化類 ????????????$object?=?$reflect->newInstanceArgs($args); ????????????//?執(zhí)行容器回調(diào) ????????????$this->invokeAfter($class,?$object); ????????????return?$object; ????????}?catch?(ReflectionException?$e)?{ ????????????throw?new?ClassNotFoundException('class?not?exists:?'?.?$class,?$class,?$e); ????????} ????}

以上代碼可看出,在一個類中,添加__make() 方法,在類實例化時,會最先被調(diào)用。以上最值得一提的是 bindParams() 方法:

protected?function?bindParams($reflect,?array?$vars?=?[]):?array { ????//如果參數(shù)個數(shù)為0,直接返回 ????if?($reflect->getNumberOfParameters()?==?0)?{ ????????return?[]; ????} ????//?判斷數(shù)組類型?數(shù)字數(shù)組時按順序綁定參數(shù) ????reset($vars); ????$type???=?key($vars)?===?0???1?:?0; ????//通過反射獲取函數(shù)的參數(shù),比如,獲取Http類構(gòu)造函數(shù)的參數(shù),為「App?$app」 ????$params?=?$reflect->getParameters(); ????$args???=?[]; ????foreach?($params?as?$param)?{ ????????$name??????=?$param->getName(); ????????$lowerName?=?self::parseName($name); ????????$class?????=?$param->getClass(); ????????//如果參數(shù)是一個類 ????????if?($class)?{ ????????????//將類型提示的參數(shù)實例化 ????????????$args[]?=?$this->getObjectParam($class->getName(),?$vars); ????????}?elseif?(1?==?$type?&&?!empty($vars))?{ ????????????$args[]?=?array_shift($vars); ????????}?elseif?(0?==?$type?&&?isset($vars[$name]))?{ ????????????$args[]?=?$vars[$name]; ????????}?elseif?(0?==?$type?&&?isset($vars[$lowerName]))?{ ????????????$args[]?=?$vars[$lowerName]; ????????}?elseif?($param->isDefaultValueAvailable())?{ ????????????$args[]?=?$param->getDefaultValue(); ????????}?else?{ ????????????throw?new?InvalidArgumentException('method?param?miss:'?.?$name); ????????} ????} ????return?$args; }

而這之中,又最值得一提的是 getObjectParam() 方法:

protected?function?getObjectParam(string?$className,?array?&$vars) { ????$array?=?$vars; ????$value?=?array_shift($array); ????if?($value?instanceof?$className)?{ ????????$result?=?$value; ????????array_shift($vars); ????}?else?{ ????????//實例化傳入的類 ????????$result?=?$this->make($className); ????} ????return?$result; }

getObjectParam() 方法再一次光榮地調(diào)用 make() 方法,實例化一個類,而這個類,正是從 Http 的構(gòu)造函數(shù)提取的參數(shù),而這個參數(shù)又恰恰是一個類的實例 ——App 類的實例。到這里,程序不僅通過 PHP 的反射類實例化了 Http 類,而且實例化了 Http 類的依賴 App 類。假如 App 類又依賴 C 類,C 類又依賴 D類…… 不管多少層,整個依賴鏈條依賴的類都可以實現(xiàn)實例化。

總的來說,整個過程大概是這樣的:需要實例化 Http 類 ==> 提取構(gòu)造函數(shù)發(fā)現(xiàn)其依賴 App 類 ==> 開始實例化 App 類(如果發(fā)現(xiàn)還有依賴,則一直提取下去,直到天荒地老)==> 將實例化好的依賴(App 類的實例)傳入 Http 類來實例化 Http 類。

這個過程,起個裝逼的名字就叫做「依賴注入」,起個摸不著頭腦的名字,就叫做「控制反轉(zhuǎn)」。

這個過程,如果退回遠古時代,要實例化 Http 類,大概是這樣實現(xiàn)的(假如有很多層依賴):

. . . $e?=?new?E(); $d?=?new?D($e); $c?=?new?D($d); $app?=?new?App($c); $http?=?new?Http($app); . . .

這得有多累人。而現(xiàn)代 PHP,交給「容器」就好了。容器還有不少功能,后面再詳解。

? 版權(quán)聲明
THE END
喜歡就支持一下吧
點贊5 分享