文章內容純屬個人對理解,如有不對的地方還望大佬多多指教。文章內容基於Laravel5.5
,加黑字型的是關鍵詞可以幫助加深理解。
核心概念
Laravel的核心概念是服務容器和服務提供者,他們是框架的基礎。框架提供的所有能力都由服務提供者引導繫結到容器中。容器主要定義了一些繫結和解析服務的方式,這些方式也可以理解為容器對外提供的介面。服務提供者則引導了容器如何去解析和例項化該服務。下面詳細剖析服務容器和服務提供者的概念:
1、服務容器
服務容器是一個用於管理類依賴以及實現依賴注入的強有力工具,它主要有laravel/framework/src/Illuminate/Container/Container.php
類提供服務,文中具體程式碼可以在該類中找到。
1.1 繫結服務介面
容器類用來管理繫結關係的屬性主要有兩個bindings
和instance
,其中instance
屬性的繫結關係是單例的。下面看繫結服務的幾種方式:
// 1、透過bind繫結
$this->app->bind('redis.connection', function ($app) {
return $app['redis']->connection();
});
// 2、繫結單例
$this->app->singleton('redis', function ($app) {
$config = $app->make('config')->get('database.redis');
return new RedisManager(Arr::pull($config, 'client', 'predis'), $config);
});
// 3、直接繫結例項(單例)
$this->app->instance('test', new Test);
再看這三種繫結方式的具體實現:
1.1.1bind
函式
bind
在將繫結關係放入到了bindings
屬性,此處只是繫結抽象和實現的關係,具體實現不會被例項化。
// 解釋:將抽象$abstract繫結到具體的實現$concrete。$shared定義該實現是否為單例
public function bind($abstract, $concrete = null, $shared = false)
{
// 刪除alias和instance的繫結,將抽象$abstract繫結到$bindings
$this->dropStaleInstances($abstract);
// 如果沒有給出具體型別,則設定具體型別為該抽象型別
if (is_null($concrete)) {
$concrete = $abstract;
}
// 如果具體的實現$concrete不是匿名函式、則把它包裝為匿名函式
if (! $concrete instanceof Closure) {
$concrete = $this->getClosure($abstract, $concrete);
}
// 將抽象的實現繫結到bindings屬性
$this->bindings[$abstract] = compact('concrete', 'shared');
// 如果該抽象已在容器中解析,則觸發該抽象繫結的回撥。
if ($this->resolved($abstract)) {
$this->rebound($abstract);
}
}
1.1.2singleton
函式
可見singleton
函式呼叫的是bind
,第三個引數shared
引數為true
,指定該服務為單例。
// 繫結單例
public function singleton($abstract, $concrete = null)
{
$this->bind($abstract, $concrete, true);
}
1.1.3instance
函式
instance
函式繫結的是例項,也可以是其他基礎型別,解析時作為單例處理。
// 繫結例項
public function instance($abstract, $instance)
{
// 刪除abstractAliases的繫結
$this->removeAbstractAlias($abstract);
// 檢查是否已繫結到$bindings、$instances或$aliases
$isBound = $this->bound($abstract);
unset($this->aliases[$abstract]);
$this->instances[$abstract] = $instance;
// 如果該抽象已繫結過(屬於重複繫結),則觸發該抽象繫結的回撥。
if ($isBound) {
$this->rebound($abstract);
}
return $instance;
}
關於繫結大概就這些,此處先不考慮服務延遲載入,上下文繫結等等…先拋開復雜的處理看下繫結的本質,下面看下對解析的處理。
2.2 解析服務介面
解析對外提供的函式有app()->make()
,app()->makeWith()
,resolve()
,app()
,app()->get()
這幾個,其中app()->get()
會提前判斷要解析的服務是否存在,不存在話丟擲EntryNotFoundException
異常,其他介面則直接丟擲ReflectionException
異常。最終的解析邏輯都一樣,主要依賴容器中的resolve
和build
函式,下面具體看下這兩個函式的實現:
2.2.1 resolve
函式
protected function resolve($abstract, $parameters = [])
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);
// 對單例服務的處理,不需要處理上下文的情況下,如果存在直接返回,不會重新new一個新的例項
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
$this->with[] = $parameters;
// 嘗試從bindings中獲取具體實現
$concrete = $this->getConcrete($abstract);
// 如果抽象$abstract和實現$concrete完全相等 或者 $concrete是匿名函式,則用build去解析,否則將具體實現$concrete再走一遍make,最終也是走本介面,可以看作是一個遞迴呼叫
if ($this->isBuildable($concrete, $abstract)) {
// 這快處理了類的例項化,依賴檢查等,下面具體看
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
// 對這個服務做一些擴充套件處理
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
// 對單例的處理
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
// 觸發解析回撥
$this->fireResolvingCallbacks($abstract, $object);
// 標記抽象已被解析
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;
}
resolve
它主要做了對單例的處理,上下文檢查,解析事件的觸發和服務的擴充套件性處理。
2.2.2 build
函式
此處是依賴處理和例項化物件的關鍵。
public function build($concrete)
{
// 如果具體實現是個匿名函式,直接呼叫匿名函式,由其返回實現即可。
if ($concrete instanceof Closure) {
// getLastParameterOverrid的用意是?
return $concrete($this, $this->getLastParameterOverride());
}
// 獲取類的反射例項
$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);
}
到這裡,服務繫結和服務解析就差不多了。這些東西有什麼用呢?下面再看服務提供者中怎麼使用它們。
2、服務提供者
所有的服務提供者都繼承於Illuminate\Support\ServiceProvider
抽象類,該抽象類對子類暴露了容器變數$app
用來操作容器。在服務提供者中包會含一個 register
和一個 boot
函式。在 register
方法中, 你可以呼叫容器對外提供的繫結介面註冊服務。boot
函式會在所有服務註冊完之後依次呼叫。下面看一個具體的服務提供者例子
namespace App\Providers;
use Riak\Connection;
use Illuminate\Support\ServiceProvider;
/**
* 繼承自ServiceProvider類
*/
class RiakServiceProvider extends ServiceProvider
{
/**
* 是否延時載入該服務提供者,此處意味著在框架啟動階段不會呼叫register函式進行服務繫結,只會將該服務放入延遲載入列表,這些資料會放在bootstrap/cache/services.php檔案中,該檔案包含了所有服務提供者提供的所有服務,框架每次啟動時都會檢查是否需要重新生成。
* @var bool
*/
protected $defer = true;
// 在服務容器裡註冊(框架啟動階段呼叫)
public function register()
{
$this->app->singleton(Connection::class, function ($app) {
return new Connection(config('riak'));
});
$this->app->bind('redis', function ($app) {
return new Redis;
});
$this->app->instance('test', new Test);
}
// 所有的服務提供者載入完後,會依次呼叫boot函式
public function boot()
{
... code
}
/**
* 獲取提供器提供的服務。
* 此處用於生成bootstrap/cache/services.php檔案時獲取該提供者提供的服務列表
* @return array
*/
public function provides()
{
return [Connection::class, 'redis', 'test'];
}
}
新增完服務提供者之後,需要將該提供者在config/app.php
配置檔案中註冊,才會生效。然後你就可以在框架的任何地方使用了。
3、具體使用
怎麼使用呢?這要看框架支援怎樣的依賴注入了,主要有以下幾種方式:
3.1、建構函式注入
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class TestController
{
/**
* TestController constructor.
* 這裡$request變數已經是Request的例項了,不用顯示的new(也不能去new,Request建構函式也會依賴其他服務,這些依賴處理交給框架的服務容器去解析就好了)
*/
public function __construct(Request $request)
{
dump($request->all());
}
}
3.2、函式引數注入
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class TestController
{
/**.
* 和建構函式注入同理
*/
public function hello(Request $request)
{
dump('Hello'.$request->name);
}
}
3.3、手動解析
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class TestController
{
public function test(Request $request)
{
// 1、解析服務,這裡取出來的redis物件是否為單例取決於哪種方式註冊
$redis = app()->make('redis');
$redis = app()->makeWith('redis', ['param' => 'test']);
$redis = resolve('redis');
$redis = app('redis');
$redis = app()->get('redis');
// 2、例項化類
app(\App\Test::class, ['var1' => 'var1'])
app(\App\Test::class, ['var2' => 'var2', 'var1' => 'var1'])
}
}
例項化類為什麼不直接new
呢?因為這樣例項化,不用處理Test
的依賴Request
。
// 示例類
<?php
namespace App;
use Illuminate\Http\Request;
class Test
{
public $var1;
public $var2;
/**
* Test constructor. * @param $var1
* @param $var2
*/
public function __construct(Request $request, $var1, $var2 = 'var2')
{
$this->var1 = $var1;
$this->var2 = $var2;
// 不用處理,交給容器解析
dump($request->all());
}
}
4、總結
文章主要講了容器對外提供的繫結和解析服務的幾種方式,隨後講了服務提供者對容器的使用,也給了具體的例子,希望對大家有所幫助。文中只給出了一些基本的實現邏輯,關於上下文的處理,有興趣的可以結合社群文件繼續深挖。
本作品採用《CC 協議》,轉載必須註明作者和本文連結