老司機帶你深入理解 Laravel 之 Facade

Dennis_Ritchie發表於2019-12-03

前言

老司機帶你深入理解Laravel之Facade
時間真的過的很快啊,今天都2019年12月2號了,準確的說,寫這篇部落格的時間是晚上21點40分,剛從公司加班回來,洗完澡就坐下來寫這篇文章了,不知不覺除這篇部落格外,我已經寫了11篇了,要講的東西實在是太多了,來日方長。今天要講的東西,我感覺是兄弟們一直沒搞懂的,那就是Laravel中非常重要的Facade,我相信凡是使用Laravel的兄弟,肯定使用過Facade了,很明顯,他就是我們今天的主角了。

閱讀建議

在閱讀這篇文章之前,我希望您對Laravel的容器具有一定的使用和了解,如果不熟悉的話,請閱讀Laravel容器,這方面的知識對於理解我今天要講的東西非常有必要,再次提醒一下各位,這篇博文容量很大,仔細體會消化,希望能有所收穫。

註冊Facade

如果你使用過第三方的composer包,它會提醒你,把它的ServiceProvider和Facade寫入到config/app.php檔案中,如下:

老司機帶你深入理解Laravel之Facade
當然了,註冊Facade和ServiceProvider不止這麼一種方式,你感興趣的話,可以看看官方的文件有很清楚的描述,開發第三方Laravel包

這些東西都沒啥可說的,如果我就說這些,也許兄弟們會說,我都知道,你還說個啥?也確實,如果就說這個,我也不好意思了,可是,後面的內容就不那麼容易了。

為了給兄弟們講Facade,我還是大致的給大家講解一下Laravel的引導過程(只關注與Facade有關的部分),大家都知道Laravel的入口檔案為public/index.php,下面的這行程式碼很關鍵。

老司機帶你深入理解Laravel之Facade

那麼這個檔案幹啥了呢?現在我們只關心下面這兩行:

老司機帶你深入理解Laravel之Facade

上面建立了一個Application類的物件,這個類代表了我們當前的應用程式,也是整個Laravel最為核心的類,注意了Application類繼承自Container類(這個類就是Laravel容器的核心),所以我們可以在Application類的物件上操作容器的方法,這就是為啥我在這篇博文的開頭,提醒大家需要一定的容器的知識。

老司機帶你深入理解Laravel之Facade

上面的這段程式碼,向容器中新增了一個單例,所以當我們建立Illuminate\Contracts\Http\Kernel::class物件的時候,實際上是建立的App\Http\Kernel::class類的物件,這一點極其重要,希望大家一定要記住,這在後面會使用到。

回到index.php檔案中,繼續看下面這行程式碼:

老司機帶你深入理解Laravel之Facade

Illuminate\Contracts\Http\Kernel::class指向的是誰啊?不就是我們上面說的App\Http\Kernel::class,這個檔案的位置如下:

老司機帶你深入理解Laravel之Facade

這個Kernel就是我們的目標了,我們開啟看一下這個檔案:

老司機帶你深入理解Laravel之Facade

這個Kernel類繼承自了Illuminate\Foundation\Http\Kernel類,兄弟們記住這一點,後面會使用到,下面我們回到index.php檔案中,laravel呼叫:

老司機帶你深入理解Laravel之Facade

$kernel的handle方法被呼叫,通過上面的分析,我們知道這個handle方法屬於Illuminate\Foundation\Http\Kernel類的,對於我們分析Facade來說,只有一行程式碼是關鍵的,就是下面:

老司機帶你深入理解Laravel之Facade

我們看一下這個方法sendRequestThroughRouter,我們不必關心它的引數$request是啥,它對我們分析當前的目標一點兒關係都沒有,這個函式中也只有一行是我們關心的,如下:

老司機帶你深入理解Laravel之Facade

bootstrap方法很簡單,如下:

老司機帶你深入理解Laravel之Facade

這裡首先呼叫bootstrappers方法,這個方法的返回值是一個陣列,它的內容如下:

老司機帶你深入理解Laravel之Facade

上面這個我們只需要關心RegisterFacades類,回到bootstrap方法中,它呼叫app->bootstrapWith方法,這裡的app是誰呢?他就是Application類的物件,這個物件在整個Laravel的生命週期中是唯一的,因為他是單例的,既然知道這個了,我們看Application物件的bootstrapWith方法。

老司機帶你深入理解Laravel之Facade

還記得我們的Application類繼承自Container類麼?所以它可以使用make方法,上面說了,我們當前只關心RegisterFacades這個bootstrapper,所以,我們進入到這個類的bootstrap方法:

老司機帶你深入理解Laravel之Facade

老司機帶你深入理解Laravel之Facade

因為這篇博文主要給大家講解Facade的整個實現的,所以我會忽略掉一些細節,關於這些細節,以後我會給大家講解,但是在這裡我會先說明他們的作用。上面這張圖,我已經標註了序號,序號1這行程式碼返回了config/app.php檔案的aliases欄位值,我們自己的Facade就註冊在了這個地方,在這篇博文的開頭,我已經給大家說過了。序號2的作用是幹啥呢?還記得我開始說的麼?當我們開發laravel包的時候,可以讓laravel自動載入我們的ServiceProvider和Facade,我們所要做的就是在我們的composer中,加入下面的這段:

老司機帶你深入理解Laravel之Facade

上面這個截圖,大家應該可以看的很清楚,我就不再詳述了,PackageManifest類的作用就是負責自動載入我們在composer.json檔案中的ServiceProvider和Facade。這麼說大家應該明白了吧。

回到RegisterFacades類的bootstrap方法中,array_merge方法合併1和2的Facade,並把它傳遞給AliasLoader的getInstance靜態方法。
這個getInstance方法返回了一個AliasLoader類的物件,下面我們看它的register方法:

老司機帶你深入理解Laravel之Facade

這個方法很簡單,直接呼叫方法prependToLoaderStack,如下:

老司機帶你深入理解Laravel之Facade

spl_autoload_register這個方法可能很多人不知道,因為現在都是使用成熟的框架了,簡單來說,它的作用就是負責載入我們的類檔案的,你有沒有好奇過,php是如何找到並載入我們的php類檔案的,這當中的功臣就是spl_autoload_register了,如果你不知道它,請參考php的官方文件spl_autoload_register,它的第一個引數是一個回撥方法,作用就是負責載入類檔案的,我們的程式中可以多次呼叫spl_autoload_register方法,也就是說可以註冊多個載入函式,關於spl_autoload_register的介紹就這麼多了,回到當前的程式碼中,laravel註冊的自動載入函式為AliasLoader物件的load方法,我們看哈:

老司機帶你深入理解Laravel之Facade

這個方法我們只需要看標註出來的部分,aliases屬性儲存著之前解析的所有的Facade,部分截圖如下:

老司機帶你深入理解Laravel之Facade

之所以我會把部分標出來,是因為我後面會用到,上面的程式碼中使用到了class_alias方法,這個方法是給一個類取個別名,比如說對於Illuminate\Support\Facades\Route::class這個類,它的別名為Route,為了證實這一點,我們來測試一下,在我們的路由檔案中,我們經常這麼做:

老司機帶你深入理解Laravel之Facade

注意了我們並沒有引入Route這麼一個東西,但是為啥php沒報錯呢?這就是我們上面給Illuminate\Support\Facades\Route::class取了Route這個別名的原因,你可以把class_alias這段程式碼刪除掉,肯定會報錯的:

老司機帶你深入理解Laravel之Facade

你再重新整理一下頁面,頁面報錯了,哈哈,就是這麼刺激:

老司機帶你深入理解Laravel之Facade

例項分析

上面分析了Laravel的整個Facade註冊的過程,是不是有點兒懵?不要慌,後面還有,任重而道遠啊。

老司機帶你深入理解Laravel之Facade

在Facade註冊一節中,我們標註了Route這個Facade,所以這一節,就已它為例來進行講解,Route類如下:

老司機帶你深入理解Laravel之Facade

所有的Facade都繼承自Illuminate\Support\Facades\Facade類,並且都必須實現getFacadeAccessor這個方法,不然會丟擲異常的,我們看Route的getFacadeAccessor方法如下,他返回字串"router",至於它的作用,我們後面會講到:

老司機帶你深入理解Laravel之Facade

為來給大家講解後面的問題,我寫了一個很簡單的例子,如下:

老司機帶你深入理解Laravel之Facade

在路由檔案中,我用Route註冊了一個路由,這裡呼叫了get方法,但是我們開啟Illuminate\Support\Facades\Facade\Route類,這個方法是不存在的,它的父類也沒有,然而我們注意到了Illuminate\Support\Facades\Facade類實現了__callStatic方法,如下:

老司機帶你深入理解Laravel之Facade

callStatic簡單來說就是如果你呼叫某個類的靜態方法,但是這個靜態方法不存在的話,就會呼叫這個類的callStatic方法,如果你還是不清楚,可以網上查閱相關資料,這裡不再闡述。好了,廢話不多說了,我們回到Facade的__callStatic方法中,這個方法首先呼叫getFacadeRoute方法,如下:

老司機帶你深入理解Laravel之Facade

看到沒,這裡就是我上面說的Facade為啥必須實現getFacadeAccessor方法,在當前的例項中,它返回的是 "router".。
resolveFacadeInstance方法是啥呢?很簡單,但是我還是準備貼出來:

老司機帶你深入理解Laravel之Facade

因為Illuminate\Support\Facades\Facade類是所有的Facade的父類,所以任何的Facade呼叫靜態方法,都會進入到這個方法中,靜態屬性$resolvedInstance儲存著當前所有被解析的Facade對應的例項物件,你要記住任何的Facade後面都有一個物件的,而且這個物件在整個Laravel程式的生命週期中是唯一的,只有這麼一個例項,上面標註的1首先檢查之前是否已經解析過這麼一個物件,如果解析過了,直接返回就是了,這是單例的常見手法。如果之前沒有解析過的話,那麼程式碼就會走到2整個地方了,我們知道$app就是全域性唯一的Application類物件,它繼承了Illuminate\Container\Container,而Illuminate\Container\Container又實現了介面ArrayAccess,對於ArrayAccess介面不熟悉的同學,可以查閱相關的資料,簡單來說,如果你的類實現了ArrayAccess介面,那麼你就可以像獲取陣列元素一樣,獲取物件的內容而不會出錯。
老司機帶你深入理解Laravel之Facade

這個介面有幾個方法必須實現,offsetGet方法是其中之一,當你採用陣列的寫法作用在物件上時,offsetGet會被呼叫,我們看Illuminate\Container\Container類的offsetGet方法,如下

老司機帶你深入理解Laravel之Facade

在當前情況下我們獲取的是$app['router'],所以這裡的引數$key就是"router",關於容器的make方法,請大家參考文件,非常簡單:

老司機帶你深入理解Laravel之Facade

make是容器暴露給我們獲取容器註冊內容的少有幾個方法,好了,現在我們的疑問是我們什麼時候註冊了一個"router"這麼一個東西,大家如果使用的是phpstorm的話,可以這麼做:

老司機帶你深入理解Laravel之Facade

搜尋內容為"router",如下:

老司機帶你深入理解Laravel之Facade

通過搜尋我們知道,在Illuminate\Routing\RoutingServiceProvider這個類中,註冊了router的單例,你可能會問,這段程式碼是啥時候呼叫的,也就是registerRouter方法是啥時候被呼叫的,當前的RoutingServiceProvider中的register方法許下:

老司機帶你深入理解Laravel之Facade

那麼register方法是怎麼被呼叫的呢?不知道沒關係,我細細道來,在之前的laravel框架引導過程中,建立Application類例項的時候,它的建構函式如下:

老司機帶你深入理解Laravel之Facade
registerBaseServiceProviders方法如下:

老司機帶你深入理解Laravel之Facade

看見沒,這個地方出現了RoutingServiceProvider類物件,我們再進入到Application類的register方法中,如下:

老司機帶你深入理解Laravel之Facade

啊哈,register方法被呼叫了,這個時候名為router的單例就被註冊了,分析了這些,我們回到RoutingServiceProvider類的registerRouter方法中。

老司機帶你深入理解Laravel之Facade

這裡直接返回了Illuminate\Routing\Router類的例項,這個實際上就是Laravel全域性唯一的路由器物件,路由就是靠它來實現的,分析了這些,我們再一次回到Illuminate\Support\Facades\Facade類的resolveFacadeInstance方法中。

老司機帶你深入理解Laravel之Facade

這裡把解析的例項儲存到$resolvedInstance屬性中,這樣下次就不需要解析了,resolveFacadeInstance方法呼叫完畢之後,返回到Facade的getFacadeRoot方法中。

老司機帶你深入理解Laravel之Facade

上面也是直接返回剛才獲取到的物件Router例項物件,getFacadeRoot方法呼叫完畢之後,繼續返回到__callStatic方法中。

老司機帶你深入理解Laravel之Facade

紅色的程式碼就是翻譯一下就是:

 $router->get('/',function () {
    echo "Hello World";
})

令人欣喜的是奇蹟出現了,Illuminate\Routing\Router類有如下的程式碼:

老司機帶你深入理解Laravel之Facade

總結

Laravel的原始碼錯綜複雜,理解起來不是那麼容易,上面給大家標註出了主要的脈絡,希望大家仔細體會和理解。不知不覺寫完這篇部落格已經凌晨40分了,整整三個小時,這效率還可以的哈。如果大家有什麼不懂的可以聯絡我,我有一個qq群,感興趣的可以加一下,如下:

如果有不懂的地方,可以加我的qq:1174332406,或者是微信:itshardjs,公眾號:LearnCodeHard

相關文章