使用 Zend Framework 实现 SOAP 服务

引言 php

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

常见缩略词

  • 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 服务的工具包。 sql

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

回页首 数据库

理解 SOAP apache

首先,咱们要理解一些关于 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 欢迎页面

第 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 请求结果

回页首

添加、删除和更新数据

在了解了如何经过 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 文件

如今 SOAP 服务器和客户端均可以使用这个 WSDL 文件了,而不须要手动指定uri和location参数。清单 18 也说明了这一点,经过修改soapAction(),它将 WSDL URL 传递给 Zend_Soap_Server 构造函数,使它以 WSDL 模式启动。链接 SOAP 的客户端就可以使用这个 WSDL URL 自动发现 SOAP 服务 API 了。

相关文章
相关标签/搜索