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

原文鏈接 by Patkos Csaba18 Apr 2013
反射通常被定義為一個程序在執(zhí)行的時候自我檢查和修改自身的邏輯的能力。用較少的專業(yè)術(shù)語來說,反射就是讓一個對象告訴你它自身的屬性與方法,并改變哪些成員(即使是私有的)。在這一課,我們將會升入了解是如何實現(xiàn)的,以及何時可能證明是有用的。

簡史

在編程時代的初期,只有匯編語言。用匯編語言編寫的程序駐留在電腦的物理寄存器中。通過讀取寄存器,可以隨時檢查其組成,方法和值。甚至,你可以在程序運行時通過修改簡單的修改寄存器來改變程序。這需要對正在運行的程序的有一些滲入的認知,但是這是底層的反射。

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

隨著高級編程語言(例如 C語言)的出現(xiàn),這種反射逐漸淡出并消失。后來它被重新引入了面向?qū)ο蟮某绦蛟O(shè)計中。

今天,絕大部分的語言都支持反射。靜態(tài)類型的語言,就像Java,極少有程序不使用反射的。然而,我發(fā)現(xiàn)有趣的是,所有的動態(tài)類型語言(例如PHP或者Ruby)都是基于反射的。如果沒有反射的概念,鴨子類型將不可能實現(xiàn)。當你傳遞一個對象給另一個人(參數(shù)),接收對象無法知道該對象的結(jié)構(gòu)和類型。它所能做的就是通過反射來標識可以和不能在接收的對象上調(diào)用的方法。

簡單舉例

反射在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;
    }
 
}

在上述代碼中,我們直接調(diào)用了一個已知類型的且本地初始化的變量。很明顯我們在在publishNextArticle()方法中創(chuàng)建了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();
    }
 
}

現(xiàn)在Nettuts已經(jīng)很明顯與Editor類沒有任何關(guān)聯(lián)。它已經(jīng)不包含那個文件,它沒有初始化那個類也不知道它的存在。我能夠在publishNextArticle()方法中傳遞任何類型的對象且代碼能夠正常工作。

diagram

這個類就如你從上面的圖片中看到的,Nettus只有與Manager有一個直接的關(guān)聯(lián)關(guān)系。Manager創(chuàng)建了它,Manager依賴于Nettues。但是Nettuts與Editor類沒有任何關(guān)聯(lián)關(guān)系,且Editor也只是與Manager有關(guān)聯(lián)。

At runtime, Nettuts uses an Editor object, thus the <<uses>> and the question mark.在運行時,PHP檢查接收的對象且檢驗它實現(xiàn)的setNextArticle()方法和publish()方法。

對象成員信息

我們可以是PHP顯示一個對象的詳情。我們創(chuàng)建一個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);
    }
 
}

現(xiàn)在,在Nettuts中添加一個var_dump()方法:

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

運行單元測試類,然后觀察輸出結(jié)果中的神奇顯:

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());
    }
 
}

在這段代碼中,我們將反射類的實例復(fù)制給$reflector變量,現(xiàn)在我們可以很方便的處罰它的方法了。反射類公開了許多用于獲取對象信息的方法。這其中一個方法就是getMethods(),它能返回一個包含每個方法的信息的數(shù)組。

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()方法中返回的數(shù)組中的元素的類型分別是反射方法以及反射屬性;這些對象都是相當有用的:

// 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"的方法;獲取到的結(jié)果就是一個反射方法類型的對象。然后我們調(diào)用invoke()方法,通過傳遞$editor對象再一次執(zhí)行editor的publish()方法。

在我們的例子中,這個過程很簡單,因為我們已經(jīng)有一個Editor對象傳遞給invoke()方法。我們可能需要幾個Editor對象在一些情景中,給予我們豐富的對象去選擇使用。在其他的情景中,我們可能沒有相應(yīng)的對象可以使用,在這種情況下我們將會需要從反射類中獲取一個對象。

讓我們修改Editor的publish()方法去演示兩次調(diào)用:

// 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)

操縱實例數(shù)據(jù)

我們也可以在代碼執(zhí)行的時候進行修改。那如果修改沒有公共設(shè)置器的私有變量 呢?我們在Editor里面添加一個方法去檢索editor的name變量:

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

這是一個行的方法被調(diào)用,getEditorName(),且只是簡單的返回了從私有變量$name中獲取的值。$name變量在構(gòu)建時就被賦值了,且我們沒有公開的方法讓我們?nèi)バ薷乃5俏覀兛梢酝ㄟ^反射來訪問這個變量。你可能首先嘗試更明顯的方法:

// 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.

為了解決這個問題,我們需要使用反射屬性對象授權(quán)我們使用私有變量以及方法:

// 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));
    }
 
}

調(diào)用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)

如你所看到的,我們設(shè)法去讀取私有變量。第一行輸出數(shù)據(jù)是來自對象自身的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的一些內(nèi)置功能間接使用單一反射通過調(diào)用call_user_func()這個方法。

回調(diào)

call_user_func()方法接收一組數(shù)組:第一個元素指向一個對象,第二個是方法的名稱。你可以提供一個可選參數(shù),然后將其傳遞給被調(diào)用函數(shù)。例如:

// 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')));
    }
 
}

以下輸出表明代碼檢索到的數(shù)據(jù)正確:

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)

使用變量的值

關(guān)于間接反射的另外一個例子是通過變量的值調(diào)用方法,與直接調(diào)用相反。例如:

// 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簡單地用它代表的字符串替換變量并調(diào)用該方法。當你想通過使用類名的變量來創(chuàng)建對象時,它仍然可以工作。

我們何時需要使用反射?

現(xiàn)在我們已經(jīng)了解了技術(shù)的細節(jié),我們需要思考何時才去使用反射?這里有少許的場景:

  • 如果沒有反射 動態(tài)類型 將會不可能實現(xiàn)。
  • 面向方面編程 從方法調(diào)用中偵聽并放置代碼在方法周圍,所有這些都通過反射完成。
  • PHPUnit 極其依賴反射,與其他模擬框架一樣。
  • Web框架 通常使用反射用于不同的目的。一些框架將反射用于初始化模型,構(gòu)造視圖對象以及更多的東西。Laravel在依賴注入中大量的使用了反射。
  • 元編程,就像我們最后一個例子,這是一個隱藏反射。
  • 代碼分析框架 使用反射來理解你的代碼。

最后思考

和任何酷的玩具一樣,使用反射,但不要濫用它。當你檢查許多對象時,反射時昂貴的,它有可能使您的項目的架構(gòu)和設(shè)計復(fù)雜化。我建議你在反射能真正給你帶來優(yōu)勢或者你沒有任何其他可行選擇的時候使用它。

就個人而言,我只在幾個示例中使用反射,最常見的是使用缺少文檔的第三方模塊。當你的MVC響應(yīng)包含一個方法"add"和"remove"值得變量時,很方便調(diào)用正確的方法。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容