[php]laravel框架容器管理的一些要點

原文地址:http://www.javashuo.com/article/p-fvkhrcsv-e.htmlphp

本文面向php語言的laravel框架的用戶,介紹一些laravel框架裏面容器管理方面的使用要點。文章很長,可是內容應該頗有用,但願有須要的朋友能看到。php經驗有限,不到位的地方,歡迎幫忙指正。laravel

1. laravel容器基本認識

laravel框架是有一個容器框架,框架應用程序的實例就是一個超大的容器,這個實例在bootstrap/app.php內進行初始化:數據庫

image

這個文件在每一次請求到達laravel框架都會執行,所建立的$app便是laravel框架的應用程序實例,它在整個請求生命週期都是惟一的。laravel提供了不少服務,包括認證,數據庫,緩存,消息隊列等等,$app做爲一個容器管理工具,負責幾乎全部服務組件的實例化以及實例的生命週期管理。這種方式可以很好地對代碼進行解耦,使得應用程序的業務代碼沒必要操心服務組件的對象從何而來,當須要一個服務類來完成某個功能的時候,僅須要經過容器解析出該類型的一個實例便可。從最終的使用方式來看,laravel容器對服務實例的管理主要包括如下幾個方面:編程

  • 服務的綁定與解析
  • 服務提供者的管理
  • 別名的做用
  • 依賴注入

弄清這幾個方面的思想, 以及laravel容器的實現機制,就能熟練掌握laravel容器的管理。bootstrap

2. 如何在代碼中獲取到容器實例

laravel容器實例在整個請求生命週期中都是惟一的,且管理着全部的服務組件實例。那麼有哪些方式可以拿到laravel容器的實例呢?經常使用的有如下幾種方式:數組

1) 經過app這個help函數:緩存

app這個輔助函數定義在 
image 
文件裏面,這個文件定義了不少help函數,而且會經過composer自動加載到項目中。因此,在參與http請求處理的任何代碼位置都可以訪問其中的函數,好比app()。閉包

2)經過App這個Facadeapp

經過App這個Facade拿容器實例的方式,跟上面不一樣的是,不能把App先賦給一個變量,而後經過變量來調用容器的方法。這是由於App至關於只是一個類名,咱們不能把一個類名複製一個變量。$app = App;不是一個合法的可執行的語句,而$app = app();倒是一個合法的可執行的語句,由於它後面有app(),表示函數調用。App::basePath();也是一個合法的語句,它就是在調用類的靜態方法。

再補充2點:

第一點: Facade是laravel框架裏面比較特殊的一個特性,每一個Facade都會與容器裏面的一個實例對象關聯,咱們能夠直接經過Facade類靜態方法調用的形式來調用它關聯的實例對象的方法。好比App這個Facade,調用App::basePath()的時候,實際至關於app()->basePath()。這個底層機制也是依賴於php語言的特性才能實現的,須要在每個Facade裏面,設定一個靜態成員並關聯到一個服務的實例對象,當調用Facade類的靜態方法的時候,解析出調用的方法名,再去調用關聯的服務實例的同名方法,最後把結果返回。我認爲理解Facade能起到什麼做用就夠了,不必定要深究到它底層去了解實現的細節,畢竟在實際的開發中,不用Facade,也徹底不影響laravel框架的使用。另外在實際編碼中,要自定義一個Facade也很是容易,只要繼承laravel封裝的Facade基類便可:

實現Facade基類的getFacadeAccessor方法,laravel框架就知道這個Facade類該與哪一個服務實例關聯起來了。實際上這個getFacadeAccess方法,返回的名稱就是後面要介紹的服務綁定名稱。在laravel容器裏面,一個服務實例,都會有一個固定的綁定名稱,經過這個名稱就能找到這個實例。因此爲啥Facade類只要返回服務綁定名稱便可。

咱們能夠看看App這個Facade類的代碼:

它的getFacadeAccessor返回的就是一個字符串「app」,這個app就是laravel容器本身綁定本身時用的名稱。

第二點: 從上一點最後App這個Facade的源碼能夠看出,App這個Facade的全類名實際上是:Illuminate\Support\Facades\App,那爲何咱們在代碼裏面可以直接經過App這個簡短的名稱就能訪問到呢:

你看以上代碼徹底沒有用到use或者徹底限定的方式來使用Illuminate\Support\Facades\App。實際上App跟Illuminate\Support\Facades\App是徹底等價的,只不過App比Illuminate\Support\Facades\App要簡短不少,並且不須要use,因此用起來方便,那麼它是怎麼實現的?這跟laravel容器配置的別名有關係,在config/app.php中,有一節aliases專門用來配置一些類型的別名:

而後在laravel框架處理請求過程當中,會經過Illuminate\Foundation\Bootstrap\RegisterFacades這個類來註冊這些別名到全局環境裏面:

因此咱們才能直接經過別名,代替完整的類型名作一樣的訪問功能。若是你本身寫了一些類,名稱很長,而且在代碼裏面用的特別多,也能夠考慮配置到config/app.php別名裏面去,laravel會幫咱們註冊。

3)另一種方式拿到laravel容器實例就是在服務提供者裏面直接使用$this->app

服務提供者後面還會介紹,如今只是引入。由於服務提供者類都是由laravel容器實例化的,這些類都繼承自Illuminate\Support\ServiceProvider,它定義了一個實例屬性$app:

image

laravel在實例化服務提供者的時候,會把laravel容器實例注入到這個$app上面。因此咱們在服務提供者裏面,始終能經過$this->$app訪問到laravel容器實例,而不須要再使用app()函數或者App Facade了。

3. 直觀的認識laravel容器

一直在說容器,既然它是用來存取實例對象的時候,那麼它裏面應該至少有一個數組充當容器存儲功能的角色才行,因此咱們能夠經過打印的方式來直觀地看下laravel容器實例的結構:

結果以下: 
image 
從這個結構能夠看出,laravel容器實例上包含了不少的數組,其中紅框部分的數組,從名字也能夠猜想出它們跟後面要介紹的服務,服務提供者與服務別名之間的聯繫。理清這幾個數組的存儲結構,天然就明白了laravel容器如何管理服務。

4. 如何理解服務綁定與解析

淺義層面理解,容器既然用來存儲對象,那麼就要有一個對象存入跟對象取出的過程。這個對象存入跟對象取出的過程在laravel裏面稱爲服務的綁定與解析。

先來看服務綁定,在laravel裏面,服務綁定到容器,有多種形式:

singleton是laravel服務綁定的方法之一,詳細做用後面會介紹,目前只是用它來展示服務綁定的形式。籠統的說容器的時候,咱們說容器管理的是服務對象,可是laravel的容器能夠管理不只僅是對象,它可以管理的是任意類型的數據,包括基本數據類型和對象。因此在服務綁定的時候,咱們也能夠綁定任意的數據,正如以上代碼展現的那樣。在綁定的時候,咱們能夠直接綁定已經初始化好的數據(基本類型、數組、對象實例),還能夠用匿名函數來綁定。用匿名函數的好處在於,這個服務綁定到容器之後,並不會當即產生服務最終的對象,只有在這個服務解析的時候,匿名函數纔會執行,此時纔會產生這個服務對應的服務實例。

實際上,當咱們使用singleton,bind方法以及數組形式,(這三個方法是後面要介紹的綁定的方法),進行服務綁定的時候,若是綁定的服務形式,不是一個匿名函數,也會在laravel內部用一個匿名函數包裝起來,這樣的話, 不輪綁定什麼內容,都能作到前面介紹的懶初始化的功能,這對於容器的性能是有好處的。這個能夠從bind的源碼中看到一些細節:

image

服務綁定時的第一個參數就是服務的綁定名稱。服務綁定完成後,容器會把這個服務的綁定記錄存儲到實例屬性bindings裏面:

image

這個bindings裏面的每條記錄,表明一個服務綁定。它的key值就是服務的綁定名稱,value值也是一個數組,這個數組的concrete屬性就是服務綁定時產生的匿名函數,也就是閉包;另一個參數表示這個服務在屢次解析的時候,是否只返回第一次解析獲得的對象。這個參數在介紹服務綁定方法時會再繼續介紹。

接下來看看服務綁定的幾種方法及區別:

a. 經過bind方法

bind是laravel服務綁定的底層方法,它的簽名是:

image

第一個參數服務綁定名稱,第二個參數服務綁定的結果,第三個參數就表示這個服務是否在屢次解析的時候,始終返回第一次解析出的實例。它的默認值是false,意味着這樣的服務在每次解析的時候都會返回一個新的實例。它的值與bindings裏面服務綁定記錄value數組裏面的share屬性是對應的。

b. 經過singleton方法

舉例略。它跟bind的區別在於,它始終是以shared=true的形式進行服務綁定,這是由於它的源碼是這樣的:

image

c. 經過數組的形式

爲何能夠直接把容器實例直接當成數組來用呢,這是由於容器實現了php的ArrayAccess接口:

因此實際上以上這種數組形式的綁定實際上至關於沒有第三個參數的bind方法。

再來看服務的解析。上面的內容都是在說明把如何獲取服務實例的方式綁定到容器,那麼如何從容器獲取到須要的服務實例呢?這個過程就是服務解析,在laravel裏面經過make方法來完成服務的解析:

這個方法接收兩個參數,第一個是服務的綁定名稱和服務綁定名稱的別名,若是是別名,那麼就會根據服務綁定名稱的別名配置,找到最終的服務綁定名稱,而後進行解析;第二個參數是一個數組,最終會傳遞給服務綁定產生的閉包。

咱們能夠經過make的源碼理解服務解析的邏輯,這個是Illuminate\Container\Container類中的make方法源碼,laravel的容器實例是Illuminate\Foundation\Application類的對象,這個類繼承了Illuminate\Container\Container,這裏暫時只展現Illuminate\Container\Container類中的make方法的代碼,先不涉及Illuminate\Foundation\Application類的make方法,由於後者覆蓋了Illuminate\Container\Container類中的make方法,加了一些服務提供者的邏輯,因此這裏先不介紹它。其實前面的不少源碼也都是從Illuminate\Container\Container中拿出來的,不過那些代碼Application沒有覆蓋,不影響內容的介紹。

從這個源碼能夠看到:

a. 在解析一個服務的時候,它會先嚐試把別名轉換成有效的服務綁定名稱;

b. 若是這個服務是一個shared爲true的服務綁定,且以前已經作過解析的話,就會直接返回以前已經解析好的對象;

c. 若是這個服務是一個shared爲true的服務綁定,而且是第一次解析的話,就會把已解析的對象存入到instances這個容器屬性裏面去,也就是說只有shared爲true的服務綁定,在解析的時候纔會往instances屬性裏面存入記錄,不然不會存入;

d. 解析完畢,還會在容器的resolved屬性裏面存入一條記錄,表示這個服務綁定解析過;

e. resolved,instances數組的key值跟bindings數組的key值同樣,都是服務綁定名稱;

f. 服務綁定的shared屬性在整個服務綁定生命週期內都是不能更改的。

服務的解析也有多種形式,經常使用的有:

a. make方法

b. 數組形式

這個的原理仍是跟容器實現了ArrayAccess的接口有關係:

因此數組形式的訪問跟不使用第二個參數的make方法形式是同樣的。

c. app($service)的形式

看了app這個help函數的源碼就明白了:

原來app這個函數在第一個參數爲空的時候,返回的是容器實例自己。在有參數的時候等價於調用容器實例的make方法。

以上就是服務綁定與解析的主要內容,涉及的要點較多,但願描述的比較清楚。

5. 服務提供者的做用與使用

前面介紹了服務的綁定。那麼服務的綁定應該在哪一個位置處理呢?雖說,可以拿到容器實例的地方,就都能進行服務的綁定;可是咱們使用服務的綁定的目的,是爲了在合適的位置解析出服務實例並使用,若是服務綁定的位置過於隨意,那麼就很難保證在解析的位置可以準確的解析出服務實例。由於服務可以解析的前提是服務綁定的代碼先與服務解析的代碼執行;因此,服務綁定一般會在應用程序初始化的時候進行,這樣才能保證業務代碼中(一般是router和controller裏面)必定能解析出服務實例。這個最佳的位置就是服務提供者。

服務提供者,在laravel裏面,其實就是一個工廠類。它最大的做用就是用來進行服務綁定。當咱們須要綁定一個或多個服務的時候,能夠自定義一個服務提供者,而後把服務綁定的邏輯都放在該類的實現中。在larave裏面,要自定一個服務提供者很是容易,只要繼承Illuminate\Support\ServiceProvider這個類便可。下面經過一個簡單的自定義服務提供者來講明服務提供者的一些要點:

1). 首先,自定義的服務提供者都是放在下面這個目錄的: 
image 
其實你放在哪均可以,不過得告訴laravel你的服務提供者在哪,laravel纔會幫你註冊。怎麼告訴它,後面還有介紹。

2)在這個舉例裏面,能夠看到有一個register方法,這個方法是ServiceProvider裏面定義的。自定義的時候,須要重寫它。這個方法就是用來綁定服務的。你能夠在這個服務裏面,根據須要加入任意數量的服務綁定。前面要介紹過,在服務提供者裏面,始終能經過$this->app拿到容器實例,因此上面的舉例中,咱們直接用這種方式來完成服務綁定。這個方法是怎麼完成服務綁定的呢?由於當laravel找到這個服務提供者的類之後,就會初始化這個服務提供者類,獲得一個服務提供者的對象,而後調用它的register方法,天然它裏面的全部服務綁定代碼就都會執行了。

laravel初始化自定義服務提供者的源碼是:

這個代碼是在Illuminate\Foundation\Application的源碼裏面拿出來的,從中你能看到laravel會把全部的自定義服務提供者都註冊進來。這個註冊的過程其實就是前面說的實例化服務提供者的類,並調用register方法的過程。

3). 從上一步的源碼也能看到,laravel加載自定義服務提供者的時候,實際是從config/app.php這個配置文件裏面的providers配置節找到全部要註冊的服務提供者的。

因此你若是本身寫了一個服務提供者,那麼只要配置到這裏面,laravel就會自動幫你註冊它了。

4)除了register方法,服務提供者裏面還有一個boot方法,這個boot方法,會在全部的服務提供者都註冊完成以後纔會執行,因此當你想在服務綁定完成以後,經過容器解析出其它服務,作一些初始化工做的時候,那麼就能夠這些邏輯寫在boot方法裏面。由於boot方法執行的時候,全部服務提供者都已經被註冊完畢了,因此在boot方法裏面可以確保其它服務都能被解析出來。

5)前面說的服務提供者的狀況,在laravel應用程序初始化的時候,就會去註冊服務提供者,調用register方法。可是還有一種需求,你可能須要在真正用到這個服務提供者綁定的服務的時候,纔會去註冊這個服務提供者,以減小沒必要要的註冊處理,提升性能。這也是延遲處理的一種方式。那麼這種服務提供者該怎麼定義呢?

其實最前面的這個舉例已經告訴你了,只要定義一個$defer的實例屬性,並把這個實例屬性設置爲true,而後添加一個provides的實例方法便可。這兩個成員都是ServiceProvider基類裏面定義好的,自定義的時候,只是覆蓋而已。

在基類中,$defer的默認值是false,表示這個服務提供者不須要延遲註冊。provides方法,只要簡單的返回這個服務提供register方法裏面,註冊的全部服務綁定名稱便可。

延遲註冊的服務提供者的機制是:

  • 當laravel初始化服務提供者的實例後,若是發現這個服務提供者的$defer屬性爲true,那麼就不會去調用它的register方法
  • 當laravel解析一個服務的時候,若是發現這個服務是由一個延遲服務提供的(它怎麼知道這個服務是延遲服務提供的,是provides方法告訴它的),那麼就會先把這個延遲服務提供者先註冊,再去解析。這個能夠看看Illuminate\Foundation\Application的make方法就清楚了: 

6)還記得容器實例結構上幾個帶有providers名稱的屬性數組吧:

image

在瞭解以上provider的機制後,這幾個數組的做用也就比較清晰了。其中serviceProviders用來存放全部已經註冊完畢的服務提供者:

image

loadedProviders跟serviceProviders的做用相似,只是存儲的記錄形式不一樣:

image

deferredProviders用來存儲全部的延遲註冊的服務提供者:

image

跟前面兩個不一樣的是,deferredProviders存儲的記錄的key值並非服務提供者的類型名稱,而是服務提供者的provides返回數組裏面的名稱。而且若是一個服務提供者的provides裏面返回了多個服務綁定名稱的話,那麼deferredProviders裏面就會存多條記錄:

image

這樣是方便根據服務綁定名稱,找到對應的服務提供者,並完成註冊。當服務的解析的時候,會先完成延遲類型的服務提供者的註冊,註冊完畢,這個服務綁定名稱在deferredProviders對應的那條記錄就會刪除掉。不過若是一個服務提供者provides了多個服務綁定名稱,解析其中一個服務的時候,只移除該名稱對應的deferredProviders記錄,而不是全部。

7)服務提供者還有一個小問題值的注意,因爲php是一門基本語言,在處理請求的時候,都會從入口文件把全部php都執行一遍。爲了性能考慮,laravel會在第一次初始化的時候,把全部的服務提供者都緩存到bootstrap/cache/services.php文件裏面,因此有時候當你改了一個服務提供者的代碼之後,再刷新不必定能看到指望的效果,這有可能就是由於緩存所致。這時把services.php刪掉就能看到你要的效果了。

6. 服務綁定名稱的別名

前面介紹的別名是在config/app.php的aliases配置節裏面定義的,那個別名的做用僅僅是簡化類名的時候,laravel幫你把長的類型名註冊成爲簡短的名稱,而後在全局環境了裏面都能使用。laravel還存在另一個別名,就是服務綁定名稱的別名。經過服務綁定的別名,在解析服務的時候,跟不使用別的效果一致。別名的做用也是爲了同時支持全類型的服務綁定名稱以及簡短的服務綁定名稱考慮的。

1)如何指定和使用服務綁定名稱的別名

假若有一個服務作以下綁定:

那麼能夠經過容器方法alias方法指定別名:

這個方法的第一個參數是服務綁定名稱,第二個參數是別名。這個方法調用後,就會在容器實例屬性aliases數組裏面存入一條記錄:

image 
image

你看剛纔舉例中的別名就已經添加到這個數組裏面。這個數組裏面每條記錄的key值都是別名。可是value有多是服務綁定名稱,也有多是另一個別名。這是由於別名是能夠遞歸的。

2)別名支持遞歸

也就是說,能夠對別名再指定別名:

image

3)別名如何應用於服務解析

在解析服務的時候,會先肯定這個服務名稱是否爲一個別名(只要看看在aliases數組裏是否存在記錄便可),若是不是別名,直接用這個服務名稱進行解析。若是這個服務名稱是一個別名,那麼就會經過調用的方式,找到最終的服務名稱:

image

以下全部的服務解析都是等價的:

4)另一種指定別名的方式

能夠在服務綁定的時候,進行別名的指定。只要按照以下的方式進行綁定便可:

也就是把服務綁定名稱換成數組形式而已。數組記錄的key值就是服務名稱,value值就是別名。

7. 依賴注入的機制

在這個舉例中,定義了一個Service類,這個類有一個實例成員$app,它須要一個實現了\Illuminate\Contracts\Foundation\Application 接口的實例對象,也就是容器實例。而後經過直接使用類型名稱的方式把這個類快速地綁定到了容器。app()->singleton(Service::class),等價於app()->singleton(Service::class,Service:class)。這種經過類名形式的綁定,laravel在解析的時候會調用這個類型的構造函數來實例化服務。而且在調用構造函數的時候,會經過反射得到這個構造函數的參數類型,而後從容器已有的綁定中,解析出對應參數類型的服務實例,傳入構造函數完成實例化。這個過程就是所謂的依賴注入。

在以上代碼中,徹底沒有手寫的new Service(app())代碼,就能正確地解析到service實例,這就是依賴注入的好處:

image

當一個類須要某個服務類型的實例時,不須要本身去創造這個服務的實例,只要告訴容器,它須要的實例類型便可,而後容器會根據這個類型, 解析出知足該類型的服務。如何根據參數類型解析出該參數類型的服務實例呢?其實就是根據參數類型的類型名稱進行解析獲得的,因此依賴注入可以成功的前提是根據參數類型的名稱,可以成功地解析到一個服務對象。以上之因此可以經過Illuminate\Contracts\Foundation\Application 這個名稱解析到服務,那是由於在容器實例aliases數組裏面有一條Illuminate\Contracts\Foundation\Application 的別名記錄:

image

也就是說Illuminate\Contracts\Foundation\Application 其實是app這個服務綁定名稱的一個別名,因此laravel在解析Illuminate\Contracts\Foundation\Application的時候,就能獲得對應的服務實例了。

這些別名屬於laravel容器核心的別名,在laravel初始化的時候會被註冊:

依賴注入更多地用在接口編程當中,就像上面的舉例相似。再看一個自定義的例子:

按接口進行編程,像Service這種業務類,只須要聲明本身須要一個Inter類型的實例便可。接口的好處在於解耦,未來要更換一種Inter的實現,不須要改Service的代碼,只須要在實例化Service的時候,傳入另一個Inter的實例便可。有了依賴注入之後,也不用改Service實例化的代碼,只要把Inter這個服務類型,從新作一個綁定,綁定到另一個實現便可。

8. 其它

還有兩個小點,也值的介紹一下。

1) 容器實例的instance方法

這個方法其實也是完成綁定的做用,可是它跟前面介紹的三種綁定方法不一樣,它是把一個已經存在的實例,綁定到容器:

這是它的源碼:

從這個代碼能夠看到,instance方法,會直接把外部實例化好的對象,直接存儲到容器的instances裏面。若是這個服務綁定名稱存在bindings記錄,那麼還會作一下從新綁定的操做。也就是說,經過intance方法綁定,是直接綁定服務實例,而原來的bind方法其實只是綁定了一個閉包函數,服務實例要到解析的時候纔會建立。

2) 容器實例的share方法

容器實例的singleton方法,綁定的服務在解析的時候,始終返回第一次解析的對象。還有一個方式也能作到這個效果,那就是使用share方法包裝服務綁定的匿名函數:

當咱們使用app('cas')解析的時候,始終拿到的都是第一次解析建立的那個CasManager對象。這個跟share方法的實現有關係:

image

從源碼看出,share方法把服務綁定的閉包再包裝了一下,返回一個新的閉包,而且在這個閉包裏面,加了一個靜態$object變量,它會存儲原始閉包第一次解析調用後的結果,並在後續解析中直接返回,從而保證這個服務的實例只有一個。

全文完,感謝閱讀~

相關文章
相關標籤/搜索