深入探討 bindTo()
PHP 的 closure 除了 __invoke()
外,事實上還有一個 bindTo()
,在Laravel台灣的Modern PHP讀書會 Chap.2 Feature時,曾經討論過closure,但總抓不到感覺,本文以JavaScript的觀點重新理解PHP的bindTo()
,並比較兩種語言的用法與設計理念,讓bindTo()
不再遙不可及,可以真正應用在我們的專案裡。
Version
PHP 5.4
JavaScript ES5
動態建立Method
JavaScript
首先我們來看JavaScript如何動態建立
物件的method。1 1PHP的closure基本用法,請參考如何使用Closure?
1 | function Foo() { |
- 第1行宣告一個
Foo
,空空的什麼method也沒有. - 第3行建立obj物件。
- 第5行
動態建立
say()
這個method。 - 第8行執行剛剛動態建立的
say()
。
JavaScript了不起的地方就在於允許我們動態建立
物件的method,這在傳統OOP語言是做不到的,而且語法還相當漂亮,與closure無縫接軌。
PHP
PHP該如何達成動態建立
物件的method呢?目前有兩種方式 :
- 使用PHP 5.0的overload :
__call()
- 使用PHP 5.3的closure :
__invoke()
__call()
1 | class Foo |
14行1
2
3$obj->say = function () {
return 'Hello World';
};
動態建立了say()
,並且將closure直接指定給say()
,到目前為止與JavaScript很像。
第3行1
2
3
4
5
6
7public 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()
1 |
|
第7行1
2
3$obj->say = function () {
return 'Hello World';
};
一樣動態建立了say()
,並且將closure直接指定給say()
,到目前為止與JavaScript很像。
10行1
echo($obj->say->__invoke());
有趣的地方來了,在這裏我們將say
並不是看成method,而是看成一個closure物件,__invoke()
是closure物件自帶的method,可執行closure自己本身,也就是說,我們藉由呼叫該closure來達成類似動態建立
method的需求。
這裡雖然觀念不一樣,因為語法與JavaScript相近,又不用事先實作__call()
,不失為目前PHP最好的方法。
動態建立Method並存取Property
在物件導向世界裡,使用property儲存物件狀態天經地義,而method存取物件的property也是理所當然,之前動態建立
method的範例都沒存取property反而顯得不切實際。
JavaScript
1 | function Foo(name) { |
第6行1
2
3obj.say = function () {
return "Hello " + this.name;
};
因為say()
變成obj
物件的method,所以使用this.name
存取property看似理所當然。
目前JavaScript這種寫法非常漂亮,也很容易理解。
PHP
1 | class Foo |
將JavaScript的寫法等效改寫成PHP。
13行1
2
3$obj->say = function() {
return "Hello " . $this->name;
};
我們模擬了JavaScript的this
,改寫成PHP的$this
。
實際執行得到了以下錯誤訊息 :
1 | 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
呢?
1 | class Foo |
14行1
2
3$cl = function() {
return "Hello " . $this->name;
};
我們不再執著該closure一定要動態成為
$obj
的method,但要存取$obj
property
的目標不變,程式也不變,一樣使用$this
。
假如我們能將$obj
以手動注入
的方式,讓closure內部的$this
改指向$obj
,我們就能達到如JavaScript的效果了。
18行1
$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行1
echo($cl());
因為$obj
已經透過bindTo()
手動注入
進$cl()
,此時$this
已經指向$obj
,所以執行$cl()
就可順利存$obj
的property。
Conclusion
- JavaScript最為人詬病的就是
this
會隨著context而變,但也由於this
可以變來變去,所以程式中的this.name
完全不用改,只要closure動態建立
在哪一個物件上,就可以動態存取該物件的property。 - PHP並無法如JavaScript那樣
自動
去改變$this
,不過PHP也幫我們開了後門,允許我們以手動注入
方式去改變$this
所指向的物件,這就是bindTo()
了,透過手動注入
改變$this
,我們也可以達到動態建立
method並存取property,只可惜這個closure並不是真的動態建立
在該物件上,而是單獨以closure物件的方式存在,雖然沒JavaScript那樣漂亮,但最少功能可以達到,所以我才說PHP的bindTo()
就是JavaScript萬惡this
的實作。