目次


Agavi による MVC プログラミング入門

第 2 回 Agavi と Doctrine により、フォームとデータベース・サポートを追加する

Agavi フレームワークを使用してスケーラブルな Web アプリケーションを作成する方法を学ぶ

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: Agavi による MVC プログラミング入門

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:Agavi による MVC プログラミング入門

このシリーズの続きに乞うご期待。

はじめに

この連載の第 1 回では、Agavi の概要を紹介するとともに、Agavi をスケーラブルかつ標準に準拠した Web アプリケーションの作成に極めて適したフレームワークにしている、Agavi の機能のいくつかを説明しました。そして WASP (Web Automobiles Sales Platform) という名前のアプリケーションを例に、新規 Agavi プロジェクトの作成手順、Agavi に推奨されるファイルシステムのレイアウト、Agavi のコマンドライン・ビルド・スクリプトなどについての基本を説明しました。さらに、あらゆる Agavi アプリケーションに欠かせないコンポーネント (アクション、ビュー、ルート) について説明し、Agavi の組み込み入力バリデーターもいくつか紹介しました。

静的コンテンツを提供するために Agavi を使用できることはもちろんですが、Agavi がその輝きを放つのは、それよりも複雑なコンテンツを作成するために Agavi を使用したときです。この 2 回目の記事ではまさに、Agavi を使用して複雑なコンテンツを作成します。これから数ページにわたって Web フォームからの入力を受信、検証、処理する方法を、そして最後に Agavi アプリケーションを MySQL データベースに接続する方法を学びます。

Agavi によるフォームの作成

まずは、WASP アプリケーションの index ページが現在どのようになっているかを思い出してください (図 1)。

図 1. WASP アプリケーションの index ページ
WASP アプリケーションの index ページ
WASP アプリケーションの index ページ

この図から思い出せたことと思いますが、静的コンテンツへの 2 つのリンクを処理するコードはすでに配置してあるので、今回は、「Contact Us」リンクを扱います。その名前からわかるように、このリンクが指しているのは、関係者が自動車販売店への問い合わせ (Contact) に使用できるお問い合わせ (Contact Us) フォームです。この機能を実装する一般的な手順は、前回の記事で StaticContentAction を作成したときに使った手順とよく似ており、コードが違うだけに過ぎません。

まず始めに Agavi ビルド・スクリプトを起動して、以下の値を入力してください。

shell> agavi action-wizard
...
Module name: Default
Action name: Contact
Space-separated list of views to create for Contact [Success]: Input Error Success

上記によって、ContactAction が新規に作成され、3 つのビューが追加されます。そのうち、ContactSuccessView と ContactErrorView の 2 つはお馴染みの標準ビューで、それぞれアクションの成功または失敗に応じて表示されます。3 つ目の ContactInputView は今回初めて登場するビューです。ユーザーに最初に表示されるこのビューには、ユーザー入力を受け付ける Webフォームが表示されます。

新しい ContactAction へのルートは、$WASP_ROOT/app/routing.xml ファイルに追加します (リスト 1 を参照)。

リスト 1. Default/ContactAction ルートの定義
<?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> 
       ...          
      <!-- action for contact form "/contact" -->
      <route name="contact" pattern="^/contact$" module="Default" 
       action="Contact" />

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

ついでに、$WASP_ROOT/app/templates/Master.php にあるマスター・テンプレートを更新して、メイン・メニューの「Contact Us」リンクから上記のルートへのハイパーリンクを設定してください (リスト 2 を参照)。

リスト 2. マスター・テンプレート:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
...
      <div id="menu">
        <ul>
          <li><a href="<?php echo $ro->gen('index'); ?>">
           Home</a></li>
          <li><a href="#">For Sale</a></li>
          <li><a href="<?php echo $ro->gen('content', 
           array('page' => 'other-services')); ?>">Other 
            Services</a></li>
          <li><a href="<?php echo $ro->gen('content', 
           array('page' => 'about-us')); ?>">About Us</a></li>
          <li><a href="<?php echo $ro->gen('contact'); ?>">
           Contact Us</a></li>
        </ul>
      </div>
...

次に、$WASP_ROOT/app/modules/Default/actions/ContactAction.class.php にある ContactAction クラス・ファイル内に getDefaultViewName() メソッドと executeRead() メソッドを定義し、ContactInputView がデフォルトで表示されるだけでなく、すべての GET リクエストで表示されるように指定します。

リスト 3. Default/ContactAction の定義
<?php
class Default_ContactAction extends WASPDefaultBaseAction
{
  public function getDefaultViewName()
  {
    return 'Input';
  }
  
  public function executeRead(AgaviRequestDataHolder $rd)
  {           
    return 'Input';
  }
}
?>

$WASP_ROOT/app/modules/Default/templates/ContactInput.php にある対応するテンプレート・ファイルを単純な Web フォームで更新します (リスト 4 を参照)。このフォームの CSS ルールは、記事に付属のコード・アーカイブに含まれています (「ダウンロード」を参照)。

リスト 4. Default/ContactInput テンプレート
<h3>Contact Us</h3>
<form action="<?php echo $ro->gen(null); ?>" method="post">
  <label for="name" class="required">Name:</label>
  <input id="name" type="text" name="name" />
  <p/>
  <label for="email" class="required">Email address:</label>
  <input id="email" type="text" name="email" />
  <p/>
  <label for="message" class="required">Message body:</label>
  <textarea id="message" name="message" style="width:300px; height:200px">
  </textarea>
  <p/>
  <input type="submit" name="submit" class="submit" value="Send Message" />
</form>

ブラウザーで WASP index ページにもう一度アクセスし、「Contact Us」リンクをクリックすると、図 2 の Web フォームが表示されます。

図 2. WASP お問い合わせ (Contact Us) フォーム
WASP お問い合わせ (Contact Us) フォーム
WASP お問い合わせ (Contact Us) フォーム

作業はまだ半分しか終わっていません。これから、ユーザーがこのフォームのフィールドに入力してデータを送信したときの振る舞いを定義する必要があります。

フォーム入力の検証

ユーザーがフォームを送信すると、クライアント・ブラウザーは入力されたデータを POST トランザクションでサーバーに送信します。すると、前回の記事で説明したように、Agavi は極めて厳重な入力フィルターを操作します。検証ルールに明示的に規定されていない GET 変数または POST 変数はすべて、自動的に破棄されます。そのため、フォーム入力の処理方法を定義する際に常に最初のステップとなるのは、フォームの入力フィールドに対するバリデーターを定義することです。

ContactInput フォームには 3 つのフィールドがあります。バリデーターは、そのそれぞれのフィールドに定義する必要があります。$WASP_ROOT/app/modules/Default/validate/Contact.xml にある検証ルールは、以下のような内容です (リスト 5 を参照)。

リスト 5. Default/ContactAction バリデーター
<?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%/Default/config/validators.xml"
>
  <ae:configuration>
    
    <validators method="write">
      <validator class="string">
        <arguments>
          <argument>name</argument>
        </arguments>
        <errors>
          <error for="required">ERROR: Name is missing</error>
          <error>ERROR: Name is invalid</error>
        </errors>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>
      
      <validator class="email">
        <arguments>
          <argument>email</argument>
        </arguments>
        <errors>
          <error for="required">ERROR: Email address is missing</error>
          <error>ERROR: Email address is invalid</error>
        </errors>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>
      
      <validator class="string">
        <arguments>
          <argument>message</argument>
        </arguments>
        <errors>
          <error for="required">ERROR: Message body is missing</error>
          <error>ERROR: Message body is invalid</error>
        </errors>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>
    </validators>
    
  </ae:configuration>
</ae:configurations>

リスト 5 の構成では、3 つのフォーム・フィールドに対して 2 つのバリデーターをセットアップしています。1 つは namemessage フィールド用の AgaviStringValidator、もう 1 つは email フィールド用の AgaviEmailValidator です。特に注意する点として、それぞれのバリデーターに付随する <error> ブロックに注目してください。それぞれ異なる失敗シナリオに応じてエラー・メッセージを定義するこのブロックは、この後に説明する内容のなかで、重要なコンポーネントとなります (その説明を待つだけの価値は必ずあります)。

入力データが検証にパスしたとすると、Agavi は ContactAction 内で、POST されるその入力データをどう扱うかを定義する executeWrite() メソッドを探します。この例の場合に実行しなければならないのは、入力を E メール・メッセージのフォーマットに設定して、指定された E メール・アドレスに送信することです。そこで 2 番目のステップでは、メールのフォーマットに設定した入力を送信する executeWrite() メソッドを ContactAction に追加します (リスト 6 を参照)。

リスト 6. 新しい executeWrite() メソッドが追加された Default/ContactAction の定義
<?php
class Default_ContactAction extends WASPDefaultBaseAction
{
  ...

  public function executeWrite(AgaviRequestDataHolder $rd)
  {           
    $name = $rd->getParameter('name');
    $email = $rd->getParameter('email');
    $message = $rd->getParameter('message');
    $subject = 'Contact form submission';
    $to = 'webmaster@wasp.example';
    if (@mail($to, $subject, $message, "From: $name <$email>\r\n")) {
      return 'Success';
    } else {
      return 'Error';
    }     
  }
}
?>

E メール・メッセージが送信されたかどうかによって、Agavi は ContactSuccessView または ContactErrorView のいずれかをレンダリングします。どちらのビュー・テンプレートにしても、手の込んだことを行う必要はありません。単純なメッセージでフォームの送信結果を示しさえすれば十分です。$WASP_ROOT/app/modules/Default/templates/ContactSuccess.php の場合は、以下のようになります。

Your message was successfully sent!

$WASP_ROOT/app/modules/Default/templates/ContactError.php の場合は、以下のようになります。

There was an error. Your message could not be sent. Please try again later.

これで作業は完了です。実際に試して、その結果を確認してください。

フォーム入力フィルターの使用

洞察力に優れた読者はお気付きだと思いますが、現時点のセットアップでは、E メール・メッセージの送信でエラーが発生した場合も、フォーム・フィールドでの入力検証が 1 つでもパスしなかった場合も、アプリケーションは同じ振る舞いを見せることになります (つまり、ContactErrorView をレンダリングします)。実際には、この 2 つの場合でアプリケーションの応答を変えたいはずです。具体的に言えば、ユーザーの入力が検証にパスしなかった場合は、通常、アプリケーションに同じフォームを再びレンダリングさせた上で無効なフィールドを強調表示し、ユーザーがそれを修正して再送信できるようにする必要があります。

Agavi には、上記の振る舞いを簡単に実現できるという点で、ほとんど魔法に近いツールが備わっています。それが、AgaviFormPopulationFilter です。このツールの動作を確認するには、ContactAction (リスト 3) に戻ってリスト 7 のコードを追加してください。

リスト 7. AgaviFormPopulationFilter が有効にされた Default/ContactAction の定義
<?php
class Default_ContactAction extends WASPDefaultBaseAction
{
   ...
  
  public function handleError(AgaviRequestDataHolder $rd)
  {       
    return 'Input';
  }
}
?>

フォームが検証にパスしなかった場合の Agavi のデフォルトの振る舞いは、対応する Error ビューをレンダリングすることです。デフォルトとは異なるビューの名前を設定した handleError() メソッドを Default_ContactAction に追加することで、デフォルトの振る舞いはオーバーライドされます。上記のコードは Agavi に対し、フォーム・フィールドが 1 つでも検証にパスしなかった場合には ContactInputView ビューを再レンダリングするように指示しています。

Web ブラウザーで「Contact Us」フォームに戻り、データを入力しないまま送信するか、あるいは無効なデータを入力して送信してみてください。Agavi は ContactErrorView ではなく ContactInputView を再レンダリングし、無効なフィールドの下にエラー・メッセージを表示します (図 3 を参照)。

図 3. 入力エラーが強調表示された WASP お問い合わせ (Contact Us) フォーム
入力エラーが強調表示された WASP お問い合わせ (Contact Us) フォーム
入力エラーが強調表示された WASP お問い合わせ (Contact Us) フォーム

この振る舞いは、AgaviFormPopulationFilter によるものです。フォームの入力フィールドが 1 つでも検証にパスしなかった場合、このツールが自動的に無効なフィールドに対してエラー・メッセージを表示してくれます。また、有効な入力値はフォームに再設定して、ユーザーが再入力する手間を省いていることも注目の点です。エラー・メッセージ自体は、バリデーターから取得されます (これが、前に指摘した <error> ブロックの重要性です)。

Agavi アプリケーションでは、AgaviFormPopulationFilter はデフォルトで有効になっています。このツールは無効にすることも、あるいは $WASP_ROOT/app/config/global_filters.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/filters/1.0">

  <ae:configuration context="web">
    <filters>
      <filter name="FormPopulationFilter" class="AgaviFormPopulationFilter">
        
        <!-- only run for request method "write" (=POST on web) 
        by default (can be changed at runtime, of course) -->
        <!-- if you omit this, it will never run -->
        <ae:parameter name="methods">
          <ae:parameter>write</ae:parameter>
        </ae:parameter>
        
        <!-- only run for output type "html" (so it doesn't break on, 
         say, JSON data) -->
        <!-- if you omit this, it will run for all output types -->
        <ae:parameter name="output_types">
          <ae:parameter>html</ae:parameter>
        </ae:parameter>
        
        <!-- error message insertion rules -->
        <!-- they are run in sequence; once the first one matched, 
         execution stops -->
        <!--
          errors that belong to more than one field (e.g. date validator) 
          can be handled using "multi_field_error_messages"
          "normal" errors are handled through "field_error_messages"
          errors that yield no match and those that have no corresponding 
          field are inserted using rules defined in "error_messages".
        -->
        
        <!-- for all field error messages. -->
        <ae:parameter name="field_error_messages">
          <!-- ${htmlnsPrefix} is either empty (for HTML) or something like 
          "html:" for XHTML documents with xmlns="..." notation. Always use this, 
          makes your code more bullet proof. XPath needs the namespaces when the document 
          is namespaced -->
          
          <!-- all input fields that are not checkboxes or radios, and 
          all textareas -->
          <ae:parameter name="self::${htmlnsPrefix}input[not(@type='checkbox' 
           or @type='radio')] | self::${htmlnsPrefix}textarea">
            <!-- if this rule matched, then the node found by the rule is our 
             starting point for inserting the error message(s). -->
            
            <!-- can be any of "before", "after" or "child" (to insert as prev, 
            next sibling or last child) -->
            <ae:parameter name="location">after</ae:parameter>
            <!-- a container groups all errors for one element. ${errorMessages} 
            is a string containing all errors (see below) -->
            <ae:parameter name="container">
             <![CDATA[<div class="errors">${errorMessages}</div>]]>
            </ae:parameter>
            <!-- this defines the HTML for each individual error message; 
            those are then put into the container. ${errorMessage} is the 
            error message string -->
            <ae:parameter name="markup">
             <![CDATA[<p class="error">${errorMessage}</p>]]>
            </ae:parameter>
          </ae:parameter>
          
          <!-- all other inputs - note how we select the parent element and 
          insert ourselves as last child of it -->
          <ae:parameter name="parent::*">
            <ae:parameter name="location">child</ae:parameter>
            <ae:parameter name="container">
             <![CDATA[<div class="errors">${errorMessages}</div>]]>
            </ae:parameter>
            <ae:parameter name="markup">
             <![CDATA[<p class="error">${errorMessage}</p>]]>
            </ae:parameter>
          </ae:parameter>
        </ae:parameter>
        
        <!--
        <ae:parameter name="multi_field_error_messages">
        </ae:parameter>
        -->
        
        <!-- everything that did not match any of the rules above, 
        or errors that do not belong to a field -->
        <ae:parameter name="error_messages">
          <!-- insert before the element -->
          <!-- that can be an input, or a form, if the error does not belong 
          to a field or didn't match anywhere else -->
          <ae:parameter name="self::*">
            <ae:parameter name="location">before</ae:parameter>
            <!-- no container here! we just insert paragraph elements -->
            <ae:parameter name="markup">
             <![CDATA[<p class="error">${errorMessage}</p>]]>
            </ae:parameter>
          </ae:parameter>
        </ae:parameter>        
      </filter>

      ...
    </filters>
  </ae:configuration>
</ae:configurations>

AgaviFormPopulationFilter が入力検証でのエラーを処理してくれることから、$WASP_ROOT/app/modules/Default/templates/ContactError.php についても以下のメッセージを表示するように更新することができます。

There was an error sending your message. Please try again later.

Agavi と Doctrine の統合

Agavi ではフォームがどのような動作をするかがわかったところで、今度はもう少し複雑な内容に取り組んでみましょう。前の例でのアクションは、ユーザーの入力を E メール・メッセージに変換してから、PHP の mail() 関数を使って送信するだけにすぎませんでした。しかし、ユーザー入力をデータ・ストア (ファイルまたはデータベース・テーブルのいずれか) から読み取ったり、データ・ストアに書き込んだりしなければならないという要件も珍しくありません。Agavi には一般的なデータベース・システムのほとんどに対応するコネクターが備わっているため、このような要件にも至って簡単に対応することができます。

データベースを用意したら、そのデータベースを操作するためのモデルも必要になります。モデルがなければ、データの管理、操作、およびデータを利用した計算を実行することはできません。モデルは独自に作成することもできますが、それよりも簡単なのは、Doctrine や Propel などのオブジェクト・リレーショナル・マッパーを使ってモデルを自動生成するという方法です。この連載では、柔軟で使いやすくて人気がある Doctrine を使用します。

Doctrine を使用する方法を説明するため、販売員が WASP アプリケーションで販売中の中古車のリストを作成するためのアクションと、買い手がこれらのリストを表示して、購入に関する問い合わせを販売店に送信するためのアクションを新たに作成します。送信されたリストは MySQL データベースに保存され、サイトの管理人によって承認された後、WASP サイトでの表示および検索が可能になります。

上記のタスクを実行するアクション・コードを作成するには、まず、Doctrine と Agavi との間でやりとりができるようにしなければなりません。そのためのプロセスを、以下のステップで説明します。

ステップ 1: アプリケーション・データベースを作成する

まずは MySQL コマンド・プロンプトで、このアプリケーションのための空のデータベースを新規に作成します。

shell>mysql -u root -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 59
Server version: 5.1.28-rc-community MySQL Community Server (GPL)
Type 'help;' or '\h' for help. Type '\c' to clear the buffer.

mysql> CREATE DATABASE wasp;
Query OK, 1 row affected (0.00 sec)

データベースを新規に作成する際は、このデータベースのみへのアクセス権を持つユーザー・アカウントも定義してください。これはセキュリティー上、有効なプラクティスです。こうすることにより、このユーザー・アカウントのセキュリティーが侵害されたとしても、他のデータベースをセキュアな状態に維持することができます。

mysql> GRANT ALL ON wasp.* TO wasp@localhost IDENTIFIED BY 'wasp';
Query OK, 1 row affected (0.00 sec)

今度は、自動車のリストを保存する以下のテーブルを作成します。

mysql> USE wasp;
Database changed

mysql> CREATE TABLE IF NOT EXISTS `listing` (
    ->   `RecordID` int(10) unsigned NOT NULL AUTO_INCREMENT,
    ->   `RecordDate` date NOT NULL,
    ->   `OwnerName` varchar(255) NOT NULL,
    ->   `OwnerTel` varchar(25) DEFAULT NULL,
    ->   `OwnerEmail` text NOT NULL,
    ->   `VehicleManufacturerID` int(11) NOT NULL,
    ->   `VehicleModel` varchar(255) NOT NULL,
    ->   `VehicleYear` year(4) NOT NULL,
    ->   `VehicleColor` varchar(30) NOT NULL,
    ->   `VehicleMileage` int(11) NOT NULL,
    ->   `VehicleIsFirstOwned` tinyint(1) NOT NULL,
    ->   `VehicleAccessoryBit` int(11) NOT NULL,
    ->   `VehicleIsCertified` tinyint(1) NOT NULL,
    ->   `VehicleCertificationDate` date DEFAULT NULL,
    ->   `VehicleSalePriceMin` int(11) NOT NULL,
    ->   `VehicleSalePriceMax` int(11) NOT NULL,
    ->   `VehicleSalePriceIsNegotiable` tinyint(1) NOT NULL DEFAULT '0',
    ->   `Note` text,
    ->   `OwnerCity` varchar(255) NOT NULL,
    ->   `OwnerCountryID` int(11) NOT NULL,
    ->   `DisplayStatus` tinyint(1) NOT NULL,
    ->   `DisplayUntilDate` date DEFAULT NULL,
    ->   PRIMARY KEY (`RecordID`)
    -> ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.13 sec)

これがアプリケーションの主要なデータ・ストアです。このデータ・ストアには、販売用のリストに掲載された自動車のメーカーとモデル、製造年、車体の色、これまでの走行距離、所有者が在住している国および都市、販売価格などの詳細情報が格納されます。さらに、自動車のレコードごとに DisplayStatusDisplayUntilDate というフィールドもあります。この 2 つの管理フィールドによって、リストを WASP サイトに表示するかどうか、そして表示する場合の期間を制御します。

このテーブルでは、自動車のメーカー、自動車の所有者が在住している国をそれぞれ指定する VehicleManufacturerID フィールドと OwnerCountryID フィールドで外部キー参照を使用していることに注意してください。次に作成するのは、これらのキーの参照先となっているテーブルです。

mysql> CREATE TABLE IF NOT EXISTS `manufacturer` (
    ->   `ManufacturerID` int(11) NOT NULL AUTO_INCREMENT,
    ->   `ManufacturerName` varchar(255) NOT NULL,
    ->   PRIMARY KEY (`ManufacturerID`)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.05 sec)

mysql> CREATE TABLE IF NOT EXISTS `country` (
    ->   `CountryID` int(11) NOT NULL AUTO_INCREMENT,
    ->   `CountryName` varchar(255) NOT NULL,
    ->   PRIMARY KEY (`CountryID`)
    -> ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.05 sec)

この機会に、これらの参照先のテーブルにサンプル・レコードも入力しておきます。

mysql> INSERT INTO `manufacturer` (`ManufacturerID`, `ManufacturerName`) 
      VALUES(1, 'Ferrari');
Query OK, 1 row affected (0.06 sec)

mysql> INSERT INTO `manufacturer` (`ManufacturerID`, `ManufacturerName`) 
      VALUES(2, 'Porsche');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `manufacturer` (`ManufacturerID`, `ManufacturerName`) 
      VALUES(3, 'BMW');
Query OK, 1 row affected (0.00 sec)


mysql> INSERT INTO `country` (`CountryID`, `CountryName`) 
      VALUES(1, 'United States');
Query OK, 1 row affected (0.05 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`) 
      VALUES(2, 'United Kingdom');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`) 
      VALUES(3, 'India');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`) 
      VALUES(4, 'Singapore');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`) 
      VALUES(5, 'Germany');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`) 
      VALUES(6, 'France');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`) 
      VALUES(7, 'Italy');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`) 
      VALUES(8, 'Spain');
Query OK, 1 row affected (0.02 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`) 
      VALUES(9, 'Hungary');
Query OK, 1 row affected (0.00 sec)

ステップ 2: Doctrine ライブラリーをダウンロードしてアプリケーションに追加する

データベースのセットアップに続くステップは、Doctrine ライブラリーをダウンロードしてアプリケーションに追加することです。Doctrine をインストールするには、Doctrine のホーム・ページにアクセスしてソース・コード・アーカイブをダウンロードします。アーカイブを解凍したら、lib/ ディレクトリーの内容を $WASP_ROOT/libs/doctrine/ にコピーしてください。Doctrine Web サイトへのリンクは、「参考文献」に記載されています。この記事では Doctrine V. 1.1 を使用します。

shell> cd /usr/local/apache/htdocs/wasp/libs
shell> mkdir doctrine
shell> cp -R /tmp/Doctrine-1.1.1/lib/* doctrine/

ステップ 3: Doctrine モデルを作成してアプリケーションに追加する

次のステップでは、アプリケーションの Doctrine モデルを生成します。モデルの生成は、Doctrine の generateModelsFromDb() メソッドを使用すれば自動的に行うことができます。それにはまず、出力モデルの一時ディレクトリーを作成します。

shell> cd /tmp
shell> mkdir models

次に、前に作成したデータベース・オブジェクトのモデルを Doctrine によって自動生成する単純な PHP スクリプトを作成します。リスト 8 はこのコードの一例、/tmp/doctrine-gen.php です (このコードがどのような動作をするかについて詳しくは、Doctrine マニュアルを参照してください。リンクは「参考文献」に記載されています)。

リスト 8. Doctrine モデルを生成する PHP スクリプト
<?php
// include main Doctrine class file
// change this per your system
include '/usr/local/apache/htdocs/wasp/libs/doctrine/Doctrine.php';
spl_autoload_register(array('Doctrine', 'autoload'));

// create Doctrine manager
$manager = Doctrine_Manager::getInstance();

// create database connection
$conn = Doctrine_Manager::connection('mysql://wasp:wasp@localhost/wasp', 'doctrine');

// auto-generate models
Doctrine::generateModelsFromDb('models', array('doctrine'), 
 array('generateTableClasses' => true));
?>

コマンドラインから、PHP インタープリターを使って上記のスクリプトを実行します。

shell> php doctrine-gen.php

Doctrine がデータベース・テーブルに対応したモデルの生成を開始します。スクリプトの実行が完了したら、/tmp/models/ と /tmp/models/generated/ の中身を見てください。図 4 のような内容になっているはずです。

図 4. 自動生成された Doctrine モデル
自動生成された Doctrine モデル
自動生成された Doctrine モデル

図 4 に示したファイルは、データベース・オブジェクトを表すクラスです。/tmp/models/generated/ ディレクトリー内のクラスは Doctrine によって生成された基底クラスで、/tmp/models 内のクラスは、基底クラスに機能を追加するために使用できる子クラスです。例えば、/tmp/models/generated/BaseListing.php の内部を見てみると、MySQL データベース・テーブルのフィールドに対応するプロパティーを持つ Doctrine オブジェクトがあることがわかります。

<?php

/**
 * BaseListing
 * 
 * This class has been auto-generated by the Doctrine ORM Framework
 * 
 * @property integer $RecordID
 * @property date $RecordDate
 * @property string $OwnerName
 * @property string $OwnerTel
 * @property string $OwnerEmail
 * @property integer $VehicleManufacturerID
 * @property string $VehicleModel
 * @property integer $VehicleYear
 * @property string $VehicleColor
 * @property integer $VehicleMileage
 * @property integer $VehicleIsFirstOwned
 * @property integer $VehicleAccessoryBit
 * @property integer $VehicleIsCertified
 * @property date $VehicleCertificationDate
 * @property integer $VehicleSalePriceMin
 * @property integer $VehicleSalePriceMax
 * @property integer $VehicleSalePriceIsNegotiable
 * @property string $Note
 * @property string $OwnerCity
 * @property integer $OwnerCountryID
 * @property integer $DisplayStatus
 * @property date $DisplayUntilDate
 * 
 * @package    ##PACKAGE##
 * @subpackage ##SUBPACKAGE##
 * @author     ##NAME## <##EMAIL##>
 * @version    SVN: $Id: Builder.php 5441 2009-01-30 22:58:43Z jwage $
 */
abstract class BaseListing extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->setTableName('listing');
        $this->hasColumn('RecordID', 'integer', 4, 
         array('type' => 'integer', 'length' => 4, 'unsigned' => 1, 
         'primary' => true, 'autoincrement' => true));
        $this->hasColumn('RecordDate', 'date', null, 
         array('type' => 'date', 'notnull' => true));
        $this->hasColumn('OwnerName', 'string', 255, 
         array('type' => 'string', 'length' => 255, 'notnull' => true));
        $this->hasColumn('OwnerTel', 'string', 25, 
         array('type' => 'string', 'length' => 25));
        $this->hasColumn('OwnerEmail', 'string', null, 
         array('type' => 'string', 'notnull' => true));
        $this->hasColumn('VehicleManufacturerID', 'integer', 4, 
         array('type' => 'integer', 'length' => 4, 'notnull' => true));
        $this->hasColumn('VehicleModel', 'string', 255, 
         array('type' => 'string', 'length' => 255, 'notnull' => true));
        $this->hasColumn('VehicleYear', 'integer', null, 
         array('type' => 'integer', 'notnull' => true));
        $this->hasColumn('VehicleColor', 'string', 30, 
         array('type' => 'string', 'length' => 30, 'notnull' => true));
        $this->hasColumn('VehicleMileage', 'integer', 4, 
         array('type' => 'integer', 'length' => 4, 'notnull' => true));
        $this->hasColumn('VehicleIsFirstOwned', 'integer', 1, 
         array('type' => 'integer', 'length' => 1, 'notnull' => true));
        $this->hasColumn('VehicleAccessoryBit', 'integer', 4, 
         array('type' => 'integer', 'length' => 4, 'notnull' => true));
        $this->hasColumn('VehicleIsCertified', 'integer', 1, 
         array('type' => 'integer', 'length' => 1, 'notnull' => true));
        $this->hasColumn('VehicleCertificationDate', 'date', null, 
         array('type' => 'date'));
        $this->hasColumn('VehicleSalePriceMin', 'integer', 4, 
         array('type' => 'integer', 'length' => 4, 'notnull' => true));
        $this->hasColumn('VehicleSalePriceMax', 'integer', 4, 
         array('type' => 'integer', 'length' => 4, 'notnull' => true));
        $this->hasColumn('VehicleSalePriceIsNegotiable', 'integer', 1, 
         array('type' => 'integer', 'length' => 1, 'default' => '0', 'notnull' => true));
        $this->hasColumn('Note', 'string', null, array('type' => 'string'));
        $this->hasColumn('OwnerCity', 'string', 255, 
         array('type' => 'string', 'length' => 255, 'notnull' => true));
        $this->hasColumn('OwnerCountryID', 'integer', 4, 
         array('type' => 'integer', 'length' => 4, 'notnull' => true));
        $this->hasColumn('DisplayStatus', 'integer', 1, 
         array('type' => 'integer', 'length' => 1, 'notnull' => true));
        $this->hasColumn('DisplayUntilDate', 'date', null, 
         array('type' => 'date'));
    }

}
?>

上記の BaseListing クラスを継承する /tmp/models/Listing.php の Listing クラスは、現在以下のようになっています。

<?php

/**
 * Listing
 * 
 * This class has been auto-generated by the Doctrine ORM Framework
 * 
 * @package    ##PACKAGE##
 * @subpackage ##SUBPACKAGE##
 * @author     ##NAME## <##EMAIL##>
 * @version    SVN: $Id: Builder.php 5441 2009-01-30 22:58:43Z jwage $
 */
class Listing extends BaseListing
{

}
?>

上記の子クラスは今のところ空なので、カスタム・メソッドや必要なプロパティーを追加するには、うってつけの場所です。子クラスは、モデル間の関係を定義する場所にもなります。このタスクは手動で行わなければなりません。一例として、上記の空の Listing クラスを以下のコード (リスト 9) で更新してください。

リスト 9. 継承による Listing モデル
<?php
class Listing extends BaseListing
{
    public function setUp()
    {
        $this->hasOne('Manufacturer', array(
                'local' => 'VehicleManufacturerID',
                'foreign' => 'ManufacturerID'
            )
        );
        $this->hasOne('Country', array(
                'local' => 'OwnerCountryID',
                'foreign' => 'CountryID'
            )
        );
    }
} 
?>

リスト 9 のコードは、あらゆる Listing が 1 つの Manufacturer と 1 つの Country を持つように指定しています。

以下のように、Manufacturer モデルと Country モデルに逆の関係を指定することもできます (リスト 10 およびリスト 11)。

リスト 10. 継承による Country モデル
<?php
class Country extends BaseCountry
{
    public function setUp()
    {
        $this->hasMany('Listing', array(
                'local' => 'CountryID',
                'foreign' => 'OwnerCountryID'
            )
        );
    }
}
?>
リスト 11. 継承による Manufacturer モデル
<?php
class Manufacturer extends BaseManufacturer
{
    public function setUp()
    {
        $this->hasMany('Listing', array(
                'local' => 'ManufacturerID',
                'foreign' => 'VehicleManufacturerID'
            )
        );
    }
}
?>

Doctrine モデルの内部構造とモデルの相互関係についての詳細は、「参考文献」に記載されている Doctrine マニュアルへのリンクを参照してください。

生成されたモデルを Agavi アプリケーションに追加するには、アプリケーション・ルートの下にある $WASP_ROOT/app/lib/doctrine という名前のディレクトリーにモデルをコピーします。

shell> cd /usr/local/apache/htdocs/wasp/app/lib
shell> mkdir doctrine
shell> cp /tmp/models/* doctrine/
shell> cp /tmp/models/generated/* doctrine/

このステップが終わる段階では、Doctrine ライブラリーが $WASP_ROOT/libs/doctrine にインストールされ、モデルが $WASP_ROOT/app/lib/doctrine にインストールされた状態になります。

ステップ 4: Doctrine と連動するように Agavi を構成する

仕上げのステップ (実際には複数のステップ) では、Agavi に Doctrine モデルを認識するように指示し、データベース・クエリーに Agavi の Doctrine アダプターを使用するようにアプリケーションを構成します。これには、いくつかのステップが必要になります。

中心となる Doctrine クラスを自動でロードするには、$WASP_ROOT/app/config/autoload.xml を編集して、このクラスのエントリーを追加します (リスト 12 を参照)。

リスト 12. Doctrine を自動でロードするための 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="Doctrine">
     %core.app_dir%/../libs/doctrine/Doctrine.php
    </autoload>
    
  </ae:configuration>
</ae:configurations>

$WASP_ROOT/app/config/settings.xml にあるメインのアプリケーション構成ファイルを編集して、Agavi アプリケーションでのデータベース・サポートを有効にします (リスト 13 を参照)。

リスト 13. データベース・サポートを有効にするための 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/settings/1.0">
  <ae:configuration>
    ...
    <settings>
      <setting name="app_name">WASP</setting>

      <setting name="available">true</setting>
      <setting name="debug">false</setting>

      <setting name="use_database">true</setting>
      <setting name="use_logging">false</setting>
      <setting name="use_security">true</setting>
      <setting name="use_translation">false</setting>
    </settings>

  </ae:configuration>

  ...
</ae:configurations>

$WASP_ROOT/app/config/databases.xml にあるアプリケーションのデータベース構成ファイルを編集し、Doctrine をデフォルト・データベース・アダプターとして構成します。MySQL データベースの DSN を設定し、ステップ 3 でインストールした Doctrine モデルのパス ($WASP_ROOT/app/lib/doctrine) も設定します (リスト 14 を参照)。この設定によって、Agavi は必要に応じて自動的にモデルをロードするようになります。

リスト 14. Doctrine DSN に応じた 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/databases/1.0">
  
  <ae:configuration>
    <databases default="doctrine">
      
      <database name="doctrine" class="AgaviDoctrineDatabase">
        <ae:parameter name="dsn">mysql://wasp:wasp@localhost/wasp</ae:parameter>
        <ae:parameter name="load_models">%core.lib_dir%/doctrine</ae:parameter>
      </database>
      
    </databases>
  </ae:configuration>
  
</ae:configurations>

データベース・レコードの取得

Agavi、Doctrine、MySQL との通信基盤は用意できたので、今度は MySQL データベースから個々のリストを取得して表示する ViewAction の作成に取り掛かります。まず、以下のように listing テーブルに手動でいくつかのサンプル・レコードを入力してください。こうしておくと、アクションをその開発初期段階で簡単にテストできるようになります。

mysql> INSERT INTO listing (RecordID, RecordDate, OwnerName, OwnerTel, 
      OwnerEmail, VehicleManufacturerID, VehicleModel, VehicleYear, VehicleColor, 
      VehicleMileage, VehicleIsFirstOwned, VehicleAccessoryBit, VehicleIsCertified, 
      VehicleCertificationDate, VehicleSalePriceMin, VehicleSalePriceMax, 
      VehicleSalePriceIsNegotiable, Note, OwnerCity, OwnerCountryID, DisplayStatus, 
      DisplayUntilDate) VALUES  (1, '2009-06-08', 'John Doe', '00123456789876', 
      'john@wasp.example.com', 2, 'Boxster', 2005, 'Yellow', 15457, 1, 23, 1, 
      '2008-01-01', 35000, 40000, 1, 'Well cared for. In good shape, no scratches 
      or bumps. Has prepaid annual service contract till 2009.', 'London', 2, 
      1, '2009-10-15');
Query OK, 1 row affected (0.05 sec)

mysql> INSERT INTO listing (RecordID, RecordDate, OwnerName, OwnerTel, 
      OwnerEmail, VehicleManufacturerID, VehicleModel, VehicleYear, VehicleColor, 
      VehicleMileage, VehicleIsFirstOwned, VehicleAccessoryBit, VehicleIsCertified, 
      VehicleCertificationDate, VehicleSalePriceMin, VehicleSalePriceMax, 
      VehicleSalePriceIsNegotiable, Note, OwnerCity, OwnerCountryID, DisplayStatus, 
      DisplayUntilDate) VALUES (2, '2009-06-08', 'Jane Doe', '00987654321236', 
      'jane@wasp.example.com', 2, '911 Turbo', 2003, 'Black', 17890, 1, 23, 1, 
      '2008-06-19', 17000, 25000, 1, '', 'Cambridge', 2, 1, '2009-10-15');
Query OK, 1 row affected (0.00 sec)

ここからは、以下の通常のプロセスに従って、WASP に必要な機能を追加していきます。

ステップ 1: プレースホルダー・クラスを作成する

自動車のリストは WASP アプリケーションとは機能的に独立したコンポーネントと考えることができるので、このコンポーネントに関連するアクションとビューはそれぞれ別個のモジュールに配置します。Agavi ビルド・スクリプトを起動して、以下の新規モジュールを作成します。

shell> agavi module-create
...
Module name: Listing

ここで、個々のリストの表示を処理する新しい DisplayAction を追加します。このアクションを DisplayErrorView ビューと DisplaySuccessView ビューの 2 つに関連付けるには、プロンプトに応じて以下の値を指定します。

shell> agavi action-wizard
...
Module name: Listing
Action name: Display
Space-separated list of views to create for Display [Success]: Error Success

これで、Agavi は必要なクラス・ファイルを生成して正しい場所に配置してくれるようになります。

ステップ 2: ルートを定義する

リスト 15 に従って、新しく作成したアクションを参照する新規ルートを追加します。

リスト 15. Listing/DisplayAction ルートの定義
<?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>
      ...                  
      <!-- action for listing pages "/listing" -->
      <route name="listing" pattern="^/listing" module="Listing">
        <route name=".display" pattern="^/display/(id:\d+)$" action="Display" />
      </route>
      
    </routes>
  </ae:configuration>
</ae:configurations>

ルート定義でキャプチャー・グループが使用されていることからわかるように、このルート定義は URL GET リクエストの一部として id 変数が組み込まれていることを期待します。この変数は、MySQL データベースの listing.RecordID 主キー・フィールドに対応した自動車のリストの一意の ID を表す変数です。この変数を DisplayAction のバリデーターに追加する必要があることを忘れないでください。そうしないと、変数が Agavi の入力検証フィルターを通過できないからです。

これは、ネストされたルートを読者が目にする初めての例でもあります。ネストされたルート定義では、外側のルートによって突き合わせられたパターンを内側のルートが継承するため、内側のルートはこのパターンを修正すなわち、このパターンにさらにルートを追加することもできます。この機能は、CRUD 機能を実装するときに極めて役に立ちます。CRUD では通常、以下のように URL のベースは同じながらも、サフィックスが異なるためです。

/object/display/23
/object/add
/object/edit/23
/object/delete/23

上記のルート定義を使用すると、最初に外側のルートによってパターン /listing が含まれる URL の突き合わせが行われます。続いて Agavi がパターンの残りの部分を調べ、そこに何が含まれているかによって、どの子ルートが最も一致しているかを決定し、そのルートのアクションにリクエストを転送します。上記の定義には今のところ 1 つの子ルートしか含まれていませんが、もう少し辛抱してください。この後すぐに、子ルートを追加することになります。

ステップ 3: 検証ルールを定義する

DisplayAction に渡される入力変数は 1 つだけなので、検証するのは至って簡単で、必要となるのは AgaviNumberValidator だけです (リスト 16 を参照)。

リスト 16. Listing/DisplayAction バリデーター
<?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%/Listing/config/validators.xml"
>
  <ae:configuration>
    
    <validators method="read">
      <validator class="number">
        <arguments>
          <argument>id</argument>
        </arguments>
        <ae:parameters>
          <ae:parameter name="type">int</ae:parameter>
          <ae:parameter name="required">true</ae:parameter>
          <ae:parameter name="min">1</ae:parameter>
        </ae:parameters>
      </validator>    
    </validators>
        
  </ae:configuration>
</ae:configurations>

ステップ 4: アクションのコードを作成する

これでルーティングと検証には対処できたので、次のステップでは DisplayAction のビューを指定します。DisplayAction が処理するのは GET リクエストだけなので、生成されるビューの名前で executeRead() メソッドを指定する必要があります。このアクションは、リスト 17 のようになります。

リスト 17. Listing/DisplayAction の定義
<?php
class Listing_DisplayAction extends WASPListingBaseAction
{

  public function getDefaultViewName()
  {
    return 'Success';
  }
  
  public function executeRead(AgaviRequestDataHolder $rd)
  {
    return 'Success';
  }
}
?>

ステップ 5: ビューのコードを作成する

いよいよ、このセクションの核心部分に入ります。核心部分とはつまり、自動車のリストを個々に表示する DisplaySuccessView をセットアップすることです。このビューは、リスト 18 のようになります。

リスト 18. Listing/DisplaySuccessView の定義
<?php
class Listing_DisplaySuccessView extends WASPListingBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    $this->setupHtml($rd);
    $this->setAttribute('_title', 'View Listing');
    $id = $rd->getParameter('id');
    $q = Doctrine_Query::create()
          ->from('Listing l')
          ->leftJoin('l.Manufacturer m')
          ->leftJoin('l.Country c')
          ->where('l.RecordID = ?', $id);
      $result = $q->fetchArray();
      if (count($result) == 1) {
        $this->setAttribute('listing', $result[0]);
        return 'Success';
      } else {
        return $this->createForwardContainer(
         AgaviConfig::get('actions.error404_module'), 
         AgaviConfig::get('actions.error404_action'));
      }
  }
}
?>

executeHtml() メソッドの最初の数行では、ビュー・テンプレートをセットアップしてから入力変数 $_GET['id'] の値を取得しています。この値が、データベースで一致するリストを検索する Doctrine クエリーに組み込まれます (Doctrine クエリーの構文についての詳細は、「参考文献」に記載した該当するマニュアル・ページへのリンクを参照してください)。クエリーが返す結果が 1 つのレコードだけであれば、その結果がテンプレート変数 $t['listing'] に連想配列として割り当てられます。一致する結果がない場合、あるいは複数の一致が見つかった場合には、ビューはリクエストを自動的にアプリケーションのデフォルト Error404 アクションに転送します。

この時点で、DisplayErrorView をセットアップして (リスト 19 を参照)、入力検証にパスしなかった場合の振る舞いを指定するというのも賢い考えです。

リスト 19. Listing/DisplayErrorView の定義
<?php

class Listing_DisplayErrorView extends WASPListingBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    $this->setupHtml($rd);
    return $this->createForwardContainer(
     AgaviConfig::get('actions.error404_module'), 
     AgaviConfig::get('actions.error404_action'));
  }
}

?>

この振る舞いは至って単純なもので、リクエストを同じくデフォルト Error404 アクションに転送しているにすぎません。

最後に、データベースから取得した情報を実際に表示する DisplaySuccess テンプレートをセットアップします (リスト 20 を参照)。

リスト 20. Listing/DisplaySuccess テンプレート
<h3>FOR SALE: <?php printf('%d %s %s (%s)', 
 $t['listing']['VehicleYear'], $t['listing']['Manufacturer']['ManufacturerName'], 
 ucwords(strtolower($t['listing']['VehicleModel'])), 
 ucwords(strtolower($t['listing']['VehicleColor']))); ?></h3>

  <div id="container">    
    <div id="specs">
      <table cellspacing="5">
        <tr>
          <td class="key">Listing ID: </td>
          <td class="value"><?php echo $t['listing']['RecordID']; ?></td>
        </tr>
        <tr>
          <td class="key">Year of manufacture: </td>
          <td class="value"><?php echo 
           $t['listing']['VehicleYear']; ?></td>
        </tr>
        <tr>
          <td class="key">Color: </td>
          <td class="value"><?php echo 
            $t['listing']['VehicleColor']; ?></td>
        </tr>
        <tr>
          <td class="key">Mileage: </td>
          <td class="value"><?php echo 
           $t['listing']['VehicleMileage']; ?></td>
        </tr>  
        <tr>
          <td class="key">Ownership: </td>
          <td class="value"><?php echo 
           ($t['listing']['VehicleIsFirstOwned'] == 1) ? 'First owner' : 
           'Multiple owners'; ?></td>
        </tr>  
        <tr>
          <td class="key">Certification: </td>
          <td class="value"><?php echo 
           ($t['listing']['VehicleIsCertified'] == 1) ? 'Certified, as of ' 
           . date('d M Y', strtotime($t['listing']['VehicleCertificationDate'])) 
           : 'Not certified'; ?></td>
        </tr>
        <tr>
          <td class="key">Accessories: </td>
          <td class="value">
          <?php echo ($t['listing']['VehicleAccessoryBit'] == 0) ? 
           'None <br/>' : null; ?>
          <?php echo ($t['listing']['VehicleAccessoryBit'] & 1) ? 
           'Power steering <br/>' : null; ?>
          <?php echo ($t['listing']['VehicleAccessoryBit'] & 2) ? 
           'Power windows <br/>' : null; ?>
          <?php echo ($t['listing']['VehicleAccessoryBit'] & 4) ? 
           'Audio system <br/>' : null; ?>
          <?php echo ($t['listing']['VehicleAccessoryBit'] & 8) ? 
           'Video system <br/>' : null; ?>
          <?php echo ($t['listing']['VehicleAccessoryBit'] & 16) ? 
           'Keyless entry system <br/>' : null; ?>
          <?php echo ($t['listing']['VehicleAccessoryBit'] & 32) ? 
           'GPS <br/>' : null; ?>
          <?php echo ($t['listing']['VehicleAccessoryBit'] & 64) ? 
           'Alloy wheels <br/>' : null; ?>
          </td>
        </tr>  
        <tr>
          <td class="key">Location: </td>
          <td class="value"><?php echo 
           $t['listing']['OwnerCity']; ?>,   
          <?php echo $t['listing']['Country']['CountryName']; ?></td>  
        </tr>  
        <tr>
          <td class="key">Sale price: </td>
          <td class="value"> $<?php echo 
           $t['listing']['VehicleSalePriceMin']; ?> - $<?php echo 
           $t['listing']['VehicleSalePriceMax']; ?> <?php echo 
           ($t['listing']['VehicleSalePriceIsNegotiable'] == 1) ? '(negotiable)' 
           : null; ?></td>  
        </tr>  
        <tr>
          <td class="key">Description: </td>
          <td class="value"><?php echo 
           $t['listing']['Note']; ?></td>
        </tr>
      </table>
  </div>
</div>

このテンプレートは、データベース・レコードの各種要素を $t['listing'] 連想配列のキーとして読み取り (DisplaySuccessView での setAttribute() 呼び出しを思い出してください)、この情報を整然としたフォーマットに設定された HTML の表として表示します。実際の動作を見るには、Web ブラウザーを開き、http://wasp.localhost/listing/display/1 または http://wasp.localhost/listing/display/2 にアクセスして、前の手順で MySQL データベースに追加した 2 つのサンプル・レコードにアクセスしてみてください。図 5 のような画面が表示されるはずです。

図 5. 自動車の listing テーブルのサンプル・レコードの表示画面
自動車の listing テーブルのサンプル・レコードの表示画面
自動車の listing テーブルのサンプル・レコードの表示画面

URL に無効な ID を含めた場合、あるいは ID を含めなかった場合には、Agavi がデフォルトの「Page not found」エラー・ページ (図 6 を参照) を表示します。これが、このステップでセットアップした DisplayErrorView の振る舞いです。

図 6. 無効なリスト ID によって生成されたエラー・ページ
無効なリスト ID によって生成されたエラー・ページ
無効なリスト ID によって生成されたエラー・ページ

まとめ

第 2 回目の記事は、以上で終わりです。後半では、Agavi 世界の奥深くへと足を踏み入れ、Web フォームによって送信されたユーザー入力を受け入れて検証する方法を説明し、Agavi の非常に賢いフォーム入力フィルターを紹介しました。さらに、MySQL データベースを作成するとともに、Doctrine ORM を使ってモデルを生成し、その生成されたモデルを使ってデータベースに接続してクエリーを実行する手順をとおして、Agavi アプリケーションでデータベースへのアクセスを有効にする方法を実演しました。

今回の作業によって、サンプル・アプリケーションは以前よりも少し賢くなっています。このアプリケーションはお問い合わせ (Contact Us) フォームを表示し、E メールを送信することも、自動車のリストを MySQL データベースから取得することもできます。しかし、ユーザーが独自のリストをデータベースに直接追加するのに適切なインターフェースはまだありません。この機能については、他のいくつかの機能と併せて連載第 3 回で詳しく説明します。

この記事で実装したすべてのコードについては、「ダウンロード」を参照してください。是非ともコードを入手して、いろいろと試してみてください。実際に新しい機能を追加してみるのもいいかもしれません。何も壊れることはありませんので、いい勉強になるはずです。それでは、次回の記事まで、いろいろ試して楽しんでください!


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


関連トピック


コメント

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=XML, Open source, Web development
ArticleID=424674
ArticleTitle=Agavi による MVC プログラミング入門: 第 2 回 Agavi と Doctrine により、フォームとデータベース・サポートを追加する
publish-date=08252009