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 は、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): データベースに保管されている特定の製品レコードを新しい値で更新します。
まず初めに、標準的な 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 のウェルカム・ページ
ステップ 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)
|
最後のステップは、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 拡張のラッパーにすぎないからです。そのため、準備がすべて整った時点で、リスト 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 がセットアップするスタンドアロンのサービス・クラスには、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 リクエストの結果
これで、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();
}
}
|
リスト 9 の addProduct() メソッドは、新しい製品レコードを、キーと値のペアの配列として受け入れます。続いて 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 はテーブル内のすべてのレコードを削除することになってしまいます。
最後に、リスト 11 に updateProduct() メソッドを記載します。このメソッドを使用することで、製品レコードを新しい値で更新することができます。このメソッドは、渡される引数として、製品 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());
}
?>
|
これまでのセクションで説明したメソッドすべてに共通する 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 つの方法がわかります。
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 ファイル
これで、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 クライアントも含む) へのリンクは、「ダウンロード」セクションに記載されています。記事で使用したツールについては、「参考文献」を参照してください。このコードを入手して、いろいろと試してみることをお勧めします。実際に新しい機能を追加してみるのも一考です。何も壊れることはありませんので、よい勉強になると思って楽しみながら試してください!
| 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|---|---|---|
| The example application discussed in this article | example-app-soap.zip | 8KB | HTTP |
学ぶために
- W3C SOAP 仕様: XML をベースとした情報交換のための軽量のプロトコル、SOAP についての知識を深めてください。
- 公式 Zend Framework Web サイト: PHP 5 で実装されたオープンソースのオブジェクト指向 Web アプリケーション・フレームワーク、Zend Framework の詳細を学んでください。
- Zend Framework Quickstart: Zend Framework を使用したアプリケーション開発の世界に飛び込んでください。
- Zend Framework マニュアル: Zend_Soap コンポーネントの詳細を調べてください。
- Zend Framework API マニュアル: マイナー・リリースごとの API マニュアルをダウンロードしてください。最新の API マニュアルは、オンラインマニュアルとして利用できます。
- Zend Framework コミュニティーのメーリング・リストおよび Zend Contributors Wiki: Zend Framework コミュニティーに加わって、質問を投稿して回答をもらってください。
- この著者による他の記事 (Vikram Vaswani 著、developerWorks、2007年8月から現在まで): XML や Google Base Data API 以外の Google API、そしてその他の技術に関する記事を読んでください。
- My developerWorks: developerWorks のエクスペリエンスを自分流にカスタマイズしてください。
- IBM XML 認定: XML や関連技術の IBM 認定技術者になる方法について調べてください。
- XML technical library:広範な技術に関する記事とヒント、チュートリアル、標準、そして IBM Redbooks については、developerWorks XML ゾーンを参照してください。
- developerWorks の Technical events and webcasts: これらのセッションで最新情報を入手してください。
- Twitter での developerWorks: 今すぐ登録して developerWorks のツイートをフォローしてください。
- developerWorks podcasts: ソフトウェア開発者向けの興味深いインタビューとディスカッションを聞いてください。
製品や技術を入手するために
- Zend Framework: Zend Framework の最新リリースをダウンロードしてください。
- MySQL データベース・サーバー: この記事で使用したオープンソースのトランザクション・データベースをダウンロードしてください。
- DB2 Express-C: 無料版の IBM DB2 データベース・サーバーを入手してください。中小企業にとって理想的なアプリケーション開発の基礎となります。
- IBM 製品の評価版: DB2®、Lotus®、Rational®、Tivoli®、および WebSphere® のアプリケーション開発ツールとミドルウェア製品を体験するには、評価版をダウンロードするか、IBM SOA Sandbox のオンライン試用版を試してみてください。
議論するために
- XML ゾーンのディスカッション・フォーラム: XML 関連のフォーラムに参加してください。
- developerWorks blogs: これらのブログを調べて、ブログに参加してください。

Vikram Vaswani は、オープンソースのツールと技術を専門とするコンサルティング・サービス会社、Melonfire の創業者で、現在 CEO を務めています。彼は、『MySQL Database Usage and Administration』、『PHP: A Beginners Guide』、および出版が予定されている『Zend Framework: A Beginners Guide』の著者でもあります。