使用 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 了。

相關文章
相關標籤/搜索