ZendFramework入門・その7データベースを扱う・その22007年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';
	
}
Bookmarks.php
これが、今回題材にしているデータベースの「bookmarks」テーブルを扱うためのクラスで、キーポイントは以下の2つでしょうか。
  • 「$_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で既存データを削除
このほかに、add/editActionは実際にはデータを編集するフォーム表示になるので、そこからPOSTして実際にデータをDBへ反映させる「saveAction」をIndexControllerに実装します。

また、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' );
	}
}
IndexController.php
要点はだいたいコメントに書いてありますが、以下に簡単な解説を示します。

定義部分

先ほど作成した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>
index.phtml

<?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>
edit.phtml

ファイル構成

今回のアプリケーションのファイル構成を示します。

  • htdocs/
    • zdb1/
      • application/
        • controllers/
          • IndexController.php
        • models/
          • Bookmarks.php
        • views/
          • index/
            • edit.phtml
            • index.phtml
      • index.php
      • .htaccess
      • zdb1.db
      • create_table.sql
今回のファイル一式は、こちらからダウンロードできます。

あとがき

えー、本当に久しぶりの更新になってしまいましたが、いかがでしたでしょうか?コードそのものはたいした量ではないのですが、説明することが結構増えてきたので、そろそろ全コードを掲載するのがちょっとしんどくなってきました。

次回はZend_Db関連をもう少し突っ込んでみようかと思います。Zend_Db_Tableのリレーション機能か、Zend_Select・Zend_Statementか、といったあたりでしょうか。思いつきで変更するかもしれませんが、その場合はご容赦ください。(^^;

それでは、次回はなるべく早く更新したいと思いますので、よろしくお願いいたします。

ZendFramework入門・その6データベースを扱う・その12007年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による問い合わせの結果として取得する。
これらのクラスを使い分けてデータベース操作を行っていきますが、今回はまずZend_Db_Adapterによる接続と問い合わせのサンプルを作成します。

アプリケーション構成

今回は「zdb1」という名前のアプリケーションを作成しましょう。構成はシンプルに、以下のようにします。

  • htdocs/
    • zdb1/
      • application/
        • controllers/
          • IndexController.php
        • views/
          • index/
            • index.phtml
      • index.php
      • .htaccess
      • zdb1.db
      • create_table.sql
zdb1直下にある「zdb1.db」は、SQLiteのデータベースファイルで、「create_table.sql」はこのアプリケーションで扱うテーブルを作成するためのSQLファイルです。これらについては次の項で説明します。その他のファイル構成は特に説明の必要はないかと思います。

肝心のアプリケーションですが、ものすごくシンプルなブックマークにしてみます。以下の情報を扱うことにします。

  • タイトル
  • URL
  • 登録日時
  • コメント
テーブルは1テーブルで、以下の構成にします。テーブル名は「bookmarks」とします。
キー カラム 備考
PK id INTEGER AUTO INCREMENT
  title VACHAR (100)  
IDX1 url VARCHAR (255)  
IDX2 registDate TIMESTAMP  
  comment TEXT  
「registDate」列はCURRENT_TIMESTAMPをdefault valueにしてもいいのですが、挿入される日付にロケールが考慮されないようなので、コードで挿入することにします。

データベースを作成する

まずはテーブルを作成する「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
このファイルを、今回のアプリケーションディレクトリに「create_table.sql」という名前で保存してください。

作成した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 );
	}
	
}
IndexController.phtml
以下、ポイントです。
  • Zend_Dbを使用するために、'Zend/Db.php'をrequireしている
  • Zend_Db_Adapterを取得するためにZend_Db::factoryスタティックメソッドを使用している
  • Zend_Db_Adapter::fetchAll()メソッドでSQLステートメントを実行している
Zend_Db::factoryは、Zend_Db_Adapter抽象クラスから派生した、DBMS固有のDBアダプタのインスタンスを生成するためのファクトリメソッドです。パラメータにはクラス名を示す文字列と、各アダプタクラスが必要とする初期化パラメータを示す連想配列を渡します。

クラス名は暗黙で「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'     // 接続パスワード
	)
);
Zend_Db_Adapter_Pdo_MySqlを初期化するサンプル
標準で添付されているアダプタクラスや、接続パラメータなどはAPIドキュメントを参照してください。接続パラメータは各アダプタクラスの変数「$_config」で定義されています。

そして、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>
index.phtml(手抜き版)
これでindex/indexへアクセスすると、以下のように表示されるはずです。

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/index
そう、Zend_Db_Adapter::fetchAll()は単純な連想配列を返すのです。

これを単純なテーブル表示にするには、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>
index.phtml(変更後)
ちょっと(というかかなり)見づらいコードになってしまいますが、これは標準のZend_Viewを使っている限り、ですので、大変でしょうけど慣れるようにしてください(^^;

実行結果は各自で確認してみてください。

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())
Zend/Db/Adapter/Abstract.php より抜粋
第一引数「$sql」はSQLステートメントの文字列かZend_Db_Selectを受け取ります。第二引数の「$bind」はパラメタライズドクエリを使用する場合のパラメータリストになります。

そして、肝心の処理内容は、以下のように記述されています。

        $stmt = $this->query($sql, $bind);
        $result = $stmt->fetchAll($this->_fetchMode);
        return $result;
Zend/Db/Adapter/Abstract.php より抜粋
要するに、Zend_Db_Adapter_Abstract::query()でZend_Db_Statementを生成し、そのZend_Db_Statementの実行結果を返すというプロキシ動作になってるということです。

あとがき&次回予告

ともかくデータベースの内容を表示するところまでやってみましたが、いかがでしたでしょうか?

このままでは面白くもなんともないので、次回はこれにデータを追加する機能を作成してみたいと思います。そのために「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」を指定しています。
これらがあるため、テーブル「t_test」に対しては
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 フォームを取り扱う・その32007年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' );
  }
}
IndexController.php
基本的にdumpActionで行っていた'postData'への割り当てをinitに移しただけですが、'baseUrl'への割り当てでもZend_Requestを使用するため、先にローカル変数「$request」にZend_Requestを割り当てるようにしています。また、assignでメソッドチェーンを適用しています。

また、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>
index.phtml
ちょっと見にくい(醜い?)コードになっていますが、変更点は以下のとおりです。
  • フォーム項目「mail」を追加している
  • POST先をdumpActionからindexActionに変更
  • postDataから値を取得し、value属性に設定している
まずPOST先の変更ですが、これはここまでの修正の動作確認のための一時変更です。dumpActionに飛ばしてしまうと、せっかくのフォーム項目に値を反映させるコードの動作確認が取れないからです。

そして、実際のフォーム項目への値の反映ですが、これは基本的に連想配列の対応する値を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へ飛ばす
  • 必須項目に問題がなければ結果表示を行う
これらを行うため、IndexControllerに入力値チェック用のアクションメソッドを追加します。名前はpostActionとでもしましょう。

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を追加したIndexController.php
処理の振り分けに前回登場した、Zend_Controller_Action::_forwardを使っている程度で、特に解説の必要はないと思います。

そして、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>
エラーメッセージ出力に対応したindex.phtml
isset()でerrorMessageが割り当てられているかを判断している程度で、これまた特に解説の必要はないかと思います。

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との違いを手っ取り早く見てみるには、今回のコードのpostActionで、indexActionに_forwardしているところを「_redirect('index/index')」に変更してみてください。せっかくinit()内でpostDataを割り当て、それをビューに反映させているのに、入力内容が実際には反映されないと思います。

これは、_forwardはリダイレクトが発生するわけではない(=同じリクエストを処理している)が、_redirectは実際にリダイレクトが発生するためです。

_forwardは同じリクエストを処理しているため、今回のようにPOSTされたデータをinit内でビューに割り当てている限り、_forward前のアクションでも_forward後のアクションでも同じようにデータを扱えますが、_redirectはリダイレクトすることによってリクエストの内容が変化する、ということです。

あとがき&次回予告

さて、1週間も間があいた割には、大して新しい内容が出現しませんでした(^^;が、これで一通りページを作成してユーザの入力を受け取り処理をするための基礎に触れられたと思います。

次回からは、いったんコントローラ関連の処理から離れ、これまたWebアプリケーションでは不可欠ともいえる、データベース関連の機能に移りたいと思います。

DBMSはPHP5で標準的に扱えるため環境にあまり左右されない、SQLiteを使う予定です。ですので、いきなりZend_Db関連をやるのではなく、1回くらいはSQLiteの使い方を解説するかもしれません(しないかもしれません^^;)。

それでは次回を、気長にお待ちください。

ZendFramework入門・その4 フォームを取り扱う・その22007年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()
		);
	}
}
ErrorController.php
「$this->getResponse()」はレスポンスオブジェクト「Zend_Controller_Response_Abstract」を取得するメソッドです。Zend_Controllerを使用している場合、処理中に発生したエラーはレスポンスオブジェクトに格納されるため、レスポンスオブジェクトを取得し、そのあとにgetException()メソッドで例外を取得します。そしてそれをビューへassignしています。

エラーの内容を表示する

このエラーアクションを表示する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>
error.phtml
errorAction内でassignされた「errros」を処理しているのですが、Zend_Controller_Response::getException()が例外の配列を返すためforeachで処理しています。配列の内容は例外クラス「Exception」およびその派生の例外クラスですので、getMessage()メソッドでエラーメッセージを取得することができます。Exceptionの詳細はPHPのマニュアルの「第20章 例外(exceptions)」を参照してください。

エラー処理を追加したフォルダツリー

ここまででフォルダツリーは以下のようになります。

  • htdocs/
    • zf2/
      • application/
        • controllers/
          • IndexController.php
          • ErrorController.php
        • views/
          • scripts/
            • index/
              • index.phtml
              • dump.phtml
            • error/
              • error.phtml
      • index.php
      • .htaccess

この状態でもう一度 http://<host名>/zf2/index/hoge/ へアクセスすると、今度は以下のような表示になるはずです。

以下のエラーが発生しました

  • IndexController::hogeAction() does not exist and was not trapped in __call()
index/hoge
「hogeActionが存在しない」旨のメッセージが表示されたはずです。

このように、発生したエラーは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' );
	}
}

IndexController.php(__callを追加)
上記の__callをIndexControllerに追加してhttp://<host名>/zf2/index/hoge/ へアクセスすると、先ほどはエラーが表示されていたのが、今度はindex/indexにアクセスした場合と同じ表示になります。つまりindex/hoge の呼び出しを index/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」についても解説してみたいと思います。