使用 Zend Framework 实现 SOAP 服务

使用 Zend Framework 将 SOAP 服务快速添加到您的 PHP Web 应用中

Zend Framework 是一个用于构建健壮且可扩展的 PHP Web 应用的基于 MVC 的框架。它包含了一个 Zend_Soap 组件,它允许开发人员快速有效地添加基于 SOAP 的 Web 到他们的应用中。本文详细介绍了 Zend_Soap 组件,演示了如何创建一个 SOAP Web 服务,并介绍了诸如输入验证、错误生成和自动创建 WSDL 的功能。

Vikram Vaswani, 创始人, Melonfire

Vikram Vaswani 的照片Vikram Vaswani 是 Melonfire 的创始人和 CEO,该公司是一家专门研究开源工具和技术的咨询服务公司。他还著有 PHP Programming SolutionsHow to do Everything with PHP and MySQL 等著作。



2010 年 6 月 12 日

引言

Web 服务最近非常流行,其中基于 REST 的服务吸引了大部分的关注。REST 之所以流行,是由于它简单、直接和能够处理现有 HTTP 方法。但是也要记住,REST 并不是唯一的方法:SOAP,即 Simple Object Access Protocol,是一种更正式和更标准的处理 Web 信息交换问题的方法。

常见缩略词

  • API:应用编程接口
  • HTTP:超文本传输协议
  • i18n:国际化
  • MVC:模型-视图-控制
  • OOP:面向对象编程
  • REST:具象状态传输
  • SQL:结构化查询语言
  • URI:统一资源标识符
  • URL:统一资源定位符
  • W3C:万维网联盟
  • WSDL:Web 服务描述语言
  • XML:可扩展标记语言

虽然基于 SOAP 的服务实现一般被认为是一个复杂的、耗费时间的过程,但是有许多工具可以显著简化这个过程。其中一个工具是 Zend Framework,它是使用 PHP 构建可扩展 Web 应用的一个完整的 MVC 框架。除了许多强大功能之外 — OOP 形式、i18n 支持、查询和页面缓存和 Dojo 集成等等 — Zend Framework 也提供了大量通过它的 Zend_Soap 组件创建和部署 SOAP 服务的工具包。

在本文中,您将了解使用 Zend Framework 创建一个简单的基于 SOAP Web 服务的过程。除了学习处理客户端请求和返回符合 SOAP 响应之外,您还将了解处理异常和产生 SOAP 错误的过程。最后,您也将使用 Zend_Soap 自动生成一个描述 SOAP 服务的 WSDL 文件,从而使客户端能 “自动发现” SOAP 服务 API。


理解 SOAP

首先,我们要理解一些关于 SOAP 的词汇。SOAP 是使用与语言无关的 XML 在 Web 上交换信息的方法,从而允许与使用不同语言编写的应用实现互连。这个 XML 通过 HTTP 传输协议在客户端和服务器之间进行传输,它具有强数据类型,可以保证数据完整性。

REST 以资源行为 为中心,而 SOAP 则与之不同,它基于方法数据类型。一个 REST 服务一般只有 4 个操作,它们对应于 4 个 HTTP 方法 GET、POST、PUT 和 DELETE,而 SOAP 服务则没有这样的限制;开发人员可以根据自己的需要使用更多或较少的方法。而且,这些方法一般是通过 POST HTTP 方法调用的,而这个方法与所请求的操作类型则完全没有关系。

为了演示 SOAP 的用法,我们使用一个简单的例子。假设您有一个社交书签应用,而您希望允许第三方开发人员使用 SOAP 向应用添加书签和从应用查询书签。一般情况下,您会使用 getBookmark()addBookmark() 等函数实现一组服务对象,并将这些服务对象通过一个 SOAP 服务器发布出去。这个服务也会负责将 SOAP 数据类型转换成原生数据类型,解析 SOAP 请求数据包,执行相应的服务器函数,并生成包含结果的一个 SOAP 响应数据包。

清单 1 显示了 getBookmark() 过程的一个可能的 SOAP 请求例子:

清单 1. 一个 SOAP 请求例子
POST /soap HTTP/1.1
Host: localhost
Connection: Keep-Alive
User-Agent: PHP-SOAP/5.3.1
Content-Type: application/soap+xml; charset=utf-8
Content-Length: 471

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" 
 xmlns:ns1="http://example.localhost/index/soap" 
 xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xmlns:enc="http://www.w3.org/2003/05/soap-encoding">
<env:Body>
<ns1:getBookmark env:encodingStyle="http://www.w3.org/2003/05/soap-encoding">
<param0 xsi:type="xsd:int">4682</param0>
</ns1:getBookmark>
</env:Body>
</env:Envelope>

清单 2 显示了一个示例响应:

清单 2. 一个 SOAP 响应例子
HTTP/1.1 200 OK
Date: Wed, 17 Mar 2010 17:13:28 GMT
Server: Apache/2.2.14 (Win32) PHP/5.3.1
X-Powered-By: PHP/5.3.1
Content-Length: 800
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: application/soap+xml; charset=utf-8

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" 
 xmlns:ns1="http://example.localhost/index/soap" 
 xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
 xmlns:enc="http://www.w3.org/2003/05/soap-encoding" 
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<env:Body xmlns:rpc="http://www.w3.org/2003/05/soap-rpc">
<ns1:getBookmarkResponse 
 env:encodingStyle="http://www.w3.org/2003/05/soap-encoding">
<rpc:result>return</rpc:result>
<return enc:itemType="xsd:string" enc:arraySize="3" 
 xsi:type="enc:Array">
<item xsi:type="xsd:string">http://www.google.com</item>
<item xsi:type="xsd:string">http://www.php-programming-solutions.com
</item>
<item xsi:type="xsd:string">http://www.mysql-tcr.com</item>
</return>
</ns1:getBookmarkResponse>
</env:Body>
</env:Envelope>

在一个典型的 SOAP 事务中,服务器会接收一个像 清单 1 所示的以 XML 编码的请求,解析这个 XML,执行相应的服务对象方法,然后返回一个如 清单 2 所示的以 XML 编码的响应到请求客户端。客户端通常能够解析和响应这个 SOAP,并将它转换成一个特定语言的对象或数据结构以作进一步处理。您可以选择使用一个 WSDL 文件告诉客户端关于可用函数的信息,以及输入参数和返回值的个数和数据类型。

Zend Framework 具有 SOAP 客户端和服务器的实现,同时支持自动生成 WSDL 文件。服务器和客户端实现在 PHP 中封装了 SOAP 扩展;这意味着如果 PHP 没有包含 SOAP 扩展支持,那么它们将无法生效。这样,使用带有原生扩展的 Zend Framework 库大大简化了开发过程,因为开发人员只需要定义一组实现服务 API 的对象,并将它们附加到服务器以便处理到达的请求。下面的各部分将详细讨论这个方面。


创建示例应用

在开始实现一个 SOAP 服务之前,您需要知道一些注意点和约定。在本文中,我将假定您拥有正常运行的 Apache、PHP+SOAP 和 MySQL 的开发环境,Zend Framework 会安装到您的 PHP 包含路径,同时您要熟悉 SQL、XML 和 SOAP 基础知识。我还将假定您熟悉使用 Zend Framework 进行应用开发的基本原则,理解行为与控制器之间的交互,并熟悉 Zend_Db 数据库抽象层。最后,我还假定您的 Apache Web 服务器配置支持虚拟主机和使用 .htaccess 进行 URL 重写。如果您不熟悉这些概念,您可以通过本文的 参考资料 的链接了解更多信息。

在本文中您将实现的示例 SOAP 服务允许第三方开发人员将产品添加到应用数据库,以及编辑、删除和查询应用数据库中的产品列表。它使用下面的函数,您可以使用一个标准的 SOAP 客户端访问所有这些函数:

  • getProducts():返回数据库中的所有产品
  • getProduct($id):返回数据库中的一个特定产品
  • addProduct($data):添加一个新产品到数据库中
  • deleteProduct($id):从数据库删除一个特定产品
  • updateProduct($id, $data):将数据库中一个特定产品更新为新值

第 1 步:初始化一个新应用

首先,我们要创建一个标准的 Zend Framework 应用,它包含本文所显示的代码上下文。使用 Zend Framework 工具脚本(Windows® 上则是 zf.bat,UNIX 是 zf.sh)创建一个新项目,如下所示:

shell> zf.bat create project example

您现在可以在您的 Apache 配置中为这个应用定义一个新的虚拟主机,如 http://example.localhost/,然后将虚拟主机的文档根目录指向应用的 public/ 目录。然后,如果您访问这个主机,您应该能看到默认的 Zend Framework 欢迎页面,如 图 1 所示。

图 1. 默认的 Zend Framework 欢迎页面
默认的 Zend Framework 欢迎页面屏幕截图

第 2 步:初始化应用数据库和模型

下一步是初始化应用数据库。所以,我们要创建一个新的 MySQL 表来保存产品信息,如下所示:

mysql> CREATE TABLE IF NOT EXISTS products (
    ->   id int(11) NOT NULL AUTO_INCREMENT, 
    ->   title varchar(200) NOT NULL,
    ->   shortdesc text NOT NULL,
    ->   price float NOT NULL,
    ->   quantity int(11) NOT NULL,
    ->   PRIMARY KEY (id)
    -> ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

在这个表中填入一些示例记录以便开始开发,如下所示:

mysql> INSERT INTO products (id, title, shortdesc, price, quantity) VALUES(1, 
    ->  'Ride Along Fire Engine', 'This red fire engine is ideal for toddlers who 
    ->  want  to travel independently. Comes with flashing lights and beeping horn.', 
    ->  69.99, 11);
Query OK, 1 row affected (0.08 sec)

mysql> INSERT INTO products (id, title, shortdesc, price, quantity) VALUES(2, 
    -> 'Wind-Up Crocodile Bath Toy', 'This wind-up toy is the perfect companion  
    -> for hours of bathtub fun.', 7.99, 67);
Query OK, 1 row affected (0.08 sec)

第 3 步:配置应用的名称空间

最后一步是为 Zend Framework 自动加载配置名称空间。这个步骤将在需要时实现自动加载特定应用类到应用中。在这里,我假设应用的名称空间为 Example,而特定应用类(如 SOAP 服务类)将存储在 $PROJECT/library/Example/ 中。所以,要修改应用配置文件 $PROJECT/application/configs/application.ini 并添加下面一行到文件中:

autoloaderNamespaces[] = "Example_"

您现在已经完成了创建一个 SOAP 服务的所有准备工作!


查询数据

因为这是一个示例应用,我将尽量保持简单,并只在默认模块的 IndexController 上创建一个处理 SOAP 请求的动作;然而,在实际中,您可能希望使用一个单独的控制器处理 SOAP 请求。编辑文件 $PROJECT/application/controllers/IndexController.php,然后添加新的动作,如 清单 3 所示:

清单 3. soapAction() 的定义
<?php
class IndexController extends Zend_Controller_Action
{
    public function soapAction()
    {
      // disable layouts and renderers
      $this->getHelper('viewRenderer')->setNoRender(true);
      
      // initialize server and set URI
      $server = new Zend_Soap_Server(null, 
        array('uri' => 'http://example.localhost/index/soap'));

      // set SOAP service class
      $server->setClass('Example_Manager');

      // handle request
      $server->handle();
    }
}

清单 3 传递一个 null 值到对象构造函数的第一个参数,以非 WSDL 模式初始化了一个新的 Zend_Soap_Server 对象。如果以非 WSDL 模式创建服务器,我们必须指定服务器 URI;在 清单 3 中,这是在作为第二个参数传递给构造函数的选项数组中指定的。

接下来,服务器对象的 setClass() 函数用于将一个服务类附加到服务器上。这个类实现了 SOAP 服务的可用函数;这个服务器将在 SOAP 请求的响应中自动调用这些函数。如果您喜欢,您也可以使用 addFunction()loadFunctions() 函数将用户自定义函数附加到服务器上,而不需要使用 setClass() 函数附加整个类。

正如之前所提到的,Zend_Soap_Server 类并没有提供它自己的 SOAP 服务器实现;它只是封装了 PHP 的内置 SOAP 扩展。因此,一旦所有先决条件都准备好后,清单 3 中的 handle() 函数会负责初始化内置的 PHP SoapServer 对象,将它传递给请求对象,并调用该对象的 handle() 函数处理 SOAP 请求。

虽然所有这些都做好了,但是这还远远不够,因为服务类还没有定义。接下来我们使用 清单 4 中的代码创建这个类定义,将创建的类定义保存到 $PROJECT/library/Example/Manager.php:

清单 4. 带有 get*() 函数的服务对象定义
<?php
class Example_Manager {

    /**
     * Returns list of all products in database
     *
     * @return array
     */
    public function getProducts() 
    {
      $db = Zend_Registry::get('Zend_Db');        
      $sql = "SELECT * FROM products";      
      return $db->fetchAll($sql);      
    }

    /**
     * Returns specified product in database
     *
     * @param integer $id
     * @return array|Exception
     */
    public function getProduct($id) 
    {
      if (!Zend_Validate::is($id, 'Int')) {
        throw new Example_Exception('Invalid input');          
      }
      $db = Zend_Registry::get('Zend_Db');        
      $sql = "SELECT * FROM products WHERE id = '$id'";   
      $result = $db->fetchAll($sql);      
      if (count($result) != 1) {        
        throw new Exception('Invalid product ID: ' . $id);  
      } 
      return $result;  
    }
}
?>

清单 4 创建了一个单独的服务类,它包含两个函数。getProducts() 函数使用 Zend_Db 查询表中所有的产品记录,然后将它们作为一个数组返回,而 getProduct() 函数则接收一个产品标识符,然后只返回特定的一条记录。然后 SOAP 服务器将这个方法的返回值转换成一个 SOAP 响应数据包,并将它返回给发送请求的客户端。清单 8 包含一个响应数据包的例子:

如果您还在疑惑 Zend_Db 是在哪里初始化的,我可以告诉您它是在应用启动加载器中初始化的,即 $PROJECT/application/Bootstrap.php。这个 Bootstrap.php 包含了一个 _initDatabase() 函数,它创建 Zend_Db 适配器,并将它注册到应用注册表中。清单 5 显示这部分代码:

清单 5. 数据库适配器初始化
<?php
class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
  protected function _initDatabase()
  {
    $db = new Zend_Db_Adapter_Pdo_Mysql(array(
        'host'     => 'localhost',
        'username' => 'user',
        'password' => 'pass',
        'dbname'   => 'example'
    ));
    Zend_Registry::set('Zend_Db', $db); 
  }
}

为了看到实际结果,要创建一个 SOAP 客户端(清单 6),然后使用它连接 SOAP 服务,并请求 getProducts() 函数。

清单 6. 一个示例 SOAP 客户端
<?php
// load Zend libraries
require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Soap_Client');

// initialize SOAP client
$options = array(
  'location' => 'http://example.localhost/index/soap',
  'uri'      => 'http://example.localhost/index/soap'
);

try {
  $client = new Zend_Soap_Client(null, $options);  
  $result = $client->getProducts();
  print_r($result);
} catch (SoapFault $s) {
  die('ERROR: [' . $s->faultcode . '] ' . $s->faultstring);
} catch (Exception $e) {
  die('ERROR: ' . $e->getMessage());
}
?>

SOAP 客户端将会产生一个请求数据包(清单 7)。

清单 7. getProducts() 的一个示例 SOAP 请求
<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" 
 xmlns:ns1="http://example.localhost/index/soap" 
 xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
 xmlns:enc="http://www.w3.org/2003/05/soap-encoding">
<env:Body>
<ns1:getProducts env:encodingStyle="http://www.w3.org/2003/05/soap-encoding"/>
</env:Body>
</env:Envelope>

这个服务器产生一个使用 SOAP 编码的响应(清单 8)。

清单 8. getProducts() 函数的一个示例 SOAP 响应
<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" 
 xmlns:ns1="http://example.localhost/index/soap" 
 xmlns:ns2="http://xml.apache.org/xml-soap" 
 xmlns:enc="http://www.w3.org/2003/05/soap-encoding" 
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<env:Body xmlns:rpc="http://www.w3.org/2003/05/soap-rpc">
<ns1:getProductsResponse 
 env:encodingStyle="http://www.w3.org/2003/05/soap-encoding">
<rpc:result>return</rpc:result>
<return enc:itemType="ns2:Map" enc:arraySize="2" xsi:type="enc:Array">
<item xsi:type="ns2:Map">
<item>
<key xsi:type="xsd:string">id</key>
<value xsi:type="xsd:string">1</value>
</item>
<item>
<key xsi:type="xsd:string">title</key>
<value xsi:type="xsd:string">Ride Along Fire Engine</value>
</item>
<item>
<key xsi:type="xsd:string">shortdesc</key>
<value xsi:type="xsd:string">This red fire engine is ideal 
 for toddlers who want to travel independently. 
 Comes with flashing lights and beeping horn.</value>
</item>
<item>
<key xsi:type="xsd:string">price</key>
<value xsi:type="xsd:string">69.99</value>
</item>
<item>
<key xsi:type="xsd:string">quantity</key>
<value xsi:type="xsd:string">11</value>
</item>
</item>
...
</return>
</ns1:getProductsResponse>
</env:Body>
</env:Envelope>

然后 SOAP 客户端会将这个响应转换成一个原生的 PHP 数组,它可以被进一步处理或检查,如 图 2 所示。

图 2. 被转换成一个原生 PHP 数组的 SOAP 请求结果
被转换成一个原生 PHP 数组 的 SOAP 请求结果

添加、删除和更新数据

在了解了如何通过 SOAP 查询数据后,现在我们了解一下如何添加和删除数据。

在 Example_Manager 类中实现一个 addProduct() 函数非常简单。清单 9 演示了实现方法:

清单 9. 定义了 addProduct() 函数的 SOAP 服务对象
<?php
class Example_Manager 
{
   /**
     * Adds new product to database
     *
     * @param array $data array of data values with keys -> table fields
     * @return integer id of inserted product
     */
    public function addProduct($data) 
    {      
      $db = Zend_Registry::get('Zend_Db');        
      $db->insert('products', $data);
      return $db->lastInsertId();
    }
}

清单 9 中的 addProduct() 函数接收一个新产品记录作为键-值对数组,然后使用 Zend_Db 对象的 insert() 函数将记录写入到数据库表中。它最后返回最新插入记录的 ID。

删除一个产品也一样简单:只需要增加一个 deleteProduct() 函数,它接收产品 ID 作为输入,然后使用 Zend_Db 的 delete() 函数从数据库删除这个记录。清单 10 展示了这个方法的实现:

清单 10. 定义了 deleteProduct() 函数的 SOAP 服务对象
<?php
class Example_Manager 
{
    /**
     * Deletes product from database
     *
     * @param integer $id
     * @return integer number of products deleted
     */
    public function deleteProduct($id) 
    {
      $db = Zend_Registry::get('Zend_Db');        
      $count = $db->delete('products', 'id=' . $db->quote($id));
      return $count;
    }
}

清单 10中,传递给 delete() 函数的第二个参数指定了执行 DELETE 操作时使用的约束或过滤器。使用这个参数很重要;因为如果不作限制,Zend_Db 将删除表中的所有记录。

最后,清单 11 展示了一个 updateProduct() 函数,它可用于更新一个产品记录的值。这个函数接收两个输入参数 — 产品 ID 和一个包含修改记录的数组 — 并使用 Zend_Db 的 update() 函数对数据库表执行一个 UPDATE 查询。

清单 11. 定义了 updateProduct() 函数的 SOAP 服务对象
<?php
class Example_Manager 
{
    /**
     * Updates product in database
     *
     * @param integer $id
     * @param array $data
     * @return integer number of products updated
     */
    public function updateProduct($id, $data) 
    {
      $db = Zend_Registry::get('Zend_Db');        
      $count = $db->update('products', $data, 'id=' . $db->quote($id));
      return $count;        
    }
}

您可以在如 清单 12 所示的一个 SOAP 客户端尝试所有这些函数:

清单 12. 一个示例 SOAP 客户端
<?php
// load Zend libraries
require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Soap_Client');

// initialize SOAP client
$options = array(
  'location' => 'http://example.localhost/index/soap',
  'uri'      => 'http://example.localhost/index/soap'
);

try {
  // add a new product
  // get and display product ID
  $p = array(
    'title'     => 'Spinning Top',
    'shortdesc' => 'Hours of fun await with this colorful spinning top. 
      Includes flashing colored lights.',
    'price'     => '3.99',
    'quantity'  => 57 
  );
  $client = new Zend_Soap_Client(null, $options);  
  $id = $client->addProduct($p);
  echo 'Added product with ID: ' . $result;

  // update existing product
  $p = array(
    'title'     => 'Box-With-Me Croc',
    'shortdesc' => 'Have fun boxing with this inflatable crocodile, 
      made of tough, washable rubber.',
    'price'     => '12.99',
    'quantity'  => 25 
  );
  $client->updateProduct($id, $p);
  echo 'Updated product with ID: ' . $id;

  // delete existing product
  $client->deleteProduct($id);
  echo 'Deleted product with ID: ' . $id;  
} catch (SoapFault $s) {
  die('ERROR: [' . $s->faultcode . '] ' . $s->faultstring);
} catch (Exception $e) {
  die('ERROR: ' . $e->getMessage());
}
?>

生成 SOAP 错误信息

上面所列的所有函数的一个共同问题是:它们没有作任何的输入验证。在实际中,忽略这种验证会对您的应用数据库的完整性造成严重影响,并且可能很快会导致数据损坏(最好情况)或完全破坏(最坏情况)。

幸好,Zend Framework 包含了一个 Zend_Validate 组件,它为最常见情况提供了内置的验证器。您可以将这个特性与 Zend_Soap_Server 的 registerFaultException() 函数结合,用于测试客户端所提供的请求数据,然后为不同的错误情况返回一个 SOAP 错误信息。

要了解它是如何工作的,我们要先通过扩展 Zend_Exception 创建一个自定义异常类,如 清单 13 所示:

清单 13. 一个自定义异常类
<?php
class Example_Exception extends Zend_Exception {}

将这个类保存到 $PROJECT/library/Example/Exception.php。

接下来,修改各个服务类函数,使它们包含输入验证,并在输入数据无效或缺少数据时抛出自定义异常。清单 14 展示了修改的 Example_Manager 类:

清单 14. 修改后带有输入验证和异常的 SOAP 服务对象
<?php
class Example_Manager {

    // define filters and validators for input
    private $_filters = array(
      'title'     => array('HtmlEntities', 'StripTags', 'StringTrim'),
      'shortdesc' => array('HtmlEntities', 'StripTags', 'StringTrim'),
      'price'     => array('HtmlEntities', 'StripTags', 'StringTrim'),
      'quantity'  => array('HtmlEntities', 'StripTags', 'StringTrim')
    );

    private $_validators = array(
      'title'     => array(),
      'shortdesc' => array(),
      'price'     => array('Float'),
      'quantity'  => array('Int')
    );

    /**
     * Returns list of all products in database
     *
     * @return array
     */
    public function getProducts() 
    {
      $db = Zend_Registry::get('Zend_Db');
      $sql = "SELECT * FROM products";
      return $db->fetchAll($sql);
    }

    /**
     * Returns specified product in database
     *
     * @param integer $id
     * @return array|Example_Exception
     */
    public function getProduct($id)
    {
      if (!Zend_Validate::is($id, 'Int')) {
        throw new Example_Exception('Invalid input');
      }
      $db = Zend_Registry::get('Zend_Db');
      $sql = "SELECT * FROM products WHERE id = '$id'";
      $result = $db->fetchAll($sql);
      if (count($result) != 1) {
        throw new Example_Exception('Invalid product ID: ' . $id); 
      } 
      return $result;
    }

    /**
     * Adds new product to database
     *
     * @param array $data array of data values with keys -> table fields
     * @return integer id of inserted product
     */
    public function addProduct($data) 
    {
      $input = new Zend_Filter_Input($this->_filters,
        $this->_validators, $data);
      if (!$input->isValid()) {
        throw new Example_Exception('Invalid input');
      }
      $values = $input->getEscaped();
      $db = Zend_Registry::get('Zend_Db');
      $db->insert('products', $values);
      return $db->lastInsertId();
    }

    /**
     * Deletes product from database
     *
     * @param integer $id
     * @return integer number of products deleted
     */
    public function deleteProduct($id) 
    {
      if (!Zend_Validate::is($id, 'Int')) {
        throw new Example_Exception('Invalid input');
      }
      $db = Zend_Registry::get('Zend_Db');
      $count = $db->delete('products', 'id=' . $db->quote($id));
      return $count;
    }

    /**
     * Updates product in database
     *
     * @param integer $id
     * @param array $data
     * @return integer number of products updated
     */
    public function updateProduct($id, $data) 
    {
      $input = new Zend_Filter_Input($this->_filters, 
        $this->_validators, $data);
      if (!Zend_Validate::is($id, 'Int') || !$input->isValid()) {
        throw new Example_Exception('Invalid input');
      } 
      $values = $input->getEscaped();
      $db = Zend_Registry::get('Zend_Db');
      $count = $db->update('products', $values, 'id=' . $db->quote($id));
      return $count;
    }    

}

清单 14 中,服务 API 增强后包含了对所有输入参数的验证。对于大多数 API 函数,Zend_Validate::is() 静态函数提供了一种测试输入参数的便捷方法;在某些情况下,一个额外的 Zend_Filter_Input 过滤器链会用于验证和过滤输入。在输入验证过程中发生的任何错误都会产生一个 Example_Exception 类的实例。

最后一步是告诉 SOAP 服务器自动将所产生的 Example_Exception 实例转换成 SOAP 错误。一般通过使用 registerFaultException() 函数将异常类注册到 SOAP 服务器上,如 清单 15 所示的修改后的 IndexController::soapAction

清单 15. 修改的 soapAction() 定义,支持产生作为错误的自定义异常
<?php
class IndexController extends Zend_Controller_Action
{

    public function soapAction()
    {
      // disable layouts and renderers
      $this->getHelper('viewRenderer')->setNoRender(true);
      
      // initialize server and set URI
      $server = new Zend_Soap_Server(null, 
        array('uri' => 'http://example.localhost/index/soap'));

      // set SOAP service class      
      $server->setClass('Example_Manager');
      
      // register exceptions that generate SOAP faults
      $server->registerFaultException(array('Example_Exception'));
      
      // handle request
      $server->handle();
    }
}

要了解它是如何工作的,可以尝试对 getProduct() 函数发送一个 SOAP 请求,然后给它传递一个无效的 ID。清单 16 显示了一个这样的 SOAP 请求例子:

清单 16. 一个带有无效输入参数的 SOAP 请求
<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" 
 xmlns:ns1="http://example.localhost/index/soap" 
 xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xmlns:enc="http://www.w3.org/2003/05/soap-encoding">
<env:Body>
<ns1:getProduct env:encodingStyle="http://www.w3.org/2003/05/soap-encoding">
<param0 xsi:type="xsd:string">nosuchproduct</param0>
</ns1:getProduct>
</env:Body>
</env:Envelope>

服务器将验证输入,然后发现它是无效的,从而产生一个 Example_Exception,它将会被转化成一个 SOAP 错误,并将它返回给客户端。清单 17 展示所产生的响应数据包:

清单 17. 所生成的一个 SOAP 错误
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope 
 xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/>
<SOAP-ENV:Body>
<SOAP-ENV:Fault>
<faultcode>Receiver</faultcode>
<faultstring>Invalid input</faultstring>
</SOAP-ENV:Fault>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

对于 SOAP 客户端来说,将这个 SOAP 调用封装在一个 try-catch 代码块中是很好的做法,这样类似于上面的 SOAP 错误就会被顺利地捕捉和处理。如果您重新访问 清单 12 中的示例 SOAP 客户端,您将看到展示它的实现方法的一个例子。


添加 WSDL 支持

PHP 的原生 SOAP 扩展的一个缺点是它不支持为 SOAP 服务自动生成 WSDL 文件。WSDL 文件是很有用的,因为它们包含了关于可用的 SOAP API 函数的信息,并且可以被连接客户端用于 “自动发现” SOAP API。

然而,Zend Framework 包含了一个 Zend_Soap_AutoDiscover 组件,您可以使用它为一个 SOAP 服务自动生成一个 WSDL 文件。它是通过读取 SOAP 服务类中的 PHPDoc 注释实现自动生成 WSDL 的。如果您回顾本文之前的清单,您会看到每一个函数都带有 PHPDoc 注释;这是专门用于简化 WSDL 自动生成的。

清单 18 展示了如何使用 Zend_Soap_AutoDiscover 组件实现 WSDL 自动生成:

清单 18. wsdlAction() 定义
<?php
class IndexController extends Zend_Controller_Action
{
    public function soapAction()
    {
      // disable layouts and renderers
      $this->getHelper('viewRenderer')->setNoRender(true);

      // initialize server and set WSDL file location
      $server = new Zend_Soap_Server('http://example.localhost/index/wsdl');
      // set SOAP service class      
      $server->setClass('Example_Manager');

      // register exceptions that generate SOAP faults
      $server->registerFaultException(array('Example_Exception'));

      // handle request
      $server->handle();
    }

    public function wsdlAction()
    {
      // disable layouts and renderers
      $this->getHelper('viewRenderer')->setNoRender(true);

      // set up WSDL auto-discovery
      $wsdl = new Zend_Soap_AutoDiscover();

      // attach SOAP service class
      $wsdl->setClass('Example_Manager');

      // set SOAP action URI
      $wsdl->setUri('http://example.localhost/index/soap');

      // handle request
      $wsdl->handle();
    }
}

清单 18 定义了一个新的 wsdlAction(), 它初始化了 Zend_Soap_AutoDiscover 组件的一个实例,然后将它指向 Example_Manager 类。通过调用这个实例的 handle() 函数,它就会读取特定的类,解析其中的 PHPDoc 注释,然后产生一个符合标准的 WSDL 文档,这个文档完整地描述了该服务对象。

要看到这个结果,需要在您的浏览器上访问 http://example.localhost/index/wsdl,然后您应该能够看到如 图 3 所示结果:

图 3. 一个动态生成的 WSDL 文件
一个动态生成的 WSDL 文件的屏幕截图

现在 SOAP 服务器和客户端都可以使用这个 WSDL 文件了,而不需要手动指定 urilocation 参数。清单 18 也说明了这一点,通过修改 soapAction(),它将 WSDL URL 传递给 Zend_Soap_Server 构造函数,使它以 WSDL 模式启动。连接 SOAP 的客户端就能够使用这个 WSDL URL 自动发现 SOAP 服务 API 了。


结束语

Zend Framework 提供了快速有效地将一个 SOAP API 添加到一个 Web 应用的完整工具包。通过这个工具包,您可以在 Web 应用之间使用众所周知的 SOAP 标准实现一种经济有效的信息交换方法。Zend Framework 对于 SOAP 客户端和服务器的内置支持,以及 WSDL 自动生成,使它成为快速 SOAP 服务实现和部署的非常好的方法。而最后,因为 Zend Framework 是一个基于 MVC 模式的框架,它还能轻松地将 SOAP API 移植到一个现有的 Zend Framework 应用,而不会对现有的代码库产生重大影响(并且不会产生太多的回归问题)。

本文所实现的所有代码的链接,以及您可用于添加、编辑、删除和查询产品的一个简单 SOAP 客户端,请参见 下载 部分。本文所使用的工具,请参见 参考资料。我建议您下载这些代码,然后试验这些代码,并且您可以尝试在里面自己添加新代码。我保证不会出现问题,而这肯定会让您有所收获。尽情享受吧!


下载

描述名字大小
本文中所讨论的示例应用example-app-soap.zip8KB

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML, Open source, SOA and web services
ArticleID=495590
ArticleTitle=使用 Zend Framework 实现 SOAP 服务
publish-date=06122010