Laravel原始碼解析 — 服務容器

侯大寶發表於2021-04-12

前言

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

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

服務容器

1.什麼是IoC

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

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

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

總結:藉助於“第三方”實現具有依賴關係的物件之間的解耦。

舉個例子

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

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

Laravel原始碼解析 — 服務容器

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

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 的流程就結束了,這就是其中控制反轉、依賴注入,閉包,反射等概念的關係及應用。

相關文章