利用 PHP 的反射机制实现依赖注入

认识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');
var_dump($price);
 
//获取属性默认值
$defaultProperties = $reflector->getDefaultProperties();
var_dump($defaultProperties);
 
//检测是否含有某个属性
$result = $reflector->hasProperty('price');
var_dump($result);

20220213202811.png

静态属性相关的方法

//获取某个静态属性值
$value = $reflector->getStaticPropertyValue('name');
var_dump($value);
 
//获取所有静态属性
$staticProperties = $reflector->getStaticProperties();
var_dump($staticProperties);

20220213202921.png

常量相关的方法

//获取某一个常量,类似的有 getConstants() ,获取一组常量
$width = $reflector->getConstant('WIDTH');
var_dump($width);
 
//检测是否含有某个常量
$result = $reflector->hasConstant('HEIGHT');
var_dump($result);

20220213202958.png

方法相关的方法

//获取某个方法,类似的有 getMethods() ,获取一组方法
$method = $reflector->getMethod('drive');
var_dump($method);
 
//检测是否含有某个方法
$result = $reflector->hasMethod('fuel');
var_dump($result);

20220213203112.png

类自身相关的方法

//获取类文件所在的文件名
$filename = $reflector->getFileName();
var_dump($filename);
 
//获取带命名空间的类名
$className = $reflector->getName();
var_dump($className);
 
//获取不带命名空间的类名
$shortClassName = $reflector->getShortName();
var_dump($shortClassName);
 
//检测是否能被实例化,因为抽象类和接口不能被实例化
$result = $reflector->isInstantiable();
var_dump($result);
 
//获取构造器
$constructor = $reflector->getConstructor();
var_dump($constructor);
 
//获取注释
$docs = $reflector->getMethod('drive')->getDocComment();
var_dump($docs);

20220213214918.png

getConstructor()

其余还有一些方法,暂时放一放。还需要再深入认识上文中提到的getConstructor,因为下文中要用到。

getConstructor()方法返回的是一个ReflectionMethod类,ReflectionMethod类可以反射(映射、反映)出一个方法中的相关信息。所以接着看一看上文中的 $constructor

//获取构造器,得到的是 ReflectionMethod 类
$constructor = $reflector->getConstructor();
var_dump($constructor);
//通过构造器获取其参数,得到的是一个数组
$parameters = $constructor->getParameters();
var_dump($parameters);

20220213203231.png

现在获得了一个数组,数组中每个元素都是ReflectionParameter类,ReflectionParameter类中有一个方法叫做getClass(),返回一个ReflectionClass类,也就是文章一开始提到的那个类。

//获取构造器,得到的是ReflectionMethod类
$constructor = $reflector->getConstructor();
var_dump($constructor);
//通过构造器获取其参数,得到的是一个数组
$parameters = $constructor->getParameters();
var_dump($parameters);
 
//将数组中第一个元素拿出来,调用getClass(),得到一个ReflectionClass类
$dependency = $parameters[0]->getType()->getName();
var_dump($dependency);

20220213203958.png

php8 中不再建议使用ReflectionParameter::getClass(),可以使用$param->getType()->getName() 代替。

完整代码

<?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');
var_dump($price);
 
//获取属性默认值
$defaultProperties = $reflector->getDefaultProperties();
var_dump($defaultProperties);
 
//检测是否含有某个属性
$result = $reflector->hasProperty('price');
var_dump($result);
 
//获取某个静态属性值
$value = $reflector->getStaticPropertyValue('name');
var_dump($value);
 
//获取所有静态属性
$staticProperties = $reflector->getStaticProperties();
var_dump($staticProperties);
 
//获取某一个常量,类似的有getConstants(),获取一组常量
$width = $reflector->getConstant('WIDTH');
var_dump($width); // 输出2
 
//检测是否含有某个常量
$result = $reflector->hasConstant('HEIGHT');
var_dump($result); // 输出 true
 
//获取某个方法,类似的有getMethods(),获取一组方法
$method = $reflector->getMethod('drive');
var_dump($method);
 
//检测是否含有某个方法
$result = $reflector->hasMethod('fuel');
var_dump($result);
 
//获取类文件所在的文件名
$filename = $reflector->getFileName();
var_dump($filename);
 
//获取带命名空间的类名
$className = $reflector->getName();
var_dump($className);
 
//获取不带命名空间的类名
$shortClassName = $reflector->getShortName();
var_dump($shortClassName);
 
//检测是否能被实例化,因为抽象类和接口不能被实例化
$result = $reflector->isInstantiable();
var_dump($result);
 
//获取构造器
$constructor = $reflector->getConstructor();
var_dump($constructor);
 
//获取注释
$docs = $reflector->getMethod('drive')->getDocComment();
var_dump($docs);
 
//获取构造器,得到的是ReflectionMethod类
$constructor = $reflector->getConstructor();
var_dump($constructor);
//通过构造器获取其参数,得到的是一个数组
$parameters = $constructor->getParameters();
var_dump($parameters);
 
//将数组中第一个元素拿出来,调用getClass(),得到一个ReflectionClass类
$dependency = $parameters[0]->getType()->getName();
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);

20220213203738.png

上面的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
* 的 getType() 方法,获得一个 ReflectionType 的实例,再接着调用 ReflectionType 的
* getName() 方法,取得类名,即 Models\Chassis ,但还不可以 new Models\Chassis,
* 因为 Models\Chassis 也有依赖,故要递归的去调用 getInstance 进一步去寻找该
* 类的依赖,周而复始,直到触发上面的 if(!$constructor) ,最终停止递归。
*/
return $this->getInstance($parameter->getType()->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);

20220213204750.png

这时候,无论有多少依赖,有多少层依赖,都可以友好的注入。但是目前还有一个问题,如果一个类的构造器中的参数没有限定类型,上面的代码就会报错。假设将上文中的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,问题出在了这里:

$dependencies = array_map(function ($parameter) {
/**
* 这里是递归的去寻找每一个类的依赖,例如当第一次执行的时候,程序发现汽车 Car 类依赖底盘 Chassis
* 类,此时 $parameter 是一个 ReflectionParameter 的实例,接着调用 ReflectionParameter
* 的 getType() 方法,获得一个 ReflectionType 的实例,再接着调用 ReflectionType 的
* getName() 方法,取得类名,即 Models\Chassis ,但还不可以 new Models\Chassis,
* 因为 Models\Chassis 也有依赖,故要递归的去调用 getInstance 进一步去寻找该
* 类的依赖,周而复始,直到触发上面的 if(!$constructor) ,最终停止递归。
*/
return $this->getInstance($parameter->getType()->getName());
}, $parameters);
 
//最后,使用 ReflectionClass 类提供的 newInstanceArgs ,方法去实例化类,参数将会传入构造器中
return $reflector->newInstanceArgs($dependencies);

原因是$parameter->getType()的结果是null,这也是必然的。查看手册发现这样的一段描述,ReflectionParameter::getType — Gets a parameter’s type,上面加入的$width,没有做类型提示,$parameter->getType()得到的结果必然是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->getType()) {
//处理没有类型提示的参数
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->getType()->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);

20220213205351.png

可以看到传入的普通参数$width也能够被正确的处理了。