活用 PHP5 的 magic methods - __set(), __get() and __call()
PHP5 在動態性及個體導向兩方面都做了大幅度的加強。其中 Magic methods 概念的引用,更為 PHP5 帶來許多靈活性。
本文說明如何活用 Magic methods 重整 (refactoring) 程式碼。讓 PHP 的程式碼更易於使用。主要重點在 __set(), __get()
,同時也示範了 __toString(), __isset(), __call()
的用途。
首先看一個 PHP4 語法的類別。See also: PHP Manual - Classes and Objects (PHP 4)
X.php in PHP4
<?php
class X {
var $_value1;
var $_value2;
function X4() {
$this->_value1 = 0;
$this->_value2 = 0;
}
function setValue1($v) {
$this->_value1 = $v;
}
function getValue1() {
return $this->_value1;
}
function setValue2($v) {
$this->_value2 = $v;
}
function getValue2() {
return $this->_value2;
}
}
$x = new X;
$x->setValue1(1);
$x->setValue2(2);
echo '(' . $x->getValue1() . ',' . $x->getValue2() . ')', "\n";
?>
第一步應該要先將它改成 PHP5 語法。See also: PHP Manual - Classes and Objects (PHP 5)
在重整過程中,除了語法的改寫外,也會改變私有資料成員 $_value1, $_value2 的結構,改以一個陣列放置所有私有屬性。此處先說明一下本文用語。本文通常稱呼用於表示個體內部資訊、狀態的資料項目為「屬性 (property)」。如果一個「屬性」直接指派給一個變數成員時,即 $this->value1 的形式,則本文稱這個「屬性」同時也是一個「資料成員 (data member)」。另一方面,也可用一個容器 (陣列) 作為「資料成員」,而將所有或大部份的「屬性」存放在這個「資料成員」的容器中。此時,本文就不稱呼這個「屬性」為一個「資料成員」。例如:陣列 $this->propertyMap 是一個「資料成員」,而 $this->propertyMap['value1'] 是一個「屬性」;在本文的用語中,此時的 value1 是一個屬性,但不是一個資料成員。底下是重整後的 X.php 。
X.php in PHP5
<?php
class X {
protected $_ = array(
'value1' => 0,
'value2' => 0
);
public function __construct() {
}
public function setValue1($v) {
$this->_['value1'] = $v;
}
public function getValue1() {
return $this->_['value1'];
}
public function setValue2($v) {
$this->_['value2'] = $v;
}
public function getValue2() {
return $this->_['value2'];
}
}
$x = new X;
$x->setValue1(1);
$x->setValue2(2);
echo '(' . $x->getValue1() . ',' . $x->getValue2() . ')', "\n";
?>
為何重整後改以關聯陣列的資料成員儲存私有屬性,而不直接用資料成員表示私有屬性呢?這跟 PHP 的「自識」能力 (或稱「反射」, Reflection) 有關。雖然關聯陣列和資料成員都可以動態增加內容,但受限於 PHP 的自識能力,使用關聯陣列儲存私有屬性較資料成員方便。因為使用資料成員的形式時無法以 foreach
走訪所有私有屬性。若用關聯陣列為容器存放屬性,便可以 foreach
走訪所有私有屬性。稍候的重整會顯示此用法的意義。
第二步,為了方便展示使用 magic methods 前後差異,自 X 類別衍生一個新類別 X2 ,在 X2 類別實作 magic methods 的內容。
X2.php
<?php
require_once 'X.php';
class X2 extends X {
public function __construct() {
parent::__construct();
}
public function __toString() {
$s = '(';
foreach ($this->_ as $v) {
$s .= $v . ',';
}
$s = substr($s, 0, strrpos($s, ',')) . ')';
return $s;
}
public function __set($k, $v) {
$method = 'set' . ucfirst($k);
if (method_exists($this, $method))
$this->$method($v);
else if (isset($this->_[$k]))
$this->_[$k] = $v;
else
throw new Exception('property undefined!');
}
public function __get($k) {
$method = 'get' . ucfirst($k);
if (method_exists($this, $method))
return $this->$method();
else if (isset($this->_[$k]))
return $this->_[$k];
else
throw new Exception('property undefined!');
}
public function __isset($k) {
return isset($this->_[$k]);
}
}
$x = new X;
$x->setValue1(1);
$x->setValue2(2);
echo '(' . $x->getValue1() . ',' . $x->getValue2() . ')', "\n";
$x2 = new X2;
$x2->value1 = 1;
$x2->value2 = 2;
echo $x2->value1, "\n";
echo 'isset $x2->value2: ', (isset($x2->value2) ? 'true' : 'false'), "\n";
echo $x2, "\n";
?>
我們覆載 __set(), __get()
這兩個 magic methods 之後,表面上我們直接存取資料成員 $x2->value1, $x2->value2 ,但實際上根本沒有這兩個資料成員,而是藉由 __set()
去調用屬性 value1, value2 的專屬存取行為 $this->setValue1(), $this->setValue2()(X2.php 第19行)。我們用 method_exists() 判斷是否定義了專屬存取行為,用 變數函數 (variable function) 的形式調用。同樣地,雖然實際上沒有 $x2->value1, $x2->value2 資料成員,但表面上有,因此又覆載了 __isset()
以判斷是否設定了屬性 value1, value2 。最後可以看到 __toString()
的覆載作用。而在 __toString()
中也可以看出用關聯陣列存放屬性時,我們可以很方便地使用 foreach
走訪所有屬性。
接下來的重整目標是函數名稱的命名方式。其實在 PHP 中,我們不需用 setValue1()/getValue1()
的命名方式。利用預設參數就可以將 getter 和 setter 寫在一起,直接用 value1()
就可以表示兩者。而且用這種命名方式時, X2.php 第17行的 $method = 'set' . ucfirst($k);
之動作便可省略。 __set()/__get()
的處理效率會更好。
繼續自 X2 類別衍生一個新類別 X3 ,利用預設參數撰寫新的屬性存取行為。同時再利用 __call()
增加對函數名稱的相容性。
X3.php
<?php
require_once 'X2.php';
class X3 extends X2 {
public function __construct() {
parent::__construct();
$this->_['value3'] = 1;
}
public function value3($v = NULL) {
if ($v === NULL)
return $this->_['value3']; // be a getter
else
$this->_['value3'] += $v*2; // be a setter
}
public function __set($k, $v) {
if (method_exists($this, $k))
$this->$k($v);
else if (isset($this->_[$k]))
$this->_[$k] = $v;
else
throw new Exception('property undefined!');
}
public function __get($k) {
if (method_exists($this, $k))
return $this->$k();
else if (isset($this->_[$k]))
return $this->_[$k];
else
throw new Exception('property undefined!');
}
public function __call($k, $args) {
if (preg_match_all('/(set|get)([A-Z][a-z0-9]+)+/', $k, $words)) {
$firstWord = $words[1][0]; // set or get
$methodName = strtolower(array_shift($words[2]));
//first word of property name
foreach ($words[2] as $word) {
$methodName .= ucfirst($word);
}
if (method_exists($this, $methodName)) {
$methodObj = array(&$this, $methodName);
if ($firstWord == 'set') {
call_user_func_array($methodObj, $args);
return;
}
else {
return call_user_func_array($methodObj, NULL);
}
}
}
throw new Exception('property undefined!');
}
}
$x3 = new X3;
$x3->value1 = 1;
$x3->value2 = 2;
$x3->value3 = 3;
echo $x3->value3(), "\n";
echo $x3->getValue3(), "\n";
$x3->value3(2);
$x3->setValue3(2);
echo $x3, "\n";
try {
$x3->value4 = 0;
}
catch (Exception $e) {
echo $e;
}
?>
X3.php 的第62行 (echo $x3->getValue3();
) 與第64行 ($x3->setValue3(2);
) ,都是透過 __call()
的覆載,才能調用到正確的 $x3->value3() (call_user_func_array())。不用擔心這會降低程式效能,因為 PHP5 只有在找不到明確定義的函數行為時,才會調用 __call()
。因此只要統一使用一致的函數名稱,便沒有機會調用 __call()
了。在本例中先後用了不一致的函數命名方式,才利用 __call()
容錯。
本文以繼承方式,利用 magic methods 逐一重整程式碼。實務上則不妨直接重整原有類別,無需繼承。透過 __set()/__get()
,除了將語法從函數調用修飾為成員存取,也提供我們幾乎不用增加任何函數行為就可以增加新的屬性,並以預設的方式保有對屬性的保護與控制之能力。應善用之。