反射在 PHP 中的应用
43

反射在每个面向对象的编程语言中都存在,它的主要目的就是在运行时分析类或者对象的状态,导出或提取出关于类、方法、属性、参数等的详细信息,包括注释。 反射是操纵面向对象范型中元模型的 API,可用于构建复杂,可扩展的应用。反射在日常的 Web 开发中其实用的不多,更多的是在偏向底层一些的代码中,比如说框架的底层中依赖注入、对象池、动态代理、自动获取插件列表、自动生成文档以及一些设计模式等等,都会大量运用到反射技术。
PHP 的反射 API 很多,但是常用的一般都是 ReflectionClassReflectionMethod:
1.ReflectionClass
这个是用来获取类的信息,可以简单测试一下:

class Student {
    private    $name;

    public function setName($name)
    {
         $this->name = $name;
    }

    protected function getName()
    {
        return $this->name;
    }
 }

获取类的方法列表:

$ref = new ReflectionClass(Student::class);
var_dump($ref->getMethods());

返回的是一个 ReflectionMethod 的数组:

array(1) {
  [0]=>
  object(ReflectionMethod)#2 (2) {
    ["name"]=>
    string(7) "setName"
    ["class"]=>
    string(7) "Student"
  }
}

附上一些常用方法,详细的可以查看文档:

ReflectionClass::getMethods     获取方法的数组
ReflectionClass::getName        获取类名
ReflectionClass::hasMethod      检查方法是否已定义
ReflectionClass::hasProperty    检查属性是否已定义
ReflectionClass::isAbstract     检查类是否是抽象类(abstract)
ReflectionClass::isFinal        检查类是否声明为 final
ReflectionClass::isInstantiable 检查类是否可实例化
ReflectionClass::newInstance    从指定的参数创建一个新的类实例

2.ReflectionMethod
这个主要是针对方法的反射,我们可以简单执行一下:

$stu = new Student();
$ref = new ReflectionClass(Student::class);
$method = $ref->getMethod('setName');
$method->invoke($stu, 'john');
var_dump($stu->name);

可以输出:

john

附上一些常用的方法,详细的可以去看看文档:

ReflectionMethod::invoke        执行
ReflectionMethod::invokeArgs    带参数执行
ReflectionMethod::isAbstract    判断方法是否是抽象方法
ReflectionMethod::isConstructor 判断方法是否是构造方法
ReflectionMethod::isDestructor  判断方法是否是析构方法
ReflectionMethod::isFinal       判断方法是否定义 final
ReflectionMethod::isPrivate     判断方法是否是私有方法
ReflectionMethod::isProtected   判断方法是否是保护方法 (protected)
ReflectionMethod::isPublic      判断方法是否是公开方法
ReflectionMethod::isStatic      判断方法是否是静态方法
ReflectionMethod::setAccessible 设置方法是否访问

接下来说一些反射在实际开发中比较常见的应用。

执行私有方法

其实反射不仅可以执行私有方法,还可以读取私有属性。这个主要应用在一些设计不合理的 SDK 里面,一些很好用的方法和属性却不对外开放。

class Student {
    private    $name;

    private function setName($name)
    {
        $this->name = $name;
    }
}

执行私有方法:

$stu = new Student();
$ref = new ReflectionClass($stu);
$method = $ref->getMethod('setName');
$method->setAccessible(true);
$method->invoke($stu, 'john');

读取私有属性:

$stu = new Student();
$ref = new ReflectionClass($stu);
$prop = $ref->getProperty('name');
$prop->setAccessible(true);
$val = $prop->getValue($stu);
var_dump($val);

动态代理

其实 PHP 有魔术方法,所以实现动态代理已经很简单了,但是通过魔术方法来实现的都不完美,个人理解最好的实现应该还是 JDK 中的动态代理,基于一个接口进行扫描实现实在 PHP 中也可以实现。我们先来看看动态代理在 JDK 中是怎么使用的:
1.首先定义一个实现类的接口,JDK 的动态代理必须基于接口(Cglib则不用)

package com.yao.proxy;
public interface Helloworld {
    void sayHello();
}

2.定义一个实现类,这个类就是要被代理的对象

package com.yao.proxy;
import com.yao.HelloWorld;

public class HelloworldImpl implements HelloWorld {
    public void sayHello() {
        System.out.print("hello world");
    }
}

3.调用被代理对象方法的实现类

package com.yao.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class MyInvocationHandler implements InvocationHandler{
    private Object target;
    public MyInvocationHandler(Object target) {
        this.target=target;
    }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("前置工作!");
        Object obj = method.invoke(target,args);
        System.out.println("后置工作!");
        return obj;
    }

4.测试

package com.yao.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class Demo {
    public static void main(String[] args) {
        HelloworldImpl realSubject = new HelloworldImpl();
        MyInvocationHandler handler = new MyInvocationHandler(realSubject);

        ClassLoader loader = realSubject.getClass().getClassLoader();
        Class[] interfaces = realSubject.getClass().getInterfaces();

        HelloworldImpl proxySubject = (HelloworldImpl) Proxy.newProxyInstance(loader, interfaces, handler);
        String hello = proxySubject.sayHello();
    }
}

JDK 的动态代理在底层实际上是扫描实现的接口,然后动态生成类的字节码文件。PHP 是动态语言,所以可以用 eval 来实现。
1.定义调度器接口

interface InvocationHandler  
{  
    function invoke($method, array $arr_args);  
}  

2.动态代理实现
定义一个类的 stub:

return new Class($handler,$target) implements %s {  
    private $handler;  
    private $target;
    public function __construct(InvocationHandler $handler, $target) {
        $this->handler = $handler;  
        $this->target = $target;
    }  
    %s  
};

定义一个方法的 stub:

public function %s(%s) {  
    $args = func_get_args();  
    $method = explode("::", __METHOD__);  
    $this->handler->invoke(new ReflectionMethod($this->target, $method[1]), $args);
}

Proxy 实现:

final class Proxy  
{   
    const CLASS_TEMPLATE = class_stub;      //这里显示上面定义的,为了方便阅读
    const FUNCTION_TEMPLATE = function_stub;    //同上

    public static function newProxyInstance($target, array $interfaces, InvocationHandler $handler)  {

    }
    protected static function generateClass(array $interfaces) {

    }
    protected static function checkInterfaceExists(array $interfaces)  {

    }
}  

其中 newProxyInstancegenerateClass 代码:

public static function newProxyInstance($target, array $interfaces, InvocationHandler $handler)  {
        self::checkInterfaceExists ($interfaces);
        $code = self::generateClass ($interfaces);
        return eval($code);
    }
protected static function generateClass(array $interfaces)
    {
        $interfaceList = implode(',', $interfaces);
        $functionList = '';
        foreach ($interfaces as $interface) {
            $class = new ReflectionClass ($interface);
            $methods = $class->getMethods();
            foreach ($methods as $method){
                $parameters = [];
                foreach ($method->getParameters() as $parameter){
                    $parameters[] = '$' . $parameter->getName();
                }
                $functionList .= sprintf( self::FUNCTION_TEMPLATE, $method->getName(), implode( ',', $parameters ) );
            }
        }
        return sprintf ( self::CLASS_TEMPLATE, $interfaceList, $functionList );
    }

其中generateClass就是通过反射扫描接口方法,然后根据 stub 模板生成方法拼接成代码,最后通过 eval 执行。

2.测试

interface Test1{
    public function t1();
}
interface Test2{
    public function t2();
}
class TestImpl implements Test1,Test2{
    public function t1(){
        echo 't1';
    }
    public function t2(){
        echo 't2';
    }
}
$impl = new TestImpl();
class Handler implements InvocationHandler {
    private $target;
    public function __construct($impl){
        $this->target = $impl;
    }
    function invoke(ReflectionMethod $method, array $arr_args){
        echo '前置操作';
        $method->invokeArgs($this->target, $arr_args);
        echo '后置操作';
    }
}

$proxy = Proxy::newProxyInstance($impl, ['Test1', 'Test2'], new Handler($impl));
$proxy->t1();

输出:

前置操作
t1
后置操作

依赖注入

依赖注入是现代化框架中非常常见的一个功能,它必须和服务容器结合使用。用过 Laravel 框架的童鞋应该很熟悉,我们可以在任意需要服务的地方通过类型提示声明,运行时框架就会自动帮我们注入所需要的对象。以 Laravel 框架的源码简单解析下:
在 Laravel 框架中,我们解析一个对象的方法可以这样:

$obj = App::make(ClassName);

make方法实际上底层也是调用了Illuminate\Container\Contaiern::build($concrete)这个方法,整理一下源码就是:

public function build($concrete){
        $reflector = new ReflectionClass($concrete);
        $constructor = $reflector->getConstructor();

        if (is_null($constructor)) {
            return new $concrete;
        }

        $dependencies = $constructor->getParameters();
        $instances = $this->resolveDependencies($dependencies);
        return $reflector->newInstanceArgs($instances);
    }

实际代码很简单,反射类获取构造方法,然后解析依赖参数,传入执行。

订阅号,欢迎关注 :smile:
file

本帖由系统于 9个月前 自动加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 3
尼古丁

今天刚好想看一下反射,就有文章了 不错不错

9个月前

之前用过反射,合并多个配置文件,配置文件中有function函数,因此需要用反射来得到function的代码,参看:http://www.fancyecommerce.com/2017/04/10/yii2-%e9%85%8d%e7%bd%ae%e5%8a%a0%e9%80%9f-n%e4%b8%aa%e9%85%8d%e7%bd%ae%e6%96%87%e4%bb%b6%e7%94%9f%e6%88%90%e4%b8%80%e4%b8%aa%e9%85%8d%e7%bd%ae%e6%96%87%e4%bb%b6/

8个月前

var_dump($stu->name);
这里执行会报错,这是一个私有属性,不能直接访问。

2天前

  • 请注意单词拼写,以及中英文排版,参考此页
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`, 更多语法请见这里 Markdown 语法
  • 支持表情,使用方法请见 Emoji 自动补全来咯,可用的 Emoji 请见 :metal: :point_right: Emoji 列表 :star: :sparkles:
  • 上传图片, 支持拖拽和剪切板黏贴上传, 格式限制 - jpg, png, gif
  • 发布框支持本地存储功能,会在内容变更时保存,「提交」按钮点击时清空
  请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!