「Laravel 核心学习」类的反射和依赖注入

PHP具有完整的反射 API,提供了对类、接口、函数、方法和扩展进行逆向工程的能力。通过类的反射提供的能力我们能够知道类是如何被定义的,它有什么属性、什么方法、方法都有哪些参数,类文件的路径是什么等很重要的信息。也正式因为类的反射很多PHP框架才能实现依赖注入自动解决类与类之间的依赖关系,这给我们平时的开发带来了很大的方便。 本文主要是讲解如何利用类的反射来实现依赖注入(Dependency Injection),并不会去逐条讲述 PHP Reflection 里的每一个 API ,详细的 API 参考信息请查阅官方文档


为了更好地理解,我们通过一个例子来看类的反射,以及如何实现依赖注入。

下面这个 Car 类代表汽车的信息。

/**
 * Car 汽车信息
 */
class Car
{
    public $gearbox;
    public $engine;

    /**
     * @param $gearbox // 变速箱
     * @param $engine // 汽车引擎
     */
    public function __construct($gearbox = '4AT', $engine = 'V8')
    {
        $this->gearbox = $gearbox;
        $this->engine = $engine;
    }
}

接下里这个 Brand 类代表汽车品牌,它在构造函数中依赖了 Car 汽车信息类。

/**
 * Brand 汽车品牌
 */
class Brand
{
    public $car;
    public $name;

    const WHEEL = 4; // 4个轮子

    /**
     * @param Car $car // 依赖 Car 类
     * @param $name // 品牌名称
     */
    public function __construct(Car $car, $name = '五菱')
    {
        $this->car = $car;
        $this->name = $name;
    }

    public function carInfo()
    {
        printf($this->name . '汽车是一辆' . $this->car->gearbox . '变速箱' . $this->car->engine . '发动机的' . self::WHEEL . '轮车。');
    }
}

ReflectionClass

下面我们通过反射来对 Brand 这个类进行反向工程。 把 Brand 类的名字传递给 ReflectionClass 来实例化一个 ReflectionClass 类的对象。

$reflectionClass = new ReflectionClass(Brand::class);

// 返回值
object(ReflectionClass)#1 (1) {
  ["name"]=>
  string(5) "Brand"
}

反射出类的常量

$reflectionClass->getConstants();

// 返回值
array(1) {
  ["WHEEL"]=>
  int(4)
}

反射出类中定义的方法

$reflectionClass->getMethods();

// 返回值:由 ReflectionMethod 对象构成的数组
array(2) {
  [0]=>
  object(ReflectionMethod)#2 (2) {
    ["name"]=>
    string(11) "__construct"
    ["class"]=>
    string(5) "Brand"
  }
  [1]=>
  object(ReflectionMethod)#3 (2) {
    ["name"]=>
    string(7) "carInfo"
    ["class"]=>
    string(5) "Brand"
  }
}

我们还可以通过 getConstructor() 来单独获取类的构造方法,其返回值为一个 ReflectionMethod 对象。

$constructor = $reflection->getConstructor();

反射出某方法的参数

$parameters = $constructor->getParameters();

// 返回值:ReflectionParameter 对象构成的数组。
array(2) {
  [0]=>
  object(ReflectionParameter)#3 (1) {
    ["name"]=>
    string(3) "car"
  }
  [1]=>
  object(ReflectionParameter)#4 (1) {
    ["name"]=>
    string(4) "name"
  }
}

依赖注入

接下来我们编写一个名为 make 的方法,传递类名称给 make 以返回类的对象,在 make 里它会帮我们注入类的依赖,即在本例中帮我们注入 Car 对象给 Brand 类的构造方法。

/**
 * 构建类与类的依赖
  * @param $className
  * @return object|null
 * @throws ReflectionException
 */function make($className)
{
    $reflectionClass = new ReflectionClass($className);
    $constructor = $reflectionClass->getConstructor(); // 获取构造函数方法
    $parameters = $constructor->getParameters(); // 获取构造函数的参数
    $dependencies = getDependencies($parameters); // 开始解析依赖

    return $reflectionClass->newInstanceArgs($dependencies); // 返回实体类
}

/**
 * 依赖解析 递归调用
 * @param $parameters // 由 ReflectionParameter 对象组成的数组
 * @return array
 * @throws ReflectionException
 */ 
function getDependencies($parameters)
{
    $dependencies = [];

    foreach($parameters as $parameter) {

        /**
         * 通过参数名称,使用 getClass() 方法, 获取 ReflectionClass 类的对象
         * 我们 Brand 的构造函数第一个参数是 Car 类,这里的代码流程就是:
         * 第一次循环时: $parameter 的参数为 car, $dependency 此时是通过 getClass() 方法获取到了 Car 的反射类。
         *   - 因为获取到了反射类,所以不会走 if(is_null()),而是走到下面的 else 的递归调用。
         *   - 再次返回到上面的 make() 方法,此时获取的反射类是 Car Car Car! $paramters 有两个参数,这两个参数分别是 Car 的构造方法中的 $gearbox 和 $engine
         *   - 当再次进入这个 foreach 循环时, $parameter 无法再通过 getClass() 方法获取反射类,就会走到 if(is_null()) 方法,isDefaultValueAvailable() 方法是用于判断构造函数的参数是否有默认值,如果有将它加入到 $dependencies 数组中。如果没有,我们需要「补0」,用于构造函数必须含有参数的情况。
         *   - 到此,Car 类就被依赖注入成功了。
         * 第二次循环时:因为 $dependency 无法通过 $name 的值获取到反射类,那和上面的 $gearbox 与 $engine 的流程相同。
         */
        $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;
}

$brand = make(Brand::class);
$brand->carInfo(); // 执行结果:五菱汽车是一辆4AT变速箱V8发动机的4轮车。

到此,一个 Brand 已经被实例化成功了,你能通过上面的代码想象出 Brand 实例的数据结构吗?给你时间思考:

5

4

3

2

1

$brand 的最终数据结构是:

object(Brand)#5 (2) {
  ["car"]=>
  object(Car)#10 (2) {
    ["gearbox"]=>
    string(3) "4AT"
    ["engine"]=>
    string(2) "V8"
  }
  ["name"]=>
  string(6) "五菱"
}

是否和你的想法一致呢?如果一致说明你已经理解类的反射与依赖注入原理了,恭喜你 :clap: :clap: :clap:


如果还是不理解,也不要灰心,从头再看一遍,依赖注入是 Laravel 框架中最核心的部分,理解它对于你后面的学习是非常有帮助的,加油。:two_hearts:

Github:github.com/hiccup711/Learning_Lara...

本作品采用《CC 协议》,转载必须注明作者和本文链接
悲观者永远正确,乐观者永远前行。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 2

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!