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

NO IMAGE

閱讀目錄

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

1. laravel容器基本認識

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

image

這個檔案在每一次請求到達laravel框架都會執行,所建立的$app即是laravel框架的應用程式例項,它在整個請求生命週期都是唯一的。laravel提供了很多服務,包括認證,資料庫,快取,訊息佇列等等,$app作為一個容器管理工具,負責幾乎所有服務元件的例項化以及例項的生命週期管理。這種方式能夠很好地對程式碼進行解耦,使得應用程式的業務程式碼不必操心服務元件的物件從何而來,當需要一個服務類來完成某個功能的時候,僅需要通過容器解析出該型別的一個例項即可。從最終的使用方式來看,laravel容器對服務例項的管理主要包括以下幾個方面:

  • 服務的繫結與解析
  • 服務提供者的管理
  • 別名的作用
  • 依賴注入

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

2. 如何在程式碼中獲取到容器例項

laravel容器例項在整個請求生命週期中都是唯一的,且管理著所有的服務元件例項。那麼有哪些方式能夠拿到laravel容器的例項呢?常用的有以下幾種方式:

1) 通過app這個help函式:

– Hide code

$app = app();

app這個輔助函式定義在 
image 
檔案裡面,這個檔案定義了很多help函式,並且會通過composer自動載入到專案中。所以,在參與http請求處理的任何程式碼位置都能夠訪問其中的函式,比如app()。

2)通過App這個Facade

– Hide code

<?php
Route::get('/', function () {
dd(App::basePath());
return '';
});

通過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基類即可:

– Hide code

<?php
namespace ThirdProviders\CasServer\Facades;
use Illuminate\Support\Facades\Facade;
use ThirdProviders\CasServer\CasServerManager;
class CasServer extends Facade
{
protected static function getFacadeAccessor()
{
return CasServerManager::class;
}
}

實現Facade基類的getFacadeAccessor方法,laravel框架就知道這個Facade類該與哪個服務例項關聯起來了。實際上這個getFacadeAccess方法,返回的名稱就是後面要介紹的服務繫結名稱。在laravel容器裡面,一個服務例項,都會有一個固定的繫結名稱,通過這個名稱就能找到這個例項。所以為啥Facade類只要返回服務繫結名稱即可。

我們可以看看App這個Facade類的程式碼:

– Hide code

<?php
namespace Illuminate\Support\Facades;
/**
* @see \Illuminate\Foundation\Application
*/
class App extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'app';
}
}

它的getFacadeAccessor返回的就是一個字串“app”,這個app就是laravel容器自己繫結自己時用的名稱。

第二點: 從上一點最後App這個Facade的原始碼可以看出,App這個Facade的全類名其實是:Illuminate\Support\Facades\App,那為什麼我們在程式碼裡面能夠直接通過App這個簡短的名稱就能訪問到呢:

– Hide code

<?php
Route::get('/', function () {
dd(App::basePath());
return '';
});

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

– Hide code

'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
'Blade' => Illuminate\Support\Facades\Blade::class,
'Bus' => Illuminate\Support\Facades\Bus::class,
'Cache' => Illuminate\Support\Facades\Cache::class,
'Config' => Illuminate\Support\Facades\Config::class,
'Cookie' => Illuminate\Support\Facades\Cookie::class,
'Crypt' => Illuminate\Support\Facades\Crypt::class,
'DB' => Illuminate\Support\Facades\DB::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class,
'File' => Illuminate\Support\Facades\File::class,
'Gate' => Illuminate\Support\Facades\Gate::class,
'Hash' => Illuminate\Support\Facades\Hash::class,
'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class,
'Mail' => Illuminate\Support\Facades\Mail::class,
'Notification' => Illuminate\Support\Facades\Notification::class,
'Password' => Illuminate\Support\Facades\Password::class,
'Queue' => Illuminate\Support\Facades\Queue::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
'Redis' => Illuminate\Support\Facades\Redis::class,
'Request' => Illuminate\Support\Facades\Request::class,
'Response' => Illuminate\Support\Facades\Response::class,
'Route' => Illuminate\Support\Facades\Route::class,
'Schema' => Illuminate\Support\Facades\Schema::class,
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class
],

然後在laravel框架處理請求過程中,會通過Illuminate\Foundation\Bootstrap\RegisterFacades這個類來註冊這些別名到全域性環境裡面:

– Hide code

<?php
namespace Illuminate\Foundation\Bootstrap;
use Illuminate\Support\Facades\Facade;
use Illuminate\Foundation\AliasLoader;
use Illuminate\Contracts\Foundation\Application;
class RegisterFacades
{
/**
* Bootstrap the given application.
*
* @param  \Illuminate\Contracts\Foundation\Application  $app
* @return void
*/
public function bootstrap(Application $app)
{
Facade::clearResolvedInstances();
Facade::setFacadeApplication($app);
AliasLoader::getInstance($app->make('config')->get('app.aliases', []))->register();
}
}

所以我們才能直接通過別名,代替完整的型別名做同樣的訪問功能。如果你自己寫了一些類,名稱很長,並且在程式碼裡面用的特別多,也可以考慮配置到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容器例項的結構:

– Hide code

<?php
Route::get('/', function () {
dd(app());
return '';
});

結果如下: 
image 
從這個結構可以看出,laravel容器例項上包含了很多的陣列,其中紅框部分的陣列,從名字也可以猜測出它們跟後面要介紹的服務,服務提供者與服務別名之間的聯絡。理清這幾個陣列的儲存結構,自然就明白了laravel容器如何管理服務。

4. 如何理解服務繫結與解析

淺義層面理解,容器既然用來儲存物件,那麼就要有一個物件存入跟物件取出的過程。這個物件存入跟物件取出的過程在laravel裡面稱為服務的繫結與解析。

先來看服務繫結,在laravel裡面,服務繫結到容器,有多種形式:

– Hide code

app()->singleton('service', 'this is service1');
app()->singleton('service2', [
'hi' => function(){
//say hi
}
]);
class Service {
}
app()->singleton('service3', function(){
return new Service();
});

singleton是laravel服務繫結的方法之一,詳細作用後面會介紹,目前只是用它來展現服務繫結的形式。籠統的說容器的時候,我們說容器管理的是服務物件,但是laravel的容器可以管理不僅僅是物件,它能夠管理的是任意型別的資料,包括基本資料型別和物件。所以在服務繫結的時候,我們也可以繫結任意的資料,正如以上程式碼展示的那樣。在繫結的時候,我們可以直接繫結已經初始化好的資料(基本型別、陣列、物件例項),還可以用匿名函式來繫結。用匿名函式的好處在於,這個服務繫結到容器以後,並不會立即產生服務最終的物件,只有在這個服務解析的時候,匿名函式才會執行,此時才會產生這個服務對應的服務例項。

實際上,當我們使用singleton,bind方法以及陣列形式,(這三個方法是後面要介紹的繫結的方法),進行服務繫結的時候,如果繫結的服務形式,不是一個匿名函式,也會在laravel內部用一個匿名函式包裝起來,這樣的話, 不輪繫結什麼內容,都能做到前面介紹的懶初始化的功能,這對於容器的效能是有好處的。這個可以從bind的原始碼中看到一些細節:

image

服務繫結時的第一個引數就是服務的繫結名稱。服務繫結完成後,容器會把這個服務的繫結記錄儲存到例項屬性bindings裡面:

image

這個bindings裡面的每條記錄,代表一個服務繫結。它的key值就是服務的繫結名稱,value值也是一個陣列,這個陣列的concrete屬性就是服務繫結時產生的匿名函式,也就是閉包;另外一個參數列示這個服務在多次解析的時候,是否只返回第一次解析得到的物件。這個引數在介紹服務繫結方法時會再繼續介紹。

接下來看看服務繫結的幾種方法及區別:

a. 通過bind方法

– Hide code

app()->bind('service', function(){
return new Service();
},true);

bind是laravel服務繫結的底層方法,它的簽名是:

image

第一個引數服務繫結名稱,第二個引數服務繫結的結果,第三個引數就表示這個服務是否在多次解析的時候,始終返回第一次解析出的例項。它的預設值是false,意味著這樣的服務在每次解析的時候都會返回一個新的例項。它的值與bindings裡面服務繫結記錄value陣列裡面的share屬性是對應的。

b. 通過singleton方法

舉例略。它跟bind的區別在於,它始終是以shared=true的形式進行服務繫結,這是因為它的原始碼是這樣的:

image

c. 通過陣列的形式

– Hide code

app()['service'] = function(){
return new Service();
};

為什麼可以直接把容器例項直接當成陣列來用呢,這是因為容器實現了php的ArrayAccess介面:

– Hide code

/**
* Set the value at a given offset.
*
* @param  string  $key
* @param  mixed   $value
* @return void
*/
public function offsetSet($key, $value)
{
// If the value is not a Closure, we will make it one. This simply gives
// more "drop-in" replacement functionality for the Pimple which this
// container's simplest functions are base modeled and built after.
if (! $value instanceof Closure) {
$value = function () use ($value) {
return $value;
};
}
$this->bind($key, $value);
}

所以實際上以上這種陣列形式的繫結實際上相當於沒有第三個引數的bind方法。

再來看服務的解析。上面的內容都是在說明把如何獲取服務例項的方式繫結到容器,那麼如何從容器獲取到需要的服務例項呢?這個過程就是服務解析,在laravel裡面通過make方法來完成服務的解析:

– Hide code

$service= app()->make('service');

這個方法接收兩個引數,第一個是服務的繫結名稱和服務繫結名稱的別名,如果是別名,那麼就會根據服務繫結名稱的別名配置,找到最終的服務繫結名稱,然後進行解析;第二個引數是一個陣列,最終會傳遞給服務繫結產生的閉包。

我們可以通過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沒有覆蓋,不影響內容的介紹。

– Hide code

public function make($abstract, array $parameters = [])
{
$abstract = $this->getAlias($this->normalize($abstract));
// If an instance of the type is currently being managed as a singleton we'll
// just return an existing instance instead of instantiating new instances
// so the developer can keep using the same objects instance every time.
if (isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}
$concrete = $this->getConcrete($abstract);
// We're ready to instantiate an instance of the concrete type registered for
// the binding. This will instantiate the types, as well as resolve any of
// its "nested" dependencies recursively until all have gotten resolved.
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete, $parameters);
} else {
$object = $this->make($concrete, $parameters);
}
// If we defined any extenders for this type, we'll need to spin through them
// and apply them to the object being built. This allows for the extension
// of services, such as changing configuration or decorating the object.
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
// If the requested type is registered as a singleton we'll want to cache off
// the instances in "memory" so we can return it later without creating an
// entirely new instance of an object on each subsequent request for it.
if ($this->isShared($abstract)) {
$this->instances[$abstract] = $object;
}
$this->fireResolvingCallbacks($abstract, $object);
$this->resolved[$abstract] = true;
return $object;
}

從這個原始碼可以看到:

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

b. 如果這個服務是一個shared為true的服務繫結,且之前已經做過解析的話,就會直接返回之前已經解析好的物件;

c. 如果這個服務是一個shared為true的服務繫結,並且是第一次解析的話,就會把已解析的物件存入到instances這個容器屬性裡面去,也就是說只有shared為true的服務繫結,在解析的時候才會往instances屬性裡面存入記錄,否則不會存入;

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

e. resolved,instances陣列的key值跟bindings陣列的key值一樣,都是服務繫結名稱;

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

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

a. make方法

b. 陣列形式

– Hide code

app()['service'];

這個的原理還是跟容器實現了ArrayAccess的介面有關係:

– Hide code

public function offsetGet($key)
{
return $this->make($key);
}

所以陣列形式的訪問跟不使用第二個引數的make方法形式是一樣的。

c. app($service)的形式

– Hide code

app('service');

看了app這個help函式的原始碼就明白了:

– Hide code

function app($make = null, $parameters = [])
{
if (is_null($make)) {
return Container::getInstance();
}
return Container::getInstance()->make($make, $parameters);
}

原來app這個函式在第一個引數為空的時候,返回的是容器例項本身。在有引數的時候等價於呼叫容器例項的make方法。

以上就是服務繫結與解析的主要內容,涉及的要點較多,希望描述的比較清楚。

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

前面介紹了服務的繫結。那麼服務的繫結應該在哪個位置處理呢?雖然說,能夠拿到容器例項的地方,就都能進行服務的繫結;但是我們使用服務的繫結的目的,是為了在合適的位置解析出服務例項並使用,如果服務繫結的位置過於隨意,那麼就很難保證在解析的位置能夠準確的解析出服務例項。因為服務能夠解析的前提是服務繫結的程式碼先與服務解析的程式碼執行;所以,服務繫結通常會在應用程式初始化的時候進行,這樣才能保證業務程式碼中(通常是router和controller裡面)一定能解析出服務例項。這個最佳的位置就是服務提供者。

服務提供者,在laravel裡面,其實就是一個工廠類。它最大的作用就是用來進行服務繫結。當我們需要繫結一個或多個服務的時候,可以自定義一個服務提供者,然後把服務繫結的邏輯都放在該類的實現中。在larave裡面,要自定一個服務提供者非常容易,只要繼承Illuminate\Support\ServiceProvider這個類即可。下面通過一個簡單的自定義服務提供者來說明服務提供者的一些要點:

– Hide code

<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
protected $defer = true;
public function boot()
{
//
}
public function register()
{
$this->app->singleton('service1', function(){
return 'service1';
});
$this->app->singleton('service2', function(){
return 'service2';
});
$this->app->singleton('service3', function(){
return 'service3';
});
}
public  function provides()
{
return ['service1','service2','service3'];
}
}

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

2)在這個舉例裡面,可以看到有一個register方法,這個方法是ServiceProvider裡面定義的。自定義的時候,需要重寫它。這個方法就是用來繫結服務的。你可以在這個服務裡面,根據需要加入任意數量的服務繫結。前面要介紹過,在服務提供者裡面,始終能通過$this->app拿到容器例項,所以上面的舉例中,我們直接用這種方式來完成服務繫結。這個方法是怎麼完成服務繫結的呢?因為當laravel找到這個服務提供者的類以後,就會初始化這個服務提供者類,得到一個服務提供者的物件,然後呼叫它的register方法,自然它裡面的所有服務繫結程式碼就都會執行了。

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

– Hide code

public function registerConfiguredProviders()
{
$manifestPath = $this->getCachedServicesPath();
(new ProviderRepository($this, new Filesystem, $manifestPath))
->load($this->config['app.providers']);
}

這個程式碼是在Illuminate\Foundation\Application的原始碼裡面拿出來的,從中你能看到laravel會把所有的自定義服務提供者都註冊進來。這個註冊的過程其實就是前面說的例項化服務提供者的類,並呼叫register方法的過程。

3). 從上一步的原始碼也能看到,laravel載入自定義服務提供者的時候,實際是從config/app.php這個配置檔案裡面的providers配置節找到所有要註冊的服務提供者的。

– Hide code

'providers' => [
/*
* Laravel Framework Service Providers...
*/
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
Illuminate\Cache\CacheServiceProvider::class,
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
Illuminate\Cookie\CookieServiceProvider::class,
Illuminate\Database\DatabaseServiceProvider::class,
Illuminate\Encryption\EncryptionServiceProvider::class,
Illuminate\Filesystem\FilesystemServiceProvider::class,
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
Illuminate\Hashing\HashServiceProvider::class,
Illuminate\Mail\MailServiceProvider::class,
Illuminate\Notifications\NotificationServiceProvider::class,
Illuminate\Pagination\PaginationServiceProvider::class,
Illuminate\Pipeline\PipelineServiceProvider::class,
Illuminate\Queue\QueueServiceProvider::class,
Illuminate\Redis\RedisServiceProvider::class,
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
Illuminate\Session\SessionServiceProvider::class,
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
/*
* Package Service Providers...
*/
//
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
Xavrsl\Cas\CasServiceProvider::class,
ThirdProviders\CasServer\CasServerProvider::class
],

所以你如果自己寫了一個服務提供者,那麼只要配置到這裡面,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方法就清楚了: 

    – Hide code

    public function make($abstract, array $parameters = [])
    {
    $abstract = $this->getAlias($abstract);
    if (isset($this->deferredServices[$abstract])) {
    $this->loadDeferredProvider($abstract);
    }
    return parent::make($abstract, $parameters);
    }

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)如何指定和使用服務繫結名稱的別名

假如有一個服務做如下繫結:

– Hide code

app()->singleton('service1', function(){
new CasServerManager();
});

那麼可以通過容器方法alias方法指定別名:

– Hide code

app()->alias('service1', 'alias_a');

這個方法的第一個引數是服務繫結名稱,第二個引數是別名。這個方法呼叫後,就會在容器例項屬性aliases陣列裡面存入一條記錄:

image 
image

你看剛才舉例中的別名就已經新增到這個陣列裡面。這個陣列裡面每條記錄的key值都是別名。但是value有可能是服務繫結名稱,也有可能是另外一個別名。這是因為別名是可以遞迴的。

2)別名支援遞迴

也就是說,可以對別名再指定別名:

– Hide code

app()->alias('alias_a', 'alias_b');
app()->alias('alias_b', 'alias_c');

image

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

在解析服務的時候,會先確定這個服務名稱是否為一個別名(只要看看在aliases陣列裡是否存在記錄即可),如果不是別名,直接用這個服務名稱進行解析。如果這個服務名稱是一個別名,那麼就會通過呼叫的方式,找到最終的服務名稱:

image

如下所有的服務解析都是等價的:

– Hide code

app('alias_c');
app('alias_b');
app('alias_a');
app('service1');

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

可以在服務繫結的時候,進行別名的指定。只要按照如下的方式進行繫結即可:

– Hide code

app()->singleton(['service1' => 'alias'], function(){
new CasServerManager();
});

也就是把服務繫結名稱換成陣列形式而已。陣列記錄的key值就是服務名稱,value值就是別名。

7. 依賴注入的機制

– Hide code

<?php
class Service{
protected $app;
public function __construct(\Illuminate\Contracts\Foundation\Application $app)
{
$this->app = $app;
}
}
app()->singleton(Service::class);
Route::get('/', function () {
dd(app(Service::class));
return '';
});

在這個舉例中,定義了一個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初始化的時候會被註冊:

– Hide code

public function registerCoreContainerAliases()
{
$aliases = [
'app'                  => ['Illuminate\Foundation\Application', 'Illuminate\Contracts\Container\Container', 'Illuminate\Contracts\Foundation\Application'],
'auth'                 => ['Illuminate\Auth\AuthManager', 'Illuminate\Contracts\Auth\Factory'],
'auth.driver'          => ['Illuminate\Contracts\Auth\Guard'],
'blade.compiler'       => ['Illuminate\View\Compilers\BladeCompiler'],
'cache'                => ['Illuminate\Cache\CacheManager', 'Illuminate\Contracts\Cache\Factory'],
'cache.store'          => ['Illuminate\Cache\Repository', 'Illuminate\Contracts\Cache\Repository'],
'config'               => ['Illuminate\Config\Repository', 'Illuminate\Contracts\Config\Repository'],
'cookie'               => ['Illuminate\Cookie\CookieJar', 'Illuminate\Contracts\Cookie\Factory', 'Illuminate\Contracts\Cookie\QueueingFactory'],
'encrypter'            => ['Illuminate\Encryption\Encrypter', 'Illuminate\Contracts\Encryption\Encrypter'],
'db'                   => ['Illuminate\Database\DatabaseManager'],
'db.connection'        => ['Illuminate\Database\Connection', 'Illuminate\Database\ConnectionInterface'],
'events'               => ['Illuminate\Events\Dispatcher', 'Illuminate\Contracts\Events\Dispatcher'],
'files'                => ['Illuminate\Filesystem\Filesystem'],
'filesystem'           => ['Illuminate\Filesystem\FilesystemManager', 'Illuminate\Contracts\Filesystem\Factory'],
'filesystem.disk'      => ['Illuminate\Contracts\Filesystem\Filesystem'],
'filesystem.cloud'     => ['Illuminate\Contracts\Filesystem\Cloud'],
'hash'                 => ['Illuminate\Contracts\Hashing\Hasher'],
'translator'           => ['Illuminate\Translation\Translator', 'Symfony\Component\Translation\TranslatorInterface'],
'log'                  => ['Illuminate\Log\Writer', 'Illuminate\Contracts\Logging\Log', 'Psr\Log\LoggerInterface'],
'mailer'               => ['Illuminate\Mail\Mailer', 'Illuminate\Contracts\Mail\Mailer', 'Illuminate\Contracts\Mail\MailQueue'],
'auth.password'        => ['Illuminate\Auth\Passwords\PasswordBrokerManager', 'Illuminate\Contracts\Auth\PasswordBrokerFactory'],
'auth.password.broker' => ['Illuminate\Auth\Passwords\PasswordBroker', 'Illuminate\Contracts\Auth\PasswordBroker'],
'queue'                => ['Illuminate\Queue\QueueManager', 'Illuminate\Contracts\Queue\Factory', 'Illuminate\Contracts\Queue\Monitor'],
'queue.connection'     => ['Illuminate\Contracts\Queue\Queue'],
'queue.failer'         => ['Illuminate\Queue\Failed\FailedJobProviderInterface'],
'redirect'             => ['Illuminate\Routing\Redirector'],
'redis'                => ['Illuminate\Redis\Database', 'Illuminate\Contracts\Redis\Database'],
'request'              => ['Illuminate\Http\Request', 'Symfony\Component\HttpFoundation\Request'],
'router'               => ['Illuminate\Routing\Router', 'Illuminate\Contracts\Routing\Registrar'],
'session'              => ['Illuminate\Session\SessionManager'],
'session.store'        => ['Illuminate\Session\Store', 'Symfony\Component\HttpFoundation\Session\SessionInterface'],
'url'                  => ['Illuminate\Routing\UrlGenerator', 'Illuminate\Contracts\Routing\UrlGenerator'],
'validator'            => ['Illuminate\Validation\Factory', 'Illuminate\Contracts\Validation\Factory'],
'view'                 => ['Illuminate\View\Factory', 'Illuminate\Contracts\View\Factory'],
];
foreach ($aliases as $key => $aliases) {
foreach ($aliases as $alias) {
$this->alias($key, $alias);
}
}
}

依賴注入更多地用在介面程式設計當中,就像上面的舉例類似。再看一個自定義的例子:

– Hide code

<?php
interface Inter{
public function method();
}
class InterImpl implements Inter{
public function method(){
//
}
}
class Service{
protected $inter;
public function __construct(Inter $inter)
{
$this->inter = $inter;
}
}
app()->singleton(Inter::class,InterImpl::class);
app()->singleton(Service::class);
Route::get('/', function () {
dd(app(Service::class));
return '';
});

按介面進行程式設計,像Service這種業務類,只需要宣告自己需要一個Inter型別的例項即可。介面的好處在於解耦,將來要更換一種Inter的實現,不需要改Service的程式碼,只需要在例項化Service的時候,傳入另外一個Inter的例項即可。有了依賴注入以後,也不用改Service例項化的程式碼,只要把Inter這個服務型別,重新做一個繫結,繫結到另外一個實現即可。

– Hide code

app()->singleton(Inter::class,InterImpl2::class);

8. 其它

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

1) 容器例項的instance方法

這個方法其實也是完成繫結的作用,但是它跟前面介紹的三種繫結方法不同,它是把一個已經存在的例項,繫結到容器:

– Hide code

$service = new Service();
app()->instance('service',$service);

這是它的原始碼:

– Hide code

public function instance($abstract, $instance)
{
$abstract = $this->normalize($abstract);
// First, we will extract the alias from the abstract if it is an array so we
// are using the correct name when binding the type. If we get an alias it
// will be registered with the container so we can resolve it out later.
if (is_array($abstract)) {
list($abstract, $alias) = $this->extractAlias($abstract);
$this->alias($abstract, $alias);
}
unset($this->aliases[$abstract]);
// We'll check to determine if this type has been bound before, and if it has
// we will fire the rebound callbacks registered with the container and it
// can be updated with consuming classes that have gotten resolved here.
$bound = $this->bound($abstract);
$this->instances[$abstract] = $instance;
if ($bound) {
$this->rebound($abstract);
}
}

從這個程式碼可以看到,instance方法,會直接把外部例項化好的物件,直接儲存到容器的instances裡面。如果這個服務繫結名稱存在bindings記錄,那麼還會做一下重新繫結的操作。也就是說,通過intance方法繫結,是直接繫結服務例項,而原來的bind方法其實只是繫結了一個閉包函式,服務例項要到解析的時候才會建立。

2) 容器例項的share方法

容器例項的singleton方法,繫結的服務在解析的時候,始終返回第一次解析的物件。還有一個方式也能做到這個效果,那就是使用share方法包裝服務繫結的匿名函式:

– Hide code

$this->app['cas'] = $this->app->share(function()
{
$config = $this->app['config']->get('cas');
return new CasManager($config);
});

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

image

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

全文完,感謝閱讀~

如果您覺得本文對你有用,不妨幫忙點個贊,或者在評論裡給我一句讚美,小小成就都是今後繼續為大家編寫優質文章的動力,流雲拜謝! 歡迎您持續關注我的部落格:)

作者:流雲諸葛

出處:http://www.cnblogs.com/lyzg/

版權所有,歡迎保留原文連結進行轉載:)