能夠通過 Web 服務為其他基於 Internet 的 Web 應用程序提供數據和功能正迅速成為重大開發中必不可或缺的組成部分。盡管 Oracle 提供了許多托管 Web 服務的方法,但這麼做始終不是最有效的方法,特別是在已經使用 PHP 來開發 Web 應用程序的情況下。在本手冊中,我將引導您使用 PHP 逐步開發 SOAP 客戶端和服務器,並使用 Oracle 作為數據的後端。
要真正了解這個問題的答案,您需要了解PHP腳本執行的生命周期以及 Web 服務器對該生命周期的影響,本手冊將從此開始逐步展開論述。
必需組件
出於本文的需要,您將使用一個非常簡單的數據庫後端,該數據庫後端將在一個表中存儲有關已發表書籍的某些基本信息,該表由以下 CREATE 語句表示:
CREATE TABLE books(isbn VARCHAR(32) PRIMARY KEY, author VARCHAR(50), title VARCHAR(50), price FLOAT);
該表將充當 SOAP 服務器的數據源,而數據源又會根據需要將數據提供給一個或多個 SOAP 客戶端。盡管在實際應用程序中,您的數據庫可能比較復雜,但這裡描述的方法仍然適用。
建立數據庫(最好在其中放置一些虛擬數據)之後,您現在就可以深入了解用 PHP 開發 SOAP 服務器所涉及的內容了。
SOAP 服務在 PHP 中的工作方式
用 PHP 開發 SOAP 服務的選擇有多種,所有方法都涉及到 SoapServer PHP 類。該類是所有基於 PHP 的 SOAP 服務的核心部分,其語法如下:
$server = new SoapServer($wsdl [, $options]);
其中,$wsdl 是描述托管服務的 Web 服務描述語言 (WSDL) 文檔的位置;$options 是一組鍵/值對,其中包含了在創建服務時需要考慮的所有設置選項。稍後,您將了解有關 WSDL 文檔的更多內容;現在,我們來看一下在創建新的 SOAP 服務時可用的選項:
◆soap_version:與客戶端通信時使用的 SOAP 協議版本。可能的選項是用於 SOAP 1.1 版的常量 SOAP_1_1 或用於 SOAP 1.2 版的 SOAP_1_2。
◆encoding:用於該 SOAP 服務的字符編碼(即字符串 ISO-8859-1)。
◆actor:該 SOAP 服務的角色 URI。
◆classmap:將 WSDL 數據類型映射到 PHP 中的類名的一組鍵/值對本身。如果使用該選項,PHP 將根據 WSDL 中定義的類型將這些類呈現給連接客戶端。
因此,要使用名為 bookman.wsdl 的 WSDL 文檔創建一個使用 SOAP v1.2 協議的 SOAP 服務,您應該按如下方法構建服務器:
$server = new SoapServer(“bookman.wsdl”, array(‘soap_version’ => SOAP_1_2));
該過程的下一步是創建服務方法。在 PHP 中,這可以使用兩個主要方法完成。第一個(也是最靈活的)方法是使用 addFunction() 方法手動指定要托管在服務中的每個函數,並將函數名傳遞給該方法以公開到客戶端:
function add($a, $b) { return $a + $b; } $server->addFunction(‘add’);
您還可以通過提供一組函數名來添加多個函數:
function add($a, $b) { return $a + $b; } function sub($a, $b) { return $a - $b; } $server->addFunction(array(‘add’, ‘sub’));
最後,您可以通過傳遞特殊常量 SOAP_FUNCTIONS_ALL 而非函數名來導出所有定義的函數,如下所示:
function add($a, $b) { return $a + $b; } function sub($a, $b) { return $a - $b; } $server->addFunction(SOAP_FUNCTIONS_ALL);
正如您從上述示例中看到的那樣,公開為 SOAP 服務的函數看上去與常規 PHP 函數完全相同。但是,根據定義,適用於在 SOAP 服務上下文中使用的函數的幾個規則並不適用於常規 PHP 函數:
◆函數必須以相同的順序接受相同的輸入參數,如提供給服務器的 WSDL 文檔定義的那樣。
◆函數不能輸出任何內容(即打印/回顯)。
◆函數必須返回一個或多個值(多個值以一組關聯的鍵/值對的形式返回)。
由於從體系結構或審美的角度看,在過程函數中表示所有公開的服務調用並不總是明智的選擇,因此 PHP 還提供了一種使用對象表示 SOAP 服務的方法。通過使用 addClass() 方法,您可以指定一個類來表示整個 SOAP 服務的函數,其中的所有公共方法將自動公開為服務調用:
class math { public function add($a, $b) { return $a + $b; } public function sub($a, $b) { return $a - $b; } } $server->addClass("math");
您稍後將看到,該方法是最簡潔、最模塊化的一個方法。
要完成 SOAP 服務器,您必須指導它處理從連接的 SOAP 客戶端傳入的任何請求。這是通過 handle() 方法完成的,該方法不需要參數。
總之,用PHP 創建SOAP 服務器就像以下示例一樣簡單:
<?PHP class math { public function add($a, $b) { return $a + $b; } public function sub($a, $b) { return $a - $b; } } $server = new SoapServer(‘math.wsdl’); $server->addClass(‘math’); $server->handle(); ?>
如果出現錯誤
用 PHP 創建 SOAP 服務時必須解決的一個問題是:在出現錯誤的情況下,如何將錯誤報告給客戶端。根據 SOAP 協議中的規范,在請求期間出現的錯誤應該通過將特殊 SOAP Fault 響應返回給請求客戶端的方式來處理。在 PHP 中,這是通過發出 SoapFault 類的實例、向該類提供錯誤代碼和描述錯誤本質的可選錯誤消息來完成的,如下所示:
public Function div($a, $b) { if($b == 0) { throw new SoapFault(-1,
“Cannot divide by zero!”); } return $a / $b; }
注意,除了應用程序本身表達的意思外,這兩個 SoapFault 參數沒有任何其他含義。因此,盡管我在這裡使用了負數作為錯誤代碼,但仍然可以十分輕松地選擇其他所需的整數值。
生成WSDL
盡管前面的示例確實是一個用於創建 SOAP 服務的完整 PHP 腳本,但它根本沒有解決 WSDL 文檔的問題。查看 WSDL 文檔是整個過程的一個重要組成部分,生成 WSDL 文檔則需要采取一些額外的操作。
遺憾的是,由於 PHP 的無類型本質,目前 PHP 還不能像強類型化語言(如 Java)或 .Net 服務那樣擁有即席自動生成 WSDL 文檔的合理方法。WSDL 文檔必須指定每個參數的類型,因此您需要使用其他方法在腳本中表達,因為變量 $a 和 $b 提供的是非類型化信息。有多種選擇可用:
◆自己手動編寫 WSDL 文檔。
◆通過手動輸入每個方法和類型化信息,使用基於 Web 的 WSDL 生成器來生成文件。
◆使用 Zend Studio 的自動 WSDL 生成器。
盡管這三個選擇都可行,但我將演示如何使用 Zend Studio 的 WSDL 生成器來生成 WSDL 文檔,原因有兩個:第一,這是目前為止生成 WSDL 文檔的最簡單、最可靠的方法;第二,Zend Studio 幾乎在每個正規的 PHP 櫃台都有售。
為了使用 Studio WSDL 生成器生成 WSDL 文檔,您首先必須為每個公開方法識別其參數的類型化信息,然後使用名為 PHPDoc(常用 JavaDoc 的 PHP 版本)的內嵌文檔注釋來返回值。PHPDoc 只是一個置於每個函數開頭的塊注釋,其使用的特定可分析語法可用於自動生成文檔。Zend Studio 還使用該信息收集生成 WSDL 文檔所需的類型化信息。
繼續前面的示例,下面是先前使用的同一 math 類,但這次使用的是 PHPDoc 注釋:
/** * A simple math utility class * @author John Coggeshall [email protected] */ class math { /** * Add two integers together * * @param integer $a The first integer of the addition * @param integer $b The second integer of the addition * @return integer The sum of the provided integers */ public function add($a, $b) { return $a + $b; } /** * Subtract two integers from each other * * @param integer $a The first integer of the subtraction * @param integer $b The second integer of the subtraction * @return integer The difference of the provided integers */ public function sub($a, $b) { return $a - $b; } }
正確使用這些 PHPDoc 注釋之後,通過執行 Studio 的 Tools 菜單下的 WSDL 生成器,您可以讓 Zend Studio 為該類自動生成合適的 WSDL 文檔:
正確使用 PHPDoc 注釋之後,就可以減少為 SOAP 服務器生成 WSDL 文檔所需的其他繁瑣而無意義的任務,而只需遵循一個非常簡單的分步向導即可。完成後,Studio 將打開其中的 WSDL 文檔,以供您查看並保存到所選的位置。
生成文檔之後,必須將該文檔放在服務器能夠訪問的位置(在實例化類時需要),以及可能使用該服務的潛在 SOAP 客戶端能夠訪問的位置。通常,這很容易實現,只需將 WSDL 文檔與托管 SOAP 服務的終端 PHP 腳本放在同一位置即可。
創建BookManager 類
現在,您已經熟悉了用 PHP 實施 SOAP 服務的所有內容,下面我們來討論數據庫。出於本手冊的需要,我創建了一個名為 BookManager 的類。該類的作用將與前面示例中的 math 類相同,除了要與數據庫進行交互,並提供一個 SOAP 服務,以允許您執行一般維護並查詢本教程開頭描述的書籍表。具體而言,BookManager 類將實施以下要公開為 SOAP 調用的方法:
addBook($isbn, $author, $title, $price);
// Adds a Book to the database delBook($isbn); // Deletes a book by ISBN number findBookISBNByAuthor($author);
// Returns an array of ISBN numbers of books written by a // specific author findBookISBNByTitle($title);
// Returns an array of ISBN numbers of books whose title // matches the substring provided getBookByISBN($isbn);
// Returns the details of the book identifIEd by ISBN listAllBooks();
// Returns an array of all ISBN numbers in the database
盡管該類本身有其他幾個方法,但只有上述六個方法是聲明的公共方法(當然,除了構造函數以外),因而也是僅有的公開為 SOAP 服務的方法。雖然詳細說明每個方法會超出本教程討論范圍(特別是它們在形式上基本相同),但出於完整性需要,我們來看一下 delBook() 方法:
/** * Delete a book from the database by ISBN * * @param string $isbn The ISBN serial number of the book to delete * * @return mixed SOAP Fault on error, true on success */ public function delBook($isbn) { $query = "DELETE FROM books WHERE isbn = :isbn"; $stmt = oci_parse($this->getDB(), $query); if(!$stmt) { throw new SoapFault(-1, "Failed to prepare query (reason: " . oci_error($stmt) . ")"); } oci_bind_by_name($stmt, "isbn", $isbn, 32); if(!oci_execute($stmt)) { oci_rollback($this->getDB()); throw new SoapFault(-1, "Failed to execute query (reason: " . oci_error($stmt) . ")"); } oci_commit($this->getDB()); return true; }
對於那些熟悉用於 PHP 的 Oracle API 的開發人員來說,上述方法應該很簡單。對於其余開發人員來說,我們從 oci_parse() 方法開始來探究該函數的某些關鍵點。該方法以字符串的形式接受 SQL 查詢(如果需要,在查詢中包含每個變量的占位符),然後返回表示該查詢的語句資源。在這裡,該語句資源的占位符可以通過 oci_bind_by_name() 方法直接映射到 PHP 變量,該方法將接受語句、占位符名稱、對應的 PHP 變量以及可選的當前最大列長度作為參數。一旦 PHP 將每個占位符綁定到一個 PHP 變量,就可以執行語句並獲得結果了。當然,由於該操作是一個針對表的 write 操作,您可以通過將更改提交到數據庫並返回成功狀態,來成功完成函數執行。
為便於參考,下面是一個完整的 BookManager 類,以及使用該類公開 SOAP 服務的相應服務器腳本。
* @author John Coggeshall <[email protected]> * * @throws SoapFault */ class BookManager { private $objDB; const DB_USERNAME="demo"; const DB_PASSWORD="passWord"; const DB_DATABASE="myOracle"; /** * Object Constructor: Establishes DB connection * */ function __construct() { $this->objDB = oci_connect(self::DB_USERNAME, self::DB_PASSWord, self::DB_DATABASE); if($this->objDB === false) { throw new SoapFault(-1, "Failed to connect to database backend (reason: " . oci_error() . ")"); } } /** * Private method to return the DB connection and make sure it exists * * @return unknown */ private function getDB() { if(!$this->objDB) { throw new SoapFault(-1, "No valid database connection"); } return $this->objDB; } /** * Add a new book to the database * * @param string $isbn The ISBN serial number for the book (32 char max) * @param string $author The name of the primary author (50 char max) * @param string $title The title of the book (50 char max) * @param float $price The price of the book in USD * * @return mixed SOAP Fault on error, true on success */ public function addBook($isbn, $author, $title, $price) { $query = "INSERT INTO books (isbn, author, title, price) VALUES (:isbn, :author, :title, :price)"; $stmt = oci_parse($this->getDB(), $query); if(!$stmt) { throw new SoapFault(-1, "Failed to prepare query (reason: " . oci_error($stmt) . ")"); } // The numbers 32, 50, 50 are the max column lengths oci_bind_by_name($stmt, "isbn", $isbn, 32); oci_bind_by_name($stmt, "author", $author, 50); oci_bind_by_name($stmt, "title", $title, 50); oci_bind_by_name($stmt, "price", $price); if(!oci_execute($stmt)) { oci_rollback($this->getDB()); throw new SoapFault(-1, "Failed to execute query (reason: " . oci_error($stmt) . ")"); } oci_commit($this->getDB()); return true; } /** * Delete a book from the database by ISBN * * @param string $isbn The ISBN serial number of the book to delete * * @return mixed SOAP Fault on error, true on success */ public function delBook($isbn) { $query = "DELETE FROM books WHERE isbn = :isbn"; $stmt = oci_parse($this->getDB(), $query); if(!$stmt) { throw new SoapFault(-1, "Failed to prepare query (reason: " . oci_error($stmt) . ")"); } oci_bind_by_name($stmt, "isbn", $isbn, 32); if(!oci_execute($stmt)) { oci_rollback($this->getDB()); throw new SoapFault(-1, "Failed to execute query (reason: " . oci_error($stmt) . ")"); } oci_commit($this->getDB()); return true; } /** * Return a list of books with a specific substring in their title * * @param string $name The name of the author * * @return mixed SOAP Fault on error, an array of ISBN numbers on success */ public function findBookISBNByTitle($title) { $query = "SELECT isbn FROM books WHERE title LIKE :titlefragment"; $stmt = oci_parse($this->getDB(), $query); if(!$stmt) { throw new SoapFault(-1, "Failed to prepare query (reason: " . oci_error($stmt) . ")"); } $bindVar = "%$title%"; oci_bind_by_name($stmt, ":titlefragment", $bindVar, 50); if(!oci_execute($stmt)) { throw new SoapFault(-1, "Failed to execute query (reason: " . oci_error($stmt) . ")"); } $rows = array(); while($row = oci_fetch_array($stmt, OCI_ASSOC)) { $rows[] = $row['ISBN']; } return $rows; } /** * Return a list of books written by a specific author * * @param mixed $author SOAP Fault on error, on array of ISBN numbers on success */ public function findBookISBNByAuthor($author) { $query = "SELECT isbn FROM books WHERE author = :author"; $stmt = oci_parse($this->getDB(), $query); if(!$stmt) { throw new SoapFault(-1, "Failed to prepare query (reason: " . oci_error($stmt) . ")"); } oci_bind_by_name($stmt, ":author", $author, 50); if(!oci_execute($stmt)) { throw new SoapFault(-1, "Failed to execute query (reason: " . oci_error($stmt) . ")"); } $rows = array(); while($row = oci_fetch_array($stmt, OCI_ASSOC)) { $rows[] = $row['ISBN']; } return $rows; } /** * Return a list of all ISBN numbers in the database * * @return array An array of ISBN numbers in the database */ public function listAllBooks() { $query = "SELECT isbn FROM books"; $stmt = oci_parse($this->getDB(), $query); if(!$stmt) { throw new SoapFault(-1, "Failed to prepare query (reason: " . oci_error($stmt) . ")"); } if(!oci_execute($stmt)) { throw new SoapFault(-1, "Failed to execute query (reason: " . oci_error($stmt) . ")"); } $rows = array(); while($row = oci_fetch_array($stmt, OCI_ASSOC)) { $rows[] = $row['ISBN']; } return $rows; } /** * Return the details of a specific book by ISBN * * @param string $isbn The ISBN of the book to retrIEve the details on * * @return mixed SOAP Fault on error, an array of key/value pairs for the ISBN on * success */ public function getBookByISBN($isbn) { $query = "SELECT * FROM books WHERE isbn = :isbn"; $stmt = oci_parse($this->getDB(), $query); if(!$stmt) { throw new SoapFault(-1, "Failed to prepare query (reason: " . oci_error($stmt) . ")"); } oci_bind_by_name($stmt, ":isbn", $isbn, 32); if(!oci_execute($stmt)) { throw new SoapFault(-1, "Failed to execute query (reason: " . oci_error($stmt) . ")"); } $row = oci_fetch_array($stmt, OCI_ASSOC); return $row; } } ?> <?php require_once 'BookManager.class.PHP'; $server = new SoapServer("bookman.wsdl"); $server->setClass("BookManager"); $server->handle(); ?>
用 PHP 創建 SOAP 客戶端
前面已經說明了如何使用 PHP 創建 SOAP 服務,下面我們來看一下如何創建 SOAP 客戶端,以供您的服務器與之通信。
盡管使用 PHP SOAP 實施通過 SOAP 執行遠程過程調用的方法有很多,但我們建議的方法是使用 WSDL 文檔。您已經生成了該文檔以使 SOAP 服務運行,因此該文檔已經存在。
要使用 PHP 創建 SOAP 客戶端,您必須創建一個 SoapClIEnt 類的實例,該類具有以下構造函數:
$client = new SoapClIEnt($wsdl [, $options]);
對於 SoapServer 類,$wsdl 參數是要訪問服務的 WSDL 文檔的位置,可選參數 $options 是配置客戶端連接的一組鍵/值對。以下是一些可用選項(請參見 www.PHP.Net/ 以獲得完整列表):
◆soap_version:要使用的 SOAP 協議版本,其值為常量 SOAP_1_1 或 SOAP_1_2
◆login:如果在 SOAP 服務器上使用 HTTP 身份驗證,這是要使用的登錄名
◆passWord:如果在 SOAP 服務器上使用 HTTP 身份驗證,這是要使用的密碼
◆proxy_host:如果通過代理服務器連接,這是服務器的地址
◆proxy_port:如果通過代理服務器連接,這是代理監聽的端口
◆proxy_login:如果通過代理服務器連接,這是登錄時使用的用戶名
◆proxy_passWord:如果通過代理服務器連接,這是登錄時使用的密碼
◆local_cert:如果連接到一個通過安全 HTTP (https) 通信的 SOAP 服務器,這是本地認證文件的位置
◆passphrase:與 local_cert 結合使用,以提供認證文件的密碼短語(如果有)
◆compression:如果設置為 true,PHP 將嘗試使用壓縮的 HTTP 請求與 SOAP 服務器通信
◆classmap:將 WSDL 數據類型映射到 PHP 類以便在客戶端使用的一組鍵/值對
如果 PHP 中的 SOAP 客戶端通過 WSDL 文檔實例化,就可以使用返回的客戶端對象調用在 SOAP 服務器上公開的方法(就好像它們是自帶 PHP 調用),並處理任何可能作為原生 PHP 異常發生的 SOAP 錯誤。例如,返回到原始 math SOAP 服務示例,以下是一個完整的 PHP SOAP 客戶端:
<?PHP $client = new SoapClIEnt
(“http://www.example.com/math.wsdl”); try { $result = $clIEnt->div(10,rand(0,5);
// will cause a Soap Fault if divide by zero print “The answer is: $result”; } catch(SoapFault $f) { print “Sorry an error was caught
executing your request: {$e->getMessage()}”; } ?>
正如您看到的那樣,使用 SoapClIEnt 類訪問 SOAP 服務(無論它們是否在 PHP 中實施)很簡單。實際上,通過 SOAP 服務為您的書籍數據庫創建一個基於 Web 的管理系統是件輕而易舉的事!如下所示,與讓查詢接口直接與 SOAP 服務交互相比,開發這個簡單查詢接口的邏輯和界面明顯需要更多的編碼工作。
<HTML> <HEAD><TITLE>Oracle / SOAP Example by John Coggeshall</TITLE></HEAD> <BODY> <?php $client = new SoapClient("bookman.wsdl"); try { switch(@$_GET['mode']) { case 'title': if(!empty($_GET['title'])) { $isbns = $client->findBookISBNByTitle($_GET['title']); } else { print "<B>Error:</B> You must specify at a title fragment!BR/>"; } break; case 'author': if(!empty($_GET['author'])) { $isbns = $client->findBookISBNByAuthor($_GET['author']); } else { print "<B>Error:</B> You must specify the author to search!<BR/>"; } break; default: $isbns = $client->listAllBooks(); } print "<TABLE WIDTH='600'><TR><TD>ISBN</TD><TD>Author</TD>"; print "<TD>Title</TD><TD>Price</TD></TR>"; if(!isset($isbns) || !is_array($isbns)) { print "<TR><TD COLSPAN='4' ALIGN='CENTER'><I>No Results Available</I></TD></TR>"; } else { foreach($isbns as $isbn) { $details = $clIEnt->getBookByISBN($isbn); print "<TR>"; print "<TD>{$details['ISBN']}</TD><TD>{$details['AUTHOR']}</TD>"; print "<TD>{$details['TITLE']}</TD><TD>{$details['PRICE']}</TD>"; print "</TR>"; } } print "</TABLE>"; } catch(SoapFault $e) { $msg = (!$e->getMessage()) ? $e->faultstring : $e->getMessage(); print "Sorry, an error was returned: $msg<HR>"; } ?> <TABLE> <FORM ACTION="<?php print $_SERVER['PHP_SELF']; ?>" METHOD="GET"> <INPUT TYPE="hidden" NAME="mode" VALUE="title"> <TR><TD><B>Search By Title:</B></TD> <TD> <INPUT TYPE="text" NAME="title" SIZE="50" MAXLENGTH="50"> <INPUT TYPE="submit" VALUE="Search"> </TD></TR> </FORM> <FORM ACTION="<?php print $_SERVER['PHP_SELF']; ?>" METHOD="GET"> <INPUT TYPE="hidden" NAME="mode" VALUE="author"> <TR><TD><B>Search By Author:</B></TD> <TD><INPUT TYPE="text" NAME="author" SIZE="50" MAXLENGTH="50"> <INPUT TYPE="submit" VALUE="Search"> </TD></TR> </FORM> <TR> <TD COLSPAN='2' ALIGN='center'> <A HREF="<?php print $_SERVER['PHP_SELF']?>">Display All Books</A> </TD> </TABLE> </BODY> </Html>
在執行時,這將通過 PHP 驅動的 Web 服務為 Oracle 數據庫提供一個難看但功能完善的界面。
結論
現在,您應該具備了所有必備知識,可以使用 Oracle 支持的數據庫,並將它們與 PHP 中的 SOAP 功能相結合,以創建強大的 Web 服務。隨著 Internet 的演化越來越接近神奇的 Web 2.0,這些服務構成了面向服務體系結構的重要部分,也成為了豐富的 Internet 客戶端體驗的一個特點。盡管我們沒有涵蓋 PHP 中的 SOAP 功能的每個細節,但我們只忽略了僅在很少情況下(例如,不使用 WSDL 文檔連接到服務)可用的那些功能。