fgetcsvでハマってみたり2007年10月10日 02時43分46秒

ハマりネタ2発目。今度はPHP。

普段は自分の開発PC(WinXP)上のApache+PHPで開発を行っているのだが、試験用のデータを登録するため、本番系のサーバで動作させてみた。

今回動作させたのが、CSVファイルのアップロードなのだが、かなり不可思議な現象に見舞われた。

アップしたCSVのデータのうち、マルチバイトの文字が欠落するのだ。それも全部が全部ではなく、二重引用符や丸括弧で囲っているデータは大丈夫で、それ以外の文字が落ちるという現象だ。

たとえば、

注文ID(消さないでください),注文日,...
なんてデータをfgetcsv()で読み込んで行ごとにprint_r()してみると、
Array(
    [0] => ID(消さないでください),
    [1] =>
     :
     :
)
のように、最初のフィールドでは「注文ID(消さないでください)」の「注文」部分のみが欠落、次のフィールドではすべての文字(すべてマルチバイト文字)が欠落、といった感じで、どうもシングルバイト文字が出現するまで無視されているような感じになるのだ。

fgetcsv()で読み込む前に、アップロードしたファイルを一括して文字コードを変更し、それを読み込む処理をしているのだが、文字コード変換後のファイルをfile_get_contents()でダンプしてみると、特に文字化けもしていない。

print_r()の結果を見たところ、フィールドの区切りも誤認識しているわけではないし、さらに、自分の開発PC上ではまったく同じコード・同じデータで問題が発生していない。もうどうなってるやらさっぱり

と、泣きそうな気持ちで必死に調べたところ、「[PHP-dev 1206] Re: PHP5のfgetcsv()関数について」と、大いに関係ありそうな記事が見つかった。

この記事によると、

PHP 5.0 の fgetcsv() はロケールの設定に依存します。
とのこと。さらにこのスレッドの先を見てみると、
少々乱暴ですが、 setlocale(LC_ALL, 'ja_JP.UTF-8'); で、UTF-8のCSVファイルについてはfgetcsv()は希望通りに動作しました。
とあったので、試してみたら、これビンゴ。多分PHP.INIの設定の違いだったのかなぁ。

一瞬自前でCSVデコードせにゃならんかと思って結構鬱になったりならなかったりだったが、なんにしても解決策(というか回避策?)が見つかってほっとした。ちとバッドノウハウくさい気もするが。PHP.INIを見直しておくかなぁ。

VS.Php?2007年09月27日 03時04分14秒

こりゃなかなかすごそう。要するにVSのIDEを使ったPHP開発環境で、”Visual Studioのワークベンチが組み込まれて”いる「VS.Php Standalone」と、”Visual Studio上でのPHP開発を実現するソフトウェア”な「VS.Php for Visual Studio 2005」のラインナップ。

価格は、CNETの記事によると、Standaloneのダウンロード版が\34,800、for VS2005のダウンロード版が\24,800だそうな。

シンタックスハイライトやコードアウトラインなどなど、イマドキな機能は当然のことながら、なかなか驚いたのは、これ。

配列やオブジェクトの型を判断し、メソッドや配列のキーなどの入力を支援します。
オブジェクトメンバはともかく、「配列のキー」って。ちょっと他であまり見ない気がする。

実際、製品紹介のページのスクリーンショット見ると、

$a = array();
// 中略
$a[2] = array();
$a[2]['demo'] = new foo();
$a[2][
まで入力された状態で、カーソル下に補完候補としてキー「demo」が出現してる。すげー。

まあ、これ以外にあげられてる機能はだいたいPDTにもあるので(euc対応はないが)、わざわざ金払ってもって気も多少はするが、PDTは長いソースになると華麗に重たくなって、実用性激減てなステキさがあるので、もしそこまでパフォーマンスが落ちることがないのであれば決して高くない感じ。いや、OSネイティブモノとJava製のPDT比べるのは間違ってるってのはわかるんだけど、最新の正式リリース版が動作しなかったり、キー入力してから画面に挿入されるまで数秒のラグがでるくらい重くなったりってのはやっぱちょっとしんどいもんで...

ちなみにトライアル版もダウンロードできるみたいだが、Standalone版で 81M 90M 程度。比べるもんじゃないってのはわかってる(しつこいな)けど、PDTのall-in-oneより30MもDLサイズ小さいってのはなんか笑ったw ちょっと試してみようかな。

訂正(07.09.27 10:47)

お試し版のDLサイズ、89.92M(細かい..)でしたな。いずれにしろPDTのall-in-oneより30Mほど小さい。がインストーラが重たいなぁ。

PDT1.0が動かん。2007年09月26日 22時38分02秒

朝からなんとなくPDT1.0をインストールしてみることにした。いや、なんとなくだったんだけど。

といっても、四の五の考えずにall-in-oneを落として解凍するだけのお手軽インストールなのだが。

過去のいきさつもあるので、うまく解凍できるかちょっとだけ緊張したが、問題なくEclipseのスプラッシュが表示されて、特に問題なさそう。ワークスペースは現在のバージョン(S20070401-RC3)で使っているところをそのまま指定。よし、順調だ

と、思ったら、PHPのソースがぜんぜん構文強調されていない

現在のパースペクティブが「リソース」になっていることに気づいたので、「PHP」パースペクティブを追加しようと思ったが、そんなものありゃしねぇ

.phpファイルを「Open With」しようとしてもPHP Editrorも存在しないし、preferenceにもPHPのカテゴリはなし。ぐぅ。

構成の管理から確認してみると、ちゃんとPDTはアクティブになってるんだけど、これ以上なにが必要だと?

結局1時間半ほど格闘した挙句成果はなく、あきらめてS20070401-RC3を使い続けることになったとさ。なにが悪いんだろ?

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入門・その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の使い方を解説するかもしれません(しないかもしれません^^;)。

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