これって文字列なのか?2007年12月17日 02時16分44秒

PHPってワケワカラン深いなぁ

前回のエントリにいただいたトラックバックの記事で、またもPHPの非常識興味深い挙動に出くわした。

こちらの記事では、JavaグラマにPHPのコード組ませたら、
for ($i=0; i < count($value); $i++) {
という記述で無限ループしたとの記述。???

どんな仕様だよ

上記コードでループ条件の比較に「i」(!=$i、要するに変数じゃない)を用いたため、これが暗黙で数値「0」と見なされ無限ループになったとのことだが、これパースエラーにならんの??

ためしにこんなコードを試してみる。

C:\Documents and Settings\dara-j>php -r "echo ijk;"
ijk
C:\Documents and Settings\dara-j>
ちっともパースエラーになぞならず、しっかりと「ijk」という文字列として扱われている。わけわからん

一応マニュアルの文字列の項をざっと見てみたけど、引用符か二重引用符でくくるかヒアドキュメントにするかってくらいしか書いてなくて、どこをどう押したらこんな記述が許容されるのかさっぱりわからん。 これバグ誘発しやすいと思うけどなぁ。

ボヤキ

前回のエントリで触れた、PHPの文字列→数値の自動変換って、JavaScriptのparseInt()とほぼ同じ働きみたいだね。あんまparseInt()使ったことないけど。

PHPにおける数値と文字列のヘンな比較結果2007年12月16日 04時09分25秒

0 == 'str' // true

PHPSPOTでこんなエントリがあった。

タイトルだけみたときは「0 == '0'のことか?そりゃ当然じゃん」とか思って記事を見たところ、
$a = 0;
$b = 'str';

if( $a == $b ) {
	echo 'equal';
} else {
	echo 'not equal';
}
なんてコードが掲載されてた。試してみると確かに「equal」と出力される。どういうこっちゃ?

仕様を調べてみる

ちょっとこの現象は、どういう理屈に基づいてこうなるのかさっぱりわからないので、仕様上どうなっているのか調べてみた。

違う型同士を比較するので、暗黙の型変換が発生するだろうと思い、マニュアルの「型の相互変換」を見てみたがそれっぽいことは記述されていなかった。

このページの最後に参照リソースとして「PHP 型の比較表」があったのでそちらを見てみると、 「==による緩やかな比較」という表には確かに数値の「0」と文字列「php」の比較結果は「TRUE」になると記載されているので、今回の現象はどうやら仕様らしいことはわかった。が、納得いかん

で、他に関連しそうなところがないかと思って目次を探してたら、「比較演算子」の項があったので見てみると、

整数値を文字列と比較する際、文字列が 数値に変換されます。
とある。ってことは、文字列'str'が暗黙的に整数に変換されたものと整数0が比較されたということのようだ。

文字列 → 数値変換ってどうなっとるの?

これで足がかりが見つかったので、もう一度「型の相互変換」から追っかけてみたところ、「文字列」の「文字列変換」の節

文字列の最初の部分により値が決まります。文字列が、 有効な数値データから始まる場合、この値が使用されます。その他の場合、 値は 0 (ゼロ) となります。
とあった。

検証してみる

なるほど、上記仕様なら、文字列'str'は数値0と見なされるので、最初のコードで「等しい」と判断されるわけだ。ではそのとおりに振舞うか、テストしてみよう。

文字列の先頭に数字を入れればその数字が示す数値に変換されるはずだし、比較する数値を0以外にしても等値にならないはずなので、最初のコードを含めて3通り試してみた。

C:\Documents and Settings\dara-j>php -r "echo 0 == 'abc' ? 'equal' : 'not equal';"
equal
C:\Documents and Settings\dara-j>php -r "echo 0 == '1abc' ? 'equal' : 'not equal';"
not equal
C:\Documents and Settings\dara-j>php -r "echo 1 == 'abc' ? 'equal' : 'not equal';"
not equal
C:\Documents and Settings\dara-j>
おお、予想通り。やっと納得がいった。

結論

比較だけなのに勝手に文字列→数値変換が発生するので気をつけようってことね。しかしこれ、仕様としてはあまり行儀が良くない気がするなぁ...

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

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

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()」して例外補足したほうがいいんじゃねーの?って思う。

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