認識 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);
靜態屬性相關的方法
//獲取某個靜態屬性值
$value = $reflector->getStaticPropertyValue('model');
echo "靜態屬性model值:<br>";
var_dump($value);
//獲取所有靜態屬性
$staticProperties = $reflector->getStaticProperties();
echo "所有靜態屬性:<br>";
var_dump($staticProperties);
常量相關的方法
//獲取某一個常量,類似的有 getConstants() ,獲取一組常量
$width = $reflector->getConstant('WIDTH');
echo "WIDTH常量:<br>";
var_dump($width);
//檢測是否含有某個常量
$result = $reflector->hasConstant('HEIGHT');
echo "常量HEIGHT是否定義:<br>";
var_dump($result);
方法相關的方法
//獲取某個方法,類似的有 getMethods() ,獲取一組方法
$method = $reflector->getMethod('drive');
echo "方法drive:<br>";
var_dump($method);
//檢測是否含有某個方法
$result = $reflector->hasMethod('fuel');
echo "方法fuel是否定義:<br>";
var_dump($result);
類自身相關的方法
//獲取類檔案所在的檔名
$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);
其餘還有一些方法,暫時放一放。
還需要再深入認識一下上文中提到的
getConstructor
,因為下文中要用到。
getConstructor()
方法返回的是一個 ReflectionMethod
類, ReflectionMethod
類可以反射(對映、反映)出一個方法中的相關資訊。所以接著看一看上文中的 $constructor
。
//獲取構造器,得到的是 ReflectionMethod 類
$constructor = $reflector->getConstructor();
//通過構造器獲取其引數,得到的是一個陣列
$parameters = $constructor->getParameters();
var_dump($parameters);
現在獲得了一個陣列,陣列中每個元素都是 ReflectionParameter
類, ReflectionParameter
類中有一個方法叫做 getClass()
,返回一個 ReflectionClass
類,也就是文章一開始提到的那個類。
//將陣列中第一個元素拿出來,呼叫 getClass() ,得到一個 ReflectionClass 類
$dependency = $parameters[0]->getClass();
var_dump($dependency);
利用反射機制例項化類
無依賴的情況
要例項化一個類,獲得其類名即可,實際專案中還需要結合自動載入,這裡為了方便說明情況,就將所有類寫在同一個檔案中。這個操作很簡單。
<?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);
這時候,無論有多少依賴,有多少層依賴,都可以友好的注入。但是目前還有一個問題,如果一個類的構造器中的引數沒有限定型別,上面的程式碼就會報錯。假設將上文中的 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);
可以看到傳入的普通引數 $width
也能夠被正確的處理了。