ZendFramework入門・その7データベースを扱う・その2 ― 2007年11月13日 04時51分20秒
モーレツに久しぶりになりましたが、ZendFramework入門、久々の更新です。もしこの記事を楽しみにしてくださってる方がいらしたら、申し訳ありませんでした。なんとか仕事のばたばたは多少収束したので、また週1くらいのペースに戻せたらよいなぁと思っています。
前回のおさらい
前回が9/23と、7週間以上も空いているので、ちょっとおさらいを。
前回よりZend_Db+SQLiteを使って、単純な構造の単一テーブルをにURLとタイトルを保存する簡易ブックマークを題材として取り上げています。
ブックマークを格納するテーブル「bookmarks」の構造は以下のように定義しました。
キー | カラム | 型 | 備考 |
---|---|---|---|
PK | id | INTEGER | AUTO INCREMENT |
title | VACHAR (100) | ||
IDX1 | url | VARCHAR (255) | |
IDX2 | registDate | TIMESTAMP | |
comment | TEXT |
この「bookmarks」テーブルを、Zend_Db_Adapterで検索、表示するところまでが前回分です。テーブルの作成やファイル構成などは前回の記事を参照してください。
Zend_Db_Tableを使う
さて、ようやく今回の内容に入ります。
前回分では導入ということもあり、Zend_Db_Adapter(正確にはZend_Db_Adapter_Pdo_Sqlite)でデータベースへの接続を開き、そのまま原始的にクエリを発行して連想配列を取得するという、最低限の使い方のみでしたが、今回の題材のように単純なテーブルを対象にする場合は、Zend_Db_Tableを使用して、オブジェクトとして扱ったほうがラクに扱えます。
Zend_Db_Tableは抽象クラスなので、自分で使用する場合はここから派生させたクラスを作成する必要があります。また、Zend_Db_Tableは、基本的にクラスの定義が実際のテーブルとのマッピングになるようなイメージで使用することになります。具体的なコードを示してみましょう。
<?php require_once 'Zend/Db/Table.php'; class Bookmarks extends Zend_Db_Table { protected $_name = 'bookmarks'; protected $_primary = 'id'; }
- 「$_name」プロテクトフィールドに実際のテーブル名を設定
- 「$_primary」プロテクトフィールドに対象のテーブルのプライマリキーの名前を設定
また、dara-jは試していませんが、クラス名がテーブル名と厳密に一致する場合は$_nameを省略しても良しなにマッピングしてくれるようです。
自動インクリメントやシーケンス
MySQLやSQLiteはプライマリキーに自動インクリメントを設定できますが、Zend_Db_Tableもこの機能を利用するように設計されています。自動インクリメントに対応するにはプロテクトフィールド「$_sequence」にtrueを設定します。
といっても、$_sequenceはデフォルト値がtrueなため、特に指定しないとプライマリキーを自動インクリメント列と見なされるため、自動インクリメント機能を利用する場合は上記のように特に$_sequenceを設定する必要がありません。
また、OracleやPostgreSQLなどは、自動的にプライマリキーに値を割り当てるのにシーケンスを利用できますが、$_sequenceはこの機能にも対応しています。使い方は簡単で、$_sequenceにシーケンス名を割り当てるだけです。例えばここで扱っているbookmarksのid用にシーケンス「seq_bookmark_id」を定義している場合は
protected $_sequnce = 'seq_bookmark_id';のように定義するだけです。
使用するDBMSが自動インクリメントやシーケンスに対応していない場合や、アプリケーションからプライマリキー値を設定するような設計にする場合は、明示的に$_sequenceにfalseを設定し、データ挿入時に明示的に値を設定する必要がありますので注意してください。
アプリケーションの仕様
さて、このBookmarksクラスを使用して、実際にデータを追加・更新・削除できる機能を追加していきましょう。
やり方はいろいろあるかと思いますが、今回は単純な設計ですので、以下のような仕様にします。
- コントローラはIndexControllerのみ使用
- indexActionで全データをリスト表示
- addActionで新しいデータを作成
- editActionで既存データを変更
- deleteActionで既存データを削除
また、addActionとeditActionは初期データの有無を除けばだいたい同じになるので、addActionとeditActionでビュースクリプトを共用することにします。
IndexController
いきなりコードです。
<?php require_once 'Zend/Controller/Action.php'; // Zend_Dbをrequre require_once 'Zend/Db.php'; // Bookmarks.phpをrequire require_once './application/models/Bookmarks.php'; // IndexController class IndexController extends Zend_Controller_Action { // Zend_Db_Adapter private $_db; // initメソッド。初期化処理用のフックメソッド // アクセスレベルが「public」な点に注意 public function init() { // リクエストオブジェクト(Zend_Controller_Request_Abstract)を取得 $request = $this->getRequest(); // ベースURLを取得 $baseUrl = getApplicationUrl( $request ); // ビューへ割り当てる $this->view->assign( 'baseUrl', $baseUrl ); // Zend_Db_Adapterを生成 $this->_db = Zend_Db::factory( // Zend_Db_Adapter_Pdo_Sqlite クラスを使用する "Pdo_Sqlite", // 生成パラメータ。SQLiteは接続先を示す'dbname'のみでOK array( 'dbname' => 'zdb1.db' ) ); } // indexアクション public function indexAction() { // SELECTステートメントを実行 $result = $this->_db->fetchAll( 'SELECT * FROM bookmarks ORDER BY registDate DESC' ); // 実行結果をビューへ割り当てる $this->view->assign( 'rows', $result ); } // editアクション // 既存ブックマークの編集を行う public function editAction() { // Bookmarksを生成 $bookmarks = new Bookmarks( array( 'db' => $this->_db ) ); // リクエストからパラメータ'id'を取得 // (リクエスト中に含まれない場合は-1とする) $id = $this->getRequest()->getParam( 'id', -1 ); $this->view->assign( 'bookmark', $bookmarks->find( $id )->current() ); // edit.phtmlをaddActionと共用するので、呼び出し元を明示しておく $this->view->assign( 'mode', 'edit' ); } // addアクション // ブックマークの新規追加を行う public function addAction() { $this->view->assign( 'mode', 'add' ); // ビュースクリプトはeditActionと同じ'edit.phtml' $this->_helper->viewRenderer( 'edit' ); } // saveアクション // editAction/addActionから呼び出され、編集されたデータをDBへ保存する public function saveAction() { $bookmarks = new Bookmarks( array( 'db' => $this->_db ) ); $req = $this->getRequest(); $mode = $req->getParam( 'mode', 'add' ); $row = $mode == 'edit' ? $bookmarks->find( $req->getParam( 'id', -1 ) )->current() : $bookmarks->fetchNew(); // データ追加時はregistDateに現在日時を設定 if( $mode == 'add' ) { $row->registDate = date('Y-m-d H:i:s'); } // id と registDate 以外のカラム名でループ処理 foreach( array( 'title', 'url', 'comment' ) as $key ) { $row->{$key} = $req->getParam( $key ); } // saveメソッドでDBへ反映 $row->save(); // index/indexへリダイレクト $this->_redirect( 'index/index' ); } // deleteアクション // 指定ブックマークを削除する public function deleteAction() { $bookmarks = new Bookmarks( array( 'db' => $this->_db ) ); $row = $bookmarks->find( $this->getRequest()->getParam('id', -1) )->current(); if( $row != null ) { $row->delete(); } $this->_redirect( 'index/index' ); } }
定義部分
先ほど作成したBookmarksクラスを定義する「Bookmarks.php」をrequireしています。配置はパスが通ればどこでもいいんですが(ってわけでもないでしょうけど)、application下にmodelsを切って、そこに配置することにしました。このあたりは別にZend Frameworkが何かしてくれるわけではないので、他の場所でも問題ありません。
また、Zend_Db_Adapterを格納する変数を前回はindexAction内のローカル変数にしていましたが、今回は各アクションメソッドから共通で利用するため、プライベートフィールド「$_db」に格納することにしました。
コンストラクタ
大筋は前回と変わりませんが、$_db(=Zend_Db_Adapter)の初期化をここで行っています。
indexAction
Zend_Db_Adapterの初期化コードが__constructに移動した以外は前回と変わりません。
editAction
さて、ここからが今回追加分のコードになります。まずは既存データの編集アクション「editAction」です。
まず、Bookmarksクラスを初期化していますが、コンストラクタパラメータとしてZend_Db_Adapterを渡しています。Zend_Db_Tableのコンストラクタパラメータは連想配列を渡す仕様になっており、ここではキー「db」にZend_Db_Adapterを設定して使用しています。その他のキーについてはAPIドキュメントとZend_Db_Table自身のソースコードが一番参考になると思います。
次に、Zend_Controller_Request::getParam()で編集するブックマークのidを受け取っています。第二引数に「-1」を指定しているのは、リクエストに「id」が含まれていなかった場合はデフォルト値として「-1」を使用するためです。こうすることでリクエストのnullチェックなどをしないでもデフォルトの動作を決めることができるので、dara-jは比較的よく使うパターンです。
Zend_Db_Table::find()
その次にいきなりZend_Viewへ「bookmark」をassignしていますが、割り当てる値を取得しているのが、Zend_Db_Table::find()メソッドになります。Zend_Db_Table::find()は、プライマリキーを指定して一致するデータをZend_Db_Table_Rowsetで取得するためのメソッドです。
Zend_Db_Table_Rowset
Zend_Db_Table_Rowsetはその名の通り「行セット」を表すクラスです。これ自身はたいした機能を提供しておらず、問い合わせに対して実際の行データであるZend_Db_Table_Rowを0行以上返すために使用されます。 Zend_Db_Table_RowsetはIteratorインターフェイスを実装しているため、foreach構文でループ処理が可能です。また、Countableインターフェイスも実装しているため、count()メソッドで、結果の行数を取得することもできます。
ちょっとわき道にそれますが、findメソッドがZend_Db_Table_Rowsetを返す理由を説明しておきます。
先ほど、findメソッドは「プライマリキーを指定して一致するデータを取得する」と説明しましたが、疑問に思われた方もいらっしゃるかも知れません。テーブル内でユニークなプライマリキーで検索をするのに、なぜZend_Db_Table_Rowを直接返さず、不要とも思われる複数行対応のRowsetを返すのでしょうか?と。
答えは非常に単純で、「find()の引数に複数のプライマリキーをarrayで渡すことができ、複数行を取得できる」からです。また、foreachが可能であることから、引数を単体で指定しようがarrayを渡そうが、取得できた行に対してループ処理を行えば、余計なnullチェックもしなくて済むでしょう。戻りが0行なら、foreach内のブロックが実行されないだけですので。
さて、本題に戻ります。ビューへassignしているのは取得したZend_Db_Table_Rowsetのcurrent()メソッドの戻り値です。
current()メソッドは名前から想像がつくでしょうが、Rowset中の「現在行」を取得するためのメソッドで、戻り型はZend_Db_Table_Rowになります。
ここでのコードはfindに単独の値を指定しているため、戻りのRowsetは0行または1行のRowを含んでいるはずです。もし指定したプライマリキーに一致するデータがない場合、current()メソッドはnullを返します。 Zend_Db_Table_Rowについては後ほど説明します。
そして、editActionの最後です。キー「mode」に値「edit」を設定しているのは、先ほど触れたとおりaddActionとビュースクリプトを共有するため、どちらのアクションメソッドからビューを実行しているかの識別のために設定しています。
addAction
こちらはきわめて単純で、editActionの最後の部分と同じようにキー「mode」をZend_Viewへassignしています。これはaddActionを示すために「add」という値を割り当てています。
そしてここで終わると「add.phtml」を探しにいってしまうので、ViewRendererアクションヘルパーを呼び出して「edit.phtml」でレンダリングするように指示しています。ViewRendererアクションヘルパーについては「7.8.4. 組み込みのアクションヘルパー」のViewRendererの部分を参照してください。
saveAction
先にビュースクリプトのコードの後に説明したほうがいいかとも思ったのですが、ちょっと解説が長くなっているのでソースから遠くならないうちに先に説明します。
まず、このアクションメソッド内で扱っている、Zend_Db_Table_Rowの簡単な説明から。
このクラスは名前の通り、データベースの行オブジェクトを表現するクラスで、テーブルのカラム名に一致するプロパティを実装しています(実際にはマジックメソッドですが)。そして、そのプロパティの読み書きを行うことでデータを操作します。
例えばカラム「comment」に値を設定するには、
// $tableがBookmarksのインスタンス、$commentがコメントデータの文字列とします $row = $table->find( $id )->current(); $row->comment = $comment; // save()メソッドでデータベースへ書き込みを行います $row->save();といった具合に使用します。
さて、メソッド全体の説明です。このアクションメソッドはeditAction、addActionがレンダリングしたフォーム(=edit.phtml)のPOST先になります。
edit.phtmlのフォームは、テーブルのカラム名に一致するフィールドを定義しておきます(後ほどコードを掲載しますが)。
Bookmarksを初期化し、getRequest()->getParam('mode')でモードを取得するところまでは問題ないと思います。その次の
$row = $mode == 'edit' ?...の部分は、editActionとaddActionでZend_Db_Table_Rowの取得方法を変えています。
$modeが'edit'(すなわちeditActionから遷移してきた)場合はeditActionと同様にfind()メソッドで既存のデータを取得してますが、addActionからの遷移の場合はfetchNew()で新しい空の行オブジェクトを取得しています。
その直後の「$row->registDate」は先ほど述べたとおり、「registData」列に日付データをセットしています。
その次のforeachループはちょっとわかりづらいかも知れませんが、値を設定したいプロパティ名(=フォームフィールド名にも一致)をループ処理して、値を割り当てています。
そしてsave()メソッドでデータベースへ更新を反映させた後にindex/indexへリダイレクトしています。
deleteAction
ようやく最後のアクションメソッドです。データを取得するところまでは問題ないでしょう。
取得したZend_Db_Table_Rowのdelete()メソッドでデータを削除しています。delete()後はsave()は不要ですのでお間違えないように。
削除が完了したら、index/indexへリダイレクトします。
ビュースクリプト
ビュースクリプトのソースを以下に示します。ここでは特に解説しませんが、特に難しいことはしていないので、皆さんで確認してみてください。
<html> <head> <title>zdb1</title> <!-- base要素の出力。末尾の「/」に注意 --> <base href="<?php echo $this->baseUrl; ?>/"></base> </head> <body> <h3> bookmarksの内容 </h3> <hr> <!-- 新規追加へのリンク --> <a href="index/add">新規追加</a> <!-- テーブルで表示 --> <table border="1" cellpadding="0" cellspacing="0"> <?php foreach( $this->rows as $i => $row ) { if( $i == 0 ) { // 最初の行のみ ?> <tr> <?php // ヘッダ行出力 foreach( $row as $key => $value ) { echo '<th>' . $this->escape( $key ) . '</th>'; } echo '<th>操作</th>'; ?> </tr> <?php // if 終わり } ?> <tr> <?php // 行出力 foreach( $row as $key => $value ) { echo '<td>' . $this->escape( $value ) . '</td>'; } echo "<td><a href=\"index/edit/id/{$row['id']}\">編集</a><br><a href=\"index/delete/id/{$row['id']}\">削除</a></td>"; ?> </tr> <?php // foreach終わり } ?> </table> </body> </html>
<?php $title = $this->mode == 'edit' ? "id {$this->bookmark->id} の編集" : "新規作成"; ?> <html> <head> <title><?php echo $this->escape( $title ); ?></title> <!-- base要素の出力。末尾の「/」に注意 --> <base href="<?php echo $this->baseUrl; ?>/"></base> </head> <body> <h3><?php echo $this->escape( $title ); ?></h3> <hr> <form action="index/save" method="post"> <input type="hidden" name="mode" value="<?php echo $this->mode; ?>"> <input type="hidden" name="id" value="<?php echo $this->bookmark->id; ?>"> <dl> <?php foreach( array( 'id', 'title', 'url', 'registDate', 'comment' ) as $key ) { ?> <dt><?php echo $this->escape( $key ); ?></dt> <dd><?php if( $key != 'id' && $key != 'registDate' ) { echo "<input type=\"text\" name=\"{$key}\" value=\"" . $this->escape( $this->bookmark->{$key} ) . "\">"; } else { echo $this->escape( $this->bookmark->{$key} ); } ?></dd> <?php } ?> </dl> <hr> <button type="submit">保存</button><button type="reset">リセット</button> <a href="index/index">戻る</a> </form> </body>
ファイル構成
今回のアプリケーションのファイル構成を示します。
- htdocs/
- zdb1/
- application/
- controllers/
- IndexController.php
- models/
- Bookmarks.php
- views/
- index/
- edit.phtml
- index.phtml
- index/
- controllers/
- index.php
- .htaccess
- zdb1.db
- create_table.sql
- application/
- zdb1/
あとがき
えー、本当に久しぶりの更新になってしまいましたが、いかがでしたでしょうか?コードそのものはたいした量ではないのですが、説明することが結構増えてきたので、そろそろ全コードを掲載するのがちょっとしんどくなってきました。
次回はZend_Db関連をもう少し突っ込んでみようかと思います。Zend_Db_Tableのリレーション機能か、Zend_Select・Zend_Statementか、といったあたりでしょうか。思いつきで変更するかもしれませんが、その場合はご容赦ください。(^^;
それでは、次回はなるべく早く更新したいと思いますので、よろしくお願いいたします。
ZendFramework入門・その6データベースを扱う・その1 ― 2007年09月23日 15時09分51秒
はじめに
またも1週間のご無沙汰になりましたが、前回の予告どおり、データベース機能の説明に入ります。
DBMSに関しては、前回に予告したとおりSQLiteを対象としますので、先にこちらの番外編を参考にして、コンソールアプリケーションを導入しておくとよいでしょう。
また、この記事はあくまでZend Framework入門ですので、SQLに関する細かい説明などはあまりしない予定です(まぁ、シンプルな使い方しかしない予定ですが)。データベースになれていない方は、他のSQL入門記事なども参考にされるとよいと思います。
それでは本文へまいりましょう。
データベース関連クラス
Zend Frameworkのデータベース機能はZend_Db名前空間に集約されていて、以下のようなクラス群で構成されています。
クラス名 | 役割 |
---|---|
Zend_Db_Adapter | DBアダプタ。データベースへの接続やZend_Db_Statement・Zend_Db_Selectのファクトリとして機能する、Zend_Dbの主要クラス。 すべてのDBアダプタは抽象クラス「Zend_Db_Adapter_Abstract」から派生し、DBMSごとの機能の違いを吸収する |
Zend_Db_Statement | DBMSへの問い合わせを行うステートメントオブジェクト。Zend_Db_Adapterと同様に、基本の抽象クラスから派生した固有のクラスがDBMS間の違いを吸収する。主にZend_Db_Adapter::query()をファクトリとしてインスタンスを取得する |
Zend_Db_Select | オブジェクト操作によるSELECTステートメントを組み立てるためのビルダクラスで、__toString()による文字列化でSQLステートメントを出力する |
Zend_Db_Table | データベースの表(Table)を表現する抽象クラスで、Zend_DbにおけるO/Rマッピングを実現する。基本的にはDBMS上のテーブルと対になるクラスをZend_Db_Tableから派生させる。データベースへの問い合わせや更新もこのクラス(および関連のRow/Rowset)から行えるが、扱い方がZend_Db_AdapterやZend_Db_Statementと異なる点に注意 |
Zend_Db_Table_Row | データベースの行(Row)を表現する抽象クラス。Zend_Db_Tableの操作により生成される。データベースの列(Column)に相当するプロパティを備えており、プロパティ値の変更 → save()メソッドの実行 で行を更新することができる。 |
Zend_Db_Table_Rowset | 複数のZend_Db_Rowを扱うためのIteratorクラスで、Zend_Db_Tableによる問い合わせの結果として取得する。 |
アプリケーション構成
今回は「zdb1」という名前のアプリケーションを作成しましょう。構成はシンプルに、以下のようにします。
- htdocs/
- zdb1/
- application/
- controllers/
- IndexController.php
- views/
- index/
- index.phtml
- index/
- controllers/
- index.php
- .htaccess
- zdb1.db
- create_table.sql
- application/
- zdb1/
肝心のアプリケーションですが、ものすごくシンプルなブックマークにしてみます。以下の情報を扱うことにします。
- タイトル
- URL
- 登録日時
- コメント
キー | カラム | 型 | 備考 |
---|---|---|---|
PK | id | INTEGER | AUTO INCREMENT |
title | VACHAR (100) | ||
IDX1 | url | VARCHAR (255) | |
IDX2 | registDate | TIMESTAMP | |
comment | TEXT |
データベースを作成する
まずはテーブルを作成する「create_table.sql」です。テーブル作成とインデックスの作成、初期データの挿入をしています。
CREATE TABLE bookmarks ( id INTEGER PRIMARY KEY, title VARCHAR (100), url VARCHAR(255), registDate TIMESTAMP, comment TEXT ); CREATE INDEX idx_bookmarks_1 ON bookmarks(url); CREATE INDEX idx_bookmarks_2 ON bookmarks(registDate); INSERT INTO bookmarks ( title, url, registDate, comment ) VALUES ( 'dara-j', 'http://dara-j.asablo.jp/blog/', ( select datetime('now') ), 'だらだらとしたブログ' ); INSERT INTO bookmarks ( title, url, registDate, comment ) VALUES ( 'dara-clip', 'http://dara-j.tumblr.com/', ( select datetime('now') ), 'dara-jのtumblr。ぬこ分多目。' );
作成したcreate_table.sqlをsqlite3で以下の要領で実行する手順です。 まずは今回のアプリケーションディレクトリからコマンドプロンプトを起動します。この例では D:\wwwroot\zdb1 がそれにあたります。そして、そこからsqlite3を実行します。
D:\wwwroot\zdb1>sqlite3 zdb1.db SQLite version 3.4.2 Enter ".help" for instructions sqlite>次に、「.read」コマンドで、先ほど作成したcreate_table.sqlを実行します。
sqlite> .read create_table.sql
sqlite>
.readコマンドはエラーがなければなにも表示しないので、sqlite_master(メタデータテーブル)を確認してみます。
sqlite> select * from sqlite_master; table|bookmarks|bookmarks|2|CREATE TABLE bookmarks ( id INTEGER PRIMARY KEY, title VARCHAR (100), url VARCHAR(255), registDate TIMESTAMP, comment TEXT ) index|idx_bookmarks_1|bookmarks|3|CREATE INDEX idx_bookmarks_1 ON bookmarks(url) index|idx_bookmarks_2|bookmarks|4|CREATE INDEX idx_bookmarks_2 ON bookmarks(regi stDate) sqlite>テーブル・2つのインデックスとも定義されていればOKです。ついでに同時に実行したinsertが正常にできているかを、以下のように確認します。
sqlite> select * from bookmarks; 1|dara-j|http://dara-j.asablo.jp/blog/|2007-09-23 05:01:25|だらだらとしたブログ 2|dara-clip|http://dara-j.tumblr.com/|2007-09-23 05:01:25|dara-jのtumblr。ぬこ分 多目。 sqlite>datetime()関数は、CURRENT_TIMESTAMPと同様にロケールを考慮しないので、GMTでデータが挿入されてしまいますが、これは初期データなのでまぁ気にしないでください(^^; 確認できたらsqlite3を終了しましょう。
sqlite> .quit
データベースへ接続の接続と基本的な問い合わせ
今度はPHP側のコードを作成していきましょう。まず起動スクリプト「index.php」ですが、これは「ZendFramework入門・その2 アクションメソッドの追加とリンクの扱い方」で決定したものをそのまま流用できます(つまり、zf2のもの)。
IndexControllerは次のようになります。
<?php require_once 'Zend/Controller/Action.php'; // Zend_Dbをrequre require_once 'Zend/Db.php'; // IndexController class IndexController extends Zend_Controller_Action { // initメソッド。初期化処理用のフックメソッド // アクセスレベルが「public」な点に注意 public function init() { // リクエストオブジェクト(Zend_Controller_Request_Abstract)を取得 $request = $this->getRequest(); // ベースURLを取得 $baseUrl = getApplicationUrl( $request ); // ビューへ割り当てる $this->view->assign( 'baseUrl', $baseUrl ); } // indexアクション public function indexAction() { // Zend_Db_Adapterを生成 $db = Zend_Db::factory( // Zend_Db_Adapter_Pdo_Sqlite クラスを使用する "Pdo_Sqlite", // 生成パラメータ。SQLiteは接続先を示す'dbname'のみでOK array( 'dbname' => 'zdb1.db' ) ); // SELECTステートメントを実行 $result = $db->fetchAll( 'SELECT * FROM bookmarks ORDER BY registDate DESC' ); // 実行結果をビューへ割り当てる $this->view->assign( 'rows', $result ); } }
- Zend_Dbを使用するために、'Zend/Db.php'をrequireしている
- Zend_Db_Adapterを取得するためにZend_Db::factoryスタティックメソッドを使用している
- Zend_Db_Adapter::fetchAll()メソッドでSQLステートメントを実行している
クラス名は暗黙で「Zend_Db_Adapter_」プレフィックスがつけられますので、今回の例の「PdoSqlite」はZend_Db_Adapter_Pdo_Sqlite」を作成することを意味しています。
第二引数の初期化パラメータ配列ですが、SQLiteの場合は他のオプションがほとんどないのでキー'dbname'のみを指定していますが、他のDBMSの場合は「username」「password」などがあります。たとえばローカルで動作しているMySQLのスキーマ「mydb」に接続する場合は以下のようになります。
$db = Zend_Db::factory( "Pdo_MySql", array( 'host' => 'localhost', // 接続するMySQLサーバのホスト名 'dbname' => 'mydb', // 接続するデータベース(スキーマ) 'username' => 'mysqluser', // 接続するユーザID 'password' => 'passwd' // 接続パスワード ) );
そして、index.phtmlです。まずは、Zend_Db_Adapter::fetchAll()が何を返すのかを単純に確認するため、以下のようにvar_dump()を使ったシンプルな実装をしてみましょう。(手抜き、ともいう)
<html> <head> <title>zdb1</title> <!-- base要素の出力。末尾の「/」に注意 --> <base href="<?php echo $this->baseUrl; ?>/"></base> </head> <body> <h3> bookmarksの内容 </h3> <hr> <!-- var_dumpしてみる --> <pre> <?php var_dump( $this->rows ); ?> </pre> </body> </html>
bookmarksの内容
array(2) { [0]=> array(5) { ["id"]=> string(1) "2" ["title"]=> string(9) "dara-clip" ["url"]=> string(25) "http://dara-j.tumblr.com/" ["registDate"]=> string(19) "2007-09-23 05:01:25" ["comment"]=> string(28) "dara-jのtumblr。ぬこ分多目。" } [1]=> array(5) { ["id"]=> string(1) "1" ["title"]=> string(6) "dara-j" ["url"]=> string(29) "http://dara-j.asablo.jp/blog/" ["registDate"]=> string(19) "2007-09-23 05:01:25" ["comment"]=> string(20) "だらだらとしたブログ" } }
これを単純なテーブル表示にするには、index.phtmlを以下のように変更します。
<html> <head> <title>zdb1</title> <!-- base要素の出力。末尾の「/」に注意 --> <base href="<?php echo $this->baseUrl; ?>/"></base> </head> <body> <h3> bookmarksの内容 </h3> <hr> <!-- テーブルで表示 --> <table border="1" cellpadding="0" cellspacing="0"> <?php foreach( $this->rows as $i => $row ) { if( $i == 0 ) { // 最初の行のみ ?> <tr> <?php // ヘッダ行出力 foreach( $row as $key => $value ) { echo '<th>' . $this->escape( $key ) . '</th>'; } ?> </tr> <?php // if 終わり } ?> <tr> <?php // 行出力 foreach( $row as $key => $value ) { echo '<td>' . $this->escape( $value ) . '</td>'; } ?> </tr> <?php // foreach終わり } ?> </table> </body> </html>
実行結果は各自で確認してみてください。
Zend_Db_Adapter::fetchAll
参考までにですが、Zend_Db_Adapter::fetchAllについて少し解説します。Zend_Db_Adapter::fetchAllメソッドは以下のように定義されています。
/** * Fetches all SQL result rows as a sequential array. * Uses the current fetchMode for the adapter. * * @param string|Zend_Db_Select $sql An SQL SELECT statement. * @param mixed $bind Data to bind into SELECT placeholders. * @return array */ public function fetchAll($sql, $bind = array())
そして、肝心の処理内容は、以下のように記述されています。
$stmt = $this->query($sql, $bind); $result = $stmt->fetchAll($this->_fetchMode); return $result;
あとがき&次回予告
ともかくデータベースの内容を表示するところまでやってみましたが、いかがでしたでしょうか?
このままでは面白くもなんともないので、次回はこれにデータを追加する機能を作成してみたいと思います。そのために「Zend_Db_Table」の解説をする予定です。
それではまた次回。
ZendFramework入門・番外編1 SQLiteの導入 ― 2007年09月18日 03時06分07秒
Zend Framework入門でデータベースを扱ううえで、ターゲットのDBMSとしてSQLiteを使うつもりなので、「番外編」として、SQLiteコンソールアプリケーションの導入方法と簡単な使い方を説明しておきます。
ここではWindows環境での手順のみ示します。Linuxユーザの方は、ダウンロードするバイナリやパスを環境に合わせて読み替えてください。
入手
公式サイトのダウンロードページ(http://www.sqlite.org/download.html)から入手できます。
「Precompiled Binaries For Windows」セクションの「sqlite-3_4_2.zip」をダウンロードしてください。
※:2007.9.4に3.5.0がリリースされていますが、これは以前のバージョンと互換性がないとアナウンスされています。当然PHP5に標準添付のバージョンとも互換性がないため、3.4.xを使用するようにしてください。
参考:Moving From SQLite 3.4.2 to 3.5.0
インストール
ダウンロードしたアーカイブを解凍すると、中身は単独の実行可能ファイルが1つあるだけです。これをパスがとおっている場所(たとえばC:\WINDOWS)、または任意の場所に設置してください。任意の場所に設置した場合は、環境変数でパスを通すようにしてください。
動作確認
コマンドプロンプトを起動します。(この例はWindows2000の場合です)
Microsoft Windows 2000 [Version 5.00.2195] (C) Copyright 1985-2000 Microsoft Corp. C:\Documents and Settings\dara-j>この状態でコマンド「sqlite3 -help」を入力します。
Microsoft Windows 2000 [Version 5.00.2195] (C) Copyright 1985-2000 Microsoft Corp. C:\Documents and Settings\dara-j>sqlite3 -help Usage: sqlite3 [OPTIONS] FILENAME [SQL] FILENAME is the name of an SQLite database. A new database is created if the file does not previously exist. OPTIONS include: -init filename read/process named file -echo print commands before execution -[no]header turn headers on or off -bail stop after hitting an error -interactive force interactive I/O -batch force batch I/O -column set output mode to 'column' -csv set output mode to 'csv' -html set output mode to HTML -line set output mode to 'line' -list set output mode to 'list' -separator 'x' set output field separator (|) -nullvalue 'text' set text string for NULL values -version show SQLite version C:\Documents and Settings\dara-j>このように、アプリケーションの説明が表示されればOKです。
コマンドが見つからない場合は環境変数をもう一度見直してください。
※:環境変数を設定したら、コマンドプロンプトを起動しなおしてください。
起動してみる
コマンドプロンプトから「sqlite3」と入力すると以下のように起動して入力待ち状態になります。
C:\Documents and Settings\dara-j>sqlite3 SQLite version 3.4.2 Enter “.help” for instructions sqlite>
sqlite3.exeは、このようにコマンドラインベースの対話型アプリケーションで、このままSQLを発行して使用できます。
SQLを発行する以外にユーティリティコマンドも実装されていて、これらはすべて「.」(ピリオド)から始まります。 例えば、「.help」を入力すると、以下のようになります。
C:\Documents and Settings\dara-j>sqlite3 SQLite version 3.4.2 Enter ".help" for instructions sqlite> .help .bail ON|OFF Stop after hitting an error. Default OFF .databases List names and files of attached databases .dump ?TABLE? … Dump the database in an SQL text format .echo ON|OFF Turn command echo on or off .exit Exit this program .explain ON|OFF Turn output mode suitable for EXPLAIN on or off. .header(s) ON|OFF Turn display of headers on or off .help Show this message .import FILE TABLE Import data from FILE into TABLE .indices TABLE Show names of all indices on TABLE .load FILE ?ENTRY? Load an extension library .mode MODE ?TABLE? Set output mode where MODE is one of: csv Comma-separated values column Left-aligned columns. (See .width) html HTML <table> code insert SQL insert statements for TABLE line One value per line list Values delimited by .separator string tabs Tab-separated values tcl TCL list elements .nullvalue STRING Print STRING in place of NULL values .output FILENAME Send output to FILENAME .output stdout Send output to the screen .prompt MAIN CONTINUE Replace the standard prompts .quit Exit this program .read FILENAME Execute SQL in FILENAME .schema ?TABLE? Show the CREATE statements .separator STRING Change separator used by output mode and .import .show Show the current values for various settings .tables ?PATTERN? List names of tables matching a LIKE pattern .timeout MS Try opening locked tables for MS milliseconds .width NUM NUM … Set column widths for “column” mode sqlite>各ユーティリティコマンドの使い方はこちらなどが参考になるでしょう。
ところで、この状態では実はデータベースに接続していない(開いていない)状態なので、どんなクエリを発行してもまったく無意味です。なので、一旦終了させます。
.timeout MS Try opening locked tables for MS milliseconds .width NUM NUM ... Set column widths for "column" mode sqlite> .quit C:\Documents and Settings\dara-j>これでsqliteを終了しました。
新しいデータベースを開く
先の例では、コマンドライン引数をまったく与えずに起動しました。その結果、データベースを開いていない状態になりました。 今度は新しいデータベースを開いて起動してみることにします。
やり方は至極簡単、コマンドライン引数に存在しないファイル名を与えると、それをデータベースとして開きます。 ここでカレントディレクトリ(C:\Documents and Settings\dara-j)に「test.db」というデータベースを作成して開いてみます。
C:\Documents and Settings\dara-j>sqlite3 test.db SQLite version 3.4.2 Enter “.help” for instructions sqlite>一見先ほどとなにも違っていないようですが、これでデータベースを開いていることになります。
まずはテスト用のテーブルを作成してみます。 ピリオドで始まるユーティリティコマンド以外の入力時は、最後に「;」(セミコロン)がつくまでは入力モードのままなので、普通に複数行入力できます。コマンドを確定させる場合に末尾にセミコロンを打ってからEnterしてください。
C:\Documents and Settings\dara-j>sqlite3 test.db SQLite version 3.4.0 Enter ".help" for instructions sqlite> create table t_test( ...> id INTEGER PRIMARY KEY, ...> name VARCHAR(20), ...> regist TIMESTAMP DEFAULT CURRENT_TIMESTAMP ...> ); sqlite>エラーが出ていなければテーブルが作成されているはずです。テーブル一覧は「.tables」コマンドで確認できます。
sqlite> .tables t_test sqlite>先ほどCREATE TABLEした「t_test」が存在することが確認できました。
なお、上記のCREATE TABLEに関して、以下2点補足です。
- 「id」列を「INTEGER PRIMARY KEY」指定しているので、これは暗黙でAUTO INCREMENT列になります。
- 「regist」列はDEFAULT属性で「CURRENT_TIMESTAMP」を指定しています。
sqlite> INSERT INTO t_test ( name ) VALUES ( 'test' );のみで新しいレコードが挿入されます。
「INTEGER PRIMARY KEY」指定で暗黙のAUTO INCREMENT列にするのは、SQLiteでは定番ですので、ぜひ覚えておいてください。
外部SQLコマンドの実行
サンプルコードは示しませんが、「.read」ユーティリティコマンドで外部のSQLファイルを読み込んで実行できます。
たとえば、先ほどのcreate tableをファイルに記述しておいて、「.read <create tableが記述されたファイル>」といった具合に使います。
関連リソース
最後にSQLite関連の代表的なリソースへのリンクを示しておきます。 Mac OS XやFirefox、PHP5、さらにはGoogle Gearsが採用していることもあり、SQLiteに関する情報は以前に比べてよく見つかるようになっているので、適当に検索をしてもよいと思いますが、とりあえずの参考にはなると思います。
SQL As Understood By SQLite
本家のSQL言語リファレンス。
SQLite が認識できるクエリー言語
上記の翻訳版だが、最終更新日が4年前といささか古い。本家のリファレンスを読むときの参考程度に
sqlite: SQLite データベースと会話するプログラム
本編中でも紹介した、コマンドラインユーティリティの説明ページ
rktSQLite : SQLiteのSQLコマンド一覧
一部未説明はあるが、日本語でのコマンド一覧
メモ: SQLiteにCURRENT_TIMESTAMPがない? - dseg の日記
直接参考にはならないが、SQLite2系でCURRENT_TIMESTAMPをサポートしていないので、同様の機能を実現する方法が書いてあるZendFramework入門・その5 フォームを取り扱う・その3 ― 2007年09月17日 06時02分12秒
お詫びというか訂正というか訂正しないというか
一週間もほったらかした上にいきなりお詫びからです。
前々回、前回に扱った「zf2」のIndexController::dumpActionですが、前々回では「$this->getRequest()->getParams()」としていたところを、前回掲載のソースでは「$this->getRequest()->getPost()」とメソッドが変わっていました。どこかで変更した検証コードをそのまま確かめもせずに掲載してしまったようです。
フォームのメソッドがPOSTなので機能上はほとんど違いがないのですが、なんの断りもなく違うメソッドになっていたので混乱された方もいらっしゃるかもしれませんのでここでお詫びいたします。申し訳ありませんでした。
今回も引き続き「zf2」を流用しますが、あえて「getParams()」に戻す理由もないので、今回も「getPost()」のままにしますのでご了承ください。
入力内容を検査し、動作を変える
さて、今回の内容です。
前回まででフォームの内容をサーバに送信し、その内容を確認するところまで作りました。今回はそれに少しだけ機能を足して、
- 送信する内容を「名前」「性別」に加えて「メールアドレス」を追加する
- 「名前」と「メールアドレス」はどちらも入力を必須とする
- 必須項目が未入力の場合は入力画面に戻り、再度入力させる
- すべて入力されていたら、入力内容確認画面を表示する
この仕様変更に伴い、以下の処理を追加する必要があります。
- 入力内容の検査(空かどうかのみ)
- 検査結果による表示内容の変更
- 入力値をフォームへ反映させる
入力内容をフォームへ反映させる準備
まず、未入力項目があった場合への対応で、入力フォーム(=indexAction+index.phtml)へ、POSTされた内容を反映させる準備をします。
といっても特別なことではなく、前回までのdumpActionで行っていたように、getPost()で取得した連想配列をviewへassignするだけです。
dumpActionでやっていた、「$this->veiw->assign('postData', $this->getRequest()->getPost()」をそのままindexActionの先頭に記述してもいいのですが、どのみち今回の仕様では未入力の有無にかかわらずビューへ入力データを渡す必要があるので、共通の処理としましょう。そう、init()内で行ってしまうのです。
そして、実際のフォームフィールドへの反映ですが、これまた単純で、HTMLフォームの値として、ビュースクリプト内でechoするだけになります。
ここまでのコードを掲載します。まずはIndexController。
<?php require_once 'Zend/Controller/Action.php'; class IndexController extends Zend_Controller_Action { // 初期化処理 public function init() { // リクエストオブジェクトの取得 $request = $this->getRequest(); // BASE要素向けのベースURL $this->view->assign( 'baseUrl', getApplicationUrl( $request ) )->assign( 'postData', array_merge( array( 'name' => '', 'mail' => '', 'sex' => 1 ),$request->getPost() ) ); } // indexアクション public function indexAction() { } // dumpアクション public function dumpAction() { // postDataへの割り当てをinit()に移動したため // ここではなにもしない } public function __call($name, $args) { $this->_forward( 'index' ); } }
また、postDataへ直接getPost()の値を割り当てているのではなく、array_merge()を使用しています。これは、フォーム送信ではなく、直接index/indexを呼び出した場合への対応で、「name」「mail」「sex」の3つのキーが常にpostDataに割り当てられた状態にするための下処理として行っています。
index.phtmlは以下のようになります。
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS"> <base href="<?php echo $this->baseUrl; ?>/"></base> <title>フォーム テスト</title> </head> <body> <h3>入力フォーム</h3> <form action="index/index" method="post"> <ul> <li> <label for="name">名前</label> <input type="text" name="name" id="name" size="20" value="<?php echo $this->escape( $this->postData['name'] ); ?>"> <li> <label for="mail">メールアドレス</label> <input type="text" name="mail" id="mail" size="40" value="<?php echo $this->escape( $this->postData['mail'] ); ?>"> <li> <label for="sex">性別</label> <select name="sex" id="sex" value="<?php echo $this->postData['sex']; ?>"> <option value="1"<?php if( $this->postData['sex'] == 1 ) echo ' selected'; ?>>男性 <option value="2"<?php if( $this->postData['sex'] == 2 ) echo ' selected'; ?>>女性 </select> </ul> <input type="submit" value="送信"> </form> </body> </html>
- フォーム項目「mail」を追加している
- POST先をdumpActionからindexActionに変更
- postDataから値を取得し、value属性に設定している
そして、実際のフォーム項目への値の反映ですが、これは基本的に連想配列の対応する値をvalue属性に出力するだけですが、$this->escape()で出力をエスケープしています。
出力のエスケープ処理
この「$this->escape()」(=Zend_Veiew::escapeメソッド)は、名前のとおり出力のエスケープ処理を行うZend_Viewのメソッドで、今のようにデフォルトのまま扱っている場合は、htmlspecialchars() 関数がその正体となります。
Zend_View::escapeメソッドはZend_View::setEscape()メソッドでのカスタマイズも可能になっていますが、ここでは扱いません。詳細はリファレンスガイドの「35.3.1. 出力のエスケープ」を参照してください。
入力値をチェックする
ここまでのコードで動作させてみてください。たぶん「送信」しても画面が一切変わり映えしない、つまらない結果になるでしょう。ですので、当初の予定どおり、入力値をチェックしてその結果による振り分け処理を実装してみましょう。
チェック処理は以下のような考え方になります。
- indexActionからのPOST先はチェック処理を行うアクションになる
- チェックアクション内で、必須項目に対してempty()で空かどうかの判断を行う
- 2つの必須項目のうち、1つでも空であればもう一度indexActionへ飛ばす
- 必須項目に問題がなければ結果表示を行う
postActionを追加したIndexControllerは以下のようになります。
<?php require_once 'Zend/Controller/Action.php'; class IndexController extends Zend_Controller_Action { // 初期化処理 public function init() { // リクエストオブジェクトの取得 $request = $this->getRequest(); // BASE要素向けのベースURL $this->view->assign( 'baseUrl', getApplicationUrl( $request ) )->assign( 'postData', array_merge( array( 'name' => '', 'mail' => '', 'sex' => 1 ),$request->getPost() ) ); } // indexアクション public function indexAction() { } // postアクション public function postAction() { $postData = $this->getRequest()->getPost(); // 未入力チェック $is_empty = empty( $postData['name'] ) || empty( $postData['mail'] ); if( $is_empty ) { // 未入力があったのでエラーメッセージをセットしてindexActionへforward $this->view->assign( 'errorMessage', '未入力項目があります' ); $this->_forward( 'index' ); return; } // 正常だったのでdumpへforward $this->_forward( 'dump' ); } // dumpアクション public function dumpAction() { } public function __call($name, $args) { $this->_forward( 'index' ); } }
そして、postAction内で'errorMessage'にエラーメッセージを割り当てていますので、これを出力するコードをindex.phtmlに実装する必要があります。
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS"> <base href="<?php echo $this->baseUrl; ?>/"></base> <title>フォーム テスト</title> </head> <body> <?php if( isset( $this->errorMessage ) ) { ?> <div style="color: red"><?php echo $this->escape( $this->errorMessage ); ?></div> <?php } ?> <h3>入力フォーム</h3> <form action="index/post" method="post"> <ul> <li> <label for="name">名前</label> <input type="text" name="name" id="name" size="20" value="<?php echo $this->escape( $this->postData['name'] ); ?>"> <li> <label for="mail">メールアドレス</label> <input type="text" name="mail" id="mail" size="40" value="<?php echo $this->escape( $this->postData['mail'] ); ?>"> <li> <label for="sex">性別</label> <select name="sex" id="sex" value="<?php echo $this->postData['sex']; ?>"> <option value="1"<?php if( $this->postData['sex'] == 1 ) echo ' selected'; ?>>男性 <option value="2"<?php if( $this->postData['sex'] == 2 ) echo ' selected'; ?>>女性 </select> </ul> <input type="submit" value="送信"> </form> </body> </html>
Zend_Controller_Action::_redirect
さて、ここまでで今回の予定の機能はすべて実装できました。
前回の予告で「_redirectについても解説」といっていたのですが、うまい使い道が思いつかなかったので、簡単な解説だけにとどめさせていただきます。
Zend_Controller_Action::_redirectは、_forwardと同様にZend_Controller_Actionのプロテクトメソッドで、目的のURLへリダイレクトするためのメソッドです。メソッドシグニチャは以下のようになっています。
void _redirect(string $url, [ $options = array()])使い方は_forwardに近いのですが、以下のような違いがあります。
- 第一引数はアクションではなくURLを指定する
- _forwardと違い、実際に指定URLへのリダイレクトが発生する(ブラウザの表示URLが実際に変更する)
これは、_forwardはリダイレクトが発生するわけではない(=同じリクエストを処理している)が、_redirectは実際にリダイレクトが発生するためです。
_forwardは同じリクエストを処理しているため、今回のようにPOSTされたデータをinit内でビューに割り当てている限り、_forward前のアクションでも_forward後のアクションでも同じようにデータを扱えますが、_redirectはリダイレクトすることによってリクエストの内容が変化する、ということです。
あとがき&次回予告
さて、1週間も間があいた割には、大して新しい内容が出現しませんでした(^^;が、これで一通りページを作成してユーザの入力を受け取り処理をするための基礎に触れられたと思います。
次回からは、いったんコントローラ関連の処理から離れ、これまたWebアプリケーションでは不可欠ともいえる、データベース関連の機能に移りたいと思います。
DBMSはPHP5で標準的に扱えるため環境にあまり左右されない、SQLiteを使う予定です。ですので、いきなりZend_Db関連をやるのではなく、1回くらいはSQLiteの使い方を解説するかもしれません(しないかもしれません^^;)。
それでは次回を、気長にお待ちください。
ZendFramework入門・その4 フォームを取り扱う・その2 ― 2007年09月10日 04時18分44秒
未実装のアクションを呼び出す
さて、今回は例外処理からはじめたいと思います。とはいってもそう難しいことをやるわけではなく、Zend_Controllerが標準的に用意している仕組みの導入方法を説明するだけですが。
まずは例外を発生させるところからはじめましょう。やりかたは至極簡単、前回作成した「zf2」アプリケーションで、存在しないアクションを呼び出すだけです。 前回までの実装では、IndexControllerに実装したアクションメソッドは「index」アクションと「dump」アクションの2つのみでした。この実装状況のままで、存在しないアクションメソッド、そうたとえば「hoge」を呼び出してみましょう。
http://<host名>/zf2/index/hoge/ へアクセスしてみてください。いきなり「Fatal error」が表示されるはずです(表示されない場合はdisplay_errorsがfalseになっていると思われるので、PHP.iniを見直すか、index.phpでini_setを行うようにしてください)。
エラーハンドラを作成する
次にこの例外をキャッチして、エラーの内容を表示するようにしてみます。やり方は簡単、専用のアクションコントローラ「ErrorController」にアクションメソッド「errorAction()」を実装し、対応するビュースクリプト「error.phtml」をviews/scripts/error/に設置するだけです。
まずはErrorControllerを作成しましょう。内容は以下のように、結構シンプルなものになります。
<?php require_once 'Zend/Controller/Action.php'; class ErrorController extends Zend_Controller_Action { public function errorAction() { // レスポンスオブジェクトから取得した // 例外情報をビューへassignする $this->view->assign( 'errors', $this->getResponse()->getException() ); } }
エラーの内容を表示する
このエラーアクションを表示するerror.phtmlは以下のようにしてみましょう。
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS"> <base href="<?php echo $this->baseUrl; ?>/"></base> <title>フォーム テスト - エラー</title> </head> <body> <h3>以下のエラーが発生しました</h3> <ul> <?php foreach($this->errors as $error) { echo '<li>' . $error->getMessage() . '</li>'; } ?> </ul> </body> </html>
エラー処理を追加したフォルダツリー
ここまででフォルダツリーは以下のようになります。
- htdocs/
- zf2/
- application/
- controllers/
- IndexController.php
- ErrorController.php
- views/
- scripts/
- index/
- index.phtml
- dump.phtml
- error/
- error.phtml
- index/
- scripts/
- controllers/
- index.php
- .htaccess
- application/
- zf2/
この状態でもう一度 http://<host名>/zf2/index/hoge/ へアクセスすると、今度は以下のような表示になるはずです。
以下のエラーが発生しました
- IndexController::hogeAction() does not exist and was not trapped in __call()
このように、発生したエラーはErrorController::errorActionで処理させるのがZend_Controllerの標準的な例外処理になります。この仕組みは、Zend Frameworkに標準で添付されている「エラーハンドラ」コントロールプラグインの機能により提供されています。詳細はリファレンスガイドの「7.12. MVC での例外」や「7.10.5. 標準の配布パッケージに含まれるプラグイン」を参照してください。
未実装のアクションをデフォルトアクションに振り分ける
さて、上の例では、未実装のアクションを呼び出すことで例外を発生させましたが、場合によっては未実装のアクションを例外にせず、他の動作に付け替えたい場合もでてくるかもしれません。
このような場合は、PHP5の標準的な機能である「__call」マジックメソッドを利用できます。__callは、未定義のメソッドが呼び出された場合にオーバーロードとして呼び出されるメソッドです。(「オーバーロード」の意味合いが、たとえばC#のようなほかのプログラミング言語と違う点に注意してください)
__callメソッドのシグニチャは以下のように定義されています。
mixed __call ( string $name, array $arguments )第一引数の「$name」は呼び出された(存在しない)メソッドの名前、第二引数は呼び出しに使用された引数のリストになります。
ここでは、「未定義のアクションが呼び出されたら、デフォルトアクション(=indexAction)を代わりに実行する」ようにしてみます。追加するコードはシンプルです。
<?php require_once 'Zend/Controller/Action.php'; class IndexController extends Zend_Controller_Action { // 初期化処理 public function init() { // BASE要素向けのベースURL $this->view->assign( 'baseUrl', getApplicationUrl( $this->getRequest() ) ); } // indexアクション public function indexAction() { } // dumpアクション public function dumpAction() { $this->view->assign( 'postData', $this->getRequest()->getPost() ); } // __callオーバーロード public function __call($name, $args) { // indexアクションへ処理を委譲 $this->_forward( 'index' ); } }
_forward プロテクトメソッド
アクションコントローラ内で、あるメソッドから他のアクションメソッドへ処理を委譲する場合は「_forward」メソッドを使用します。「_forward」メソッドはZend_Controller_Actionに実装されたプロテクトメソッドで、シグニチャは以下のようになっています。
final protected function _forward($action, $controller = null, $module = null, array $params = null)第二引数でコントローラ名も指定できるのですが、通常は同じコントローラ内のほかのアクションメソッドを呼び出すと思われるためたいていは第一引数にターゲットのアクション名を指定するだけです。
ここで注意が必要なのは、_forwardの呼び出し実行時は、呼び出されるアクションコントローラが再度初期化される点です。 たとえば
public function hogeAction() { // 自身のhogeParamプロパティに値を設定 $this->hogeParam = 'ほげほげ'; // fugaアクションへ_forward $this->_forward( 'fuga' ); }とした場合、その後呼び出されたfugaAction()内で「$this->hogeParam」を参照してもhogeAction内で設定した値は格納されていません。このあたりはZend_Controllerの内部における「ディスパッチ」処理の動作によるもので、いずれ解説をしたいと思いますが、ここでは「_forwardはメソッド呼び出しではなく、次のアクションの予約を入れている」ようなもの、と認識してください。
まとめ
今回の要点は以下のとおりです。
- ErrorController::errorActionが定義されている場合、それが標準のエラーハンドラになる
- エラー情報の取得はZend_Controller_Response::getException()で行う。戻り値はExceptionの配列である
- __callメソッドで未定義のアクション呼び出しをカバーできる
- Zend_Controller_Action::_forwardメソッドで他のアクションメソッドへ処理を委譲できる
あとがき&次回予告
またも予想より記事が長くなってしまったので、今回はフォーム処理周りに触れられませんでした(タイトルに偽りあり、ですね^^;)。次回こそは前回のフォームを拡張し、入力内容によって処理を分岐させるようなサンプルを行いたいと思います。また今回でてきた「_forward」の親戚筋にあたる「_redirect」についても解説してみたいと思います。
最近のコメント