天天看點

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()方法中傳遞任何類型的對象且代碼能夠正常工作。

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

這個類就如你從上面的圖檔中看到的,Nettus隻有與Manager有一個直接的關聯關系。Manager建立了它,Manager依賴于Nettues。但是Nettuts與Editor類沒有任何關聯關系,且Editor也隻是與Manager有關聯。

At runtime, Nettuts uses an Editor object, thus the <> 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  by Sebastian Bergmann.

.object(ReflectionClass)#197 (1) {
  ["name"]=>
  string() "Editor"
}


Time:  seconds, Memory: Mb

OK ( test,  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  by Sebastian Bergmann.

.array() {
  []=>
  &object(ReflectionMethod)#196 (2) {
    ["name"]=>
    string() "__construct"
    ["class"]=>
    string() "Editor"
  }
  []=>
  &object(ReflectionMethod)#195 (2) {
    ["name"]=>
    string() "setNextArticle"
    ["class"]=>
    string() "Editor"
  }
  []=>
  &object(ReflectionMethod)#194 (2) {
    ["name"]=>
    string() "publish"
    ["class"]=>
    string() "Editor"
  }
}

Time:  seconds, Memory: Mb

OK ( test,  assertions)
           

其他的方法,getProperties(),檢索對象的屬性(即使是私有屬性!):

PHPUnit  by Sebastian Bergmann.

.array() {
  []=>
  &object(ReflectionProperty)#196 (2) {
    ["name"]=>
    string() "name"
    ["class"]=>
    string() "Editor"
  }
  []=>
  &object(ReflectionProperty)#195 (2) {
    ["name"]=>
    string() "articleId"
    ["class"]=>
    string() "Editor"
  }
}

Time:  seconds, Memory: Mb

OK ( test,  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  by Sebastian Bergmann.

.HERE
HERE

Time:  seconds, Memory: Mb

OK ( test,  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  by Sebastian Bergmann.

Estring() "John Doe"


Time:  seconds, Memory: 50Mb

There was  error:

) ReflectionTest::testItCanReflect
ReflectionException: Cannot access non-public member Editor::name

[...]/Reflection in PHP/Source/NetTuts.php:
[...]/Reflection in PHP/Source/Tests/ReflectionTest.php:
/usr/bin/phpunit:

FAILURES!
Tests: , Assertions: , Errors: 
           

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

// 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  by Sebastian Bergmann.

.string() "John Doe"
string() "John Doe"


Time:  seconds, Memory: Mb

OK ( test,  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  by Sebastian Bergmann.

.string() "John Doe"
string() "Mark Twain"


Time:  seconds, Memory: Mb

OK ( test,  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  by Sebastian Bergmann.

.string() "John Doe"
string() "Mark Twain"
string() "Mark Twain"


Time:  seconds, Memory: Mb

OK ( test,  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”值得變量時,很友善調用正确的方法。

回報與建議

如有翻譯錯誤之處,歡迎指正。

- 郵箱:[email protected]

感謝閱讀這份文檔