Delegateクラス ― 2008年03月11日 03時00分03秒

zf入門も書かずにコネタクラスです。ごめんなさい。今回はZend_Logのサンプルもちょっとだけ載せてるので勘弁してください。Zend_Logは説明してないけど。

string | array が気持ち悪いので

PHPで動的な関数・メソッド呼び出しを行う場合、関数名を示す文字列を使うか、配列にオブジェクト(or クラス名)とメソッド名を格納して渡す。マニュアル中で「callback」という呼称で呼ばれている擬似的な型なやつだ。

これ、短いスコープで使う場合はまぁそれでもいいかと思えるんだけど、大きいスコープの変数に格納した場合、本当に適切かどうかの判断がすぐにつかなくて気持ち悪い。$callbackとかって変数にint放り込んでも許容されるので実行時にチェックするのって面倒くさいじゃん。is_callback()とかあるわけじゃなし。

だもんで、.NETのDelegateのような感じで、あらかじめラッピングしちゃえば、関数やメソッドが実在するかはともかくとして「呼び出し可能なオブジェクト」として保持しておけるだろうと思って、こんなクラスを作ってみた。まんま「Delegate」というクラス。使いどこ、少なそうなんですが。

<?php
class Delegate {
  /**
   * @static
   *
   * 指定の引数がコールバック形式かを判別する
   *
   * @param mixed $callback コールバック形式かを判別する引数
   * @return bool $callbackがコールバック形式の場合はtrue、それ以外はfalse
   */
  public static function isCallback($callback) {
    // 文字列か配列のみ許容
    if( is_string($callback) || is_array( $callback ) ) {
      if( is_array($callback) ) {
        if(
          // 配列長が2ではないか
          count($callback) != 2 ||
          // 第一要素がオブジェクトまたは文字列ではないか
          ( ! is_object($callback[0]) && ! is_string($callback[0]) ) ||
          // 第二要素が文字列ではない場合は
          ! is_string($callback[1])
        ) {
          // コールバックではない
          return false;
        }
      }
      return true;
    } else {
      // 文字列・配列以外はコールバックではない
      return false;
    }
  }

  /**
   * @access protected
   *
   * コールバックオブジェクト
   *
   * @var string|array
   */
  protected $_callback;

  /**
   * Delegateの新しいインスタンスを初期化する
   *
   * @param mixed $obj コールバック関数名、クラス名またはオブジェクトインスタンス
   * @param  string|null $method $objがクラス名かオブジェクトインスタンスの場合はメソッド名
   */
  public function __construct($obj, $method = null) {
    $callback = ( is_string($obj) && empty($method) ) ?
      // $objが文字列で$methodがnullの場合は関数名指定
      $obj :
      // それ以外はメソッド指定
      array( $obj, "$method" );

    // コールバック形式かをチェック
    if( ! self::isCallback($callback) ) {
      throw new Exception( '引数がコールバック形式ではありません' );
    }

    $this->_callback = $callback;
  }

  /**
   * コールバック呼び出しを実行する
   *
   * @param [mixed parameter [, mixed ...]] 可変長パラメータ
   * @return mixed コールバックの実行結果
   */
  public function invoke() {
    return $this->invokeArray(func_get_args());
  }

  /**
   * パラメータを配列で指定してコールバック呼び出しを実行する
   *
   * @param array $params コールバック関数・メソッドに指定する引数の配列
   * @return mixed コールバックの実行結果
   */
  public function invokeArray(array $params) {
    return call_user_func_array($this->_callback, $params);
  }
}


ま、元の発想のまま「Delegate」なんて名前にしてるけど、別に「Callback」クラスとか「MethodInvoker」とかでもいいです。

ドキュメントコメントが適当なのは勘弁してください。ちゃんと調べよう、そのうち。

「Delegate」とはいいつつも

当然ながら、.NETのDelegateクラスとはちみっと違う。まぁ、あちらは明示的にDelegate/MulticastDelegateから派生型作れるわけではなく、構文からコンパイラが勝手に派生させるっていう特殊な型なんだけども。

コールバックの引数でバインドするわけではないので、invoke()するときの引数に関してなんの保証もないので、ランタイムエラーになりやすいかも。

使い方

コンストラクタの引数に、「callback」型を指定する。

例えば関数の場合ならその関数名の文字列。クラスメソッド(スタティックメソッド)だったら「array( 'ClassName', 'methodName' )」のように文字列を2つ、インスタンスメソッドなら「array( $obj, 'methodName' )」みたいな感じ。

んで、格納したコールバックを呼び出す場合はinvoke()メソッドを使う。引数は可変長で受け取るので、元の関数(またはメソッド)に与える引数と同じような引数を与えてやる。元の関数(メソッド)が値を返すならinvoke()の戻りで受け取れる。

サンプル

以下はZend_Logの各種ログ出力メソッドをDelegateに収めてループで呼び出しするサンプル。

<?php
require_once 'Delegate.php';
require_once 'Zend/Log.php';
require_once 'Zend/Log/Writer/Stream.php';

$log = new Zend_Log( new Zend_Log_Writer_Stream('php://output') );

$list = array(
  // Zend_Log::emerg()を実行するDelegate
  array( 'name' => 'EMERG', 'callback' => new Delegate( $log, 'emerg' ) ),

  // Zend_Log::err()を実行するDelegate
  array( 'name' => 'ERR',   'callback' => new Delegate( $log, 'err' ) ),

  // Zend_Log::warn()を実行するDelegate
  array( 'name' => 'WARN',  'callback' => new Delegate( $log, 'warn' ) ),

  // Zend_Log::debug()を実行するDelegate
  array( 'name' => 'DEGUB', 'callback' => new Delegate( $log, 'debug' ) ),

);
// フィルタ設定なしで実行 → すべてのログが出力される
foreach( $list as $config ) {
  $callback = $config['callback'];
  $callback->invoke( "name = {$config['name']}" );
}

// フィルタ条件を変えてもう一度ループ実行
// フィルタでZend_Log::ERR以下のプライオリティをブロックする
echo "Zend_Log::ERRでフィルタ指定\n";
$log->addFilter( new Zend_Log_Filter_Priority( Zend_Log::ERR ) );
foreach( $list as $config ) {
  $callback = $config['callback'];
  $callback->invoke( "name = {$config['name']}" );
}


このサンプルではDelegateのリストをベタでコーディングしちゃったけど、Zend_Logのプライオリティ名の配列を元にループ処理で組み立ててもよいかも。

しかし、微妙か。

こんな感じで使うんだけど、微妙。独自クラスにしちゃってるので当然array_filter()とかのコールバックにはそのまま使えないし($_callbackを返すgetterでも実装すればいいのだが)、コンストラクタで引数の型はチェックしているけど、別に関数・メソッドの実在をチェックしてるわけじゃないので、ランタイムでエラーになるかもしれないし。第一、ちょっとした用途なら素直にcall_user_func()とか使ったほうが手っ取り早いし。

それでも個人的には上のサンプルみたいに同じ形のメソッドを呼び出すちょっとしたテストに使ったりとかでそれなりに便利には使ってるんだけどね。

多様的にメソッド呼び出し行うなら、インターフェイス(か抽象クラス)から派生・実装させるのが本筋なんだろうけど、これなら組み込み関数も自作クラスのインスタンスメソッドもシグニチャが同じものなら同じように代替させられるので、その点だけはメリットかな、と思うんだけど。でもやっぱりcall_user_func()するのがPHPの流儀かしら。

Zend_DateでHTTP日付を出力 ― 2007年11月15日 22時02分13秒

Zend_Dateのコネタでやす。

HTTP日付って?

昨日今日と、ちょっとIEのタコっぷりにやられてた都合で(あとで書くかも)、PHPから Last-Modified をレスポンスヘッダに出力してやる必要があったので、ちょっと調べてみた。

検索して引っかかったこちらのサイトの記述によると、

Last-Modified = "Last-Modified" ":" HTTP-date
ってな定義らしい。んで、この「HTTP-date」は、
リソースの最終更新時刻を記述するためのフィールドです。ここに指定する時刻は、HTTP 日付を使用しなければなりません。
だそうな。サンプルを見ると、
Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT
てな出力になる。

んで、さらにここのサイトさんはHTTP日付についても掲載していてくれて、それによると、

最初のフォーマットはインターネット標準としてより好まれ、RFC 1123 (RFC 822 の改定) にて定義される固定長サブセットを表す。
ってな話らしい。要するにRFC 822か、RFC 1123の形式で出力できればよさげ。

Zend_Dateでいけるかにゃー

ZFのマニュアル「9.6 日付関数全般の定数」をつらつら眺めると、「表 9.14. 日付フォーマットに関する定数 (タイムゾーンを含むフォーマット)」に、

  • Zend_Date::RFC_822
  • Zend_Date::RFC_1123
  • なんて、そのままズバリの定数があるので、これをZend_Date::get()に渡せばよかろうと。

    <php
    require_once 'Zend/Date.php';
    
    $date = new Zend_Date('2007/11/15');
    echo $date->get( Zend_Date::RFC_1123 );
    
    とやったら、出力は
    Thu, 15 Nov 2007 00:00:00 +0900
    
    
    となった。これでいいのかと思いきや、
    全ての HTTP 日付/時刻スタンプは、例外を除いてグリニッジ標準時刻 (GMT) で表されなければならない。
    なんてあるじゃないですか!タイムゾーンを設定せねば。

    Zend_Dateにタイムゾーンを設定してみる

    Zend_Dateには「getTimeZone()」やら「setTimeZone()」なんてメソッドがあるので、これ使えばいいかと。

    ただ、マニュアルに載ってるサンプルコードは特定のタイムゾーンに設定するものばかりで、明示的にGMTを設定するコードが載ってない。はて、どうしたもんかと30秒くらい悩んだ挙句、ダメもとで文字列'GMT'と渡してみることに。

    $date->setTimeZone( 'GMT' );
    echo $date->get( Zend_Date::TIMEZONE_NAME );
    
    // -> 'GMT'
    
    
    あら、ビンゴ。んじゃさっきのZend_Date::RFC_1123と組み合わせてみるか。
    $date->setTimeZone( 'GMT' );
    echo $date->get( Zend_Date::RFC_1123 );
    
    // -> 'Wed, 14 Nov 2007 15:00:00 +0000'
    
    
    って、「+0000」ってなんやねん。

    なんとか「GMT」と出力したい

    このままほっとこうかなーとも思ったが、クライアントが意図どおり解釈してくれるかちと不安になったので、なんとか「+0000」を「GMT」に変えて出力したい。

    もちろん、細かく書式指定子を与えれば欲しい出力が得られるのはわかってるんだけど、なるたけ不精したいじゃない。

    なんとかならんかとまた「 9.6 日付関数全般の定数」をつらつら眺めたら、「表 9.13. タイムゾーンに関する定数」におあつらえ向きの「Zend_Date::GMT_DIFF」なんてのが載ってたので、どんな出力になるのか試してみた。

    echo $date->get( Zend_Date::GMT_DIFF );
    
    // -> '+0000'
    
    
    おし、思ったとおり「+0000」の出力が得られた!これを単純に'GMT'に置き換えればいいワケだ。多分。

    そんなこんなで結論

    置換は特に問題ないだろうと思うので、シンプルにstr_replace()を使うことにして、最終的には、こんな感じになった。

    <php
    require_once 'Zend/Date.php';
    
    $date = new Zend_Date('2007/11/15');
    
    // タイムゾーン設定
    $date->setTimeZone( 'GMT' );
    
    echo str_replace(
    	$date->get( Zend_Date::GMT_DIFF ),      // search => '+0000'
    	$date->get( Zend_Date::TIMEZONE_NAME ), // replace => 'GMT'
    	$date->get( Zend_Date::RFC_1123 )
    );
    
    // -> 'Wed, 14 Nov 2007 15:00:00 GMT'
    
    
    ...でも、ちと面倒くさいなーー。このくらいのフォーマット用定数、用意しておいてよ > Zend_Date

    余談

    前にZend_Date取り上げた時のエントリのタイトル、「Zend_Dete」って...orz

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

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

Zend_Deteにゃやられた ― 2007年10月13日 23時39分13秒

ごめん、ZF入門、また遅れてます。その代わりってわけじゃないけど、Zend_Dateネタを2つほど。

Zend_Date::addYear()にゃやられた

DBで「現在日から1年以内」を検索するって、まぁありがちなパターンを処理する必要があるので、日付周りでラクしようと思ってあんまりつかってなかったZend_Dateを使ってみることにした。メソッド調べたらおあつらえ向きに「addYear」だの「addDay」だのがあるので、

$date1 = new Zend_Date();
$date2 = $date->addYear(-1)->addDay(1);
なんて具合にして、「BETWEEN $date2 AND $date1」ってクエリ組み立ててみたんだけど全然データが引っかかんない。なんで?

仕方ないので組み立てたクエリ確認したら、$date1と$date2に同じデータが入ってるじゃん!

感覚的に、日付型は値型のように思っていて、addYear()すると演算結果が反映されたクローンが返ってくるように思ってたんだけど、破壊的なメソッドだったのね。むぅ。

仕方ないのでクローンを作成したかったんだけどクローンメソッドもないみたいなので、

$date1 = new Zend_Date();
$date2 = new Zend_Date( $date1 );
$date2->addYear(-1)->addDay(1);
ってな感じに落ち着いたんだけど。

しかし、PHPってなんでnewしたオブジェクトをそのまま使えないんかな。

$date2 = new Zend_Date($date1)->addYear(-1)->addDay(1);
とかって記述できりゃいいのに。じゃなければ、
Zend_Date::create( $date1 );
なんてファクトリがあればそのままメソッドチェーンできるのに。

Zend_Date::isDate()にゃやられた

DBからデータ取得してページにレンダリングするって用途はよく話で、数値や日付をレンダリング時にフォーマットしたいってのもよくある話。なので、値とデータ型とフォーマット指定を渡してついでにエスケープ(つか、htmlspecialchars())も通すビューヘルパーを作ってみたのだが、これ使うとやけにサーバからのレスポンスが遅い。

最初は発行しているSQLに問題あるのかなと思ったけどそちらはあまり問題がないので、フォーマット処理そのものに問題があるのかと思ってさらに調べてみたら、以外なものがボトルネックになっていた。

日付型としてフォーマット指示を出した際に、値が日付型にできるかをチェックするため「Zend_Date::isDate()」でチェックしていたのだが、これのコストがかなり高くついていたのだ。

流し込むデータの出所はDBからfetchしたデータに限られるので割り切って単純な正規表現でのチェックに切り替えたとたん、嘘のようにレスポンスがよくなってびっくり。

まぁ文字列の解析処理になるだろうから、そう軽い処理じゃないのはわからんこともないけど、ここまでコスト高いと、むりやり「new Zend_Date()」して例外補足したほうがいいんじゃねーの?って思う。

ループ処理中にこれ持ち込むと、想像以上に足を引っ張られるのでご注意を。

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による問い合わせの結果として取得する。
これらのクラスを使い分けてデータベース操作を行っていきますが、今回はまず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」の解説をする予定です。

それではまた次回。