淺析如何通過PHP類的反射來實現依賴注入

kevinyan發表於2019-02-16

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

再次宣告這裡實現的依賴注入非常簡單,並不能應用到實際開發中去,可以參考我的另一篇文章來了解laravel框架的服務容器(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服務容器的文章。

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

相關文章