類的反射和依賴注入

kevinyan發表於2019-02-28

在講服務容器之前我想先梳理下PHP反射相關的知識,PHP反射是程式實現依賴注入的基礎,也是Laravel的服務容器實現服務解析的基礎,如果你已經掌握了這方面基礎知識,那麼可以跳過本文直接看服務容器部分的內容。

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

再次宣告這裡實現的依賴注入非常簡單,並不能應用到實際開發中去,可以參考後面的文章服務容器(IocContainer), 瞭解Laravel的服務容器是如何實現依賴注入的。

為了更好地理解,我們通過一個例子來看類的反射,以及如何實現依賴注入。
下面這個類代表了座標系裡的一個點,有兩個屬性橫座標x和縱座標y。

/**
 * Class Point
 */
class Point
{
    public $x;
    public $y;

    /**
     * Point constructor.
     * @param int $x  horizontal value of point`s coordinate
     * @param int $y  vertical value of point`s coordinate
     */
    public function __construct($x = 0, $y = 0)
    {
        $this->x = $x;
        $this->y = $y;
    }
}
複製程式碼

接下來這個類代表圓形,可以看到在它的建構函式裡有一個引數是Point類的,即Circle類是依賴與Point類的。

class Circle
{
    /**
     * @var int
     */
    public $radius;//半徑

    /**
     * @var Point
     */
    public $center;//圓心點

    const PI = 3.14;

    public function __construct(Point $point, $radius = 1)
    {
        $this->center = $point;
        $this->radius = $radius;
    }
    
    //列印圓點的座標
    public function printCenter()
    {
        printf(`center coordinate is (%d, %d)`, $this->center->x, $this->center->y);
    }

    //計算圓形的面積
    public function area()
    {
        return 3.14 * pow($this->radius, 2);
    }
}
複製程式碼

ReflectionClass

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

$reflectionClass = new reflectionClass(Circle::class);
//返回值如下
object(ReflectionClass)#1 (1) {
  ["name"]=>
  string(6) "Circle"
}
複製程式碼

反射出類的常量

$reflectionClass->getConstants();
複製程式碼

返回一個由常量名稱和值構成的關聯陣列

array(1) {
  ["PI"]=>
  float(3.14)
}
複製程式碼

通過反射獲取屬性

$reflectionClass->getProperties();
複製程式碼

返回一個由ReflectionProperty物件構成的陣列

array(2) {
  [0]=>
  object(ReflectionProperty)#2 (2) {
    ["name"]=>
    string(6) "radius"
    ["class"]=>
    string(6) "Circle"
  }
  [1]=>
  object(ReflectionProperty)#3 (2) {
    ["name"]=>
    string(6) "center"
    ["class"]=>
    string(6) "Circle"
  }
}
複製程式碼

反射出類中定義的方法

$reflectionClass->getMethods();
複製程式碼

返回ReflectionMethod物件構成的陣列

array(3) {
  [0]=>
  object(ReflectionMethod)#2 (2) {
    ["name"]=>
    string(11) "__construct"
    ["class"]=>
    string(6) "Circle"
  }
  [1]=>
  object(ReflectionMethod)#3 (2) {
    ["name"]=>
    string(11) "printCenter"
    ["class"]=>
    string(6) "Circle"
  }
  [2]=>
  object(ReflectionMethod)#4 (2) {
    ["name"]=>
    string(4) "area"
    ["class"]=>
    string(6) "Circle"
  }
}
複製程式碼

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

$constructor = $reflectionClass->getConstructor();
複製程式碼

反射出方法的引數

$parameters = $constructor->getParameters();
複製程式碼

其返回值為ReflectionParameter物件構成的陣列。

array(2) {
  [0]=>
  object(ReflectionParameter)#3 (1) {
    ["name"]=>
    string(5) "point"
  }
  [1]=>
  object(ReflectionParameter)#4 (1) {
    ["name"]=>
    string(6) "radius"
  }
}
複製程式碼

依賴注入

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

//構建類的物件
function make($className)
{
    $reflectionClass = new ReflectionClass($className);
    $constructor = $reflectionClass->getConstructor();
    $parameters  = $constructor->getParameters();
    $dependencies = getDependencies($parameters);
    
    return $reflectionClass->newInstanceArgs($dependencies);
}

//依賴解析
function getDependencies($parameters)
{
    $dependencies = [];
    foreach($parameters as $parameter) {
        $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;
}
複製程式碼

定義好make方法後我們通過它來幫我們例項化Circle類的物件:

$circle = make(`Circle`);
$area = $circle->area();
/*var_dump($circle, $area);
object(Circle)#6 (2) {
  ["radius"]=>
  int(1)
  ["center"]=>
  object(Point)#11 (2) {
    ["x"]=>
    int(0)
    ["y"]=>
    int(0)
  }
}
float(3.14)*/
複製程式碼

通過上面這個例項我簡單描述了一下如何利用PHP類的反射來實現依賴注入,Laravel的依賴注入也是通過這個思路來實現的,只不過設計的更精密大量地利用了閉包回撥來應對各種複雜的依賴注入。

本文的示例程式碼的下載連結

本文已經收錄在系列文章Laravel原始碼學習裡,歡迎訪問閱讀。

相關文章