Reflect in PHP(PHP中的反射机制)

原文链接 by Patkos Csaba18 Apr 2013
反射通常被定义为一个程序在执行的时候自我检查和修改自身的逻辑的能力。用较少的专业术语来说,反射就是让一个对象告诉你它自身的属性与方法,并改变哪些成员(即使是私有的)。在这一课,我们将会升入了解是如何实现的,以及何时可能证明是有用的。

简史

在编程时代的初期,只有汇编语言。用汇编语言编写的程序驻留在电脑的物理寄存器中。通过读取寄存器,可以随时检查其组成,方法和值。甚至,你可以在程序运行时通过修改简单的修改寄存器来改变程序。这需要对正在运行的程序的有一些渗入的认知,但是这是底层的反射。

As with any cool toy, use reflection, but don't abuse it.

随着高级编程语言(例如 C语言)的出现,这种反射逐渐淡出并消失。后来它被重新引入了面向对象的程序设计中。

今天,绝大部分的语言都支持反射。静态类型的语言,就像Java,极少有程序不使用反射的。然而,我发现有趣的是,所有的动态类型语言(例如PHP或者Ruby)都是基于反射的。如果没有反射的概念,鸭子类型将不可能实现。当你传递一个对象给另一个人(参数),接收对象无法知道该对象的结构和类型。它所能做的就是通过反射来标识可以和不能在接收的对象上调用的方法。

简单举例

反射在PHP中很流行。事实上,很多场景中你是用了反射但是你自己却不知道。例如:

// Nettuts.php
 
require_once 'Editor.php';
 
class Nettuts {
 
    function publishNextArticle() {
        $editor = new Editor('John Doe');
        $editor->setNextArticle('135523');
        $editor->publish();
    }
 
}

以及:

// Editor.php
 
class Editor {
 
    private $name;
    public $articleId;
 
    function __construct($name) {
        $this->name = $name;
    }
 
    public function setNextArticle($articleId) {
        $this->articleId = $articleId;
    }
 
    public function publish() {
        // publish logic goes here
        return true;
    }
 
}

在上述代码中,我们直接调用了一个已知类型的且本地初始化的变量。很明显我们在在publishNextArticle()方法中创建了editor变量,$editor变量就是Editor类型。这里并不需要反射,但是我们来使用一个新的类,命名为Manager:

// Manager.php
 
require_once './Editor.php';
require_once './Nettuts.php';
 
class Manager {
 
    function doJobFor(DateTime $date) {
        if ((new DateTime())->getTimestamp() > $date->getTimestamp()) {
            $editor = new Editor('John Doe');
            $nettuts = new Nettuts();
            $nettuts->publishNextArticle($editor);
        }
    }
 
}

然后, 修改Nettuts文件, 如下:

// Nettuts.php
 
class Nettuts {
 
    function publishNextArticle($editor) {
        $editor->setNextArticle('135523');
        $editor->publish();
    }
 
}

现在Nettuts已经很明显与Editor类没有任何关联。它已经不包含那个文件,它没有初始化那个类也不知道它的存在。我能够在publishNextArticle()方法中传递任何类型的对象且代码能够正常工作。

diagram

这个类就如你从上面的图片中看到的,Nettus只有与Manager有一个直接的关联关系。Manager创建了它,Manager依赖于Nettues。但是Nettuts与Editor类没有任何关联关系,且Editor也只是与Manager有关联。

At runtime, Nettuts uses an Editor object, thus the <<uses>> and the question mark.在运行时,PHP检查接收的对象且检验它实现的setNextArticle()方法和publish()方法。

对象成员信息

我们可以是PHP显示一个对象的详情。我们创建一个PHP单元测试类来帮助我们更方便的演练我们的代码:

// ReflectionTest.php
 
require_once '../Editor.php';
require_once '../Nettuts.php';
 
class ReflectionTest extends PHPUnit_Framework_TestCase {
 
    function testItCanReflect() {
        $editor = new Editor('John Doe');
        $tuts = new Nettuts();
        $tuts->publishNextArticle($editor);
    }
 
}

现在,在Nettuts中添加一个var_dump()方法:

// Nettuts.php
 
class NetTuts {
 
    function publishNextArticle($editor) {
        $editor->setNextArticle('135523');
        $editor->publish();
        var_dump(new ReflectionClass($editor));
    }
 
}

运行单元测试类,然后观察输出结果中的神奇显:

PHPUnit 3.6.11 by Sebastian Bergmann.
 
.object(ReflectionClass)#197 (1) {
  ["name"]=>
  string(6) "Editor"
}
 
 
Time: 0 seconds, Memory: 2.25Mb
 
OK (1 test, 0 assertions)

我们的反射类有一个属性名为name值为$editor变量的原始类型:Editor,但是这里并没有多少信息。我们的Editor的方法呢?

// Nettuts.php
 
class Nettuts {
 
    function publishNextArticle($editor) {
        $editor->setNextArticle('135523');
        $editor->publish();
 
        $reflector = new ReflectionClass($editor);
        var_dump($reflector->getMethods());
    }
 
}

在这段代码中,我们将反射类的实例复制给$reflector变量,现在我们可以很方便的处罚它的方法了。反射类公开了许多用于获取对象信息的方法。这其中一个方法就是getMethods(),它能返回一个包含每个方法的信息的数组。

PHPUnit 3.6.11 by Sebastian Bergmann.
 
.array(3) {
  [0]=>
  &object(ReflectionMethod)#196 (2) {
    ["name"]=>
    string(11) "__construct"
    ["class"]=>
    string(6) "Editor"
  }
  [1]=>
  &object(ReflectionMethod)#195 (2) {
    ["name"]=>
    string(14) "setNextArticle"
    ["class"]=>
    string(6) "Editor"
  }
  [2]=>
  &object(ReflectionMethod)#194 (2) {
    ["name"]=>
    string(7) "publish"
    ["class"]=>
    string(6) "Editor"
  }
}
 
Time: 0 seconds, Memory: 2.25Mb
 
OK (1 test, 0 assertions)

其他的方法,getProperties(),检索对象的属性(即使是私有属性!):

PHPUnit 3.6.11 by Sebastian Bergmann.
 
.array(2) {
  [0]=>
  &object(ReflectionProperty)#196 (2) {
    ["name"]=>
    string(4) "name"
    ["class"]=>
    string(6) "Editor"
  }
  [1]=>
  &object(ReflectionProperty)#195 (2) {
    ["name"]=>
    string(9) "articleId"
    ["class"]=>
    string(6) "Editor"
  }
}
 
Time: 0 seconds, Memory: 2.25Mb
 
OK (1 test, 0 assertions)

从getMethod()以及getProperties()方法中返回的数组中的元素的类型分别是反射方法以及反射属性;这些对象都是相当有用的:

// Nettuts.php
 
class Nettuts {
 
    function publishNextArticle($editor) {
        $editor->setNextArticle('135523');
        $editor->publish(); // first call to publish()
 
        $reflector = new ReflectionClass($editor);
        $publishMethod = $reflector->getMethod('publish');
        $publishMethod->invoke($editor); // second call to publish()
    }
 
}

这里我们使用getMethod()方法去检索一个名为"publish"的方法;获取到的结果就是一个反射方法类型的对象。然后我们调用invoke()方法,通过传递$editor对象再一次执行editor的publish()方法。

在我们的例子中,这个过程很简单,因为我们已经有一个Editor对象传递给invoke()方法。我们可能需要几个Editor对象在一些情景中,给予我们丰富的对象去选择使用。在其他的情景中,我们可能没有相应的对象可以使用,在这种情况下我们将会需要从反射类中获取一个对象。

让我们修改Editor的publish()方法去演示两次调用:

// Editor.php
 
class Editor {
 
    [ ... ]
 
    public function publish() {
        // publish logic goes here
        echo ("HERE\n");
        return true;
    }
 
}

以及新的输出信息:

PHPUnit 3.6.11 by Sebastian Bergmann.
 
.HERE
HERE
 
Time: 0 seconds, Memory: 2.25Mb
 
OK (1 test, 0 assertions)

操纵实例数据

我们也可以在代码执行的时候进行修改。那如果修改没有公共设置器的私有变量 呢?我们在Editor里面添加一个方法去检索editor的name变量:

// Editor.php
 
class Editor {
 
    private $name;
    public $articleId;
 
    function __construct($name) {
        $this->name = $name;
    }
 
    [ ... ]
 
    function getEditorName() {
        return $this->name;
    }
 
}

这是一个行的方法被调用,getEditorName(),且只是简单的返回了从私有变量$name中获取的值。$name变量在构建时就被赋值了,且我们没有公开的方法让我们去修改它。但是我们可以通过反射来访问这个变量。你可能首先尝试更明显的方法:

// Nettuts.php
 
class Nettuts {
 
    function publishNextArticle($editor) {
        var_dump($editor->getEditorName());
 
        $reflector = new ReflectionClass($editor);
        $editorName = $reflector->getProperty('name');
        $editorName->getValue($editor);
 
    }
 
}

即使它在var_dump()行有输出值,它还是抛出了一个错误当我们尝试通过反射检索值时:

PHPUnit 3.6.11 by Sebastian Bergmann.
 
Estring(8) "John Doe"
 
 
Time: 0 seconds, Memory: 2.50Mb
 
There was 1 error:
 
1) ReflectionTest::testItCanReflect
ReflectionException: Cannot access non-public member Editor::name
 
[...]/Reflection in PHP/Source/NetTuts.php:13
[...]/Reflection in PHP/Source/Tests/ReflectionTest.php:13
/usr/bin/phpunit:46
 
FAILURES!
Tests: 1, Assertions: 0, Errors: 1.

为了解决这个问题,我们需要使用反射属性对象授权我们使用私有变量以及方法:

// Nettuts.php
 
class Nettuts {
 
    function publishNextArticle($editor) {
        var_dump($editor->getEditorName());
 
        $reflector = new ReflectionClass($editor);
        $editorName = $reflector->getProperty('name');
        $editorName->setAccessible(true);
        var_dump($editorName->getValue($editor));
    }
 
}

调用setAccessible()方法且传递true标志:

PHPUnit 3.6.11 by Sebastian Bergmann.
 
.string(8) "John Doe"
string(8) "John Doe"
 
 
Time: 0 seconds, Memory: 2.25Mb
 
OK (1 test, 0 assertions)

如你所看到的,我们设法去读取私有变量。第一行输出数据是来自对象自身的getEditorName()方法,接着就是来自于反射。但是修改私有变量呢?使用setValue()方法:

// Nettuts.php
 
class Nettuts {
 
    function publishNextArticle($editor) {
        var_dump($editor->getEditorName());
 
        $reflector = new ReflectionClass($editor);
        $editorName = $reflector->getProperty('name');
        $editorName->setAccessible(true);
        $editorName->setValue($editor, 'Mark Twain');
        var_dump($editorName->getValue($editor));
    }
 
}

就是这样。这段代码把"John Doe"改为"Mark Twain"。

PHPUnit 3.6.11 by Sebastian Bergmann.
 
.string(8) "John Doe"
string(10) "Mark Twain"
 
 
Time: 0 seconds, Memory: 2.25Mb
 
OK (1 test, 0 assertions)

间接反射的使用

PHP的一些内置功能间接使用单一反射通过调用call_user_func()这个方法。

回调

call_user_func()方法接收一组数组:第一个元素指向一个对象,第二个是方法的名称。你可以提供一个可选参数,然后将其传递给被调用函数。例如:

// Nettuts.php
 
class Nettuts {
 
    function publishNextArticle($editor) {
        var_dump($editor->getEditorName());
 
        $reflector = new ReflectionClass($editor);
        $editorName = $reflector->getProperty('name');
        $editorName->setAccessible(true);
        $editorName->setValue($editor, 'Mark Twain');
        var_dump($editorName->getValue($editor));
 
        var_dump(call_user_func(array($editor, 'getEditorName')));
    }
 
}

以下输出表明代码检索到的数据正确:

PHPUnit 3.6.11 by Sebastian Bergmann.
 
.string(8) "John Doe"
string(10) "Mark Twain"
string(10) "Mark Twain"
 
 
Time: 0 seconds, Memory: 2.25Mb
 
OK (1 test, 0 assertions)

使用变量的值

关于间接反射的另外一个例子是通过变量的值调用方法,与直接调用相反。例如:

// Nettuts.php
 
class Nettuts {
 
    function publishNextArticle($editor) {
        var_dump($editor->getEditorName());
 
        $reflector = new ReflectionClass($editor);
        $editorName = $reflector->getProperty('name');
        $editorName->setAccessible(true);
        $editorName->setValue($editor, 'Mark Twain');
        var_dump($editorName->getValue($editor));
 
        $methodName = 'getEditorName';
        var_dump($editor->$methodName());
    }
 
}

这段代码生成了与前一段代码一样的输出。PHP简单地用它代表的字符串替换变量并调用该方法。当你想通过使用类名的变量来创建对象时,它仍然可以工作。

我们何时需要使用反射?

现在我们已经了解了技术的细节,我们需要思考何时才去使用反射?这里有少许的场景:

  • 如果没有反射 动态类型 将会不可能实现。
  • 面向方面编程 从方法调用中侦听并放置代码在方法周围,所有这些都通过反射完成。
  • PHPUnit 极其依赖反射,与其他模拟框架一样。
  • Web框架 通常使用反射用于不同的目的。一些框架将反射用于初始化模型,构造视图对象以及更多的东西。Laravel在依赖注入中大量的使用了反射。
  • 元编程,就像我们最后一个例子,这是一个隐藏反射。
  • 代码分析框架 使用反射来理解你的代码。

最后思考

和任何酷的玩具一样,使用反射,但不要滥用它。当你检查许多对象时,反射时昂贵的,它有可能使您的项目的架构和设计复杂化。我建议你在反射能真正给你带来优势或者你没有任何其他可行选择的时候使用它。

就个人而言,我只在几个示例中使用反射,最常见的是使用缺少文档的第三方模块。当你的MVC响应包含一个方法"add"和"remove"值得变量时,很方便调用正确的方法。

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

推荐阅读更多精彩内容