概念
生活中,容器就是一個存放物品的器具。平時可以把雜亂的東西都放到容器中,要用的時候再去容器中拿。
類似的,Laravel 的服務容器也像一個存放服務的類,通過把要使用的服務綁定到容器中,然后在需要使用的時候從容器中獲取(它還可以通過反射自動創建服務依賴的服務)。
Laravel 框架中所有的服務都是通過容器來創建與獲取的,這樣使得所有的服務都依賴容器而不相互依賴,避免服務之間的過度耦合,讓后面服務的變化變得簡單。
使用
容器主要包括兩種方法:綁定和獲取。將需要的服務綁定到容器中,需要的時候再從容器中獲取。
服務綁定
下面是容器提供綁定的主要方法:
$app
為容器的實例。
- 簡單綁定
bind
方法。綁定的內容可以是閉包函數,類名字符串。
// 綁定閉包
$app->bind('redis.connection', function ($app) {
return $app['redis']->connection();
});
// 綁定類名
$app->bind('database', MySQLDatabase::class);
- 單例綁定
singleton
方法。綁定的內容可以是閉包函數,類名字符串。
$app->singleton('redis', function ($app) {
// ...
return new RedisManager($driver, $config);
});
單例綁定和普通綁定的 區別 是在后面獲取綁定內容時,單例綁定的會一直返回同一個對象,而普通綁定的每次獲取都會得到一個重新實例化的對象。
- 綁定實例
instance
方法。綁定的內容是現有的對象。
$this->app->instance('HelpSpot\API', new HelpSpot\API(new HttpClient));
- 綁定初始化數據
容器可以通過反射,在初始化的時候獲得需要注入的對象,但如果要指定初始化的具體值,可以在綁定的時候單獨設置
<?php
// 如果 UserConstroller 類的構造方法如下
public function __construct($variableName) {
// 其他操作 ...
}
// 綁定初始化的數據如下
$app->when('App\Http\Controllers\UserController')
->needs('$variableName')
->give($value);
- 綁定接口到實現
將接口的一個實現綁定到接口,當從容器獲取接口的實例時,會返回綁定的實現。這樣就可以實現面線接口編程。
bind
、single
方法都可以,與上面的區別是第一個參數是接口名稱。
// 項目開始使用極光推送服務
$app->bind(
'App\Contracts\Pusher',
'App\Services\JPusher'
);
// 開始推送
$app->make('App\Contracts\Pusher')->push($message);
// 后面項目改為通過信鴿推送,我們只需要修改綁定的地方,實際推送地方不需要任何修改
$app->bind(
'App\Contracts\Pusher',
'App\Services\XgPusher'
);
- 上下文綁定
有時候可能兩個不同的類使用了相同的接口,但是它們需要使用不同的實現類,可以通過下面方法對指定類綁定指定的實現類。
// 在 PhotoController 類中使用 Filesystem 接口時,提供 Storage::disk('local') 的實現
$app->when(PhotoController::class)
->needs(Filesystem::class)
->give(function () {
return Storage::disk('local');
});
服務獲取
服務獲取指從容器中獲取服務的方法。
- 直接
make
、makeWith
獲取對象
容器提供$app->make($abstract)
方法獲取指定字符串或接口對應的實現。
makeWith()
方法可以在指定對象實例化的參數。 - 全局的
app()
函數
其實app($abstract = null, array $parameters = [])
函數也是調用容器的make
方法去獲得對象
app($abstract);
- 通過容器的屬性獲取對象
因為 Laravel 容器實現了 ArrayAccess 接口,當獲取容器沒有定義的屬性時會調用 ArrayAccess 接口的offsetGet
方法,而offsetGet
方法會調用容器的make
方法來獲取對象。
// 獲取 $abstract 對應的對象
$app->$abstract;
- 通過依賴注入
注意: 只有由容器創建類或調用方法時,容器會通過反射去自動注入;如果是手動 new 去創建對象,是不會自動依賴注入的。
class AuthController extends Controller
{
// login 是控制器的一個方法,框架路由到這個方法時,
// 會根據 $request 接口類型,自動實例化 $request 對象
public function login(Request $request) {
// 這里的參數 $request 如果不是 login 的入參,需要由用戶手動傳入
$this->validateLogin($request);
// ...
}
protected function validateLogin(Request $request) {
// ...
}
}
源碼閱讀
下圖是容器主要方法的關系圖。
Laravel 在程序入口文件
bootstrap/app
中初始化 Illuminate\Foundation\Application
容器類,然后通過服務提供者將所需要的服務注冊到容器中,以供后面使用。下面主要分析
bind
方法(綁定)和 make
方法(獲取)。
bind
方法
<?php
/**
* 在容器中添加一個綁定。
*
* @param string|array $abstract 綁定的名稱
* @param \Closure|string|null $concrete 綁定的內容
* @param bool $shared 綁定內容是否在容器中共享(單例綁定就是設置的共享)
* @return void
*/
public function bind($abstract, $concrete = null, $shared = false)
{
// 刪除之前綁定名稱為 $abstract 的實例和別名
$this->dropStaleInstances($abstract);
// 如果 $concrete 為空,則將 $concrete 注冊為類名為 $abstract 的類。
if (is_null($concrete)) {
$concrete = $abstract;
}
// 如果 $concrete 是一個類名字符串,而不是閉包。需要將它包裝成為一個閉包,
// 方便后面生成綁定實例時的擴展
if (! $concrete instanceof Closure) {
$concrete = $this->getClosure($abstract, $concrete);
}
// 將 concrete、shared 的內容保存到 $this->bindings 數組中
$this->bindings[$abstract] = compact('concrete', 'shared');
// 判斷 綁定的名稱 在容器中是否已經被實例化過
if ($this->resolved($abstract)) {
// 將 $abstract 名稱綁定的對象重新實例化一次。
// 重新實例化后,如果 $abstract 有注冊重新實例化通知的回調,會出發重新實例化的回調
$this->rebound($abstract);
}
}
/**
* 對通過綁定名稱獲取對象的方法包裝為一個閉包 Get the Closure to be used when building a type.
*
* @param string $abstract 綁定名稱
* @param string $concrete 綁定內容
* @return \Closure
*/
protected function getClosure($abstract, $concrete)
{
// 返回一個匿名函數,入參為容器對象
return function ($container, $parameters = []) use ($abstract, $concrete) {
// 如果綁定名稱和綁定內容相同,直接通過反射獲取 綁定內容的對象
if ($abstract == $concrete) {
return $container->build($concrete);
}
// 通過 make 方法獲取綁定名稱為 $concrete 的綁定內容
return $container->makeWith($concrete, $parameters);
};
}
這里以 Laravel 中 bootstrap/app.php
中的內容為例:
<?php
//將類 'App\Http\Kernel' 綁定到接口 'Illuminate\Contracts\Http\Kernel' 上
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
綁定后容器中的內容如下:
容器
$app
中的 bindings
數組對應的鍵 Illuminate\Contracts\Http\Hernel
為綁定的名稱;綁定內容為數組,包含了兩個屬性:
-
concrete
綁定的內容。這里是一個閉包,第一個參數為this
容器本身,第二個參數為傳入的初始化參數。 -
shared
是否為共享。 因為這里是單例綁定,所以是true
。
綁定完成后,繼續看 Laravel 是如何從容器中獲取對象的。
make
方法
Illuminate\Foundation\Application 文件中的 make
方法:
<?php
/**
* 從容器中解析給定類名,得到對應的對象實例
*
* (重寫了 Container 中的 make 方法)
*
* @param string $abstract 給定類名字符串
* @return mixed
*/
public function make($abstract)
{
// 獲取 $abstract 類名的別名。
$abstract = $this->getAlias($abstract);
// 如果 $abstract 是延遲服務中定義的服務,則加載服務(讓服務在需要的時候再加載)
if (isset($this->deferredServices[$abstract])) {
$this->loadDeferredProvider($abstract);
}
// 調用 Container 中的 make 方法獲取 $abstract 類名對應的對象
return parent::make($abstract);
}
從綁定中容器中解析、并獲得給定類名對象的實現主要是在 Illuminate\Container\Container 文件:
<?php
// 這里實際是調用 resolve 方法生成、獲取給定類型的實例
public function make($abstract)
{
return $this->resolve($abstract);
}
protected function resolve($abstract, $parameters = [])
{
// 獲取類名的別名。如果別名存在,會去解析容器中別名對應的類
$abstract = $this->getAlias($abstract);
// 非主要部分,略過...
// 如果類名已經綁定到實例,且類名沒有設置上下文綁定,直接返回實例。
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
// 保存參數內容,
$this->with[] = $parameters;
// 從容器中獲取 $abstract 類綁定的內容;如果沒有綁定過,直接返回 $abstract 類名
$concrete = $this->getConcrete($abstract);
// 如果綁定的內容可以實例化,調用 build 方法實例化;否則地歸去容器解析。
// 可實例化的條件:
// 1. 類名與綁定內容一致
// 2. 綁定的內容是一個閉包
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
// ...
return $object;
}
// 構造綁定的內容
public function build($concrete)
{
// 如果綁定的內容 $concrete 是一個閉包,直接傳入參數,調用閉包
if ($concrete instanceof Closure) {
return $concrete($this, $this->getLastParameterOverride());
}
// 通過反射獲取 $concrete 反射類
$reflector = new ReflectionClass($concrete);
// 如果反射類不可初始化,拋出異常
if (! $reflector->isInstantiable()) {
return $this->notInstantiable($concrete);
}
$this->buildStack[] = $concrete;
// 獲取反射類的構造方法
$constructor = $reflector->getConstructor();
// 如果沒有構造方法,直接 new 類名
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
}
// 獲取構造函數的依賴參數
$dependencies = $constructor->getParameters();
// 通過反射獲取依賴參數的實例
$instances = $this->resolveDependencies(
$dependencies
);
array_pop($this->buildStack);
// 通過類名實例
return $reflector->newInstanceArgs($instances);
}
build
方法主要使用 PHP 的反射獲取類名的實例,反射知識參考Laravel 源碼學習 基礎知識。
以 Laravel public/index.php
中的內容為例:
<?php
// 獲取接口名稱 Illuminate\Contracts\Http\Kernel 的實例
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
由上面綁定部分可知,Illuminate\Contracts\Http\Kernel::class
在容器中實際綁定的內容是 App\Http\Kernel::class
類實例化的閉包。
在調用
build
方法之前 $concrete 是下面的閉包函數:
<?php
// 此時 $abstract 為 `Illuminate\Contracts\Http\Kernel::class`,
// $concrete 為 `App\Http\Kernel::class`,
// $app 為容器 $container 。
function ($container, $parameters = []) use ($abstract, $concrete) {
// 如果綁定名稱和綁定內容相同,直接通過反射獲取 綁定內容的對象
if ($abstract == $concrete) {
return $container->build($concrete);
}
// 通過 make 方法獲取綁定名稱為 $concrete 的綁定內容
return $container->makeWith($concrete, $parameters);
};
調用 build
方法后,$kernel
得到的內容是:
<?php
$kernel = $app->makeWith(App\Http\Kernel::class);
// 因為 App\Http\Kernal::class 沒有別名,通過 resolve 函數后實際為
$kernal = $app->build(App\Http\Kernel::class);
最后 Illuminate\Container\Container 容器中的 build
方法獲取 App\Http\Kernel::class
的實例。