Container
Laravel 的容器物件實現了「PSR-11」標準定義的相關規範。
基礎
Laravel 主要提供三大方面的功能
- 建立物件
- 依賴注入(DI)
- 管理物件
建立物件
binding
要讓容器建立物件,必須先告訴容器如何建立物件,這個過程在容器裡面叫做 binding ,對應在容器的API叫做 bind($abstract, $concrete = null, $shared = false)
。
引數介紹
$abstract
binding 的名稱,在建立物件的時候需要用到這個名稱。$concrete
可選值,也可以傳入一個 有返回值的Closure
型別,如果這個引數是null
,那麼$abstract
必須是一個完整的類名稱。$shared
是否共享,如果為true
容器只會建立一個該物件的例項。
例如:
class Animal
{
}
$container = \Illuminate\Container\Container::getInstance();
// 第一種
$container->bind(Animal::class); //在make該binding的物件的時候容器會建立Animal物件例項
// 定義建立物件閉包
$container->bind('OtherAnimal', function () {
return 'Other Animal';
});// 在make('OtherAnimal')的時候容器會返回一個`Other Animal`字串也就是閉包的返回值
make
binding 只是告訴容器如何建立物件,但是並沒有真正的建立物件。如果需要建立獲取 binding 的物件必須呼叫 make($abstract, array $parameters = [])
方法讓容器幫我們建立物件。
引數介紹
$abstract
binding 的名稱$parameters
建立物件的引數。被建立物件建構函式的引數必須是明確的或者是可構建的。
例如下面的程式碼會報錯,因為在 binding 的時候沒有制定引數如果建立,在 make 的時候也沒有生成,並且被建立物件的建構函式的引數既沒有制定預設值,也不是 Class型別。
class Animal
{
protected $name;
public function __construct($name)
{
$this->name = $name;
}
}
$container = \Illuminate\Container\Container::getInstance();
// 第一種用法:註冊
$container->bind(Animal::class);// 在容器裡面註冊一個 binding
$container->make(Animal::class);
上面的問題有兩種解決方式
在 binding 的時候就制定 $name
的值
$container->bind(Animal::class,function(){
return new Arimal("Tom");
});
另外一種方式就是在make的時候制定
$container->make(Animal::class,,array('name'=>'Tom'));
依賴注入(DI)
關於 DI 的介紹,可以看Wiki。
在 Laravel 裡面如果被建立的類的建構函式的引數是依賴的是一個物件的話,那麼它會去自動建立這個物件傳入。
例如
class UserProxy
{
}
class UserController
{
protected $proxy;
public function __construct(UserProxy $proxy)
{
$this->proxy = $proxy;
}
}
$container = \Illuminate\Container\Container::getInstance();
$container->bind(UserController::class);
$container->make(UserController::class);// 會自動建立一個UserProxy的物件傳給構造方法
管理物件
通常情況下在在一個生命週期內只會存在一個全域性容器物件,我們可以通過這個全域性的容器物件來建立,共享,銷燬物件。
通常我們會把一個複雜的業務邏輯分拆為一個個功能單一的方法,那麼我們如何做到資料共享呢?有兩種方法,一種是通過定義方法的引數
進行傳遞,但是如果方法的層次特別深的話,這種做法會顯得特別不方便,另外一種就是把資料儲存在容器物件裡面,需要的時候直接去容器
物件裡面去取。在 Laravel 使用的非常頻繁
例如
protected function bindPathsInContainer()
{
$this->instance('path', $this->path());
$this->instance('path.base', $this->basePath());
$this->instance('path.lang', $this->langPath());
$this->instance('path.config', $this->configPath());
$this->instance('path.public', $this->publicPath());
$this->instance('path.storage', $this->storagePath());
$this->instance('path.database', $this->databasePath());
$this->instance('path.resources', $this->resourcePath());
$this->instance('path.bootstrap', $this->bootstrapPath());
}
//在其他業務邏輯中直接使用
if (! function_exists('config_path')) {
/**
* Get the configuration path.
*
* @param string $path
* @return string
*/
function config_path($path = '')
{
return app()->make('path.config').($path ? DIRECTORY_SEPARATOR.$path : $path);
}
}
bind 與 resolve 相關細節
bind 的細節
public function bind($abstract, $concrete = null, $shared = false)
三種用法
- bind($abstract)
$abstract
必須是一個可建立的類的名稱 - bind($abstract,$class)
$abstract
是一個string
,class
是一個類名稱 - bind($abstract,$closure)
$abstract
是一個string
,$closure
是一個閉包型別
具體的實現細節
- 刪除
$abstract
對應的例項物件,如果存在的話 - 如果
$concrete
為空,那麼$abstract
賦值給$abstract
。也就是上面的第一種用法 - 儲存閉包和共享標識
- rebound 的處理
resolve 的細節
resolve 用來建立 bind 的型別。它有幾個包裝方法和別名方法
public function get($id)
public function make($abstract, array $parameters = [])
public function makeWith($abstract, array $parameters = [])
public function offsetGet($key)
陣列的形式訪問的支援
這些方法的底層方法都是 protected function resolve($abstract, $parameters = [])
resolve 具體的實現細節
- 獲取別名
- 標記建立物件是否明確的指定了引數或者上下文環境設定(如果是
true
則被建立的物件不能是共享物件) - 獲取物件建立的閉包物件
- 如果是閉包或者類,直接呼叫build方法建立,如果是巢狀方法就遞迴建立
public function build($concrete)
方法的實現細節
- 如果
$concrete
是閉包直接執行返回閉包的結果 - 建立
ReflectionClass
物件,反射的詳細介紹看Wiki和PHP反射 - 判斷給定的型別是否能建立物件
- 獲取建構函式物件,如果沒有建構函式則直接
new
給定的型別 - 獲取建構函式的引數物件
- 獲取/建立建構函式引數的值
- 建立返回物件
其他
擴充套件物件
在 bind 後,可以呼叫 public function extend($abstract, Closure $closure)
設定物件的擴充套件規則。
class Animal
{
public $extend = false;
}
$container = new \Illuminate\Container\Container();
$container->bind(Animal::class);
$container->extend(Animal::class, function (Animal $animal, \Illuminate\Container\Container $container) {
$animal->extend = true;
});
$animal = $container->make(Animal::class);// extend 為 true
hook機制
分別是
public function resolving($abstract, Closure $callback = null)
public function afterResolving($abstract, Closure $callback = null)
注意: hook和擴充套件機制設定的回掉函式最好把傳入的物件返回
陣列的方式操作容器
Container
實現了 ArrayAccess
介面,所以可以像使用「Array」 一樣操作容器物件。
ArrayAccess
定義如下介面
判斷指定的$key
是否存在
public function offsetExists($key)
{
return $this->bound($key);
}
獲取 binding 的物件
public function offsetGet($key)
{
return $this->make($key);
}
binding 型別到容器
public function offsetSet($key, $value)
{
$this->bind($key, $value instanceof Closure ? $value : function () use ($value) {
return $value;
});
}
刪除容器的 binding 和例項
public function offsetUnset($key)
{
unset($this->bindings[$key], $this->instances[$key], $this->resolved[$key]);
}
因為我們可以像下面這樣使用容器
$container = \Illuminate\Container\Container::getInstance();
if (!isset($container['name'])){// 等同於執行 public function offsetExists($key)
$container['name'] = function(){
return "This is Messi";
};// 等同於執行 public function offsetSet($key, $value) 方法
}
echo $container['name'];// 等同於執行 public function offsetGet($key)
如何閱讀原始碼
- 乾淨的container原始碼我已經準備好,直接
git clone git@github.com:dongyuhappy/laravel-container.git
下來後執行composer install
- 要理解原始碼邏輯的前提是要會用,關於怎麼用,看測試用例的程式碼比文件更直接。所有的測試用例都在 tests 目錄下
- 執行測試用例,改程式碼,執行測試用例,改程式碼........... 如此反覆
原文出自:https://iffor.me/index.php/archives/3/
本作品採用《CC 協議》,轉載必須註明作者和本文連結