利用反射機制實現依賴注入的原理

青風百里發表於2019-02-16

認識 ReflectionClass

該類實現了 Reflector 介面,使得我們可以使用該類檢視另一個類的相關資訊。所謂的反射,大概的意思就是將一個類的相關資訊給反射(對映、反映)出來。

定義兩個類以供測試

<?php
namespace Models;

class Car
{
    protected $engine; //引擎

    public static $name = '卡丁車'; //車名
    public static $model; //型號
    public $price = 200000; //售價
    public $color = 'red'; //顏色

    const WIDTH = 2; //車寬
    const HEIGHT = 1.5; //車高

    public function __construct(Engine $engine)
    {
        $this->engine = $engine;
    }

    /**
     * 開車
     *
     * @return void
     */
    public function drive()
    {
    }

    //給汽車加油
    public static function fuel()
    {
    }
}

class Engine
{
    public function __construce()
    {
    }
}

$reflector = new \ReflectionClass(new Car(new Engine()));

屬性相關的方法

//獲取一個屬性,類似的有getProperties(),獲取一組屬性
$price = $reflector->getProperty('price');
echo "價格:<br>";
var_dump($price);

//獲取屬性預設值
$defaultProperties = $reflector->getDefaultProperties();
echo "屬性預設值:<br>";
var_dump($defaultProperties);

//檢測是否含有某個屬性
$result = $reflector->hasProperty('price');
echo "屬性price是否定義:<br>";
var_dump($result);

image-20190215231348143

靜態屬性相關的方法

//獲取某個靜態屬性值
$value = $reflector->getStaticPropertyValue('model'); 
echo "靜態屬性model值:<br>";
var_dump($value);

//獲取所有靜態屬性
$staticProperties = $reflector->getStaticProperties(); 
echo "所有靜態屬性:<br>";
var_dump($staticProperties);

image-20190215231419432

常量相關的方法

//獲取某一個常量,類似的有 getConstants() ,獲取一組常量
$width = $reflector->getConstant('WIDTH'); 
echo "WIDTH常量:<br>";
var_dump($width);

//檢測是否含有某個常量
$result = $reflector->hasConstant('HEIGHT'); 
echo "常量HEIGHT是否定義:<br>";
var_dump($result);

image-20190215230639289

方法相關的方法

//獲取某個方法,類似的有 getMethods() ,獲取一組方法
$method = $reflector->getMethod('drive'); 
echo "方法drive:<br>";
var_dump($method);

//檢測是否含有某個方法
$result = $reflector->hasMethod('fuel'); 
echo "方法fuel是否定義:<br>";
var_dump($result);

image-20190215231457019

類自身相關的方法

//獲取類檔案所在的檔名
$filename = $reflector->getFileName();
echo "檔名:<br>";
var_dump($filename);

//獲取帶名稱空間的類名
$className = $reflector->getName();
echo "帶名稱空間的類名:<br>";
var_dump($className);

//獲取不帶名稱空間的類名
$shortClassName = $reflector->getShortName();
echo "短類名:<br>";
var_dump($shortClassName);

//檢測是否能被例項化,因為抽象類和介面不能被例項化
$result = $reflector->isInstantiable();
echo "是否能被例項化:<br>";
var_dump($result);

//獲取構造器
$constructor = $reflector->getConstructor();
echo "構造器:<br>";
var_dump($constructor);

//獲取註釋
$docs = $reflector->getMethod('drive')->getDocComment();
echo "註釋:<br>";
var_dump($docs);

image-20190215231523638

其餘還有一些方法,暫時放一放。

還需要再深入認識一下上文中提到的 getConstructor ,因為下文中要用到。

getConstructor() 方法返回的是一個 ReflectionMethod 類, ReflectionMethod 類可以反射(對映、反映)出一個方法中的相關資訊。所以接著看一看上文中的 $constructor

//獲取構造器,得到的是 ReflectionMethod 類
$constructor = $reflector->getConstructor();
//通過構造器獲取其引數,得到的是一個陣列
$parameters = $constructor->getParameters();
var_dump($parameters);

image-20190216001747539

現在獲得了一個陣列,陣列中每個元素都是 ReflectionParameter 類, ReflectionParameter 類中有一個方法叫做 getClass(),返回一個 ReflectionClass 類,也就是文章一開始提到的那個類。

//將陣列中第一個元素拿出來,呼叫 getClass() ,得到一個 ReflectionClass 類
$dependency = $parameters[0]->getClass();
var_dump($dependency);

image-20190216003229890

利用反射機制例項化類

無依賴的情況

要例項化一個類,獲得其類名即可,實際專案中還需要結合自動載入,這裡為了方便說明情況,就將所有類寫在同一個檔案中。這個操作很簡單。

<?php
namespace Models;

class Car
{
}

namespace Framework;

class App
{
    public function getInstance($className)
    {
        //例項化 ReflectionClass 物件
        $reflector = new \ReflectionClass($className);

        if (!$reflector->isInstantiable()) {
            //不能被例項化的邏輯
            return false;
        }

        //獲取構造器
        $constructor = $reflector->getConstructor();

        //如果沒有構造器,直接例項化
        if (!$constructor) {
            //這裡用了變數來動態的例項化類
            return new $className;
        }
    }
}

$app = new App();
$car = $app->getInstance('Models\Car');
var_dump($car); //輸出 object(Models\Car)#4 (0) { }

上面的 Car 這個類沒有其他依賴,所以操作起來很簡單,加入幾個依賴,再來看看。

帶有多層依賴的情況

假設有一個汽車依賴底盤,底盤依賴輪胎和軸承,輪胎也依賴軸承,軸承無依賴。那麼當需要例項化一個汽車類時,不友好的方式是這樣的,$car = new Car(new Chassis(new Tyre(new Axle), new Axle())) ,打腦闊。

利用依賴注入是這樣的。

<?php
namespace Framework;

//定義一個類,用於實現依賴注入
class App
{
    public function getInstance($className)
    {
        //例項化 ReflectionClass 物件
        $reflector = new \ReflectionClass($className);

        if (!$reflector->isInstantiable()) {
            //不能被例項化的邏輯,抽象類和介面不能被例項化
            return false;
        }

        //獲取構造器
        $constructor = $reflector->getConstructor();

        //如果沒有構造器,也就是沒有依賴,直接例項化
        if (!$constructor) {
            return new $className;
        }

        //如果有構造器,先把構造器中的引數獲取出來
        $parameters = $constructor->getParameters();

        //再遍歷 parameters ,找出每一個類的依賴,存到 dependencies 陣列中
        $dependencies = array_map(function ($parameter) {
            /**
             * 這裡是遞迴的去尋找每一個類的依賴,例如第一次執行的時候,程式發現汽車 Car 類依賴底盤 Chassis
             * 類,此時 $parameter 是一個ReflectionParameter 的例項,接著呼叫 ReflectionParameter
             * 的 getClass() 方法,獲得一個 ReflectionClass 的例項,再接著呼叫 ReflectionClass
             * 的 getName() 方法,取得類名,也就是 Models\Chassis ,但此時此刻還不能直接去 new
             * Models\Chassis ,因為 Models\Chassis 也有依賴,故要遞迴的去呼叫 getInstance
             * 進一步去尋找該類的依賴,周而復始,直到觸發上面的 if(!$constructor) ,停止遞迴。
             */
            return $this->getInstance($parameter->getClass()->getName());
        }, $parameters);

        //最後,使用 ReflectionClass 類提供的 newInstanceArgs ,方法去例項化類,引數將會傳入構造器中
        return $reflector->newInstanceArgs($dependencies);
    }
}

namespace Models;

class Car
{
    protected $chassis;

    //汽車依賴底盤
    public function __construct(Chassis $chassis)
    {
        $this->chassis = $chassis;
    }
}

class Chassis
{
    protected $tyre;
    protected $axle;

    //底盤依賴輪胎和軸承
    public function __construct(Tyre $tyre, Axle $axle)
    {
        $this->tyre = $tyre;
        $this->axle = $axle;
    }
}

class Tyre
{
    protected $axle;

    //輪胎也依賴軸承
    public function __construct(Axle $axle)
    {
        $this->axle = $axle;

    }
}

class Axle
{
    //軸承無依賴
}

$app = new \Framework\App();
$car = $app->getInstance('Models\Car');
var_dump($car);

image-20190216024329042

這時候,無論有多少依賴,有多少層依賴,都可以友好的注入。但是目前還有一個問題,如果一個類的構造器中的引數沒有限定型別,上面的程式碼就會報錯。假設將上文中的 Car 類改成這樣。

class Car
{
    protected $chassis;
    protected $width;
    //汽車依賴底盤
    public function __construct(Chassis $chassis, $width) // <-----多加入了一個引數且不限定型別
    {
        $this->chassis = $chassis;
        $this->width = $width;
    }
}

執行程式碼,報錯 call to function getName() on null ,問題出在了 return $this->getInstance($parameter->getClass()->getName()) 這一行,原因是 $parameter->getClass() 的結果是null,這也是必然的。檢視手冊發現這樣的一段描述,ReflectionParameter::getClass — Get the type hinted class (獲取所提示的類),上面加入的 $width ,沒有做型別提示,$parameter->getClass() 得到的結果必然是 null

故,將有型別提示的和沒有型別提示的分開處理。

處理普通引數

<?php
namespace Framework;

class App
{
    public function getInstance($className)
    {
        $reflector = new \ReflectionClass($className);

        if (!$reflector->isInstantiable()) {
            return false;
        }

        $constructor = $reflector->getConstructor();

        if (!$constructor) {
            return new $className;
        }

        $parameters = $constructor->getParameters();

        $dependencies = array_map(function ($parameter) {
            if (null == $parameter->getClass()) {
                //處理沒有型別提示的引數
                return $this->processNoHinted($parameter);
            } else {
                //處理有型別提示的引數
                return $this->processHinted($parameter);
            }
        }, $parameters);

        return $reflector->newInstanceArgs($dependencies);
    }

    protected function processNoHinted(\ReflectionParameter $parameter)
    {
        if ($parameter->isDefaultValueAvailable()) {
            return $parameter->getName();
        } else {
            //引數為空則丟擲異常
            throw new \Exception($parameter->getName() . "不能為空", 1);
        }
    }

    protected function processHinted(\ReflectionParameter $parameter)
    {
        return $this->getInstance($parameter->getClass()->getName());
    }
}

namespace Models;

class Car
{
    protected $chassis;
    protected $width;

    public function __construct(Chassis $chassis, $width = 2)
    {
        $this->chassis = $chassis;
        $this->width = $width;
    }
}

class Chassis
{
    protected $tyre;
    protected $axle;

    public function __construct(Tyre $tyre, Axle $axle)
    {
        $this->tyre = $tyre;
        $this->axle = $axle;
    }
}

class Tyre
{
    protected $axle;

    public function __construct(Axle $axle)
    {
        $this->axle = $axle;

    }
}

class Axle
{
}

$app = new \Framework\App();
$car = $app->getInstance('Models\Car');
var_dump($car);

image-20190217214006362

可以看到傳入的普通引數 $width 也能夠被正確的處理了。

青風百里

相關文章