利用PHP创建由Oracle驱动的SOAP服务
能够通过 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); |
建立数据库(最好在其中放置一些虚拟数据)之后,您现在就可以深入了解用 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; } |
最后,您可以通过传递特殊常量 SOAP_FUNCTIONS_ALL 而非函数名来导出所有定义的函数,如下所示:
function add($a, $b) { return $a + $b; } function sub($a, $b) { return $a - $b; } $server->addFunction(SOAP_FUNCTIONS_ALL); |
◆函数必须以相同的顺序接受相同的输入参数,如提供给服务器的 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; } |
生成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 john@zend.com */ 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 注释之后,就可以减少为 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 |
/** * 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; } |
为便于参考,下面是一个完整的 BookManager 类,以及使用该类公开 SOAP 服务的相应服务器脚本。
* @author John Coggeshall < john@zend.com> * * @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()}”; } ?> |
< 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> |
结论
现在,您应该具备了所有必备知识,可以使用 Oracle 支持的数据库,并将它们与 PHP 中的 SOAP 功能相结合,以创建强大的 Web 服务。随着 Internet 的演化越来越接近神奇的 Web 2.0,这些服务构成了面向服务体系结构的重要部分,也成为了丰富的 Internet 客户端体验的一个特点。尽管我们没有涵盖 PHP 中的 SOAP 功能的每个细节,但我们只忽略了仅在很少情况下(例如,不使用 WSDL 文档连接到服务)可用的那些功能。