「Laravel 核心學習」類的反射和依賴注入

MArtian發表於2022-02-25

PHP具有完整的反射 API,提供了對類、介面、函式、方法和擴充套件進行逆向工程的能力。通過類的反射提供的能力我們能夠知道類是如何被定義的,它有什麼屬性、什麼方法、方法都有哪些引數,類檔案的路徑是什麼等很重要的資訊。也正式因為類的反射很多PHP框架才能實現依賴注入自動解決類與類之間的依賴關係,這給我們平時的開發帶來了很大的方便。 本文主要是講解如何利用類的反射來實現依賴注入(Dependency Injection),並不會去逐條講述 PHP Reflection 裡的每一個 API ,詳細的 API 參考資訊請查閱官方文件


為了更好地理解,我們通過一個例子來看類的反射,以及如何實現依賴注入。

下面這個 Car 類代表汽車的資訊。

/**
 * Car 汽車資訊
 */
class Car
{
    public $gearbox;
    public $engine;

    /**
     * @param $gearbox // 變速箱
     * @param $engine // 汽車引擎
     */
    public function __construct($gearbox = '4AT', $engine = 'V8')
    {
        $this->gearbox = $gearbox;
        $this->engine = $engine;
    }
}

接下里這個 Brand 類代表汽車品牌,它在建構函式中依賴了 Car 汽車資訊類。

/**
 * Brand 汽車品牌
 */
class Brand
{
    public $car;
    public $name;

    const WHEEL = 4; // 4個輪子

    /**
     * @param Car $car // 依賴 Car 類
     * @param $name // 品牌名稱
     */
    public function __construct(Car $car, $name = '五菱')
    {
        $this->car = $car;
        $this->name = $name;
    }

    public function carInfo()
    {
        printf($this->name . '汽車是一輛' . $this->car->gearbox . '變速箱' . $this->car->engine . '發動機的' . self::WHEEL . '輪車。');
    }
}

ReflectionClass

下面我們通過反射來對 Brand 這個類進行反向工程。 把 Brand 類的名字傳遞給 ReflectionClass 來例項化一個 ReflectionClass 類的物件。

$reflectionClass = new ReflectionClass(Brand::class);

// 返回值
object(ReflectionClass)#1 (1) {
  ["name"]=>
  string(5) "Brand"
}

反射出類的常量

$reflectionClass->getConstants();

// 返回值
array(1) {
  ["WHEEL"]=>
  int(4)
}

反射出類中定義的方法

$reflectionClass->getMethods();

// 返回值:由 ReflectionMethod 物件構成的陣列
array(2) {
  [0]=>
  object(ReflectionMethod)#2 (2) {
    ["name"]=>
    string(11) "__construct"
    ["class"]=>
    string(5) "Brand"
  }
  [1]=>
  object(ReflectionMethod)#3 (2) {
    ["name"]=>
    string(7) "carInfo"
    ["class"]=>
    string(5) "Brand"
  }
}

我們還可以通過 getConstructor() 來單獨獲取類的構造方法,其返回值為一個 ReflectionMethod 物件。

$constructor = $reflection->getConstructor();

反射出某方法的引數

$parameters = $constructor->getParameters();

// 返回值:ReflectionParameter 物件構成的陣列。
array(2) {
  [0]=>
  object(ReflectionParameter)#3 (1) {
    ["name"]=>
    string(3) "car"
  }
  [1]=>
  object(ReflectionParameter)#4 (1) {
    ["name"]=>
    string(4) "name"
  }
}

依賴注入

接下來我們編寫一個名為 make 的方法,傳遞類名稱給 make 以返回類的物件,在 make 裡它會幫我們注入類的依賴,即在本例中幫我們注入 Car 物件給 Brand 類的構造方法。

/**
 * 構建類與類的依賴
  * @param $className
  * @return object|null
 * @throws ReflectionException
 */function make($className)
{
    $reflectionClass = new ReflectionClass($className);
    $constructor = $reflectionClass->getConstructor(); // 獲取建構函式方法
    $parameters = $constructor->getParameters(); // 獲取建構函式的引數
    $dependencies = getDependencies($parameters); // 開始解析依賴

    return $reflectionClass->newInstanceArgs($dependencies); // 返回實體類
}

/**
 * 依賴解析 遞迴呼叫
 * @param $parameters // 由 ReflectionParameter 物件組成的陣列
 * @return array
 * @throws ReflectionException
 */ 
function getDependencies($parameters)
{
    $dependencies = [];

    foreach($parameters as $parameter) {

        /**
         * 通過引數名稱,使用 getClass() 方法, 獲取 ReflectionClass 類的物件
         * 我們 Brand 的建構函式第一個引數是 Car 類,這裡的程式碼流程就是:
         * 第一次迴圈時: $parameter 的引數為 car, $dependency 此時是通過 getClass() 方法獲取到了 Car 的反射類。
         *   - 因為獲取到了反射類,所以不會走 if(is_null()),而是走到下面的 else 的遞迴呼叫。
         *   - 再次返回到上面的 make() 方法,此時獲取的反射類是 Car Car Car! $paramters 有兩個引數,這兩個引數分別是 Car 的構造方法中的 $gearbox 和 $engine
         *   - 當再次進入這個 foreach 迴圈時, $parameter 無法再通過 getClass() 方法獲取反射類,就會走到 if(is_null()) 方法,isDefaultValueAvailable() 方法是用於判斷建構函式的引數是否有預設值,如果有將它加入到 $dependencies 陣列中。如果沒有,我們需要「補0」,用於建構函式必須含有引數的情況。
         *   - 到此,Car 類就被依賴注入成功了。
         * 第二次迴圈時:因為 $dependency 無法通過 $name 的值獲取到反射類,那和上面的 $gearbox 與 $engine 的流程相同。
         */
        $dependency = $parameter->getClass();

        if (is_null($dependency)) {

            if($parameter->isDefaultValueAvailable()) {

                $dependencies[] = $parameter->getDefaultValue();

            } else {
                // 不是可選引數的為了簡單直接賦值為字串0
                // 針對構造方法的必須引數這個情況
                // Laravel 是通過 service provider 註冊 closure 到 IocContainer,
                // 在 closure 裡可以通過 return new Class($param1, $param2) 來返回類的例項
                // 然後在 make 時回撥這個 closure 即可解析出物件
                // 具體細節我會在另一篇文章裡面描述
                $dependencies[] = '0';
            }
        } else {

            //遞迴解析出依賴類的物件
            $dependencies[] = make($parameter->getClass()->name);

        }
    }

    return $dependencies;
}

$brand = make(Brand::class);
$brand->carInfo(); // 執行結果:五菱汽車是一輛4AT變速箱V8發動機的4輪車。

到此,一個 Brand 已經被例項化成功了,你能通過上面的程式碼想象出 Brand 例項的資料結構嗎?給你時間思考:

5

4

3

2

1

$brand 的最終資料結構是:

object(Brand)#5 (2) {
  ["car"]=>
  object(Car)#10 (2) {
    ["gearbox"]=>
    string(3) "4AT"
    ["engine"]=>
    string(2) "V8"
  }
  ["name"]=>
  string(6) "五菱"
}

是否和你的想法一致呢?如果一致說明你已經理解類的反射與依賴注入原理了,恭喜你 :clap: :clap: :clap:


如果還是不理解,也不要灰心,從頭再看一遍,依賴注入是 Laravel 框架中最核心的部分,理解它對於你後面的學習是非常有幫助的,加油。:two_hearts:

Github:github.com/hiccup711/Learning_Lara...

本作品採用《CC 協議》,轉載必須註明作者和本文連結
我從未見過一個早起、勤奮、謹慎,誠實的人抱怨命運。

相關文章