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か、といったあたりでしょうか。思いつきで変更するかもしれませんが、その場合はご容赦ください。(^^;

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

コメント

_ oka ― 2008年01月11日 12時24分26秒

とても勉強になりました。
今後も期待してます。

_ dara-j ― 2008年01月27日 02時35分02秒

okaさん、コメントありがとうございます。大変遅くなりすみませんでした。
ちょっと連載がとまっている状態なのですが、また近日中に再開したいと考えてますのでよろしくお願いします。

_ Dwknwbsj ― 2008年11月23日 06時09分29秒

see this thanks <a href=" http://mybroadband.co.za/vb/member.php?u=11393 ">porn-dot</a> %-) <a href=" http://mybroadband.co.za/vb/member.php?u=11385 ">porno-hub</a> =OO

コメントをどうぞ

※メールアドレスとURLの入力は必須ではありません。 入力されたメールアドレスは記事に反映されず、ブログの管理者のみが参照できます。

※なお、送られたコメントはブログの管理者が確認するまで公開されません。

名前:
メールアドレス:
URL:
コメント:

トラックバック

このエントリのトラックバックURL: http://dara-j.asablo.jp/blog/2007/11/13/1906434/tb

※なお、送られたトラックバックはブログの管理者が確認するまで公開されません。