目次


Zend Framework を使って SOAP サービスを実装する

Zend Framework を使用して、SOAP サービスを手軽に PHP Web アプリケーションに追加する

Comments

はじめに

Web サービスがブームとなっている今日この頃、特に大きな注目を集めているのは REST ベースのサービスです。REST の人気の理由は、これが単純で直観的であること、そして既存の HTTP メソッドで操作できることですが、REST 以外に選択の余地がないというわけではありません。Web 上での情報交換に伴う問題を解決するには、SOAP (Simple Object Access Protocol) もまた、REST 以上に規則に従って標準化された方法を提供します。

一般に、SOAP ベースのサービスは複雑で、実装するのに時間がかかると考えられていますが、この実装プロセスを大幅に単純化できるツールはいくつも存在しています。その 1 つが、Zend Framework です。スケーラブルな Web アプリケーションを PHP で構築できるこの完全な MVC フレームワークには、OOP フォームや i18n サポート、クエリーとページのキャッシング、そして Dojo 統合などの盛りだくさんの魅力の他、Zend_Soap コンポーネントを使って SOAP サービスを作成、デプロイするための包括的なツールキットがあります。

Zend Framework を使って単純な SOAP ベースの Web サービスを構築するプロセスを順に説明するこの記事では、まず、クライアント・リクエストを処理して SOAP 準拠のレスポンスを送信する方法を説明した後、例外を処理して SOAP のエラーを生成するプロセスを探ります。そして最後に Zend_Soap を使用して、SOAP サービスを記述する WSDL ファイルを自動的に生成し、クライアントによる SOAP サービス API の「自動検出」を可能にします。

SOAP の概要

まずは、SOAP について簡単に説明しておきます。SOAP は、Web 上での情報交換に特定の言語に依存しない XML を使用することによって、多種多様な言語で作成されたアプリケーションが互いに接続できるようにするための手段です。クライアントとサーバーの間での XML の送受信には、HTTP をトランスポート・プロトコルとして使用し、データに対して強い型付けを適用してデータの完全性を保証します。

リソースとアクションを中心とする REST とは異なり、SOAP で基本とするのはメソッドとデータ型です。REST サービスでは 4 つの HTTP メソッド (GET、POST、PUT、および DELETE) に対応した 4 つの操作しか使えないのが通常ですが、SOAP サービスにそのような制限はありません。開発者は定義したいと思う操作をいくらでも公開することができます。さらに、これらのメソッドの呼び出しには、リクエスト対象の操作タイプとは全く関係なく、通常は HTTP の POST メソッドが使用されます。

SOAP がどのように機能するかを説明するための単純な例として、ソーシャル・ブックマーキング・アプリケーションを引用します。例えばこのアプリケーションで、他の開発者も SOAP を使ってブックマークを追加したり、取得したりできるようにしたいとします。この場合、通常は getBookmark()addBookmark() などのメソッドで一連のサービス・オブジェクトを実装し、これらのサービス・オブジェクトを SOAP サーバーを介して公開することになります。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 トランザクションでは、サーバーが XML でエンコードされたリクエスト (リスト 1 のようなリクエスト) を受け入れて XML を構文解析し、対応するサービス・オブジェクトのメソッドを実行した後、XML でエンコードしたレスポンス (リスト 2 のようなレスポンス) をリクエストの送信元クライアントに返します。クライアントは通常、この SOAP レスポンスを構文解析し、その後の処理のために、処理言語に固有のオブジェクトまたはデータ構造に変換することができます。オプションの WSDL ファイルを使えば、クライアントに対して、使用可能なメソッドに関する情報や、入力引数と戻り値の数およびデータ型に関する情報を提供することもできます。

Zend Framework には SOAP クライアントおよびサーバーの実装、そして WSDL ファイルを自動的に生成するための実装が組み込まれています。サーバーおよびクライアントの実装は、SOAP 拡張の PHP ラッパーです。つまり、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

これで、このアプリケーションの新しい仮想ホスト (例えば、http://example.localhost/) を Apache 構成に定義し、仮想ホストのドキュメント・ルートをアプリケーションの public/ ディレクトリーに指定することができます。ブラウザーでこのホストにアクセスすると、デフォルトの Zend Framework のウェルカム・ページが表示されるはずです (図 1 を参照)。

図 1. デフォルトの Zend Framework のウェルカム・ページ
デフォルトの 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 サービスの作成に取り掛かる準備ができました。

データの取得

これはサンプル・アプリケーションなので、複雑にならないよう、SOAP リクエストを処理するためのアクションはデフォルト・モジュールの IndexController 内部に直接作成します。しかし実際のアプリケーションでは、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 では新規 Zend_Soap_Server オブジェクトを非 WSDL モードで初期化し、オブジェクトのコンストラクターに、最初の引数として null 値を渡します。サーバーを非 WSDL モードでセットアップするときには、必ずサーバーの URI を指定しなければなりません。リスト 3 でサーバーの URI が指定されている場所は、コンストラクターに 2 番目の引数として渡すオプションの配列です。

次に、サーバー・オブジェクトの setClass() メソッドを使用して、サービス・クラスをサーバーに配置します。このクラスは、SOAP サービスで使用可能なメソッドを実装します。サーバーはこれらのメソッドを、SOAP リクエストに対するレスポンスで自動的に呼び出すことになります。お望みであれば、setClass() メソッドでクラスを配置する代わりに、addFunction() メソッドと loadFunctions() メソッドを使ってユーザー定義関数をサーバーに配置することもできます。

前述のとおり、Zend_Soap_Server クラスは、それ自体では SOAP サーバーを実装しません。このクラスは単なる PHP の組み込み SOAP 拡張のラッパーにすぎないからです。そのため、準備がすべて整った時点で、リスト 3handle() メソッドが組み込み 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 がセットアップするスタンドアロンのサービス・クラスには、2 つのメソッドが含まれます。そのうちの 1 つ、getProducts() メソッドは、Zend_Db を使用して有効なすべての製品レコードをテーブルから取得し、これらのレコードを 1 つの配列として返します。もう 1 つの getProduct() メソッドは、製品 ID を受け入れ、指定された製品レコードだけを返します。いずれにしても、製品レコードが返されると、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 リクエストの結果
ネイティブ PHP 配列に変換された後の SOAP リクエストの結果

データの追加、削除、更新

これで、SOAP でデータを取得できるようになったので、今度はデータを追加、削除できるようにします。

addProduct() メソッドを Example_Manager クラスに実装するのは至って簡単です。リスト 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();
    }
}

リスト 9addProduct() メソッドは、新しい製品レコードを、キーと値のペアの配列として受け入れます。続いて Zend_Db オブジェクトの insert() メソッドを使用して、このレコードをデータベース・テーブルに書き込みます。そして最後に、新しく挿入されたレコードの ID を返します。

製品レコードを削除するのも同じく単純で、製品 ID を入力として受け入れる deleteProduct() メソッドを追加し、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() メソッドに渡される 2 番目の引数が、DELETE 操作に適用する制約、つまりフィルターを指定します。この引数を含めることが、重要な点です。この引数がなければ、Zend_Db はテーブル内のすべてのレコードを削除することになってしまいます。

最後に、リスト 11updateProduct() メソッドを記載します。このメソッドを使用することで、製品レコードを新しい値で更新することができます。このメソッドは、渡される引数として、製品 ID、そして更新後のレコードが含まれる配列の 2 つを取り、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 クライアントを使って、以上の 3 つすべてのメソッドを試してみることができます。

リスト 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 エラーの生成

これまでのセクションで説明したメソッドすべてに共通する 1 つの問題は、入力バリデーターやフィルターが何も組み込まれていないことです。実際には、この類の検証を省略するとアプリケーション・データベースの整合性に深刻な影響を及し、瞬く間に (運がよければ) データが破損するか、あるいは (最悪の場合は) 完全破壊という結果に至ることになりかねません。

幸い、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 サーバーに登録します。このように更新した IndexController::soapAction は、リスト 15 のようになります。

リスト 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 クライアントの例をもう一度見てみると、SOAP 呼び出しを try-catch ブロックの中にラップする 1 つの方法がわかります。

WSDL サポートの追加

PHP でのネイティブ SOAP 拡張には、1 つの欠点があります。それは、SOAP サービスの WSDL ファイルを自動的に生成するためのサポートが欠けていることです。WSDL ファイルは使用可能な SOAP API メソッドに関する情報が含まれる有益なファイルであり、クライアントをこのファイルに接続することで、クライアントが SOAP API を「自動検出」できるようになります。

その一方、Zend Framework には、SOAP サービスの WSDL ファイルを自動的に生成するために使用できる Zend_Soap_AutoDiscover コンポーネントが組み込まれています。このコンポーネントは SOAP サービス・クラスに含まれる PHPDoc コメントを読み取り、これらのコメントを WSDL 文書に変換することによって、SOAP サービスの 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 ファイルのスクリーン・キャプチャー
動的に生成された WSDL ファイルのスクリーン・キャプチャー

これで、uri および location パラメーターを手動で指定しなくても、SOAP サーバーとクライアントの両方がこの WSDL ファイルを使用できるようになりました。リスト 18 ではこのことが示されていて、soapAction() に変更を加えることで、このメソッドが WSDL の URL を Zend_Soap_Server コンストラクターに渡すようにしてあるので、コンストラクターは WSDL モードで起動されます。SOAP クライアントの接続でもこの WSDL の URL を使用できるので、SOAP サービス API を自動検出することができます。

まとめ

Zend Framework は、SOAP API を Web アプリケーションに簡単かつ効率的に追加するための完全なツールキットを提供します。このツールキットを使用すれば、十分理解された SOAP 標準に従って、Web アプリケーション間での経済的かつ効率的な情報交換を実現することができます。Zend Framework には SOAP クライアントおよびサーバーの組み込みサポート、そして WSDL 自動生成のサポートが組み込まれているため、SOAP サービスを迅速に実装、デプロイするには最適なフレームワークとなります。しかも、Zend Framework は MVC フレームワークです。そのため、既存の Zend Framework アプリケーションに SOAP API をごく簡単に移植できるとともに、既存のコードベースへの影響を最小限に抑えることができます (したがって、懸念する必要のあるリグレッションが少なくなります)。

この記事で実装した全コード (製品レコードの追加、編集、削除、取得を試してみるために使用できる単純な SOAP クライアントも含む) へのリンクは、「ダウンロード」セクションに記載されています。記事で使用したツールについては、「参考文献」を参照してください。このコードを入手して、いろいろと試してみることをお勧めします。実際に新しい機能を追加してみるのも一考です。何も壊れることはありませんので、よい勉強になると思って楽しみながら試してください!


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=XML, Open source, SOA and web services
ArticleID=494531
ArticleTitle=Zend Framework を使って SOAP サービスを実装する
publish-date=05112010