PHP 5.4 的 bindTo() 就是 JavaScript 萬惡 this 的實作

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
2
3
4
5
6
7
8
9
10
11
function Foo() {
}

var obj = new Foo();
obj.say = function () {
return "Hello World";
};
print(obj.say());

// Result:
// Hello World
  • 第1行宣告一個Foo,空空的什麼method也沒有.
  • 第3行建立obj物件。
  • 第5行動態建立 say()這個method。
  • 第8行執行剛剛動態建立的say()

JavaScript了不起的地方就在於允許我們動態建立物件的method,這在傳統OOP語言是做不到的,而且語法還相當漂亮,與closure無縫接軌。

PHP

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

  1. 使用PHP 5.0的overload : __call()
  2. 使用PHP 5.3的closure : __invoke()

__call()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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行

1
2
3
$obj->say = function () {
return 'Hello World';
};

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

第3行

1
2
3
4
5
6
7
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()

1
2
3
4
5
6
7
8
9
10
11
12
13
14

class Foo
{

}

$obj = new Foo('Sam');
$obj->say = function () {
return 'Hello World';
};
echo($obj->say->__invoke());

// Result:
// Hello World

第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
2
3
4
5
6
7
8
9
10
11
12
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行

1
2
3
obj.say = function () {
return "Hello " + this.name;
};

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

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

PHP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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行

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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行

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()文件 :

  1. 若要讓closure物件只能存取其他物件的public變數,只傳第1個參數即可。
  2. 若要讓closure物件存取其他物件的privateprotected變數,就要傳第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的實作。
2015-09-29