浅谈PHP框架中类成员方法的类类型形参是怎么利用ReflectionClass反射类自动实例化的(应该是全网首发)

发布时间 2023-11-03 16:05:56作者: 小松聊PHP进阶

说明

1. 或许是全网首发,我翻过很多文章,从未有一个博主讲过这个东西,很多博主只讲了IOC、DI和反射机制的常见用法,因类类型形参反射的巧妙用法有相当高的难度和学习盲区,所以从未有人讲过类类型的形参它怎么就被自动实例化的。
2. 在Laravel框架,或者是其它框架中,类的成员方法中形参的类型定义为某个类,在方法体内就直接当做对象来调用,这并不是PHP本身自带的语法,而是利用了反射机制,一直很好奇是怎么实现的。然而框架源码又太繁重,所以采用原生的方式实现。
3. 反射的功能非常强大,反射可以针对类本身做很多开挂操作,因此PHP框架才会变得这么易用与强大,类类型形参实例化,仅仅是冰山一角,官方文档

代码

<?php
/**
 * @Class 封装一个可自动实例化类的类类型的成员方法形参的容器
 */
class AutoNew {
    /**
     * @function 递归实例化类构造方法中的数据
     * @param    $class string 类名
     * @return   object|null
     * @throws   ReflectionException
     */
    public function newConstructClassTypeParam($class) {
        //实例化反射类
        $reflection = new ReflectionClass($class);

        //获取类的构造函数
        $constructor = $reflection->getConstructor();
        if ($constructor === null) {
            //如果构造函数存在,实例化这个类,(此方法支持给构造函数传参,如果有参数的话)
            return $reflection->newInstanceArgs();
        }

        //获取构造方法的参数,结果返回一个数组,若有值,返回的是ReflectionParameter类
        $params = $constructor->getParameters();

        $dependencies = [];

        foreach ($params as $param) {
            //获取构造方法参数的类型,注意:之有在变量左边声明类型的才可以,例如string $p获取的是string,但$p = 'ab'获取的就是null
            $dependencyType = $param->getType();
            //isBuiltin()方法,返回bool值,参数类型修饰符为string/int/bool/float/callable/array/object/mixed的都为true,但传输的内置外置接口,类都为false。目前使用PHP8,无法声明形参为null和resource类型
            if (($dependencyType !== null) && (! $dependencyType->isBuiltin())) {
                //获取形参声明的类型,返回字符串类型,字符串就是字符串,接口就是接口,类就是类
                $dependencyClassName = $dependencyType->getName();
                //此处可以理解为如果形参是类,或者是含有构造方法的抽象类或者接口的构造方法的形参中有以上类型,就递归实例化它
                $dependencies[] = $this->newConstructClassTypeParam($dependencyClassName);
            } else {
                //检测形参是否有默认值,如果有返回默认值,如果没有,返回null
                $dependencies[] = $param->isOptional() ? $param->getDefaultValue() : null;
            }
        }

        //如果构造函数存在,实例化这个类,(此方法支持给构造函数传参,如果有参数的话)
        return $reflection->newInstanceArgs($dependencies);
    }


    /**
     * @function 自动实例化某个类中某个方法的类类型的形参
     * @param    $class      string 类名
     * @param    $method     string 方法名
     * @param    $parameters array  参数名
     * @return   void
     * @throws   ReflectionException
     */
    public function init($class, $method, $parameters = []) {
        //实例化PHP内置的反射类
        $reflection = new ReflectionClass($class);
        //检查方法是否已定义
        if ($reflection->hasMethod($method)) {
            //创建一个实例
            $instance = $this->newConstructClassTypeParam($class);
            //返回一个ReflectionMethod对象,获取方法的形参,和其它元信息,并填充到ReflectionMethod对象中
            $methodReflection = $reflection->getMethod($method);
            //返回数组,获取干净的形参数据
            $methodParams = $methodReflection->getParameters();
            $methodDependencies = [];

            foreach ($methodParams as $param) {
                //获取方法参数的类型,注意:只有在变量左边声明类型的才可以,例如string $p获取的是string,但$p = 'ab'获取的就是null
                $paramType = $param->getType();
                //isBuiltin()方法,返回bool值,参数类型修饰符为string/int/bool/float/callable/array/object/mixed的都为true,但传输的内置外置接口,类都为false。目前使用PHP8,无法将形参声明null和resource类型
                if (($paramType !== null) && (! $paramType->isBuiltin())) {
                    //获取形参声明的类型,返回字符串类型,字符串就是字符串,接口就是接口名,类就是类名
                    $dependencyClassName = $paramType->getName();
                    //返回数组,数组的值是创建出来的对象。此处的逻辑可以理解为类成员方法的形参是类的,就实例化它
                    $methodDependencies[] = $this->newConstructClassTypeParam($dependencyClassName);
                } else {
                    //判断遍历出来的形参,在不在实际传递的实参数组中,如果在把这个值返回,如果不在,判断是否有默认值,如果有则返回,如果没有默认值,赋值为null
                    if (array_key_exists($param->getName(), $parameters)) {
                        $args = $parameters[$param->getName()];
                    } elseif ($param->isOptional()) {
                        $args = $param->getDefaultValue();
                    } else {
                        $args = null;
                    }
                    $methodDependencies[] = $args;
                }
            }

            //调用创建的实例并传参
            $methodReflection->invokeArgs($instance, $methodDependencies);
        }
    }
}

//调用端-------------------------------------------------------------------------------------
//相当于框架业务逻辑层的代码
class StudentService {
    /**
     * @function 通过班级id查询一个班有多少人
     * @param    $class_id
     * @return   int
     */
    public function getStudentNum($class_id) {
        //sql ...
        return 123;
    }
}


//相当于框架的控制器
class StudentController {
    /**
     * @function 此方法根据逻辑可要可不要
     * @return   void
     */
    public function __construct() {
        echo '构造方法被调用了' . PHP_EOL;
    }


    /**
     * @function 模拟获取学生人数的方法
     * @param    $usersService StudentService 学生服务类
     * @param    $class_id     int            班级id
     * @param    $unit         string         单位
     * @return   void
     */
    public function getStudentNum(StudentService $usersService, $class_id, $unit = '人') {
        echo $class_id . '班有' . $usersService->getStudentNum($class_id) . $unit . PHP_EOL;
    }
}


//初始化Language类并调用getStudentNum方法且传参,传参的过程相当与前端请求接口携带的参数。整体相当于框架的路由
(new AutoNew())->init(StudentController::class, 'getStudentNum', ['class_id' => '7', 'unit' => '名学生']);

//结果如下:
构造方法被调用了
7班有123名学生