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

认识 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 也能够被正确的处理了。

青风百里

本帖由系统于 7个月前 自动加精
青风百里
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 9
Destiny

「打脑阔」哈哈

7个月前 评论
青风百里

@Destiny 确实是打脑阔,一长串。

7个月前 评论

很牛逼,我居然看完了

6个月前 评论
guanhui07

挺详尽

6个月前 评论

挺好,挺清晰

6个月前 评论

讲的很好,一直到那个array_map。哎,递归还是。。。。

5个月前 评论

讲的很好, 还想请教一下, 如果构造方法的参数全部限定了类型, 像int width = 2; 这种, 有什么好的办法处理吗

4个月前 评论

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!