Laravel 核心物件之 Container

iffor發表於2017-09-22

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 物件,反射的詳細介紹看WikiPHP反射
  • 判斷給定的型別是否能建立物件
  • 獲取建構函式物件,如果沒有建構函式則直接 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 協議》,轉載必須註明作者和本文連結

相關文章