PHP 反射机制的几种应用 自动生成文档、路由实现、单元测试、容器实现

PHP反射实际应用

反射机制简介

所有的反射机制的思想作用等都是类似的,下面就一起来了解一下PHP反射机制。

个人理解:反射机制就是可以利用类名或者一个类的对象来获取关于这个类的一系列信息(类的变量,方法),然后又就可以利用得到的类的信息实例化一些类的对象

官方给的简介:反射 API,有 对类、接口、函数、方法和扩展进行反向工程的能力。

此外,反射 API 提供了方法来取出函数、类和方法中的文档注释。
一般在框架中使用到反射机制比较多(控制反转),正常情况下一般使用不到反射的

反射机制的使用

常用的类

  • ReflectionClass 通过类名获取类的信息
  • ReflectionObject 通过类的对象获取类的信息

应用1:自动生成文档

根据反射的分析类,接口,函数和方法的内部结构,方法和函数的参数,以及类的属性和方法,可以自动生成文档。

<?php
namespace Test;
/**
 * 学生类
 * Class Student
 */
class Student
{
    const NORMAL = 1;
    const FORBIDDEN = 2;
    
    /**
     * 用户ID
     * @var int
     */
    public $id;
    
    public static $user_name = 'admin';

    /**
     * 获取ID
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * 设置ID
     * @param int $id
     */
    public function setId($id = 1)
    {
        $this->id = $id;
    }

    public function __construct()
    {
        echo 111;
    }

    /**
     * 获取用户名
     * @return string
     */
    public static final function getUserName()
    {
        return self::$user_name;
    }

}

这里展示了一些PHP反射类提供的一些方法
可以去PHP官方文档查看一些其他用法https://www.php.net/manual/zh/book.reflection.php

<?php

require_once './Student.php';
$ref = new ReflectionClass('Test\Student');

echo '获取类注释:' . $ref->getDocComment() . "</br>";
echo '获取类名:' . $ref->getName() . "</br>";
echo '获取定义类的扩展名:' . $ref->getExtensionName() . "</br>";
echo '获取定义类的文件的文件名:' . $ref->getFileName() . "</br>";
echo '获取结束行:' . $ref->getEndLine() . "</br>";
echo '获取名称空间的名字:' . $ref->getNamespaceName() . "</br>";
echo '返回特征别名数组:' . $ref->getTraitAliases() . "</br>";
echo '获取短名称:' . $ref->getShortName() . "</br>";
echo '获取构造方法:' . $ref->getConstructor() . "</br>";
echo '检查是否子类:' . $ref->isSubclassOf('Test\Student') . "</br>";

var_dump('获取静态属性名称', $ref->getStaticProperties(), "</br>");
var_dump('获取具体静态属性值', $ref->getStaticPropertyValue('user_name', ''), "</br>");

echo '获取方法的数组:';
$methods = $ref->getMethods();
foreach ($methods as $row) {
    printf("%s-%s-%s-%s</br>", $row->getName(), getAccess($row), getParams($row), getComment($row));
}

// 获取权限
function getAccess($method)
{
    if ($method->isPublic()) {
        return 'Public';
    }
    if ($method->isProtected()) {
        return 'Protected';
    }
    if ($method->isPrivate()) {
        return 'Private';
    }
}

// 获取方法参数信息
function getParams($method)
{
    $str = '';
    $parameters = $method->getParameters();
    foreach ($parameters as $row) {
        $str .= $row->getName() . ',';
        if ($row->isDefaultValueAvailable()) {
            $str .= "Default: {$row->getDefaultValue()}";
        }
    }
    return $str ? $str : '';
}

// 获取注释
function getComment($var)
{
    $comment = $var->getDocComment();
    // 简单的获取了第一行的信息,这里可以自行扩展
    preg_match('/\* (.*) *?/', $comment, $res);
    return isset($res[1]) ? $res[1] : '';
}

运行上面的代码

获取类名:Test\Student
获取定义类的扩展名:
获取定义类的文件的文件名:E:\phpstudy_pro\WWW\php\study\Test\Student.php
获取结束行:52
获取名称空间的名字:Test
返回特征别名数组:Array
获取短名称:Student
获取构造方法:Method [ public method __construct ] { @@ E:\phpstudy_pro\WWW\php\study\Test\Student.php 39 - 42 }
检查是否子类:
string(24) "获取静态属性名称" array(1) { ["user_name"]=> string(5) "admin" } string(5) "
" string(27) "获取具体静态属性值" string(5) "admin" string(5) "
" 获取方法的数组:getId-Public--获取ID
setId-Public-id,Default: 1-设置ID
__construct-Public--
getUserName-Public--获取用户名

应用2:路由实现

现在好多框架都是 MVC 的架构,根据路由信息定位控制器(controller) 和方法(method) 的名称,之后使用反射实现自动调用。

<?php
namespace Test;

/**
 * 学生类
 * Class Student
 */
class StudentController
{
    const NORMAL = 1;
    const FORBIDDEN = 2;
    /**
     * 用户ID
     * @var int
     */
    public $id;

    public static $user_name = 'admin';

    /**
     * 获取ID
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * 设置ID
     * @param int $id
     */
    public function setId($id = 1)
    {
        $this->id = $id;
        echo $this->id."</br>";
    }


    public function __construct()
    {
        echo microtime(TRUE).'调用了构造方法'."</br>";
    }

    public function __destruct()
    {
        echo microtime(TRUE).'调用了析构方法'."</br>";
    }

    /**
     * 获取用户名
     * @return string
     */
    public static final function getUserName()
    {
        return self::$user_name;
    }
}
require_once './StudentController.php';
$controller = 'Student';
$namespace = 'Test';
//$method = 'test';//测试没有方法  会抛异常
//$method = 'getUserName'; // 测试没有参数的方法 可以正常使用

$method = 'setId'; // 有参数的方法需要 配合参数一起使用
$arguments = ['id' => '60'];

$class = new ReflectionClass($namespace . DIRECTORY_SEPARATOR . ucfirst($controller) . 'Controller');
$controller = $class->newInstance();
if ($class->hasMethod($method)) {
    $method = $class->getMethod($method);
    $method->invokeArgs($controller, $arguments);
} else {
    throw new Exception("{$class->getNamespaceName()} controller method {$method} not exists!");
}

运行后结果是 60

应用3:单元测试

一般情况下我们会对函数和类进行测试,判断其是否能够按我们预期返回结果,我们可以用反射实现一个简单通用的类测试用例。

<?php

namespace Test;
class Calc
{
    public function plus($a, $b)
    {
        return $a + $b;
    }

    public function minus($a, $b)
    {
        return $a - $b;
    }
}
require_once './Calc.php';

function testEqual($method, $assert, $data)
{
    $arr = explode('@', $method);
    $class = $arr[0];
    $method = $arr[1];
    $ref = new ReflectionClass($class);
    if ($ref->hasMethod($method)) {
        $method = $ref->getMethod($method);
        $res = $method->invokeArgs(new $class, $data);
        if ($res === $assert) {
            echo "测试结果正确</br>";
        };
    }
}
testEqual('Test'.DIRECTORY_SEPARATOR.'Calc@plus', 3, [1, 2]);
testEqual('Test'.DIRECTORY_SEPARATOR.'Calc@minus', -1, [1, 2]);

以上代码运行后结果为

测试结果正确
测试结果正确

这是类的测试方法,也可以利用反射实现函数的测试方法

<?php
function title($title, $name)
{
    return sprintf("%s. %s\r\n", $title, $name);
}

$function = new ReflectionFunction('title');

echo $function->invokeArgs(array('Dr', 'Phil'));

这里只是我简单写的一个测试用例,PHPUnit 单元测试框架很大程度上依赖了 Reflection 的特性,可以了解下。

应用4:配合 DI 容器解决依赖

Laravel 等许多框架都是使用 Reflection 解决依赖注入问题,具体可查看 Laravel 源码进行分析。
下面我们代码简单实现一个 DI 容器演示 Reflection 解决依赖注入问题。

这里是一个简单的容器类

<?php
namespace Test;
use Closure;
use Exception;
use ReflectionClass;
use ReflectionException;
use ReflectionParameter;
class DI
{
    protected static $data = [];

    /**
     * 给一个未定义的属性赋值时调用
     * @param $k
     * @param $v
     */
    public function __set($k, $v)
    {
        echo '__set'.$k.'</br>';
        self::$data[$k] = $v;
    }

    /**
     * 当调用一个未定义的属性时访问此方法
     * @param $k
     * @return mixed|object
     * @throws Exception
     */
    public function __get($k)
    {
        echo '__get'.$k.'</br>';
        return $this->build(self::$data[$k]);
    }

    /**
     * 获取当前容器已绑定对象列表
     * @return array
     */
    public function getData()
    {
        return self::$data;
    }

    /**
     * 获取实例
     * @param $className
     * @return mixed|object
     * @throws ReflectionException
     * @throws Exception
     */
    public function build($className)
    {
        echo 'build'.$className.'</br>';
        // 如果是匿名函数,直接执行,并返回结果
        if ($className instanceof Closure) {
            return $className($this);
        }

        // 已经是实例化对象的话,直接返回
        if (is_object($className)) {
            return $className;
        }

        // 如果是类的话,使用反射加载
        $ref = new ReflectionClass($className);

        // 监测类是否可实例化
        if (!$ref->isInstantiable()) {
            throw new Exception('class' . $className . ' not found');
        }

        // 获取构造函数
        $constructor = $ref->getConstructor();

        // 无构造函数,直接实例化返回
        if (is_null($constructor)) {
            return new $className;
        }

        // 获取构造函数参数
        $params = $constructor->getParameters();

        // 解析构造函数
        $dependencies = $this->getDependencies($params);

        // 创建新实例
        return $ref->newInstanceArgs($dependencies);

    }

    /**
     * 分析参数,如果参数中出现依赖类,递归实例化
     * @param $params
     * @return array
     * @throws ReflectionException
     */
    public function getDependencies($params)
    {
        $data = [];
        foreach ($params as $param) {
            $tmp = $param->getClass();
            if (is_null($tmp)) {
                $data[] = $this->setDefault($param);
            } else {
                $data[] = $this->build($tmp->name);
            }
        }
        return $data;
    }

    /**
     * 设置默认值
     * @param $param
     * @return mixed
     * @throws Exception
     */
    public function setDefault($param)
    {
        /**
         * @var $param ReflectionParameter
         */
        if ($param->isDefaultValueAvailable()) {
            return $param->getDefaultValue();
        }
        throw new Exception('no default value!');
    }

}

这里是一个有依赖的类 依赖上文中单元测试的那个类

<?php
namespace Test;
use Test\Calc;
class Demo
{
    public function __construct(Calc $calc)
    {
        echo $calc->plus(1, 2);
    }
}

这里是测试结果

require_once './DI.php';
require_once './Calc.php';
require_once './Demo.php';
$di = new \Test\DI();
$di->demo = 'Test'.DIRECTORY_SEPARATOR.'Demo';
$di->calc = 'Test'.DIRECTORY_SEPARATOR.'Calc';

var_dump('获取容器中数据:',$di->getData(),'</br>');
var_dump( '结果:',$di->demo,'</br>');

输出结果为3

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351