用 PHP 走向動態

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_titleset_authorset_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 對象,但是會把 getset 操作的通知發送給日誌。清單 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 實現數據庫應用程序所需要的時間。節約時間總不是壞事。

參考資料

學習

獲得產品和技術

  • 請用 IBM 試用軟件 改進您的下一個開放源碼開發項目,這些軟件可以下載或從 DVD 得到。

討論

關於作者

 

Jack D. Herrington 是有 20 多年工作經驗的高級軟件工程師。他是 Code Generation in ActionPodcasting Hacks 和即將出版的 PHP Hacks 這三本書的合著者。他還撰寫了 30 多篇文章。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章