PHP V5 新的面向對象編程特性顯著提升了這個流行語言中的功能層次。學習如何用 PHP V5 動態特性創建可以滿足需求的對象。
PHP V5 中新的面向對象編程(OOP)特性的引入顯著提升了這個編程語言的功能層次。現在不僅有了私有的、受保護的和公共的成員變量和函數 —— 就像在 Java?、 C++ 或 C# 編程語言中一樣 —— 但是還可以創建在運行時變化的對象,即動態地創建新方法和成員變量。而使用 Java、C++ 或 C# 語言是做不到這件事的。這種功能使得超級快速的應用程序開發系統(例如 Ruby on Rails)成為可能。
但是,在進入這些之前,有一點要注意:本文介紹 PHP V5 中非常高級的 OOP 特性的使用,但是這類特性不是在每個應用程序中都需要的。而且,如果不具備 OOP 的堅實基礎以及 PHP 對象語法的初步知識,這類特性將會很難理解。
動態的重要性
對象是把雙刃劍。一方面,對象是封裝數據和邏輯并創建更容易維護的系統的重大方式。但另一方面,它們會變得很繁瑣,需要許多冗余的代碼,這時可能最希望做到的就是不要犯錯。這類問題的一個示例來自數據庫訪問對象。一般來說,想用一個類代表每個數據庫表,并執行以下功能:對象從數據庫讀出數據行;允許更新字段,然后用新數據更新數據庫或刪除行。還有一種方法可以創建新的空對象,設置對象的字段,并把數據插入數據庫。
如果在數據庫中有一個表,名為 Customers,那么就應當有一個對象,名為 Customer
,它應當擁有來自表的字段,并代表一個客戶。而且 Customer
對象應當允許插入、更新或刪除數據庫中對應的記錄。現在,一切都很好,而且有也很多意義。但是,有許多代碼要編寫。如果在數據庫中有 20 個表,就需要 20 個類。
有三個解決方案可以采用。第一個解決方案就是,坐在鍵盤前,老老實實地錄入一段時間。對于小項目來說,這還可以,但是我很懶。第二個解決方案是用代碼生成器,讀取數據庫模式,并自動編寫代碼。這是個好主意,而且是另一篇文章的主題。第三個解決方案,也是我在本文中介紹的,是編寫一個類,在運行時動態地把自己塑造成指定表的字段。這個類執行起來比起特定于表的類可能有點慢 —— 但是把我從編寫大量代碼中解脫出來。這個解決方案在項目開始的時候特別有用,因為這時表和字段不斷地變化,所以跟上迅速的變化是至關重要的。
所以,如何才能編寫一個能夠彎曲 的類呢?
寫一個柔性的類
對象有兩個方面:成員變量 和方法。在編譯語言(例如 Java)中,如果想調用不存在的方法或引用不存在的成員變量,會得到編譯時錯誤。但是,在非編譯語言,例如 PHP 中,會發生什么?
在 PHP 中的方法調用是這樣工作的。首先,PHP 解釋器在類上查找方法。如果方法存在,PHP 就調用它。如果沒有,那么就調用類上的魔法方法 __call
(如果這個方法存在的話)。如果 __call
失敗,就調用父類方法,依此類推。
|
魔法方法
魔法方法是有特定名稱的方法,PHP 解釋器在腳本執行的特定點上會查找魔法方法。最常見的魔法方法就是對象創始時調用的構造函數。
|
|
__call
方法有兩個參數:被請求的方法的名稱和方法參數。如果創建的 __call
方法接受這兩個參數,執行某項功能,然后返回 TRUE,那么調用這個對象的代碼就永遠不會知道在有代碼的方法和 __call
機制處理的方法之間的區別。通過這種方式,可以創建這樣的對象,即動態地模擬擁有無數方法的情況。
除了 __call
方法,其他魔法方法 —— 包括 __get
和 __set
—— 調用它們的時候,都是因為引用了不存在的實例變量。腦子里有了這個概念之后,就可以開始編寫能夠適應任何表的動態數據庫訪問類了。
經典的數據庫訪問
先從一個簡單的數據庫模式開始。清單 1 所示的模式針對的是單一的數據表數據庫,容納圖書列表。
清單 1. MySQL 數據庫模式
DROP TABLE IF EXISTS book;
CREATE TABLE book (
book_id INT NOT NULL AUTO_INCREMENT,
title TEXT,
publisher TEXT,
author TEXT,
PRIMARY KEY( book_id )
);
|
請把這個模式裝入到名為 bookdb 的數據庫。
接下來,編寫一個常規的數據庫類,然后再把它修改成動態的。清單 2 顯示了圖書表的簡單的數據庫訪問類。
清單 2. 基本的數據庫訪問客戶機
<?php
require_once("DB.php");
$dsn = 'mysql://root:password@localhost/bookdb';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
class Book
{
private $book_id;
private $title;
private $author;
private $publisher;
function __construct()
{
}
function set_title( $title ) { $this->title = $title; }
function get_title( ) { return $this->title; }
function set_author( $author ) { $this->author = $author; }
function get_author( ) { return $this->author; }
function set_publisher( $publisher ) {
$this->publisher = $publisher; }
function get_publisher( ) { return $this->publisher; }
function load( $id )
{
global $db;
$res = $db->query( "SELECT * FROM book WHERE book_id=?",
array( $id ) );
$res->fetchInto( $row, DB_FETCHMODE_ASSOC );
$this->book_id = $id;
$this->title = $row['title'];
$this->author = $row['author'];
$this->publisher = $row['publisher'];
}
function insert()
{
global $db;
$sth = $db->prepare(
'INSERT INTO book ( book_id, title, author, publisher )
VALUES ( 0, ?, ?, ? )'
);
$db->execute( $sth,
array( $this->title,
$this->author,
$this->publisher ) );
$res = $db->query( "SELECT last_insert_id()" );
$res->fetchInto( $row );
return $row[0];
}
function update()
{
global $db;
$sth = $db->prepare(
'UPDATE book SET title=?, author=?, publisher=?
WHERE book_id=?'
);
$db->execute( $sth,
array( $this->title,
$this->author,
$this->publisher,
$this->book_id ) );
}
function delete()
{
global $db;
$sth = $db->prepare(
'DELETE FROM book WHERE book_id=?'
);
$db->execute( $sth,
array( $this->book_id ) );
}
function delete_all()
{
global $db;
$sth = $db->prepare( 'DELETE FROM book' );
$db->execute( $sth );
}
}
$book = new Book();
$book->delete_all();
$book->set_title( "PHP Hacks" );
$book->set_author( "Jack Herrington" );
$book->set_publisher( "O'Reilly" );
$id = $book->insert();
echo ( "New book id = $id\n" );
$book2 = new Book();
$book2->load( $id );
echo( "Title = ".$book2->get_title()."\n" );
$book2->delete( );
?>
|
為了保持代碼簡單,我把類和測試代碼放在一個文件中。文件首先得到數據庫句柄,句柄保存在一個全局變量中。然后定義 Book
類,用私有成員變量代表每個字段。還包含了一套用來從數據庫裝入、插入、更新和刪除行的方法。
底部的測試代碼先刪除數據庫中的所有條目。然后,代碼插入一本書,輸出新記錄的 ID。然后,代碼把這本書裝入另一個對象并輸出書名。
清單 3 顯示了在命令行上用 PHP 解釋器運行代碼的效果。
清單 3. 在命令行運行代碼
% php db1.php
New book id = 25
Title = PHP Hacks
%
|
不需要看太多,就已經得到重點了。Book
對象代表圖書數據表中的行。通過使用上面的字段和方法,可以創建新行、更新行和刪除行。
初識動態
下一步是讓類變得稍微動態一些:動態地為每個字段創建 get_
和 set_
方法。清單 4 顯示了更新后的代碼。
清單 4. 動態 get_ 和 set_ 方法
<?php
require_once("DB.php");
$dsn = 'mysql://root:password@localhost/bookdb';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
class Book
{
private $book_id;
private $fields = array();
function __construct()
{
$this->fields[ 'title' ] = null;
$this->fields[ 'author' ] = null;
$this->fields[ 'publisher' ] = null;
}
function __call( $method, $args )
{
if ( preg_match( "/set_(.*)/", $method, $found ) )
{
if ( array_key_exists( $found[1], $this->fields ) )
{
$this->fields[ $found[1] ] = $args[0];
return true;
}
}
else if ( preg_match( "/get_(.*)/", $method, $found ) )
{
if ( array_key_exists( $found[1], $this->fields ) )
{
return $this->fields[ $found[1] ];
}
}
return false;
}
function load( $id )
{
global $db;
$res = $db->query( "SELECT * FROM book WHERE book_id=?",
array( $id ) );
$res->fetchInto( $row, DB_FETCHMODE_ASSOC );
$this->book_id = $id;
$this->set_title( $row['title'] );
$this->set_author( $row['author'] );
$this->set_publisher( $row['publisher'] );
}
function insert()
{
global $db;
$sth = $db->prepare(
'INSERT INTO book ( book_id, title, author, publisher )
VALUES ( 0, ?, ?, ? )'
);
$db->execute( $sth,
array( $this->get_title(),
$this->get_author(),
$this->get_publisher() ) );
$res = $db->query( "SELECT last_insert_id()" );
$res->fetchInto( $row );
return $row[0];
}
function update()
{
global $db;
$sth = $db->prepare(
'UPDATE book SET title=?, author=?, publisher=?
WHERE book_id=?'
);
$db->execute( $sth,
array( $this->get_title(),
$this->get_author(),
$this->get_publisher(),
$this->book_id ) );
}
function delete()
{
global $db;
$sth = $db->prepare(
'DELETE FROM book WHERE book_id=?'
);
$db->execute( $sth,
array( $this->book_id ) );
}
function delete_all()
{
global $db;
$sth = $db->prepare( 'DELETE FROM book' );
$db->execute( $sth );
}
}
..
|
要做這個變化,需要做兩件事。首先,必須把字段從單個實例變量修改成字段和值組合構成的散列表。然后必須添加一個 __call
方法,它只查看方法名稱,看方法是 set_
還是 get_
方法,然后在散列表中設置適當的字段。
注意,load
方法通過調用 set_title
、set_author
和 set_publisher
方法 —— 實際上都不存在 —— 來實際使用 __call
方法。
走向完全動態
刪除 get_
和 set_
方法只是一個起點。要創建完全動態的數據庫對象,必須向類提供表和字段的名稱,還不能有硬編碼的引用。清單 5 顯示了這個變化。
清單 5. 完全動態的數據庫對象類
<?php
require_once("DB.php");
$dsn = 'mysql://root:password@localhost/bookdb';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
class DBObject
{
private $id = 0;
private $table;
private $fields = array();
function __construct( $table, $fields )
{
$this->table = $table;
foreach( $fields as $key )
$this->fields[ $key ] = null;
}
function __call( $method, $args )
{
if ( preg_match( "/set_(.*)/", $method, $found ) )
{
if ( array_key_exists( $found[1], $this->fields ) )
{
$this->fields[ $found[1] ] = $args[0];
return true;
}
}
else if ( preg_match( "/get_(.*)/", $method, $found ) )
{
if ( array_key_exists( $found[1], $this->fields ) )
{
return $this->fields[ $found[1] ];
}
}
return false;
}
function load( $id )
{
global $db;
$res = $db->query(
"SELECT * FROM ".$this->table." WHERE ".
$this->table."_id=?",
array( $id )
);
$res->fetchInto( $row, DB_FETCHMODE_ASSOC );
$this->id = $id;
foreach( array_keys( $row ) as $key )
$this->fields[ $key ] = $row[ $key ];
}
function insert()
{
global $db;
$fields = $this->table."_id, ";
$fields .= join( ", ", array_keys( $this->fields ) );
$inspoints = array( "0" );
foreach( array_keys( $this->fields ) as $field )
$inspoints []= "?";
$inspt = join( ", ", $inspoints );
$sql = "INSERT INTO ".$this->table." ( $fields )
VALUES ( $inspt )";
$values = array();
foreach( array_keys( $this->fields ) as $field )
$values []= $this->fields[ $field ];
$sth = $db->prepare( $sql );
$db->execute( $sth, $values );
$res = $db->query( "SELECT last_insert_id()" );
$res->fetchInto( $row );
$this->id = $row[0];
return $row[0];
}
function update()
{
global $db;
$sets = array();
$values = array();
foreach( array_keys( $this->fields ) as $field )
{
$sets []= $field.'=?';
$values []= $this->fields[ $field ];
}
$set = join( ", ", $sets );
$values []= $this->id;
$sql = 'UPDATE '.$this->table.' SET '.$set.
' WHERE '.$this->table.'_id=?';
$sth = $db->prepare( $sql );
$db->execute( $sth, $values );
}
function delete()
{
global $db;
$sth = $db->prepare(
'DELETE FROM '.$this->table.' WHERE '.
$this->table.'_id=?'
);
$db->execute( $sth,
array( $this->id ) );
}
function delete_all()
{
global $db;
$sth = $db->prepare( 'DELETE FROM '.$this->table );
$db->execute( $sth );
}
}
$book = new DBObject( 'book', array( 'author',
'title', 'publisher' ) );
$book->delete_all();
$book->set_title( "PHP Hacks" );
$book->set_author( "Jack Herrington" );
$book->set_publisher( "O'Reilly" );
$id = $book->insert();
echo ( "New book id = $id\n" );
$book->set_title( "Podcasting Hacks" );
$book->update();
$book2 = new DBObject( 'book', array( 'author',
'title', 'publisher' ) );
$book2->load( $id );
echo( "Title = ".$book2->get_title()."\n" );
$book2->delete( );
? >
|
在這里,把類的名稱從 Book
改成 DBObject
。然后,把構造函數修改成接受表的名稱和表中字段的名稱。之后,大多數變化發生在類的方法中,過去使用一些硬編碼結構化查詢語言(SQL),現在則必須用表和字段的名稱動態地創建 SQL 字符串。
代碼的惟一假設就是只有一個主鍵字段,而且這個字段的名稱是表名加上 _id
。所以,在 book
表這個示例中,有一個主鍵字段叫做 book_id
。主鍵的命名標準可能不同;如果這樣,需要修改代碼以符合標準。
這個類比最初的 Book
類復雜得多。但是,從類的客戶的角度來看,這個類用起來仍很簡單。也就是說,我認為這個類能更簡單。具體來說,我不愿意每次創建圖書的時候都要指定表和字段的名稱。如果我四處拷貝和粘貼這個代碼,然后修改了 book 表的字段結構,那么我可能就麻煩了。在清單 6 中,通過創建一個繼承自 DBObject
的簡單 Book
類,我解決了這個問題。
清單 6. 新的 Book 類
..
class Book extends DBObject
{
function __construct()
{
parent::__construct( 'book',
array( 'author', 'title', 'publisher' ) );
}
}
$book = new Book( );
$book->delete_all();
$book->{'title'} = "PHP Hacks";
$book->{'author'} = "Jack Herrington";
$book->{'publisher'} = "O'Reilly";
$id = $book->insert();
echo ( "New book id = $id\n" );
$book->{'title'} = "Podcasting Hacks";
$book->update();
$book2 = new Book( );
$book2->load( $id );
echo( "Title = ".$book2->{'title'}."\n" );
$book2->delete( );
?>
|
現在,Book
類真的是簡單了。而且 Book
類的客戶也不再需要知道表或字段的名稱了。
改進的空間
對這個動態類我想做的最后一個改進,是用成員變量訪問字段,而不是用笨重的 get_
和 set_
操作符。清單 7 顯示了如何用 __get
和 __set
魔法方法代替 __call
。
清單 7. 使用 __get 和 __set 方法
<?php
require_once("DB.php");
$dsn = 'mysql://root:password@localhost/bookdb';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
class DBObject
{
private $id = 0;
private $table;
private $fields = array();
function __construct( $table, $fields )
{
$this->table = $table;
foreach( $fields as $key )
$this->fields[ $key ] = null;
}
function __get( $key )
{
return $this->fields[ $key ];
}
function __set( $key, $value )
{
if ( array_key_exists( $key, $this->fields ) )
{
$this->fields[ $key ] = $value;
return true;
}
return false;
}
function load( $id )
{
global $db;
$res = $db->query(
"SELECT * FROM ".$this->table." WHERE ".
$this->table."_id=?",
array( $id )
);
$res->fetchInto( $row, DB_FETCHMODE_ASSOC );
$this->id = $id;
foreach( array_keys( $row ) as $key )
$this->fields[ $key ] = $row[ $key ];
}
function insert()
{
global $db;
$fields = $this->table."_id, ";
$fields .= join( ", ", array_keys( $this->fields ) );
$inspoints = array( "0" );
foreach( array_keys( $this->fields ) as $field )
$inspoints []= "?";
$inspt = join( ", ", $inspoints );
$sql = "INSERT INTO ".$this->table.
" ( $fields ) VALUES ( $inspt )";
$values = array();
foreach( array_keys( $this->fields ) as $field )
$values []= $this->fields[ $field ];
$sth = $db->prepare( $sql );
$db->execute( $sth, $values );
$res = $db->query( "SELECT last_insert_id()" );
$res->fetchInto( $row );
$this->id = $row[0];
return $row[0];
}
function update()
{
global $db;
$sets = array();
$values = array();
foreach( array_keys( $this->fields ) as $field )
{
$sets []= $field.'=?';
$values []= $this->fields[ $field ];
}
$set = join( ", ", $sets );
$values []= $this->id;
$sql = 'UPDATE '.$this->table.' SET '.$set.
' WHERE '.$this->table.'_id=?';
$sth = $db->prepare( $sql );
$db->execute( $sth, $values );
}
function delete()
{
global $db;
$sth = $db->prepare(
'DELETE FROM '.$this->table.' WHERE '.
$this->table.'_id=?'
);
$db->execute( $sth,
array( $this->id ) );
}
function delete_all()
{
global $db;
$sth = $db->prepare( 'DELETE FROM '.$this->table );
$db->execute( $sth );
}
}
class Book extends DBObject
{
function __construct()
{
parent::__construct( 'book',
array( 'author', 'title', 'publisher' ) );
}
}
$book = new Book( );
$book->delete_all();
$book->{'title'} = "PHP Hacks";
$book->{'author'} = "Jack Herrington";
$book->{'publisher'} = "O'Reilly";
$id = $book->insert();
echo ( "New book id = $id\n" );
$book->{'title'} = "Podcasting Hacks";
$book->update();
$book2 = new Book( );
$book2->load( $id );
echo( "Title = ".$book2->{'title'}."\n" );
$book2->delete( );
?>
|
底部的測試代碼只演示了這個語法干凈了多少。要得到圖書的書名,只需得到 title
成員變量。這個變量會調用對象的 __get
方法,在散列表中查找 title
條目并返回。
現在就得到了單個動態的數據庫訪問類,它能夠讓自己適應到數據庫中的任何表。
動態類的更多用途
編寫動態類不僅限于數據庫訪問。請看清單 8 中的 Customer
對象這個例子。
清單 8. 簡單的 Customer 對象
<?php
class Customer
{
private $name;
function set_name( $value )
{
$this->name = $value;
}
function get_name()
{
return $this->name;
}
}
$c1 = new Customer();
$c1->set_name( "Jack" );
$name = $c1->get_name();
echo( "name = $name\n" );
?>
|
這個對象足夠簡單。但是如果我想在每次檢索或設置客戶名稱時都記錄日志,會發生什么呢?我可以把這個對象包裝在一個動態日志對象內,這個對象看起來像 Customer
對象,但是會把 get
或 set
操作的通知發送給日志。清單 9 顯示了這類包裝器對象。
清單 9. 動態包裝器對象
<?php
class Customer
{
private $name;
function set_name( $value )
{
$this->name = $value;
}
function get_name()
{
return $this->name;
}
}
class Logged
{
private $obj;
function __call( $method, $args )
{
echo( "$method( ".join( ",", $args )." )\n" );
return call_user_func_array(array(&$this->obj,
$method), $args );
}
function __construct( $obj )
{
$this->obj = $obj;
}
}
$c1 = new Logged( new Customer() );
$c1->set_name( "Jack" );
$name = $c1->get_name();
echo( "name = $name\n" );
?>
|
調用日志版本的 Customer
的代碼看起來與前面相同,但是這時,對 Customer
對象的任何訪問都被記入日志。清單 10 顯示了運行這個日志版代碼時輸出的日志。
清單 10. 運行日志版對象
% php log2.php
set_name( Jack )
get_name( )
name = Jack
%
|
在這里,日志輸出表明用參數 Jack
調用了set_name
方法。然后,調用 get_name
方法。最后,測試代碼輸出 get_name
調用的結果。
結束語
如果這個動態對象素材對您來說理解起來有點難,我不會責備您。因為我自己也花了不少時間研究它并使用代碼才理解它并看出它的好處。
動態對象有許多功能,但是也有相當的風險。首先,在剛開始編寫魔法方法時,類的復雜性顯著增加。這些類更難理解、調試和維護。另外,因為集成開發環境(IDE)變得越來越智能,所以在處理動態類時它們也會遇到這類問題,因為當它們在類上查找方法時會找不到方法。
現在,并不是說應當避免編寫這類代碼。相反。我非常喜歡 PHP 的設計者這么有想法,把這些魔法方法包含在語言中,這樣我們才能編寫這類代碼。但是重要的是,既要理解優點,也要理解不足。
當然,對于應用程序(例如數據庫訪問)來說,在這里介紹的技術 —— 與廣泛流行的 Ruby on Rails 系統上使用的技術類似 —— 能夠極大地減少用 PHP 實現數據庫應用程序所需要的時間。節約時間總不是壞事。
參考資料
學習
posted on 2006-04-28 16:02
崛起的程序員 閱讀(208)
評論(0) 編輯 收藏 所屬分類:
載選文章