目次


Agavi を使って REST API を作成する

Agavi PHP フレームワークを使用して REST API を実装する

Comments

はじめに

最近では、優れた Web アプリケーションのどれもが REST API を提供しています。Flickr も然り、Google、Bit.ly、NetFlix だけでなく、他のよく使用されている多くのアプリケーションにしても然りです。REST がアーキテクチャーのパターンとしてよく使用されている理由としては、REST は既存の HTTP 動詞を一般的なデータ操作に直観的にマッピングすること、そして SOAP や RPC をベースとした既存のアーキテクチャーに代わる軽量のアーキテクチャーとして、データの型付けや適合性要件が少ないことが挙げられます。これは、REST を使用すると、時間とコストの点でメリットがあることを意味しています。一般に REST ベースの API は、SOAP ベースの API や RPC ベースの API に比べ、実装にかかる時間が短く、開発に使うにも簡単です。

以前に連載した記事では Agavi MVC フレームワークを紹介し、このフレームワークを使用することで、スケーラブルな Web アプリケーションを素早く効率的に構築できることを説明しました (「参考文献」にリンクを記載)。以下をはじめ、Agavi には Web アプリケーションを構築するうえで役立つ数多くの利点があります。

  • 高度な入力フィルタリングおよび入力検証
  • OOP に準拠したアーキテクチャー
  • カスタマイズされた URL ルーティング
  • 拡張可能なロール・ベースのアクセス制御

さらに、Agavi には REST API 開発者にとって非常に貴重な 2 つの特徴があります。それは、REST HTTP メソッドを組み込みサポートしていることと、XML や JSON などの複数の出力タイプをサポートしていることです。

この記事では、Agavi を使用して単純な REST API を作成するプロセスを手順に沿って説明します。Agavi ベースのアプリケーションをすでに使っているとしたら、この記事を読むことによって、既存のフレームワークの慣例を利用して、アプリケーションの内部をサード・パーティーの開発者に公開する方法を学ぶことができます。また、REST ベースの新規アプリケーションを作成している場合には、Agavi を使用することで、アプリケーションの構築プロセスがより単純に、そしてより効率的になる仕組みが明らかになるはずです。

REST の概要

まずは REST、すなわち Representational State Transfer について簡単に説明しておきます。REST が SOAP と異なる点は、REST の場合、メソッドとデータ型をベースとするのではなく、リソースとアクションをベースにしていることです。リソースとは単に、アクションの実行対象とするオブジェクトまたはエンティティー (/users/photos など) を参照する URL にすぎません。アクションは、GET (取得)、POST (作成)、PUT (更新)、および DELETE (削除) の 4 つの HTTP 動詞のうちの 1 つです。

この仕組みをわかりやすく説明するための単純な例として、ある写真共有アプリケーションについて考えてみましょう。このアプリケーションには、他の開発者がリモートからこのアプリケーションのデータ・ストアに新しい写真を追加したり、データ・ストアから写真を取得したりできるようにするための API メソッドが必要です。この場合、SOAP の手法を取るとしたら、通常は createPhoto()getPhoto() などの SOAP API メソッドを使用することができます。これらのメソッドは、写真のパラメーターが入力として含まれる XML にエンコードされたリクエストを受け取り、写真のレコードを作成または取得して、リクエストの成功または失敗を示すレスポンスを XML にエンコードして返します。リクエストとレスポンスのパケット・フォーマット、各種入力パラメーターのデータ型、そして考えられるレスポンス値の範囲は、SOAP WSDL が定義します。

一方、REST の手法は SOAP の場合よりもかなり単純です。REST の手法では、URL エンドポイント (/photos など) を公開し、この URL にアクセスするために使用された HTTP メソッドを調べて必要なアクションを判断します。例えば、HTTP パケットを /photos に送信して新しい写真を作成するには POST を使用し、リクエストを /photos に送信して使用可能な写真のリストを取得するには GET を使用します。この手法は既存の HTTP 動詞を CRUD 操作にマッピングすることから、ずっと簡単に理解することができます。さらに、リクエスト/レスポンスのヘッダーに必要とされるデータ型には正式な定義がないため、リソースの使用も少なくて済みます。

URL リクエストに関する一般的な REST 規約とその意味は以下のとおりです。

  • GET /items: すべての項目のリストを取得する
  • GET /items/123: 項目番号 123 を取得する
  • POST /items: 新しい項目を作成する
  • PUT /items/123: 項目番号 123 を更新する
  • DELETE /items/123: 項目番号 123 を削除する

Agavi には、これらの REST 規約のサポートが組み込まれています。Agavi に関する以前の記事を読んでいるとしたら、すでにご存知のとおり、Agavi フレームワークは GET および POST リクエストをアクションの executeRead() メソッドおよび executeWrite() メソッドに自動的にマッピングします。同じように、PUT リクエストと DELETE リクエストも自動的に、アクションの executeCreate() メソッドと executeRemove() メソッドにマッピングします。したがって、新しい REST API を定義するには、アクションのメソッドを定義してコードを入力し、これらのメソッドにリクエストを適切にルーティングするだけの話となります。そしてそれがまさに、この記事でこれから行おうとしていることです。

サンプル・アプリケーションのセットアップ

REST API の実装に取り掛かる前に、いくつかの注意事項があります。まず、この記事では一貫して、読者の皆さんが、実際に使える (Apache、PHP、および MySQL) 開発環境を使用していること、および SQL と XML の基本を十分に理解していることを前提とします。さらに Agavi でのアプリケーション開発の基本原則に精通していること、アクション、ビュー、モデル、ルートの対話を理解していること、Agavi アプリケーションでの Doctrine モデルの使用について十分理解していることも前提となります。最後の注意点として、この記事の説明は、お使いのApache Web サーバーが仮想ホスト、URL 再書き込み、および PUT リクエストと DELETE リクエストをサポートするように構成されているという前提で進めます。以上に挙げた項目についてよく理解していない場合には、この記事を読み進める前に Agavi の入門記事の連載 (「参考文献」にリンクを記載) を読んでください。

この記事で使用するサンプル・アプリケーションは、本のタイトルと著者を保管する単純なデータベースです。REST API によって、このデータベースの本をサード・パーティーの開発者が通常の REST 規約を使用して取得、追加、削除、更新できるようにします。記事の大半ではリクエストとレスポンスの本文が XML であることを前提としますが、最後のセクションで、JSON リクエストおよびレスポンスを処理する方法についても説明します。

ステップ 1: 新規アプリケーションを初期化する

まず初めに、この記事の開発目標のたたき台としての役割を果たす、単純な Agavi アプリケーションをセットアップします。Agavi ビルド・スクリプトで以下に示す値の他はデフォルトの値をそのまま使用して、新規プロジェクトを初期化してください。

shell> agavi project-wizard
Project name [New Agavi Project]: ExampleApp
Project prefix (used, for example, in the project base action) [Exampleapp]: ExampleApp
Should an Apache .htaccess file with rewrite rules be generated (y/n) [n]? y
...

この作業のついでに、Apache 構成にテスト・アプリケーション用の新しい仮想ホスト (例えば、http://example.localhost/) を定義します。ブラウザーでこのホストにアクセスすると、デフォルトの Agavi ウェルカム・ページが表示されます (図 1 を参照)。

図 1. デフォルトの Agavi ウェルカム・ページ
デフォルトの Agavi ウェルカム・ページのスクリーン・キャプチャー
デフォルトの Agavi ウェルカム・ページのスクリーン・キャプチャー

ステップ 2: 新しいモジュールおよび対応するアクションを追加する

説明を簡潔にするため、保護したいアクションのすべては Default モジュール以外のモジュール内にあることにします。コマンド・プロンプトに戻り、以下のように Agavi ビルド・スクリプトを使用して新規 Books モジュールを作成します。

shell> agavi module-wizard
Module name: Books
Space-separated list of actions to create for Books: Index Book
Space-separated list of views to create for Index [Success]: Success
Space-separated list of views to create for Book [Success]: Success Error
...

この後の手順では、上記のアクションに REST モジュールを追加することになります。

この時点で、Agavi のドキュメントで推奨されているように Welcome モジュールも削除しておいてください。

shell> rm -rf app/modules/Welcome
shell> rm -rf app/pub/welcome

ステップ 3: アプリケーションのルーティング・テーブルを更新する

今度は、$ROOT/app/config/routing.xml にあるアプリケーションのルーティング・テーブルを、前のセクションで説明した標準 REST ルートに対応するルートで更新します。リスト 1 に、必要となるルートの定義を記載します。

リスト 1. REST ルートの定義
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/routing/1.0">
  <ae:configuration>
    <routes>

      <!-- default action for "/" -->
      <route name="index" pattern="^/$" module="Default" action="Index" />

      <!-- REST-style routes -->
      <route name="books" pattern="^/books" module="Books">
        <route name=".index" pattern="^/$" action="Index" />
        <route name=".book" pattern="^/(id:\d+)$" action="Book" />
      </route>      

    </routes>
  </ae:configuration>
</ae:configurations>

これで、新しく作成したアクションにリスト 1 のルートを使用してアクセスできるようになります。これを検証するには、http://example.localhost/books/ にアクセスし、図 2 のようなスタブ・ビューが表示されることを確認してください。

図 2. Agavi のスタブ HTML ビュー
Agavi のスタブ HTML ビューのスクリーン・キャプチャー。Index というタイトルが表示されています。
Agavi のスタブ HTML ビューのスクリーン・キャプチャー。Index というタイトルが表示されています。

ステップ 4: 本のデータベースおよびモデルを初期化する

次のステップは、アプリケーションのデータベースを初期化することです。それには以下のコードを使用して、本のレコードを保管するための MySQL テーブルを新しく作成します。

mysql> CREATE TABLE IF NOT EXISTS book (
    ->   id int(11) NOT NULL AUTO_INCREMENT,
    ->   title varchar(255) NOT NULL,
    ->   author varchar(255) NOT NULL,
    ->   PRIMARY KEY (`id`)
    -> ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.13 sec)

作業を進められるように、このテーブルにレコードをいくつか追加します。

mysql> INSERT INTO `book` (`id`, `title`, `author`) 
    -> VALUES (1, 'Wolf Hall', 'Hilary Mantel');
Query OK, 1 row affected (0.08 sec)

mysql> INSERT INTO `book` (`id`, `title`, `author`) 
    -> VALUES (2, 'Prayers for Rain', 'Dennis Lehane');
Query OK, 1 row affected (0.08 sec)

レコードを追加し終わったら、Doctrine オブジェクト・リレーショナル・マッパー (「参考文献」にリンクを記載) をダウンロードし、Doctrine ライブラリーを $ROOT/libs/doctrine に追加します。また、$ROOT/app/config/settings.xml のアプリケーション設定を更新してデータベース・サポートを有効にした上で、データベース構成ファイル (通常は $ROOT/app/config/databases.xml に配置) を更新して、このファイルが Agavi の Doctrine アダプターを使用するようにしてください。リスト 2 は、このデータベース構成ファイルでの構成の一例です。

リスト 2. Doctrine ORM の構成
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/databases/1.0">
  <ae:configuration>
    <databases default="doctrine">    
      <database name="doctrine" class="AgaviDoctrineDatabase">
        <ae:parameter name="dsn">mysql://user:pass@localhost/example
        </ae:parameter>
        <ae:parameter name="load_models">%core.lib_dir%/model
        </ae:parameter>
      </database>      
    </databases>
  </ae:configuration>
</ae:configurations>

これで、Doctrine を使用して、これらのテーブルのモデルを生成できるようになりました。生成されたモデル・クラスは忘れずに、$ROOT/app/lib/model/ ディレクトリーに手動でコピーしてください。

shell> cp /tmp/models/Book.php app/lib/model/
shell> cp /tmp/models/generated/BaseBook.php app/lib/model/

Doctrine を Agavi に統合し、Doctrine を使用してデータベース・テーブルからモデルを生成するプロセスについては、Agavi の入門記事の連載第 3 回 (この記事の「参考文献」にリンクを記載) で詳しく説明しています。

ステップ 5: XML 出力タイプを定義する

デフォルトでは、Agavi は HTML 出力専用に構成されています。このサンプル REST API で本来サポートするのは XML であるため、XML の出力タイプを定義し、対応するレスポンス・ヘッダーを指定して、これにデフォルトのマークを付ける必要があります。そのためには、$ROOT/app/config/output_types.xml にある出力タイプの構成ファイルを、リスト 3 のコードで更新します。

リスト 3. XML 出力タイプの構成
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/output_types/1.0">
  <ae:configuration>
    <output_types default="xml">

      <output_type name="html">
      ...
      </output_type>

      <output_type name="xml">
        <ae:parameter name="http_headers">
          <ae:parameter name="Content-Type">text/xml; charset=UTF-8
          </ae:parameter>
        </ae:parameter>
      </output_type>
    </output_types>
  </ae:configuration> 
</ae:configurations>

上記のコードで上手くいかない場合には、ここまでで説明したステップを Agavi の入門記事の連載第 1 回 (この記事の「参考文献」にリンクを記載) で詳しく説明しているので、そちらを参照してください。あるいは、サンプル・アプリケーションの完全なコード・アーカイブを「ダウンロード」からダウンロードするという方法もあります。

GET リクエストの処理

標準的な REST API は、2 つのタイプの GET リクエストをサポートします。1 つはリソースのリストを取得するためのリクエスト (GET /books/)、そしてもう 1 つは、特定のリソースを取得するためのリクエスト (GET /books/123) です。Agavi は前のセクションで説明したルーティング・テーブルを使用して、この 2 つのタイプのリクエストを自動的に Books_IndexAction::executeRead() メソッド、Books_BookAction::executeRead() メソッドにそれぞれルーティングします。

IndexAction の executeRead() メソッドは GET /books/ リクエストに対し、200 (OK) ステータス・コードと使用可能なすべての本のレコードのリストで応答する必要があります。そのためのコードは、リスト 4 のようになります。

リスト 4. GET リクエストの IndexAction ハンドラー
<?php
class Books_IndexAction extends ExampleAppBooksBaseAction
{ 
  public function executeRead(AgaviRequestDataHolder $rd)
  {
    $q = Doctrine_Query::create()
          ->from('Book b');
    $result = $q->execute(array(), Doctrine::HYDRATE_ARRAY); 
    $this->setAttribute('result', $result);
    return 'Success';
  }
}
?>

リスト 4 では Doctrine を使用して、本のデータベース・テーブル内にあるレコードに対してクエリーを実行し、その結果を IndexSuccessView のビュー変数として設定します。次のステップは、executeXml() メソッドを IndexSuccessView に追加し、この情報を XML 文書として出力することです。そのためのコードをリスト 5 に記載します。

リスト 5. IndexSuccess の XML ビュー
<?php
class Books_IndexSuccessView extends ExampleAppBooksBaseView
{ 
  public function executeXml(AgaviRequestDataHolder $rd)
  {
    $result = $this->getAttribute('result');
    $dom = new DOMDocument('1.0', 'utf-8');
    $root = $dom->createElement('result');    
    $dom->appendChild($root);    
    $xml = simplexml_import_dom($dom);
    foreach ($result as $r) {
      $item = $xml->addChild('item');
      $item->addChild('id', $r['id']);
      $item->addChild('author', $r['author']);
      $item->addChild('title', $r['title']); 
    }
    return $xml->asXml();
  }
}
?>

上記のコードにそれほど複雑な点はありません。executeXml() メソッドは新しい DOM 文書を生成し、ルート要素を作成してから SimpleXML を使用して XML ツリーの残りの部分を組み立て、そこに Doctrine 結果セットの情報を入力します。

このコードが実際にどのように機能するかを確認するには、Web ブラウザーを使って URL http://example.localhost/books にアクセスしてください。すると、図 3 のような画面が表示されるはずです (図 3 をテキストのみで表示したものを見る)。

図 3. すべての本を要求する GET リクエストに対する XML レスポンス
すべての本を要求する GET リクエストに対する XML レスポンスのスクリーン・キャプチャー
すべての本を要求する GET リクエストに対する XML レスポンスのスクリーン・キャプチャー
すべての本を要求する GET リクエストに対する XML レスポンスをテキストのみで表示したもの (図 3)
This XML file does not appear to have any style information associated with it. 
The document tree is show below.

-<result>
  -<item>
    -<id>1</id>
    -<author>Hilary Mantel</author>
    -<title>Wolf Hall</title>
-</item>
  -<item>
    -<id>2</id>
    -<author>Dennis Lehane</author>
    -<title>Prayers for Rain</title>
  -</item>
-</result>

同様に、BookAction の executeRead() メソッドは GET /books/{id} リクエストに対して、要求された本の詳細が含まれる XML 文書で応答する必要があります。そのためのコードは、リスト 6 のようになります。

リスト 6. GET リクエストの BookAction ハンドラー
<?php
class Books_BookAction extends ExampleAppBooksBaseAction
{
  public function executeRead(AgaviRequestDataHolder $rd)
  {
    $q = Doctrine_Query::create()
          ->from('Book b')
          ->addWhere('id = ?', $rd->getParameter('id'));
    $result = $q->execute(array(), Doctrine::HYDRATE_ARRAY); 
    if (count($result) == 0) {
      return 'Error';  
    }
    $this->setAttribute('result', $result);
    return 'Success';
  }
}
?>

BookAction に対応するバリデーター定義、および対応する BookSuccess ビューをリスト 7リスト 8 にそれぞれ記載します。

リスト 7. BookAction のバリデーター
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations
  xmlns="http://agavi.org/agavi/config/parts/validators/1.0"
  xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
  parent="%core.module_dir%/Books/config/validators.xml"
>
  <ae:configuration>    
    <validators>
      <validator class="number">
        <arguments>
          <argument>id</argument>
        </arguments>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
          <ae:parameter name="min">1</ae:parameter>
        </ae:parameters>
      </validator>    
    </validators> 
  </ae:configuration>
</ae:configurations>
リスト 8. BookSuccess の XML ビュー
<?php
class Books_BookSuccessView extends ExampleAppBooksBaseView
{
  public function executeXml(AgaviRequestDataHolder $rd)
  {
    $result = $this->getAttribute('result');
    $dom = new DOMDocument('1.0', 'utf-8');
    $root = $dom->createElement('result');    
    $dom->appendChild($root);    
    $xml = simplexml_import_dom($dom);
    foreach ($result as $r) {
      $item = $xml->addChild('item');
      $item->addChild('id', $r['id']);
      $item->addChild('author', $r['author']);
      $item->addChild('title', $r['title']);      
    }
    return $xml->asXml();
  }   
}
?>

実際の動作を確認するには、Web ブラウザーで URL http://example.localhost/books/1 にアクセスします。すると、図 4 のような画面が表示されます (図 4 をテキストのみで表示したものを見るには、ここをクリックしてください)。

図 4. 個々の本を要求する GET リクエストに対する XML レスポンス
個々の本を要求する GET リクエストに対する XML レスポンスのスクリーン・キャプチャー
個々の本を要求する GET リクエストに対する XML レスポンスのスクリーン・キャプチャー
個々の本を要求する GET リクエストに対する XML レスポンスをテキストのみで表示したもの (図 4)
This XML file does not appear to have any style information associated with it. 
The document tree is show below.

-<result>
  -<item>
    -<id>1</id>
    -<author>Hilary Mantel</author>
    -<title>Wolf Hall</title>
-</item>
-</result>

指定したリソースが使用可能でない場合には、404 (Not Found) ステータス・コードを返すようにするとよいでしょう。その方法は簡単で、BookErrorView をアプリケーションのデフォルト Error404SuccessView にリダイレクトし、404 メッセージ本文を返す executeXml() メソッドによって、このデフォルト・ビューを更新すればよいだけです。リスト 9 に、このコードを記載します。

リスト 9. Error404Success の XML ビュー
<?php
class Default_Error404SuccessView extends ExampleAppDefaultBaseView
{
  public function executeXml(AgaviRequestDataHolder $rd)
  {
    $this->getResponse()->setHttpStatusCode('404');
    $dom = new DOMDocument('1.0', 'utf-8');
    $root = $dom->createElement('error'); 
    $dom->appendChild($root);
    $message = $dom->createElement('message', '404 Not Found');
    $root->appendChild($message);
    return $dom->saveXml();
  }
}
?>

POST リクエストの処理

POST リクエストを処理するとなると、少し複雑になってきます。通常の REST 規約では、POST リクエストは新規リソースを作成します。そのために使用されるのは、そのリソースに必要なすべての入力 (このサンプル・アプリケーションの場合は、著者とタイトル) が本文に含まれるリクエストです。Agavi は自動的に、URL エンコードされたリクエスト本文を読み取り、個別のリクエスト・パラメーターに変換することができますが、POST リクエストや PUT リクエストの場合にリクエスト本文に含まれるのは XML 文書です。そのため、XML データをアクション・メソッド内で使用できるようにリクエスト・パラメーターに変換するという追加の作業が必要となってきます。

リスト 10 に、新しい本のエントリーを表す XML 文書の一例を記載します。

リスト 10. 新しい本のエントリーを表す XML 文書
<book>
  <title>The Da Vinci Code</title>
  <author>Dan Brown</author>
</book>

XML データをリクエスト・パラメーターに変換するのに最も簡単な方法は、AgaviWebRequest クラスをサブクラスに分けて着信リクエストの Content-Type ヘッダーをチェックし、ヘッダーが XML リクエスト本文を示している場合には、XML で必要な処理を行うというものです。リスト 11 に、このようなサブクラスの一例を記載します。

リスト 11. カスタム HTTP リクエスト・ハンドラー・クラス
<?php
// credit: David Zuelke
class ExampleAppWebRequest extends AgaviWebRequest {
  public function initialize(AgaviContext $context, array $parameters = array()) {
    parent::initialize($context, $parameters);
    $rd = $this->getRequestData();
    if (stristr($rd->getHeader('Content-Type'), 'text/xml')) {
      switch ($_SERVER['REQUEST_METHOD']) {
        case 'PUT': {
          $xml = $rd->removeFile('put_file')->getContents();
          break;
        }
        case 'POST': {
          $xml = $rd->removeFile('post_file')->getContents();
          break;
        }
        default: {
          $xml = '';
        }
      }
      $rd->setParameters((array)simplexml_load_string($xml));
    }
  } 
}
?>

PUT データと POST データは、URL エンコードされない場合、リクエストの put_file および post_file といったファイル変数に格納されます。リスト 11 ではこのデータをリクエストから取り出し、SimpleXML を使用してデータをオブジェクトに変換してから、変換後のオブジェクトを AgaviRequestDataHolder の setParameters() メソッドでの使用に適した配列にキャストしています。このデータであれば、AgaviRequestDataHolder の getParameter() メソッドを使用して、アクション・メソッド内から通常の方法でアクセスすることができます。

更新したクラス定義を $ROOT/app/lib/request/ExampleAppWebRequest.class.php に保存し、この PHP ファイルを $ROOT/app/config/autoload.xml に追加することによって、このクラス定義をロードすることができます。リスト 12 には、この XML ファイルに追加された PHP ファイルが示されています。

リスト 12. Agavi オートローダーの構成
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns="http://agavi.org/agavi/config/parts/autoload/1.0" 
 xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 parent="%core.system_config_dir%/autoload.xml">
  <ae:configuration>
    <autoload name="ExampleAppBaseAction">
     %core.lib_dir%/action/ExampleAppBaseAction.class.php</autoload>
    <autoload name="ExampleAppBaseModel">
    %core.lib_dir%/model/ExampleAppBaseModel.class.php</autoload>
    <autoload name="ExampleAppBaseView">
    %core.lib_dir%/view/ExampleAppBaseView.class.php</autoload>
    <autoload name="ExampleAppWebRequest">
    %core.lib_dir%/request/ExampleAppWebRequest.class.php</autoload>
    <autoload name="Doctrine">
    %core.app_dir%/../libs/doctrine/Doctrine.php</autoload>    
  </ae:configuration>
</ae:configurations>

作業はまだ完了したわけではありません。通常の REST 規約では、POST リクエストは新規リソースを作成し、PUT リクエストは既存のリソースを更新することになっています。けれども Agavi のデフォルト設定ではそれとは逆に、POST リクエストが executeWrite() メソッドにマッピングされ、PUT リクエストが executeCreate() メソッドにマッピングされています。これでは REST を扱う際に混乱を招くため、通常は、これらのマッピングを切り替えるために $ROOT/app/config/factories.xml を更新して新しいマッピングを反映させるのが得策となります。そのためのコードはリスト 13 のとおりです。

リスト 13. ファクトリーでの HTTP リクエスト・メソッドと Agavi アクション・メソッドの再マッピング
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/factories/1.0">
  <ae:configuration>  
    ...

    <request class="ExampleAppWebRequest">
      <ae:parameter name="method_names">
          <ae:parameter name="POST">create</ae:parameter>
          <ae:parameter name="GET">read</ae:parameter>
          <ae:parameter name="PUT">write</ae:parameter>
          <ae:parameter name="DELETE">remove</ae:parameter>
      </ae:parameter>
    </request>
  ... 
  </ae:configuration>

以上の作業がすべて完了すれば、ついに POST リクエストを処理するように Books_IndexAction::executeCreate() メソッドを定義することが可能になります。リスト 14 に、このコードを記載します。

リスト 14. POST リクエストの IndexAction ハンドラー
<?php
class Books_IndexAction extends ExampleAppBooksBaseAction
{
  public function executeCreate(AgaviRequestDataHolder $rd)
  {
    $book = new Book;
    $book->author = $rd->getParameter('author');
    $book->title = $rd->getParameter('title');
    $book->save();    
    $this->setAttribute('result', array($book->toArray()));
    return 'Success';
  }
}
?>

これらのリクエスト変数を許容するようにアクションのバリデーターを更新することも忘れないでください (リスト 15 を参照)。

リスト 15. IndexAction のバリデーター
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations
  xmlns="http://agavi.org/agavi/config/parts/validators/1.0"
  xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
  parent="%core.module_dir%/Books/config/validators.xml"
>
  <ae:configuration>

    <validators method="create">
      <validator class="string">
        <arguments>
          <argument>author</argument>
        </arguments>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>    
      <validator class="string">
        <arguments>
          <argument>title</argument>
        </arguments>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>    
    </validators>

  </ae:configuration>
</ae:configurations>

REST 規約では、成功した POST に対するレスポンスには 201 (Created) ステータス・コードと、新規リソースの URL を示すロケーション・ヘッダー、およびリソースの表現をレスポンス本文に含めることを規定しています。この規定は、IndexSuccessView に多少の変更を加えるだけで完全に満たすことができます (リスト 16 を参照)。

リスト 16. 更新後の IndexSuccess の XML ビュー
<?php
class Books_IndexSuccessView extends ExampleAppBooksBaseView
{
  public function executeXml(AgaviRequestDataHolder $rd)
  {
    $result = $this->getAttribute('result');
    if ($this->getContext()->getRequest()->getMethod() == 'create') {
        $this->getResponse()->setHttpStatusCode('201');
        $this->getResponse()->setHttpHeader(
         'Location', 
         $this->getContext()->getRouting()->gen(
          'book', 
          array('id' => $result[0]['id']
        )));     
    }
    $dom = new DOMDocument('1.0', 'utf-8');
    $root = $dom->createElement('result');    
    $dom->appendChild($root);    
    $xml = simplexml_import_dom($dom);
    foreach ($result as $r) {
      $item = $xml->addChild('item');
      $item->addChild('id', $r['id']);
      $item->addChild('author', $r['author']);
      $item->addChild('title', $r['title']);      
    }
    return $xml->asXml();
  }
}
?>

PUT および DELETE リクエストの処理

前述のとおり、PUTリクエストは既存のリソースへの変更を指示するために使用されることから、リクエストのストリングにはリソース ID が組み込まれます。成功した PUT とは、既存のリソースが PUT リクエスト本文に指定されたリソースによって置き換えられたことを意味します。成功した PUT に対するレスポンスは、レスポンス本文に更新されたリソースの表現を含めた 200 (OK) ステータス・コードにすることも、レスポンス本文が空の 204 (No Content) ステータス・コードにすることもできます。

リスト 17 に、更新後のバリデーター定義を記載します。

リスト 17. BookAction のバリデーター
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations
  xmlns="http://agavi.org/agavi/config/parts/validators/1.0"
  xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
  parent="%core.module_dir%/Books/config/validators.xml"
>
  <ae:configuration>
    
    <validators>
      <validator class="number">
        <arguments>
          <argument>id</argument>
        </arguments>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
          <ae:parameter name="min">1</ae:parameter>
        </ae:parameters>
      </validator>    
    </validators>

    <validators method="write">
      <validator class="string">
        <arguments>
          <argument>author</argument>
        </arguments>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>    
      <validator class="string">
        <arguments>
          <argument>title</argument>
        </arguments>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>
    </validators>

  </ae:configuration>
</ae:configurations>

リスト 18 に記載するのは、PUT リクエストを処理する Books_BookAction::executeWrite() アクション・メソッドのコードです。

リスト 18. PUT リクエストの BookAction ハンドラー
<?php
class Books_BookAction extends ExampleAppBooksBaseAction
{
  public function executeWrite(AgaviRequestDataHolder $rd)
  {
    $book = Doctrine::getTable('book')->find($rd->getParameter('id'));
    if (!is_object($book)) {
      return 'Error';
    }
    $book->author = $rd->getParameter('author');
    $book->title = $rd->getParameter('title');
    $book->save();
    $this->setAttribute('result', array($book->toArray()));
    return 'Success';
  }
}
?>

同様に、executeRemove() メソッドは DELETE リクエストを処理して、指定されたリソースをデータ・ストアから削除します。リスト 19 に、このメソッドのコードを記載します。

リスト 19. DELETE リクエストの BookAction ハンドラー
<?php
class Books_BookAction extends ExampleAppBooksBaseAction
{
  public function executeRemove(AgaviRequestDataHolder $rd)
  {
    $q = Doctrine_Query::create()
          ->delete('Book')
          ->addWhere('id = ?', $rd->getParameter('id'));
    $result = $q->execute();
    $this->setAttribute('result', null);
    return 'Success';
  }
}
?>

成功した DELETE に対するレスポンスは、レスポンス本文にステータスを含んだ 200 (OK)、またはレスポンス本文が空の 204 (No Content) のいずれかにすることができます。後者は、BookSuccessView を変更することによって簡単に設定することができます (リスト 20 を参照)。

リスト 20. BookSuccess の XML ビュー
<?php
class Books_BookSuccessView extends ExampleAppBooksBaseView
{
  public function executeXml(AgaviRequestDataHolder $rd)
  {
    if ($this->getContext()->getRequest()->getMethod() == 'remove') {
      $this->getResponse()->setHttpStatusCode('204');
      return false;
    }
    $result = $this->getAttribute('result');
    $dom = new DOMDocument('1.0', 'utf-8');
    $root = $dom->createElement('result');
    $dom->appendChild($root);    
    $xml = simplexml_import_dom($dom);
    foreach ($result as $r) {
      $item = $xml->addChild('item');
      $item->addChild('id', $r['id']);
      $item->addChild('author', $r['author']);
      $item->addChild('title', $r['title']);
    }
    return $xml->asXml();
  }
}
?>

JSON サポートの追加

これまでのセクションでは、単純な XML ベースの REST API をセットアップする方法を説明してきました。その一方、データ交換フォーマットとしては JSON もよく使用されるようになってきています。そのため、REST API で JSON のリクエストおよびレスポンス本文をサポートする必要が出てくるケースもよくあります。Agavi ではこの必要に、その柔軟な出力タイプによって簡単に対処することができます。

ステップ 1: JSON 出力タイプを有効にする

まずは、JSON リクエストのハンドラーをルーティング・テーブルに追加します (リスト 21 を参照)。

リスト 21. JSON ルート・ハンドラー
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/routing/1.0">
  <ae:configuration>
    <routes>
      <!-- handler for JSON requests -->
      <route name="json" pattern=".json$" cut="true" stop="false" 
       output_type="json" />
      ...
    </routes>
  </ae:configuration>
</ae:configurations>

ルーティング・テーブルの先頭に配置されるこのルートは、.json サフィックスで終わるすべてのリクエストと一致し、リクエストで使用する出力タイプを JSON に設定します。しかし、それだけではありません。

  • cut 属性は、一致したサブストリング・セグメントが見つかった場合に、そのサブストリング・セグメントをリクエスト URL から削除してから処理を続けるようにするかどうかを指定します。上記の場合、この属性は true に設定されているため、.json サフィックスが一致すると、このサフィックスがリクエスト URL から削除されます。
  • ルート定義に含まれる stop 属性は、最初の一致が検出された後にルートの処理を続けるかどうかを指定します。上記の例では false に設定されているため、リストの最後まで残りのリクエスト URL の突き合わせが行われ、それから適切なアクションが呼び出されます。

この構成が結局のところどのように影響するかと言うと、Agavi が例えば http://example.localhost/books/1.json へのリクエストを受け取ると、ルーティング・テーブルをチェックし、すぐに先頭にある catch-all ルートとの一致を検出します。すると Agavi はリクエスト URL から .json サフィックスを取り除き、リクエストの出力タイプを JSON に設定します。その後、引き続きリクエストの残りの部分、http://example.localhost/books/1 をルーティング・テーブルに記載されたルートと照らし合わせ、books.book ルートとの一致が見つかった時点で BookAction を呼び出します。BookAction が完了すると、Agavi はビューの中で executeJson() メソッドを探して実行し、その出力をクライアントに返します。

続いて、新規 JSON 出力タイプを定義します (リスト 22 を参照)。

リスト 22. JSON 出力タイプの定義
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/output_types/1.0">
  <ae:configuration>
    <output_types default="xml">      
      <output_type name="json">
        <ae:parameter name="http_headers">
          <ae:parameter name="Content-Type">application/json</ae:parameter>
        </ae:parameter>
      </output_type>
      ...
    </output_types>    
  </ae:configuration>  
</ae:configurations>

ステップ 2: JSON Web リクエストからパラメーターを抽出する

XML の場合と同じく、Agavi は自動的には JSON パケットをリクエスト・パラメーターに変換しません。したがって、このタスクを処理するようにカスタム ExampleAppWebRequest クラスを更新します。具体的には、Content-Type ヘッダーを基に JSON であるかを判断し、JSON であれば、その値を PHP の json_decode() 関数を使用して PHP 配列に抽出して、AgaviRequestDataHolder の setParameters() メソッドに渡せるようにするということです。このコードをリスト 23 に記載します。

リスト 23. 更新後の Web リクエスト・ハンドラー
<?php
class ExampleAppWebRequest extends AgaviWebRequest {
  public function initialize(AgaviContext $context, array $parameters = array()) {
    parent::initialize($context, $parameters);
    $rd = $this->getRequestData();
    // handle XML requests
    if (stristr($rd->getHeader('Content-Type'), 'text/xml')) {
      switch ($_SERVER['REQUEST_METHOD']) {
        case 'PUT': {
          $xml = $rd->removeFile('put_file')->getContents();
          break;
        }
        case 'POST': {
          $xml = $rd->removeFile('post_file')->getContents();
          break;
        }
        default: {
          $xml = '';
        }
      }
      $rd->setParameters((array)simplexml_load_string($xml));
    }
    // handle JSON requests
    if (stristr($rd->getHeader('Content-Type'), 'application/json')) {
      switch ($_SERVER['REQUEST_METHOD']) {
        case 'PUT': {
          $json = $rd->removeFile('put_file')->getContents();
          break;
        }
        case 'POST': {
          $json = $rd->removeFile('post_file')->getContents();
          break;
        }
        default: {
          $json = '{}';
        }
      }
      $rd->setParameters(json_decode($json, true));
    } 
  } 
}
?>

ステップ 3: アプリケーションのビューを更新する

最後のステップは、JSON 出力をサポートするように各種のアプリケーション・ビューを更新することです。それには、executeJson() メソッドを IndexSuccessView (リスト 24) と BookSuccessView (リスト 25) に追加します。

リスト 24. IndexSuccess の JSON ビュー
<?php
class Books_IndexSuccessView extends ExampleAppBooksBaseView
{ 
  public function executeJson(AgaviRequestDataHolder $rd)
  {
    $result = $this->getAttribute('result');
    if ($this->getContext()->getRequest()->getMethod() == 'create') {
      $this->getResponse()->setHttpStatusCode('201');
      $this->getResponse()->setHttpHeader('Location', 
       $this->getContext()->getRouting()->gen(
        'book', 
        array('id' => $result[0]['id']
      ))); 
    }
    return json_encode($result);
  }
}
?>
リスト 25. BookSuccess の JSON ビュー
<?php
class Books_BookSuccessView extends ExampleAppBooksBaseView
{
  public function executeJson(AgaviRequestDataHolder $rd)
  {
    if ($this->getContext()->getRequest()->getMethod() == 'remove') {
      $this->getResponse()->setHttpStatusCode('204');     
      return false;
    }
    return json_encode($this->getAttribute('result'));
  }
}
?>

実際の動作を確認するには、Web ブラウザーで http://example.localhost/books/.json または http://example.localhost/books/1.json にアクセスしてください。すると、JSON にエンコードされたレスポンス・パケットおよび対応するデータを受け取るはずです。図 5 に、Firebug デバッガーに表示されたパケットの一例を示します (図 5 をテキストのみで表示したものを見る)。

図 5. すべての本を要求する GET リクエストに対する JSON レスポンス
すべての本を要求する GET リクエストに対する JSON レスポンスのスクリーン・キャプチャー
すべての本を要求する GET リクエストに対する JSON レスポンスのスクリーン・キャプチャー
Console タブの Response タブに、すべての本を要求する GET リクエストに対する JSON レスポンスをテキストのみで表示したもの (図 5)
-GET http://exampple.localhost/books/.json 200  OK 678
          Response
[{"id":"1":"Wolf Hall","author":"Hilary Mantel"},{"id":"2":"Prayers for Rain","author"
:"Dennis Lehane"},{"id":"22":"The Da Vinci Code","author":"Dan Brown"}]

重要な点として、上記で説明した JSON サポートは、アクション・コードには手を加えなくても各種のビューで変更を行うだけで有効になることに注意してください。異なる出力タイプを処理する方法を、アクションの中ではなく、ビューの中で開発者が決定できるようにすることで、Agavi はコードの重複を最小限にすると同時に、MVC の原則と DRY (Don't Repeat Yourself) の原則の両方に従っているというわけです。

まとめ

Agavi は完成された REST API の作成フレームワークとして、アプリケーション開発者がサード・パーティーのソフトウェアから軽量で直感的なアーキテクチャー・パターンを使用して、アプリケーション関数に容易にアクセスできるようにします。REST ルートのサポートが組み込まれていること、そして新しい出力タイプを簡単にサポートできることから、Agavi は迅速な API 開発およびデプロイメントには理想的なフレームワークです。また、MVC 実装であるという Agavi の性質は、実装フェーズの任意の時点、さらにはアプリケーションがデプロイされた後でも既存のビジネス・ロジックへの影響を最小限に抑えて、REST API をアプリケーションに追加できることを意味します。

この記事で実装したすべてのコードは、「ダウンロード」セクションから入手することができます。このダウンロード・ファイルには、サンプル API で GET、POST、PUT、および DELETE リクエストを行うために使用できる単純な jQuery ベースのテスト・スクリプトも含まれています。ぜひともコードを入手して、いろいろと試してみてください。実際に新しい機能を追加してみるのも一考です。何も壊れることはありませんので、よい勉強になると思って楽しみながら試してください!


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


関連トピック


コメント

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=XML, Open source
ArticleID=481473
ArticleTitle=Agavi を使って REST API を作成する
publish-date=03162010