文/朱先忠編譯
一、簡介
很幸運,PHP 5.0中引進了物件重載技術。本文將探討對於方法__call(),__set()以及__get()進行重載的可能性。在對重載理論進行簡單介紹後,我們將透過兩個例子直奔主題:第一例,實作持續儲存類別;第二例,找到實現動態的getter/setter的方法。
二、什麼是物件重載?
在PHP中談到物件重載時,我們要區別兩種類型:
·方法重載
·屬性重載
在方法重載的情況下,我們要定義一個魔術般的方法__call(),它將實現一個在對應類別中對未定義方法的籠統呼叫。只有當你想要存取類別中未定義的方法時,這種籠統方法才會被呼叫。在沒有方法重載的情況下,下面的例子將導致PHP顯示一條致命錯誤訊息:Call to undefined method ThisWillFail::bar() in/some/directory/example.php on line 9 並流產程序的執行:
< ?php
class ThisWillFail {
public function foo() {
return "Hello World!";
}
}
$class = new ThisWillFail;
$class->bar();
?>
借助方法重載的幫助,程式碼能夠捕捉到這種呼叫且能夠體面地給以處理。
屬性重載與方法重載差不多。這種情況下,類別把讀/寫操作重定向(也可稱代理)到類別的屬性,這些屬性在類別中沒有明確定義。這裡的專門方法是__set()和__get()。依賴錯誤報告等級,PHP翻譯器通常在存取未定義的屬性時,或發出通知,或延遲並潛在地定義這個變數。而如果使用屬性重載,翻譯器可以在設定一個未定義的屬性時呼叫__set(),而在存取一個未定義的屬性值時呼叫__get()。
綜上所述,利用重載技術可以實現像用PHP這樣的動態語言進行時軟體開發時間的大幅縮短。
理論介紹至此,以下分析具體編碼。
三、持續性儲存類別舉例
下列程式碼,透過使用屬性重載技術,以少於50行的PHP程式碼實作了上述所提及的持續性儲存類別。術語persistable意味著類別可以從一個資料結構中描述一個元素,並保持與底端儲存系統的同步。用編碼的解釋就是,外部代碼可以使用類別來實作從一個資料庫表中選定一行。這樣,在程式運行時,可以直接存取類別的屬性來操縱該行中的元素(讀/取)。在腳本結束時,PHP將負責把更新的行資料回送到資料庫中去。
精心研讀下面程式碼將有助於你理解什麼是屬性重載。
<?php
//裝入PEAR的<a href=" http://pear.php.net/package/DB/ "> DB package</a>
require_once "DB.php";
class Persistable {
private $data = array();
private $table = "users";
public function __construct($user) {
$this->dbh = DB::Connect("mysql://user:password@localhost/database");
$query = "SELECT id, name, email, country FROM " .
$this->table . " WHERE name = ?";
$this->data = $this->dbh->getRow($query, array($user),
DB_FETCHMODE_ASSOC);
}
public function __get($member) {
if (isset($this->data[$member])) {
return $this->data[$member];
}
}
public function __set($member, $value) {
// dataset的ID是唯讀的if ($member == "id") {
return;
}
if (isset($this->data[$member])) {
$this->data[$member] = $value;
}
}
public function __destruct() {
$query = "UPDATE " . $this->table . " SET name = ?,
email = ?, country = ? WHERE id = ?";
$this->dbh->query($query, $this->name, $this->email,
$this->country, $this->id);
}
}
$class = new Persistable("Martin Jansen");
$class->name = "John Doe";
$class->country = "United States";
$class->email = " [email protected] ";
?>
你遇到的第一個問題可能是__construct(),這是PHP 5中引入的新的構造器方法。在PHP 4時代,構造器總是與它們的類別名稱相符。在PHP 5中已不再是這樣。你不需要對建構器方法有過多的了解,除了呼叫它可以創建一個類別的實例外;並注意到,這裡使用了一個參數- 執行一個基於此參數的資料庫。此建構器把查詢結果賦值給類別屬性$data。
接下來,程式定義了兩個特別的方法__get()和__set()。你應該對它們早已熟悉:__get()用來讀取未定義的屬性值,__set()用來修改未定義的屬性值。
這意味著無論何時從持續性儲存類別中讀取/寫入一個未定義的屬性,由這些專門方法來負責管理在屬性數組變數$data中的信息,而不是直接改變類別的屬性(切記:變數$data包含著來自於資料庫中的一行!)。
類別中的最後一個方法是__construct()的對立者- 析構器__destruct()。 PHP在"腳本關閉階段"呼叫析構器,典型地這是在PHP腳本執行快要結束的時候。析構器把來自於$data屬性的資訊寫回資料庫中去。這正是前面同步(synchronization )術語的意思。
你可能早已註意到,這裡的程式碼使用了PEAR的資料庫抽象層套件(database abstraction layer package)。其實這無所謂,透過別的方式與資料庫通訊也一樣能說明本文的主題。
如果你細心觀察,會發現該持續性儲存類別的描述比較簡單。例子中僅涉及了一個資料庫表,而沒有考慮更複雜的資料模型,例如使用LEFT JOIN和其它複雜的資料庫操作技術。然而你不必受此約束,借助於屬性重載,你可以使用你自己理想的資料庫模型。只需要加入少許程式碼,你即可以在該持續性儲存類別中運用複雜的資料庫特性。
還有一個小問題- 當在析構器中查詢失敗時並沒有引入錯誤處理機制。是析構器的天性導致在這種情況下不可能顯示對應的錯誤訊息,因為建構HTML標誌常常在PHP呼叫構析器之前就已經結束了。
要解決這個問題,你可以把__destruct()重新命名為象saveData()這樣的名字並在呼叫腳本的某處手動執行此方法。這對於類別的持續性儲存的概念並沒有任何改變;僅是多寫幾行程式碼而已。作為選擇,你也可以在析構器中使用函數error_log()來記錄下屬於系統範圍的錯誤記錄檔案中的錯誤訊息。
屬性重載的工作機制就是這樣。下面我們討論一下方法重載。
四、方法重載舉例
1. 動態的Getter/Setter方法
下列程式碼實作了"動態"getter/setter方法以藉助於方法重載的幫助來控制類別。下面我們結合原始碼來分析:
<?php
class DynamicGetterSetter {
private $name = "Martin Jansen";
private $starbucksdrink = "Caramel Cappuccino Swirl";
function __call($method, $arguments) {
$prefix = strtolower(substr($method, 0, 3));
$property = strtolower(substr($method, 3));
if (empty($prefix) || empty($property)) {
return;
}
if ($prefix == "get" && isset($this->$property)) {
return $this->$property;
}
if ($prefix == "set") {
$this->$property = $arguments[0];
}
}
}
$class = new DynamicGetterSetter;
echo "Name: " . $class->getName() . "n";
echo "Favourite Starbucks flavour: " . $class->getStarbucksDrink() . "nn";
$class->setName("John Doe");
$class->setStarbucksDrink("Classic Coffee");
echo "Name: " . $class->getName() . "n";
echo "Favourite Starbucks flavour: " . $class->getStarbucksDrink() . "nn";
?>
很明顯,這裡的兩個屬性$name和$starbucksdrink都是私有的,就是說從類別的外部是不能夠存取這些屬性的。在物件導向的程式設計中,實作公共的getter/setter方法來存取或修改非公共屬性的值是很常的事情。實現這些是單調的事情,而且相當耗費時間和精力。
借助於方法重載可以容易得解決這個問題。不是為每個屬性實作getter/setter方法,上面只實作了一個通用的__call()方法。這意味著當呼叫一個未定義的getter/setter方法如setName()或getStarbucksdrink()時,PHP不會產生一個致命錯誤而流產,而是執行(或代理到)魔術般的__call()方法。
這是一些簡單介紹,以下我們將__call()做一個深入分析。
2. 詳細分析__call()方法
__call()的第一個參數是原始的且尚未確定的方法(如setName),第二個參數是一個數字索引的一維數組,它包含了原始方法的所有參數。用兩個參數(
"Martin"和42)呼叫一個未定義的方法將產生下面數組: $class->thisMethodDoesNotExist("Martin", 42);
/導引__call()的第二個參數
Array
(
[0] => Martin
[1] => 42
)
在方法__call()內部,如果原始方法以get或set開頭,則要進行某種計算以確定是否程式碼呼叫的是一個getter/setter方法。而且,這種方法還要進一步分析方法名稱的另外一組成部分(除去開始的三個字元),因為後面這部分字串正代表getter/setter參照的屬性的名字。
如果方法名稱中指示有一個getter/setter,那麼方法或傳回對應的屬性值,或是設定原始方法的第一個參數的值。如果沒有的話,它不做任何事情,繼續執行程序,好像沒有事情發生。
3. 實現目標
實質上,相應於任意的屬性,存在一種方法允許程式碼動態地調用任意的getter/setter方法,這種演算法是存在的。這在短期內開發一個程式原型的情況下是很方便的:不是花費大量時間來實現getters/setters,開發人員可以專注於建模API並保證應用程式的根本正確。把__call()方法納入到一個抽象類別中甚至有可能使你在將來的PHP工程開發中實現程式碼的重複使用!
4. 不足之外
有優點就有缺點。以上方法也有幾個缺點:較大些的項目可以會使用象phpDocumentor這樣的工具來追蹤API結構。用上面介紹的動態方法,所有的getter/setter方法當然不會出現在自動產生的文件中,這是不需要多作解釋的。
另外一個不足是,類別外面的程式碼可以存取類別內的每一個私有屬性。當使用真正的getter/setter方法時,有可能區別開外部程式碼可以存取的私有屬性和對類別外部不可見的"真正的"私有屬性- 因為我們有方法重載,而且有虛擬的getter和setter方法可以利用。
五、結論
本文透過兩個例子細緻分析了PHP 5.0中物件重載的兩種情形。很希望本文的方法能幫助你提升PHP程式的工作效率!同時,你也應該清醒地看到這個方法的不足。