Laravel源碼解析 — 服務容器

  • 2021 年 4 月 12 日
  • 筆記

前言

本文對將系統的對 Laravel 框架知識點進行總結,如果錯誤的還望指出

  • 閱讀書籍
    • 《Laravel框架關鍵技術解析》 陳昊
  • 學習課程
    • Laravel5.4快速開發簡書網站 軒脈刃
    • Laravel重構企業級電商項目 檀梵

服務容器

1.什麼是IoC

IOC 模式,不是一種技術,而是一種設計思想。在應用程式開發中,IoC 意味著將你設計好的對象交給容器控制,而不是傳統的在你的對象內部直接控制,也是一種面向介面編程的思想。

當我們以面向介面編程的時候,程式中實例之間的耦合將上升到介面層次,而不是程式碼實現層次,使用配置文件來實現類的耦合。

容器的作用很簡單,將在程式碼中使用像(new object)這樣語法進行耦合的方式,改為配置文件來管理耦合,通過這種改變,從而保證系統重構或者業務邏輯改變時,不會發生「牽一髮而動全身」的效果,從而有更好的可擴展性、可維護性。

總結:藉助於「第三方」實現具有依賴關係的對象之間的解耦。

舉個栗子

在日常開發應用中,我們要實現一個功能,在用戶處理模組中調用獲取用戶資訊Model,通常是使用像(new object)這樣的語法,在用戶處理模組中將對象創造出來,這時程式碼依賴耦合就出現了,在獲取用戶資訊 Model時則需要修改用戶處理模組里的程式碼。

而調用 IoC 容器則將依賴耦合上升到介面層次,只需要修改容器註冊時所綁定的服務即可。

其中涉及到 依賴注入控制反轉反射 的思想

2.控制反轉

控制反轉是將組件間的依賴關係從程式內部提到外部容器來管理,那麼就會出現,誰控制誰?反轉是什麼?有正轉嗎?

上述流程圖中

  1. 應用程式自身調用

    用戶處理模組 創建了 獲取用戶資訊Model ,那麼 用戶處理模組 控制了 獲取用戶資訊Model ,這種創建過程稱為 正轉

  2. IoC 模式調用

    創建權 交給了 IoC容器,由 Ioc 容器去創建 獲取用戶資訊Model ,那麼 Ioc 容器 控制了 獲取用戶資訊Model ,這種創建過程稱為 反轉

正轉:由程式本身在對象中主動控制去直接獲取依賴對象

反轉:由容器來幫忙建立及注入依賴對象

3.依賴注入(DI)

理解依賴注入我們需要先理解什麼是依賴,再理解依賴注入

依賴

在應用程式開發中由於某客戶類依賴於某個服務類稱為依賴

例:

// 實現不同交通工具類
// 腿著
class Leg
{
    public function go()
    {
        echo 'walk to Tibet!!!';
    }
}

// 開車
class Car
{
    public function go()
    {
        echo 'drive car to Tibet!!!';
    }
}

// 列車
class Train
{
    public function go()
    {
        echo 'go to Tibet!!!';
    }
}

// 設計旅遊者類,該類在實現游西藏的功能時要依賴交通工具類
class Traveller
{
    private $trafficTool;

    public function __construct()
    {
        $this->trafficTool = new Leg();
    }

    public function viisitTibet()
    {
        $this->trafficTool->go();
    }
}

$app = new Traveller();
$app->viisitTibet();

上述實例就是一個依賴的過程,當創建一個 Traveller 實例的時候,構造函數中獲取了(new Object)其中一個交通工具服務類,這時依賴就產生了,客戶類(Traveller)依賴於服務類(Leg),在實際開發中需求經常改動,那麼如果直接修改客戶類的程式碼就非常繁瑣並且不利於維護。

依賴注入

動態的向某個對象提供它所需要的其他對象,指組件的依賴通過外部以參數或其他形式注入,不在客戶類裡面用 (new object)的方式去實例化服務類而轉由外部來負責,並且面向介面編程,將參數轉為介面類,而不是具體的某個實現類,拓展性更強。

例:

// 設計公共介面
interface Visit
{
    public function go();
}
// 實現不同交通工具類
// 腿著
class Leg implements Visit
{
    public function go()
    {
        echo 'walk to Tibet!!!';
    }
}
// 開車
class Car implements Visit
{
    public function go()
    {
        echo 'drive car to Tibet!!!';
    }
}
// 列車
class Train implements Visit
{
    public function go()
    {
        echo 'go to Tibet!!!';
    }
}
// 設計旅遊者類,該類在實現游西藏的功能時要依賴交通工具類
class Traveller
{
    private $trafficTool;

    public function __construct( Visit $trafficTool)
    {
        $this->trafficTool = $trafficTool;
    }

    public function visitTibet()
    {
        $this->trafficTool->go();
    }
}

// 生成的交通工具依賴
$trafficTool = new Leg();
// 依賴注入的方式解決依賴問題
$app = new Traveller($trafficTool);
$app->visitTibet();

上述實例就是一個依賴注入的過程,當創建一個 Traveller 實例的時候,構造函數依賴一個外部的具有 Visit 介面的實例,我們傳遞一個 Leg 實例,即通過依賴注入的方式解決依賴問題。

這裡要注意的是,依賴注入需要通過介面來限制,不能隨意開放,這也體現了設計模式的另一個原則——針對介面編程,而不是針對實現編程。

4.反射

什麼是反射

PHP 反射機制是指在程式運行狀態中,動態獲取資訊以及動態調用對象。

這種動態獲取資訊以及動態調用對象的方法的功能稱為反射 API。

PHP 中獲取實例的資訊是通過 Reflection 實現

反射作用

為什麼要使用反射機制?直接創建(new)對象不就可以了嗎?

先理解動態調用對象與靜態調用對象的區別

動態調用對象

通過類的路徑或別名獲取關於類、方法、屬性、參數等詳細資訊,包括注釋,就算類成員定義為 private 也可以在外部訪問。

靜態調用對象

通過使用(new Object)的方式獲取類的實例。

程式碼靈活性

而編譯性語言區別更為明顯,靜態調用對象在編譯時確定實例的類型,綁定對象,動態調用對象則是在運行時確定實例的類型,提高了編譯性語言的靈活性,降低類之間的耦合

為什麼要使用反射機制

這樣就可以把調用一個實例的行為寫成一個類的方法或者創建函數,然後統一介面調用創建函數來創建實例對象,有點像工廠方法模式+面向介面編程的思想,這個類的方法和創建函數則是 IoC 容器

舉個栗子
<?php

class B
{

}

class A
{

    public function __construct(B $args)
    {
    }

    public function demo()
    {
        echo 'Hello world';
    }
}

//建立class A 的反射
$reflectionClass = new ReflectionClass('A');

$b = new B();

// 創建 class A 的實例
$instance = $reflectionClass->newInstanceArgs([$b]);
// 執行實例中的方法,輸出 『Hellow World』
$instance->demo();
// 獲取class A 的構造函數相關資訊
$constructor = $reflectionClass->getConstructor();
/**
 * 獲取class A 構造函數參數的相關資訊
 * 參數數組
 */
$dependencies = $constructor->getParameters();
foreach ($dependencies as $dependencie) {
    var_dump($dependencie->getClass());
    die;
}
// 獲取class A 的構造函數
var_dump($constructor);
// 獲取class A 的構造函數相關資訊
var_dump($dependencies);

5.再看IoC容器

我們再來看一下 IoC 容器的概念在日常開發應用中,我們在A服務中調用B服務要實現一個功能,或者要調用一個對象時都要使用像(new object)這樣的語法,將對象創造出來,這時你需要關心這個對象是什麼,在哪裡,如何創建,然後再創建,這時就出現了程式碼耦合,而IoC 容器就好比 」大象放進冰箱,大象取進冰箱,需要幾步「,需要三步

  1. 把冰箱門打開 2. 把大象放進去 3. 把冰箱門關上
  2. 把冰箱門打開 2. 把大象取出來 3. 把冰箱門關上

你不需要關心大象在哪,具體哪個大象,如何放進去,如何取出來,你只需要做到放和取這個動作就行了,這樣就避免了程式碼耦合,而

放大象,則需要將大象放到或者註冊到(bind)容器里

取大象,則需要將大象取出(make)就可以

服務容器可以理解為進階版的工廠模式,更是一種面向介面編程的思想。

工廠模式的大量應用降低了程式碼重複量以及利用率,但是依然還需要調用者去定位工廠。

最理想的情況是,調用者無需關心調用者的實現,也無需定位工廠,而面向介面配置化。

6.IoC容器程式碼

來看一段 IoC 容器程式碼,下面這段程式碼對 Laravel 的設計方法進行了簡化,不是 Laravel 的源碼, 而是來自一本書《laravel 框架關鍵技術解析》,這段程式碼很好的還原了 laravel 的服務容器的核心思想,程式碼有點長,可以嘗試運行調試一下,這樣易於理解:

// 設計容器類,容器類裝實例或提供實例的回調函數
class Container
{
    // 容器綁定數組
    // 用於裝提供實例的回調函數,真正的容器還會裝實例等其他內容,從而實現單例等高級功能
    protected $bindings = [];

    // 綁定介面和生成生成相應實例的回調函數
    public function bind($abstract, $concrete = null, $shared = false)
    {
        if (!$concrete instanceof Closure) {
            // 如果提供的參數不是回調參數,則產生默認的回調函數
            $concrete = $this->getClosure($abstract, $concrete);
        }

        $this->bindings[$abstract] = compact('concrete', 'shared');
    }

    // 默認生成實例的回調函數
    public function getClosure($abstract, $concrete)
    {
        // 生成實例的回調函數,$c 一般為 IoC 容器對象,在調用回調生成實例時提供
        // 即 build 函數中的 $concrete($this)
        return function ($c) use ($abstract, $concrete) {
            $method = ($abstract == $concrete) ? 'build' : 'make';
            // 調用的是容器的 build 或 make 方法生成實例
            return $c->$method($concrete);
        };
    }

    // 生成實例對象,首先解決介面和要實例化類之間的依賴關係
    public function make($abstract)
    {
        $concrete = $this->getConcrete($abstract);
        if ($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }

        return $object;
    }

    protected function isBuildable($concrete, $abstract)
    {
        return $concrete === $abstract || $concrete instanceof Closure;
    }

    // 獲取綁定的回調函數
    protected function getConcrete($abstract)
    {
        if (!isset($this->bindings[$abstract])) {
            return $abstract;
        }
        return $this->bindings[$abstract]['concrete'];
    }

    // 實例化對象
    public function build($concrete)
    {
        if ($concrete instanceof Closure) {
            return $concrete($this);
        }
        // ReflectionClass 類報告了一個類的有關資訊
        $reflector = new ReflectionClass($concrete);
        // 檢查類是否可實例化 return bool|false
        if (!$reflector->isInstantiable()) {
            echo $message = "Target [$concrete] is not instantiable.";
        }
        // 獲取類的構造函數
        $constructor = $reflector->getConstructor();
        if (is_null($constructor)) {
            return new $concrete;
        }

        // 獲取類構造函數的參數
        $dependencies = $constructor->getParameters();
        $instances = $this->getDependencies($dependencies);
        return $reflector->newInstanceArgs($instances);
    }

    protected function getDependencies($parameters)
    {
        $dependencies = [];
        foreach ($parameters as $parameter) {
            $dependency = $parameter->getClass();
            if (is_null($dependency)) {
                $dependencies[] = null;
            } else {
                $dependencies[] = $this->resolveClass($parameter);
            }
        }
        return (array)$dependencies;
    }

    protected function resolveClass(ReflectionParameter $parameter)
    {
        return $this->make($parameter->getClass()->name);
    }
}

上面的程式碼就生成了一個容器,下面是如何使用容器

// 實例化 IoC 容器
$app = new Container();
// 完成容器的填充
$app->bind("traveller", "Traveller");
$app->bind("Visit", "Train");

// 通過容器實現依賴注入,完成類的實例化
$tra = $app->make("traveller");
$tra->visitTibet();
$tra = $app->make("Visit");
$tra->go();

程式碼解析

當實例化一個容器類(Container)後,向容器中填充服務

$app->bind("traveller", "Traveller");
$app->bind("Visit", "Train");

綁定完成後,查看容器 $bindings 綁定的值

array(2) {
  ["traveller"]=>
  array(2) {
    ["concrete"]=>
    object(Closure)#2 (3) {
      ["static"]=>
      array(2) {
        ["abstract"]=>
        string(9) "traveller"
        ["concrete"]=>
        string(9) "Traveller"
      }
      ["this"]=>
      object(Container)#1 (1) {
        ["bindings":protected]=>
        array(2) {
          ["traveller"]=>
          *RECURSION*
          ["Visit"]=>
          array(2) {
            ["concrete"]=>
            object(Closure)#3 (3) {
              ["static"]=>
              array(2) {
                ["abstract"]=>
                string(5) "Visit"
                ["concrete"]=>
                string(5) "Train"
              }
              ["this"]=>
              *RECURSION*
              ["parameter"]=>
              array(1) {
                ["$c"]=>
                string(10) "<required>"
              }
            }
            ["shared"]=>
            bool(false)
          }
        }
      }
      ["parameter"]=>
      array(1) {
        ["$c"]=>
        string(10) "<required>"
      }
    }
    ["shared"]=>
    bool(false)
  }
  ["Visit"]=>
  array(2) {
    ["concrete"]=>
    object(Closure)#3 (3) {
      ["static"]=>
      array(2) {
        ["abstract"]=>
        string(5) "Visit"
        ["concrete"]=>
        string(5) "Train"
      }
      ["this"]=>
      object(Container)#1 (1) {
        ["bindings":protected]=>
        array(2) {
          ["traveller"]=>
          array(2) {
            ["concrete"]=>
            object(Closure)#2 (3) {
              ["static"]=>
              array(2) {
                ["abstract"]=>
                string(9) "traveller"
                ["concrete"]=>
                string(9) "Traveller"
              }
              ["this"]=>
              *RECURSION*
              ["parameter"]=>
              array(1) {
                ["$c"]=>
                string(10) "<required>"
              }
            }
            ["shared"]=>
            bool(false)
          }
          ["Visit"]=>
          *RECURSION*
        }
      }
      ["parameter"]=>
      array(1) {
        ["$c"]=>
        string(10) "<required>"
      }
    }
    ["shared"]=>
    bool(false)
  }
}

當執行 $tra = $app->make("traveller"); 時,程式就會用調用 make 方法,判斷是否已經綁定實例,若已綁定好則調用 build 獲取已經綁定好的閉包函數,開始解析,閉包函數在 build 方法中會執行 return $concrete($this) 將當前類作為參數為閉包函數傳參,最終又會執行到 build 方法,類似於遞歸調用,最後執行的 build 方法中 $concrete的值為字元串 Traveller,通過反射獲取 class Traveller 的類有關資訊,再進行下一步

$reflector->isInstantiable() // 檢查類是否可實例化 return bool|false

$reflector->getConstructor(); //獲取類的構造函數

$constructor->getParameters(); // 獲取類構造函數的參數

再獲取構造函數中每個參數是否含依賴,$this->getDependencies($dependencies),這個方法知道了 class Traveller 含有依賴類 Visit ,我們要做的就是解決這個依賴

// $dependency
object(ReflectionClass)#7 (1) {
  ["name"]=>
  string(5) "Visit"
}

通過 getDependencies ($parameters) 中的 $parameter->getClass() 獲取到依賴類 Visit, 再調用 resolveClass (ReflectionParameter $parameter) 就會發現之前的為什麼要 bind 介面類,而不用具體實現類的原因了,因為通過介面類的名稱,在容易中獲得實例,會獲取到所對應的具體實現類,$app->bind("Visit", "Train");

最後我們通過 return $reflector->newInstanceArgs($instances); 獲取到了 Train 的具體實現類。

array(1) {
  [0]=>
  object(Train)#9 (0) {
  }
}

到這裡 IoC 的流程就結束了,這就是其中控制反轉、依賴注入,閉包,反射等概念的關係及應用。