PHP該如何達成動態建立物件的method呢?目前有兩種方式 :

使用PHP 5.0的overload : __call()

使用PHP 5.3的closure : __invoke()

__call()


class Foo

{

    public function __call($method, $args)

    {

        if(is_callable([$this, $method])) {

            return call_user_func_array($this->$method, $args);

        }

        // else throw exception

    }


}


$obj = new Foo('Sam');

$obj->say = function () {

    return 'Hello World';

};

echo($obj->say());


// Result:

// Hello World

14行


$obj->say = function () {

    return 'Hello World';

};

動態建立了say(),並且將closure直接指定給say(),到目前為止與JavaScript很像。


第3行


public function __call($method, $args)

{

    if(is_callable([$this, $method])) {

        return call_user_func_array($this->$method, $args);

    }

    // else throw exception

}

只可惜在PHP 5.0時代,要達成動態建立method,只能靠Method Overloading機制。

當17行$obj-say()試圖呼叫一個class沒有定義的method時,會出發__call(),此時我們必須先使用is_callable()判斷使用者呼叫的method是否存在,若存在,則使用call_user_func_array()轉呼叫剛剛制訂的closure。


雖然執行結果與JavaScript一樣,但還要另外實現__call()部分。


__invoke()



class Foo

{


}


$obj = new Foo('Sam');

$obj->say = function () {

    return 'Hello World';

};

echo($obj->say->__invoke());


// Result:

// Hello World

第7行

$obj->say = function () {

    return 'Hello World';

};

一樣動態建立了say(),並且將closure直接指定給say(),到目前為止與JavaScript很像。

10行

echo($obj->say->__invoke());

有趣的地方來了,在這裏我們將say並不是看成method,而是看成一個closure物件,__invoke()是closure物件自帶的method,可執行closure自己本身,也就是說,我們藉由呼叫該closure來達成類似動態建立method的需求。

這裡雖然觀念不一樣,因為語法與JavaScript相近,又不用事先實作__call(),不失為目前PHP最好的方法。

動態建立Method並存取Property

在物件導向世界裡,使用property儲存物件狀態天經地義,而method存取物件的property也是理所當然,之前動態建立method的範例都沒存取property反而顯得不切實際。

JavaScript


function Foo(name) {

    this.name = name;

}


var obj = new Foo('Sam');

obj.say = function () {

    return "Hello " + this.name;

};

print(obj.say());


// Result:

// Hello Sam

第6行

obj.say = function () {

    return "Hello " + this.name;

};

因為say()變成obj物件的method,所以使用this.name存取property看似理所當然。


目前JavaScript這種寫法非常漂亮,也很容易理解。


PHP


class Foo

{

    private $name;


    function __construct($name)

    {

        $this->name = $name;

    }


}


$obj = new Foo('Sam');

$obj->say = function() {

    return "Hello " . $this->name;

};

$obj->say->__invoke();


// Result:

// Error

將JavaScript的寫法等效改寫成PHP。

13行

$obj->say = function() {

    return "Hello " . $this->name;

};

我們模擬了JavaScript的this,改寫成PHP的$this。

實際執行得到了以下錯誤訊息 :

PHP Fatal error:  Using $this when not in object context in /Users/oomusou/invoke_err.php on line 15

簡單的說,PHP的$this是指向say這個closure物件,而不是指向如JavaScript認為是$obj。


事實上,JavaScript的this原本也是指向closure物件,但因為closure動態成為obj的method後,this 自動指向obj了。


既然PHP的$this無法如JavaScript那樣自動改變,那PHP是否允許我們改用手動注入的方式改變$this呢?


PHP


class Foo

{

    private $name;


    function __construct($name)

    {

        $this->name = $name;

    }


}


$obj = new Foo('Sam');


$cl = function() {

    return "Hello " . $this->name;

};


$cl = $cl->bindTo($obj, $obj);

echo($cl());


// Result:

// Hello Sam

14行

$cl = function() {

    return "Hello " . $this->name;

};

我們不再執著該closure一定要動態成為 $obj的method,但要存取$obj property的目標不變,程式也不變,一樣使用$this。


假如我們能將$obj以手動注入的方式,讓closure內部的$this改指向$obj,我們就能達到如JavaScript的效果了。


18行

$cl = $cl->bindTo($obj, $obj);

bindTo()如同__invoke()一樣,是closure物件內建的method,它的目的就是讓我們能手動注入一個物件,讓closure物件的$this指向手動注入的物件$obj。


因為在closure中我們有$this->name,經過bindTo()去手動注入 $obj後,$this已經改指向$obj,所以$this->name就相當於$obj->name。


根據bindTo()文件 :


若要讓closure物件只能存取其他物件的public變數,只傳第1個參數即可。

若要讓closure物件存取其他物件的private或protected變數,就要傳第2個參數。

bindTo()對於第2個參數的要求不嚴,有幾種傳法 :


傳進欲存取物件的class名稱,是字串。

傳進欲存取的物件也可以,bindTo()會自動得知該物件的class名稱。

在此就一併傳進與第一個參數相同的$obj。


19行

echo($cl());

因為$obj已經透過bindTo() 手動注入進$cl(),此時$this已經指向$obj,所以執行$cl()就可順利存$obj的property。