2007年11月30日

CodeIgniterのValidationクラスは使えなかった(--;

前回は愚痴っぽくなりましたが、今回も半分は愚痴です。

前回の最後で検索機能を強化してみたい…などと書きましたが、やはりその前にできるだけスクリプトを綺麗にしておきたいという気持ちが強く、まあごにょごにょいじってたわけですが、そういえば入力データを全然チェックしてなかったなぁ?などと今更になって気づきました。

そこで今回はCodeIgniterのValidationクラスを使ってみることにしたのですが、結論から言えば、これは使えませんでした。

ユーザからの入力データをそのまま信用してスクリプトで用いることは、ご法度というよりもはや犯罪行為に近いかのように、巷ではうるさく言われています。まあ個人的な趣味のスクリプトとはいえ、それを踏み台にされてどーのこーの…ということが日常茶飯事らしいので、このような傾向にあるのは致し方ないとはおもいますが、スクリプトを書く身としてはメンドイことが増えるなぁ…と。

で、そうした悪事を防ぐ意味でサニタイズとかヴァリデーションとか言われていますが、サニタイズってのはぶっちゃけデータベースにクエリを発行する時とHTMLに表示する時だけ気をつけてればまあいいかなぁ?と。

前者はSQLインジェクション防止のため、後者はXSS防止のためということらしいですが、これらはSQL組立でプリペアードステートメントを使うなりアクティブレコードに任せるなりエスケープするなり、後者は表示直前にえいちてぃーえむえるすぺしゃるきゃらすれば一応大丈夫ってことになってるらしいです。共通していることはクエリ送信の直前とかHTML表示の直前とかに処理を施すってことでしょうか。

んでヴァリデーションの方はサニタイズとは逆に入力された時にそれが想定の範囲内かどうか?をチェックする〜みたいな。

ところが想定される入力値というのはまさに千差万別で、同じformから送られてくるものでも、フィールドやその他のUIなんかで微妙にチェック基準が違ってしまいます。なので手続き型だと$_POST['hoge']を直接扱うことはまずしなくて、例えば…
$hoge = preg_replace("/[^0-9]/", "", $_POST['hoge']);

のように数値以外は問答無用で削除したり、文字列の長さを調べてみたりなどしてから$hogeの方を使ったりするわけです。

で、CodeIgniterのユーザガイドを見ると…
上で挙げたプロセスには何ら複雑なものはありませんが、たいてい大量のコードが必要になり、エラーメッセージを表示させるために、フォームHTMLの中に、さまざまな制御構造が書かれることになります。フォームの検証は、難しくなくつくれますが、実装するのはいつも面倒で退屈です。

なんて書いてあって、そうそうその通りその通り!これをすっきり書きたいんだ!と諸手を上げて賛同します。さらに…
CodeIgniter では、書く必要があるコードを最低限にとどめる包括的なバリデーション(検証)フレームワークが提供されています。 また、このフレームワークを使うと、HTMLフォームから制御構造を取り除くことができ、コードをわかりやすく、メンテナンスしやすいようにすることができます。

なんて太字でしかも緑色で書いてあって「さすがCodeIgniterだ!」「やはりこのフレームワークを選択して間違いなかった」と思わせてくれます。

んで喜んでられるのはその辺までで、結局使えません(--;。いや、まあみんながみんな使えないというわけではないでしょう(ないと思いたい…)。おそらくこれはログインの画面みたいなもの(それだけともいえよう)を作る場合には重宝しそうな気はしますが…。

まずココで作ってる検索アプリですが、検索フィールドが3つ(都道府県・新市町村・旧市町村)あって、そのテキストフィールドはnameが配列になっているわけです。
都道府県<input type="text" name="condition[pref]" />
新市町村<input type="text" name="condition[nc]" />
旧市町村<input type="text" name="condition[oc]" />

で、validationクラスの説明の通り
$this->load->library('validation');

$rules['condition[pref]'] = "required";
$rules['condition[nc]'] = "required";
$rules['condition[oc]'] = "required";

$this->validation->set_rules($rules);

のように書いて試してみましたが、↑のコードは動きません。

配列のキーの書き方が問題ありっぽかったので、$rules["condition['pref']"]とか$rules[condition[pref]]とかいろいろやってみましたが全部ダメでした。

そこでそもそもこのような書き方、つまり配列のキーに配列?を書くようなことがPHP的にNGなんじゃないか?とおもい、PHPのマニュアルを見ながらechoしたりしてみましたが、どうもこの書き方自体は間違ってない?というか一応ちゃんと値もセットできるし取り出すこともできました。

そこで今度は$_POSTの配列自体がどうなってるのかとおもいprint_r()してみたら…
Array
(
[condition] => Array
(
[pref] => 神奈川
[nc] => 相模
[oc] => 津久井
)
)

当たり前ですが、多次元配列になっているわけです。なので…
$rules['condition']['pref'] = "required";

のように書いてみました。するとさっきまでと違うエラーが…
A PHP Error was encountered
Severity: Notice
Message: Array to string conversion
Filename: libraries/Validation.php
Line Number: 195

つまり配列が強制的に?文字列にされちゃったよぉ〜ということらしいので、ソースを覗いて見ました。そしたらなんと$rulesは一次元配列だけを想定されているようです。どうもValidationクラスの説明にあるルールのパイプ機能が問題っぽく、「|」でパイプされてるルールをexplodeしている部分で上記のエラーが出ているようでした。

というわけでformのテキストフィールドの名前を配列にしている以上、このままだとvalidationクラスは使えません。libraries/Validation.phpのソースを若干いじってみようかなぁと途中までやってみたりしたんですが、どうもその部分のコードが長くて挫折しました。

んで、結局検索フィールドのヴァリデーションはあきらめて自力で実装することにしたのですが、↑のソースを見ていてちょっとオヤ?とおもったのは、なんと$_POSTがハードコーディングされていたという部分です。

結局コレってPOSTにしか使えないんじゃないの?という疑問が…。そういえばユーザガイドのどこにも任意のデータを渡してごにょごにょできますみたいなことは書かれてません。ということはココで作ってる検索アプリの場合だと五十音パッドから送られてくるデータとかソートした時とかページ移動した時とかのパラメータなんかはPOSTではないので、結局validationクラスは使えないってことになります(--;。

んで上述の通り唯一(ではないけど)使えそうな検索フィールドもnameが配列になってるとダメ、残るは表示件数と表示モード(これらは独立したformでしたがうざいので結局一つにまとめちゃいました)のformから送られてくるデータですが、なんかこれだけのために結構重そう(ユーザガイドにも書いてありますが)なValidationクラスをロードするのもなんだなぁ…と。

そうすると一体このクラスはどういう用途を想定してるんだ?ということなんですが、これは冒頭述べたとおりログインフォームぐらいにしか使えないんじゃないかな?。それ以外でこのクラス使ってる人がいたら教えてください。(あ、いや教えてもらわなくてもいいや…)
ラベル:codeigniter 愚痴
posted by ciallost at 02:46| Comment(4) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

2007年11月28日

CodeIgniter:MVCの分離が怪しくなってきた

前回、慣れないオブジェクト指向でポリモーフィズム(もどき?)をやって力尽きました。一応当初の目標だったswitch文の駆逐には成功したものの、どうもコードが全般的に怪しくなってきた?というか書いてるうちにこれはControllerなのかViewなのかという区別が曖昧になってきました。

そもそもMVCというパターン?の厳密な定義?がよくわからない。自分的には勝手に

Model:データベースをごにょごにょするコード
View:HTMLを書く、ロジックは書かないで変数をechoするだけ
Controller:Viewにセットする変数を処理するところ

のような結構漠然とした感じで分けていましたが、コードを書いていくうちに↑の曖昧な定義でさえどんどん破られていくという…。

まず放っとくとViewがロジックだらけになっていくわけです(^^;。例えばテーブル出力するのにHTMLテーブルクラスを使ったとして、パラメータをセットしなきゃならない。これはそもそもVなのかCなのか?おそらく↑のような定義だとCだと思うんですが、index()内に書いたら最後、手続き型と変わらないぐらいindex()が長くなってしまう。

メソッドにしてindex()から出しちゃってもなんかすっきりしない。そうすると結局「テーブルを出力してるんだからこれはViewの仕事だ!」とかいう適当な&その場限りの&自分に都合のいい…なんか悪魔の囁きみたいなのが聞こえてきて、おもむろにViewファイルの下の方に<?phpとか書き始めちゃってるわけです。

メインのViewファイル(のHTMLの外)
<?php
function setTableHeading($th, $sort)
{
$th_tag = '';

foreach ($th as $field => $column)
{
$th_tag .= "<th>";
$th_tag .= anchor('gappei/sort/'.$field, $column);
if ($field == $sort['field'])
{
$order_mark = $sort['order']=='asc' ? '▽' : '△';
$th_tag .= " <span id=\"order_mark\">".$order_mark."</span>";
}
$th_tag .= "</th>\n";
}

return $th_tag;
}
?>

これぐらいだとまだfunctionにもなってるし、なんだったらController内にこのまま移すこともそれほど難しくない。だけど例えば五十音パッドを出力してるようなコードは、もうほとんどHTMLというより単なる手続き型のPHPスクリプトなわけで…。

ちなみに長いのでいちいちコード掲載しませんがPHPのコード部分だけで64行ありました。ほとんどが五十音パッドテーブル出力のためのパラメータのセットなんですが、こういうのはControllerに持っていった方がいいのかどうか???

そんな感じなんで、Viewファイルもパーツごと?に分けてみたりしましたが、結局どのファイルにもロジック?っぽい部分が少なからずある状態です。

Viewはそんな状態ですが、そもそものControllerの方も放っとくとすぐすごいことになってくる…っつーか、たぶん頭がオブジェクト指向的になってない?のか、CodeIgniter的作法がよくわからないというか…。

例えば前回晒した…
//検索オブジェクト取得
if ($this->phpsession->get('searchObj') <> '')
{
$searchObj = unserialize($this->phpsession->get('searchObj'));
} else {
$data['criteria'] = array('pref' => '', 'nc' => '', 'oc' => '');
$this->load->view('no_session', $data);
return;
}

//検索語句取得〜Viewに設定
if (get_class($searchObj) == 'NormalSearch')
{
foreach ($searchObj->getCriteria() as $key => $val)
{
$data['criteria'][$key] = $val;
}
} else {
$data['criteria'] = array('pref' => '', 'nc' => '', 'oc' => '');
}

この検索語句をViewにセットしている部分なんかは、勢いで適当に書いちゃってたわけで、ぐっちゃぐっちゃです。

んでこれを綺麗にしたいと思って…
//検索オブジェクト取得
if ($this->phpsession->get('searchObj') <> '')
{
$searchObj = unserialize($this->phpsession->get('searchObj'));
} else {
$this->load->view('no_session', $data);
return FALSE;
}

//検索語句取得
foreach ($searchObj->getCriteria() as $key => $val)
{
$data['criteria'][$key] = $val;
}

こんな感じに書き直したりしてみるわけです。まずNormalSearchの検索語句がない場合に配列を初期化している部分がうざいのでカットして、検索オブジェクトがNormalSearchかどうか判断してる部分もダサイのでカットして…みたいな感じで一見すっきりしてきてるようにも見えますが、これのおかげ?でViewの方でいちいち…
isset($criteria['cap'])?$criteria['cap']:'';

みたいなことを書かなきゃならないわけです。

まあissetの引数が関数の戻り値を直接書けないので、こうすればViewで受取った変数を書けるだけこっちの方がマシな気もするので、このようにしていますが、↑のMVCの定義?に照らし合わせるとどうなの?といった疑問はなかなか払拭できません。

Modelに関してはVやCと比べるとそれほど悲惨なことにはなってないような気もしないでもありませんが、SQL直書きに近い状態とか、発行したSQL“文”を再取得して使いまわしてたりとか、なんかカオスです(--;。

動きゃぁぃぃ!みたいなことを口走りましたが、今は動くけど次は動かね〜だろ!な状態です。んで、そんな中いい加減画面が見辛くなってきたので、cssでも書いてちょっと整形しようなどという考えが浮かんできてしまうわけです。

Viewがちゃんと切り分け出来てれば、本当にcssだけで整形できちゃったりするんでしょうが、ここはこれこの通りなわけで、cssだけの話じゃ済まない。

そもそもcssのファイルはどこに置けばいいんだ?という超初歩的事項から始まり〜長い道のり。

検索結果のステータス表示しようにも欲しい変数がセットされてない。取ってこようとすると今度は検索オブジェクト毎に場合分けしなきゃなんない。それをそのままベタに実装するとまたもやswitch文の嵐になるので、自作のでっちあげクラスを変更しないとなんない。

ちょっとしたアイコンやロゴ?みたいな画像を表示しようとしても相対パスで普通に書いても読み込めない(TT。base_url()とか使って絶対パスに変換してもsystem/application/images/hoge.pngとか書かなきゃなんない(のか?)。

とにかくそういう作業をしている最中も常に↑のMVCの分離?ということが頭のどこかをグルグルしてました。まあグルグルしてるだけで実際は分離とかとは程遠い状態になっているわけですが…。

そんなわけで今回はCodeIgniterとは直接関係のない愚痴もどきでしたが、なんとか紆余曲折の末cssで整形したので結果画面だけ晒しておきます。

css.jpg

本当はcssファイルとかViewやControllerで手を入れた部分をメモっとこうかとも思ったのですが、CodeIgniterとはあまりにも関係ないため止めました。いずれ完成したらソースとかもメモとして晒す予定です。

次回は検索機能を強化してみたいとおもいます。
ラベル:codeigniter 愚痴
posted by ciallost at 03:22| Comment(3) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

2007年11月25日

五十音パッドの検索部分の実装(多態編)

前回はswitch文で分岐させることによって異なる検索モード(WHERE句が微妙に違う)を実装してみましたが、案の定?というかやはりコードはみるみるうちに汚くなってしまいました。

初めてswitch文なるものを見たときはif文の羅列よりは随分整然とコードが書けるものだなぁ?などと思った記憶がありますが、改めて自分の書いたコードを眺めてみると、switch文が如何にコードを見難くしているかがわかります。

しかも下手に手続き型ではなくなっているため、却ってコードの見通しが悪くなっているというのか…。せっかくフレームワーク使ってMVCの分離だ!とかやっていても、これでは手続き型でダラダラ書いていた方がまだマシだったのではないか?と思わせられるぐらいです。

そこでフレームワークの設計もオブジェクト指向に基づいているのだろうし、ここはオブジェクト指向的にswitch文をポリモーフィズムで書換えてみようとおもいました。

と、思ったのはいいのですが、実はオブジェクト指向的なプログラムなんてほとんど経験がなく、PHPでクラスなんて作ったことないし、継承といっても各種フレームワークのマニュアルにそう書いてあるから仕方がないのでextendsとか書いてみただけだし…、ぐらいの経験しかありません。なので以下に書いてあることは間違っても鵜呑みにしないでください。

ということで自分のプログラムスタイル的にはここらへんまで考えたらとりあえず書いてみる!ってことでController(のファイル)の上の方(class Gappei extends Controllerの外側という意味)にクラスを書いてみました。
abstract class SearchMode
{
private $criteria;

function __construct(array $condition)
{
$this->criteria = $condition;
}

function getCriteria()
{
return $this->criteria;
}

function where()
{

}
}

class NormalSearch extends SearchMode
{
function __construct(array $condition)
{
parent::__construct($condition);
}

function where()
{

}
}

class CapSearch extends SearchMode
{
function __construct(array $condition)
{
parent::__construct($condition);
}

function where()
{

}
}

何故Controller(のファイル)に書いたかというと、とりあえずどこでインスタンス化するかなぁ?と考えたときに、たぶんControllerのどっか?だろうとおもい安易にここに書き連ねただけです。

構造?(クラス階層?)的にはSearchModeという抽象クラスを継承したNormalSearchとCapSearchというクラスがあるという形です。where()というメソッド内でそれぞれのWHERE句を生成する予定で一応ガワだけ書いておきました。

$criteriaは検索語句を格納する予定のメンバでNormalSearchの場合はpostで受け取った配列、capSearchの場合はURLの第3セグメントで受取った文字になります。ということでそれぞれをインスタンス化するコードを書きます。

function search()
$this->cond = new NormalSearch($this->input->post('condition'));

function kanaSearch($cap)
$this->cond = new CapSearch(array($cap));

$condはGappeiクラス内のメンバで型があるとすればsearchModeということになるっぽいですがPHPなのでよくわかりません。とりあえずこの変数にどちらかのインスタンスが格納されるってことで…。
private $cond;

で、この後はindex()で$condをごにょごにょするなりModelに送る?なり…と思ったのですが、↑の二つのメソッド内で
redirect('gappei/index/');

のようにindex()にリダイレクトしています。すると$condが初期化されちゃうんですね(--;。まあ当たり前といえば当たり前なのかな???というわけで、ってゆーかいずれにしろページングとかソートとかやったら↑の二つのメソッドを経由しないで(つまりインスタンスが作られないまま)index()を呼ぶことになってしまうわけで、これはどっちにしろオブジェクトをどこかに保存しておく必要があるというわけです。なので…

//検索オブジェクトセット
$this->phpsession->save('searchObj', serialize($this->cond));

のようにして今までと同じようにシリアライズしてオブジェクトを丸ごとセッションデータに格納することにしました。

次にModelのクエリ組立部分で前回はswitch文を使用していたところをばっさりと削除して、次のように変更します。
$searchObj = unserialize($this->phpsession->get('searchObj'));
$searchObj->where();

シリアル化を元に戻してオブジェクトを取得し、それのwhereメソッドを呼ぶだけです。あとはwhere()の実装です。

まずNormalSearchの方ですが元々は
$this->db->like(array_diff($criteria, array('', NULL)));

と書いていました。$criteriaはメソッドの引数として受け渡されたものだったので、これは今度はクラスのメンバ、つまりgetCriteria()を呼べばよいということになります。

が、問題なのは$thisでこれはModel内で書いてあったから許される?っつーか、たぶん?いやほぼ確実にこのままじゃダメっぽいということはわかります。

んでユーザガイドを見てみたらライブラリの作成のところに
$CI =& get_instance();

のようにすれば、$thisのかわりにCodeIgniterのネイティブなリソースにアクセスできる旨が書いてあったのでそうします。

ということで結局できあがったNormalSearchクラス
class NormalSearch extends SearchMode
{
function __construct(array $condition)
{
parent::__construct($condition);
}

function where()
{
$CI =& get_instance();

$CI->db->like(array_diff($this->getCriteria(), array('', NULL)));
}
}

データベースのライブラリはオートロードする設定になっているのでここには書かなくても大丈夫みたいでした。でcapSearchクラスも同じような感じで…
class CapSearch extends SearchMode
{
function __construct(array $condition)
{
parent::__construct($condition);
}

function where()
{
$CI =& get_instance();

$field = get_cookie('kanatab') == 'ocap' ? 'oyomi' : 'yomi';

$CI->db->where(array("SUBSTRING($field FROM 1 FOR 1) = " => $this->get1stCriteria()));
}
}

ここでget1stCriteria()というのは親クラスに新たに作成したメソッドで、$criteriaは配列ということにしてあるので、それの最初の要素を返すだけのメソッドです。
function get1stCriteria()
{
$temp = $this->criteria;
return $temp[0];
}

db->where()に渡している配列は、キーに演算子を含ませています。試したところどうやらキーにスペースが含まれている場合は演算子'='は出力されなかったのでこのようになっています。スペースが含まれていない、例えばarray('pref'=>'あ')のような配列だと、自動的に'='を含む pref = 'あ'のようなWHERE句を生成してくれるみたいです。

$fieldというのは五十音パッドのタブがどちら([新/旧]市町村)を選択されているかで、取ってくるフィールドの名前(yomi/oyomi)が異なるためこのようになっています。これはデータベース設計を見直せばいらない部分かもしれません。が、とりあえずこのままでいきます。

最後にindex()の件数取得部分でModelのgetCount()に渡していた引数$criteriaを削除し
$data['count'] = $this->Gdb->getCount($data['sort_key']);

Modelの方も関数の頭の部分を変更します。
function getCount($sortkey)

これでデータ取得は問題ないのですが、ビューに検索語句を渡さなければならないため、結局セッションデータから検索オブジェクトを取得してそれをビューに渡す変数$dataに格納するという処理を書かなければなりませんでした。
//検索オブジェクト取得
if ($this->phpsession->get('searchObj') <> '')
{
$searchObj = unserialize($this->phpsession->get('searchObj'));
} else {
$data['criteria'] = array('pref' => '', 'nc' => '', 'oc' => '');
$this->load->view('no_session', $data);
return;
}

//検索語句取得〜Viewに設定
if (get_class($searchObj) == 'NormalSearch')
{
foreach ($searchObj->getCriteria() as $key => $val)
{
$data['criteria'][$key] = $val;
}
} else {
$data['criteria'] = array('pref' => '', 'nc' => '', 'oc' => '');
}

最初のif文でセッションデータの有無を調べてあれば$searchObjに格納しています。初回アクセス時などセッションデータがない場合の処理で、その際はno_sessionという検索フォームと五十音パッドだけのビューに転送しています。

次のif文で検索語句を取得し$data配列に格納しています。ここでの条件判断で使用しているget_class()はインスタンスのクラス名を返すPHPの組込関数です。

いずれも検索語句がない場合に配列を初期化していますが、この処理の書き方はなんか汚いというか長くなる原因というか…後々の課題ってことで…。

それにしてもこれらの処理(ビューに渡す)がなければかなりすっきりしたのですが…。それはともかく最終的には独自クラスを別ファイルにしてController(のファイル)からrequireする形にしました。
require_once('search.php');

で、一応これで動いてます。switch文も完全になくなりました。でも自分で書いておいてよくわからない(ってゆーか不安な)部分があるのでメモっておきます。

Model内でクエリ組立てる部分がその他のところは$this->db〜のように書いているのに、オブジェクトを使った部分では$CI->db〜になっているわけで、これはきちんと同じdbオブジェクト?を指しているのだろうか?ということ。

これはなんらかのデザインパターンに当てはまる形なのか?ということ。なんとなくステートパターンとかいうのに似てる気もしないでもないが???

あとどう考えても再利用とかできそうにないんですが、こんなもんなのか?ということ。

いずれにしても自分にとって初めて動いたオブジェクト指向的プログラムなので、くれぐれも真似しないように(TT。(動きゃぁいいんだぃ!)
ラベル:codeigniter PHP
posted by ciallost at 00:05| Comment(0) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

2007年11月23日

五十音パッドの検索部分の実装(switch case編)

前回ようやく五十音パッドのUI部分ができたので、今回は検索部分の実装をしてみたいとおもいます。

実装といっても基本的には、すでに五十音(のうちの一文字)を取得するところまではできているので、あとは where yomi like 'あ%' みたいなWHERE句を生成してデータ取得すればいいだけの話です。

なんですが、これがやってみると意外とメンドイ。まず一番ネックな部分は、普通に?フォームのテキストフィールドから入力された地名を検索する場合と、五十音パッドから一文字選択されてその文字で始まる地名を検索する場合、といった感じで一口に「検索」といっても多少挙動が違う感じになっているというところです。

具体的にはフォームからの検索の場合は where nc like '%函館市%' のようなクエリを組み立てていますが、五十音パッドの場合は where yomi(の一文字目) = 'あ' みたいなWHERE句を生成したい(しなければならない)ことです。

しかもフォームからの検索の場合は(ブラウザからの)クエリが複数ある可能性があるので配列で受け取っていますが、五十音パッドからの場合は今のところ単一の文字(五十音一文字)なのでわざわざ配列にする必要はありません。

そんなわけでやり方はいろいろ考えられますが、とりあえず一番手っ取り早く、検索モードというフラグ?を導入して、switch文で分岐させることにしました。

単純なif文でなくswtch文にしたのは、この後さらに検索モードが増える可能性を考慮してのことです。というか実は大元のスクリプトでは「普通のフォームからの検索」「五十音パッドからの検索」に加えて「IDでの検索?(検索というかID指定でデータを取ってくる感じ)」の3通りの検索モードがあり、実際switch文で分岐していてすでにぐっちゃぐっちゃで手のつけられない状況になっています(^^;。

というわけでまずControllerのsearch()とkanaSearch()に検索モードフラグをセットするコードを加えます。

Controller->search()
$this->phpsession->save('search_mode', 0);

Controller->kanaSearch()
$this->phpsession->save('search_mode', 1);

要するに通常検索は0/五十音パッドは1ってことにする、ということです。

そしてsearch()の方と同様に検索条件をセッションに格納して、ソート条件をクリアします。

kanaSearch()
//検索条件セット
$this->phpsession->save("search_key", $cap);
//ソート条件初期化(ORDER nid asc)
$this->phpsession->save("sort_key", serialize(array('field'=>'nid', 'order'=>'asc')));

search()の方はpostで送られてきたデータを格納していましたが、kanaSearch()ではURIの第3セグメントでURLエンコードされた平仮名一文字を受け取っていますので、それ($cap)をそのままセッションデータに格納します。そしてindex()にリダイレクト…。

index()
//検索条件取得
switch ($this->phpsession->get('search_mode'))
{
case 0:
$data['criteria'] = array('pref'=>'', 'nc'=>'', 'oc'=>'');

if ($this->phpsession->get('search_key') <> '')
{
$condition = unserialize($this->phpsession->get('search_key'));

foreach ($condition as $key => $value)
{
$data['criteria'][$key] = $value;
}
}
break;
case 1:
$data['criteria'] = '';

if ($this->phpsession->get('search_key') <> '')
{
$data['criteria'] = $this->phpsession->get('search_key');
}
break;
}

index()の検索条件取得部分では、まず検索モードを調べてswitch文で分岐します。$data['criteria']は通常検索の場合は配列ですが、五十音検索の場合は配列にはなりません。

そしてModelもControllerと同じようにswitch文で分岐させて、それぞれの検索モードに合ったWHERE句を生成するようにします。
switch ($this->phpsession->get('search_mode'))
{
case 0:
$this->db->like(array_diff($criteria, array('', NULL)));
break;
case 1:
$field = "yomi";
if (get_cookie('kanatab') <> '')
{
if (get_cookie('kanatab') == 'ocap')
{
$field = "oyomi";
}
}
$where = "SUBSTRING($field FROM 1 FOR 1) = ".$this->db->escape($criteria);
$this->db->where($where);
break;
}

ここで$fieldというのは新旧どちらの地名の読みフィールドかを表す変数です。これは五十音パッドの上部のタブの選択状態で振り分けています。

WHERE句はほとんど手動生成(笑)なので、$this->db->escape()でエスケープ処理しています。SUBSTRINGは前回と同じMySQL側の文字列操作関数です。

動作結果(発行されたクエリだけキャプチャ)

query_50on.png

で、一応ほぼ期待通りに動作しました。↑の画はデバッグ出力の部分だけですが、ちゃんと期待通りのクエリが発行されてる様子はわかるとおもいます。

しかしながら、どーも気に食わないというか…。switch文を導入?すると途端にスクリプトが長くなりますね。ってゆーか汚い(--;まだ検索モードが2つだけなのでそれほどでもないですが、これが3つとかになるとせっかくCodeIgniter使って書き換えてる意味が半減しちゃうような気が…。

それとこれは今回の実装とは直接関係ありませんが、旧市町村の頭文字で検索された時にはその頭文字だけを持つ旧市町村名だけを出力するようにしたほうがいいような気がしてきました。

今の状態だと旧市町村の「お」をクリックすると…
北海道|せたな町|大成町|2005-09-01
|瀬棚町|
|北檜山町|
北海道| 釧路市 |釧路市|2005-10-11
|阿寒町|
|音別町|

こんな感じで「おおなりちょう」や「おとべつちょう」はちゃんと出力されますが、それと一緒に合併した旧市町村=瀬棚町、北檜山町なども同時に出力されてしまいます。これを…
北海道|せたな町|大成町|2005-09-01
北海道| 釧路市 |音別町|2005-10-11

このような出力にした方がよいのではないか?ということです。

で、これは比較的簡単に旧市町村のタブがクリックされている時は、クエリで外側のSELECTにもWHERE条件句をつけてやればいいだけです。まあそのための場合分けとかは多少必要ですが、一応そのような実装に変更しました。

ですがやはりswitch文だけはどうしても気に入らず、結局却下することにしました。なので今回の実装は、まあ無駄といえば無駄な作業だったってことで(--;。。。
ラベル:codeigniter
posted by ciallost at 22:21| Comment(0) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

2007年11月21日

CodeIgniter:一度は使ってみたかったHTMLテーブルクラス

前回は日本語を含むURIでエラーが出る場合の(我流の)対処法を書きましたが、今回はそもそもの元になった五十音パッド(これもまた正確な?というか標準的な名前がわからないので勝手に命名していますが…)を作成していきたいとおもいます。

ただ作ってみると結構複雑な問題も絡んできてBlog的には長くなりそうな悪寒なので、今回はそのGUI?っつーか表面的な部分までの話を書きます。で、どんなものかといいますと…

gojuonpad.png

こんな感じで平仮名(の一部。「ん」とか「を」とかは省く)をただ並べた表です。各文字はアンカーが設定してあってクリックした文字で始まる○市町村が一覧表示される(予定)という仕様です。

表の上に書いてある|新市町村|旧市町村|はタブとおもってください(^^;。まだcssを書いてないのでなんだかわかりづらいですが、これで新/旧を選択するようにします。

で、今回はこの表をCodeIgniterのHTMLテーブルクラスで書いてみます。ちなみに以前にもHTMLテーブルクラスを試しに使ってみましたと書いたことがありました。その時はメインのテーブルをクラスを使って出力しようとして、結局却下されてベタ打ちになりましたが、今回はなんとかクラスで出力しています。ということでまずControllerでクラスをロードします。
$this->load->library('table');

で、新たにViewファイルgojuon.phpを作成して、メインのViewからロードしておきます。
<?$this->load->view('gojuon');?>

これでさしあたっての準備はできたので、gojuon.phpの中身を書いてみます。
$kana = array('あ','い','う','え','お',
'か','き','く','け','こ',
'さ','し','す','せ','そ',
'た','ち','つ','て','と',
'な','に','ぬ','ね','の',
'は','ひ','ふ','へ','ほ',
'ま','み','む','め','も',
'や','ゆ','よ', '', 'わ');
$gojuon = $this->table->make_columns($kana, 5);
echo $this->table->generate($gojuon);

無茶苦茶シンプルですね。単に「あ〜わ」までの一次元配列を作って、それをmake_columnsに渡してるだけです。まあこの状態だとアンカーもタブもないしテーブルテンプレートも適用してないので本当にただの五十音表なわけですが…(こんなん今時小学生でも見向きもすまい)

で、アンカーを付け足します。
foreach($kana as $chr)
{
if ($chr <> '')
{
$withAnchor[] = anchor('gappei/kanaSearch/'.urlencode($chr), $chr);
} else {
$withAnchor[] = '';
}
}

URLエンコードした文字をkanaSearch()に渡すリンクです。「やゆよ」と「わ」の間に空白セルを作りたかったので、空白の場合はリンクなしにします。もちろんmake_columnsには今度はアンカー付きの$withAnchor配列を渡すようにします。

で、一応kanaSearch()もControllerに記述しておきます。といってもechoだけですが…。
function kanaSearch($cap)
{
echo $cap;
}

あえてindex()にもリダイレクトさせないでおきます。実は前回はこのあたりまで書いてから、散々五十音パッドをクリックして、エラーが出たとか出ないとかあ〜だこ〜だやっていたわけです。成功すれば「ふ」とか「む」とか平仮名一文字が表示されます(なんだか虚しい)

次に↑の画像をよく見ると「て、ね、へ、め」の部分にアンカーが設定されていないのがわかるとおもいます(リンク色と地の文字色が似ててちょっと見難い)が、これは別にバグとかではなくて正常な結果です。

何をやっているかというと、状態としては新市町村のタブをクリックした時点で、次の動作としては五十音のどれか一文字をクリックすることを期待していますが、その際に不要な文字はアンカーを付けない状態にしたかったわけです。

不要な文字とは要するに新市町村のふりがなの最初の一文字に現れない文字です。いわゆるディム化というか、仮にクリックされたとしても検索結果は一件もない文字をあらかじめ選択できない状態で表示しておくということです。

で、これを実現するにあたって旧市町村テーブル(データベース)に読み(ふりがな)フィールドを追加しなければなりません。新市町村テーブルは元々yomiフィールドを作ってありましたが、旧市町村テーブルに新たにoyomiというフィールドを追加してふりがなを入力しました。
|  1  | 函館市 | はこだてし |
| 2 | 戸井町 | といちょう |
| 3 | 恵山町 | えさんちょう |
| 4 |南茅部町| みなみかやべちょう |
| 5 |椴法華村| とどほっけむら |


| 2021|具志頭村| ぐしかみそん |

さすがに2000件以上のふりがなを入力するとかなり辛い作業でした(嘘)ンナワケナイッショ

それはさておき、要するに欲しいのは[新/旧]市町村の読みの最初の一文字の一覧なわけです。なのでまずControllerにデータを取ってくるメソッドを追加します。
function _getCap($field) { }

んで一応MVC(かなり崩壊してきてるけど…)ということで、実際データベースから取ってくるコードはModelの方に書きます。
function getCap($field)
{
$this->db->select('cap');
$this->db->from($field);
return $this->db->get();
}

ここで疑問におもわれるとおもいますが、上記のActiveRecordの記述では…
SELECT cap FROM ($field:実際はテーブル名);

というSQL文しか発行されません。capってなんぞ?なわけですが、実はデータベース上で予めビューを作成することにしました(ってかしています)。
CREATE VIEW ncap AS
SELECT DISTINCT SUBSTRING(newcities.yomi FROM 1 FOR 1) AS cap
FROM newcities
ORDER BY newcities.yomi;

つまり新市町村テーブルの読み(ふりがな)フィールドの一文字目を重複なしで順番に並べたテーブルということです。ここでSUBSTRINGというのはPHPの関数でいえばsubstrとほとんど同じものです。ただしその後のFROM xx FOR yyみたいな部分の書式?はsubstrと若干異なっていて、FROM 1で一文字目という意味になります(substrは0から)。

上はnewcitiesの方から抽出?されるビューですが、旧市町村(olocities)の方もほぼ同じようにしてocapというビューを予め作っておきます。

で、先ほどのModelに作ったgetCap()メソッドで結果を取得しますが、Controller側ではさらにもう一工夫しなければなりません。
function _getCap($field)
{
foreach($this->Gdb->getCap($field)->result_array() as $row)
{
$cap = mb_convert_kana($row['cap'], 'h', 'UTF-8');
$cap = mb_substr($cap, 0, 1, 'UTF-8');
$kana[] = mb_convert_kana($cap, 'H', 'UTF-8');
}

$kana = array_unique($kana);

return $kana;
}

ただ結果を配列に入れて返すだけなら、$this->Gdb->getCap($field)->result_array()だけでいいのですが、よくよく考えてみると市町村の読みの最初の一文字には濁点や半濁点のつくものがある可能性があります。しかし今作成中の五十音パッドには濁点・半濁点はリストする予定はなく、例えば「た」で検索された場合に「だ」で始まるものも出力しようと考えています。

そこで少々トリッキーかもしれませんがデータベースからの取得結果(これは全部平仮名です)を半角カタカナに変換します。半角カタカナというのは濁点や半濁点が別の文字になっているわけです。

だ→ダ = タ + ゙
ぴ→ピ = ヒ + ゚

そして半角カタカナに変換後最初の一文字目を取得します。こうすることで濁点・半濁点を含む地名(の読み)も清音の仮名一文字にまとめることができます。

最後にそれを再び平仮名に変換してダブりを削除して返します。ダブりというのは、例えば地名に「だいまる」と「たかやま」があった場合に上記の変換をすると「た」が2つ出力されてしまうため、array_uniqueで重複を削除しています。

んでindex()では上記メソッドを呼んで$data['cap']などに格納してViewに送ります。
$data['seltab'] = (get_cookie('kanatab') <> '') ? get_cookie('kanatab') : 'ncap';
$data['cap'] = $this->_getCap($data['seltab']);

クッキーは五十音パッドのタブ(つまり新市町村か旧市町村か?)の選択状態を取得しています。

で、ここまでで[新/旧]市町村に存在する地名の読みの一文字目が濁点・半濁点を吸収した形で一覧として取得できたわけですが、今度はこれをViewに反映させなければなりません。
$dim_kana = array_diff($kana, $cap);
foreach($kana as $chr)
{
if (in_array($chr, $dim_kana) === FALSE)
{
if ($chr <> '')
{
$withAnchor[] = anchor('gappei/kanaSearch/'.urlencode($chr), $chr);
} else {
$withAnchor[] = '';
}
} else {
$withAnchor[] = $chr;
}
}

↑の方で五十音にアンカーを付け足している部分のコードを少々変更しています。まず$cap(データベースから取得した一覧)と$kana(五十音表)の差分を取ります。この配列($dim_kana)は[新/旧]市町村の地名の一文字目に使われていない文字の配列になります。

そしてforeachの中でin_arrayでその配列に該当するかどうかを調べて、該当する場合はアンカーを付けないようにします。これでようやく↑の画像で示したように該当地名のない五十音にはリンクがつかない形で五十音パッドを出力することができます。(ふぅ、結構メンドイ)

で!ここまで書いてハタと気がつきましたが、ってか今まで気がつかなかったのかよ!といったことなんですが、なんと「らりるれろ」の行がない!(--;(真面目にやってます。本当に気づいてなかった…)

ということで急遽(爆)$kanaに付け足します。
$kana = array('あ','い','う','え','お',
'か','き','く','け','こ',
'さ','し','す','せ','そ',
'た','ち','つ','て','と',
'な','に','ぬ','ね','の',
'は','ひ','ふ','へ','ほ',
'ま','み','む','め','も',
'や','ゆ','よ', '', 'わ'
'ら','り','る','れ','ろ');

こんな感じで…(って何がこんな感じだ)オオボケです。それでもちゃんとロジックは働いてるようで…

gojuonpad2.png

どうやら旧市町村で「ら」と「れ」で始まる地名はないようですね、ふむふむ(何がふむふむなのか!・・・と)

んで突然話はHTMLテーブルクラスに戻りますが、上記のコードだけでは↑の画像のようにはなりません。もちろんタブもまだ書いてません。ってことでまずはテーブルテンプレート。
$tmpl = array('table_open'=>'<table border="1" cellpadding="2" cellspacing="0" width="154">',
'cell_start'=>'<td align="center">',
'cell_alt_start'=>'<td align="center">');
$this->table->set_template($tmpl);
$this->table->set_empty(" ");

単にボーダーを指定したかっただけです。枠なしじゃなんか“パッド”っていう感じがしなかったもので。そんでcellも中揃えにしたかったので'cell_start'=>'<td align="center">って書いてみたら、なんと奇数行しか中揃えにならない。なので止む無くaltの方も指定しました。

set_emptyは空白セルをどうするか?って話らしいです。まあ1個しかありませんが…。で、これでgenerateすれば出力されるんですが、この辺の書き方っつーか書く場所?が結構自分の中でも曖昧になってきていて…。Viewファイルには書いているものの特にクラスを作ったわけでもないし、でも一応出力系のコードは外に出すか?とかなんとかかなりあやふやです(^^;。
<div>|
<?=anchor('gappei/changeKanaTab/ncap', '新市町村');?> |
<?=anchor('gappei/changeKanaTab/ocap', '旧市町村');?>
|</div>
<?=$this->table->generate($gojuon);?>

なんとなく外(<?php ?>の外という意味)に出してみました。まあ気休めにもなってませんが(ってかすぐに<?=とか書いてるし…)。一応echoはしなかったぞ!ということで…。

んでタブ部分はchangeKanaTabをパラメータ付けて呼びます。パラメータは↑で作ったデータベースのビューの名前そのものです。呼ばれた側の中身は…。
function changeKanaTab($field)
{
set_cookie('kanatab', $field, Gappei::COOKIE_LIFE);
redirect('gappei/index/');
}

のようにまたまたクッキーを設定しているだけです。

ちなみにクッキーに保存するかセッションデータに保存するかの分類?は、

検索条件・ソート条件などそのセッション中だけに限定したいモノ
→セッション
表示件数・表示モードなどある程度の期間保っていたいモノ
→クッキー

みたいな感じで適当に(テキトーに)分けています。なので五十音パッドのタブはクッキーです。(なんでだ???ま、いっか)次回はやっと検索部分の実装を書く予定です。
ラベル:codeigniter
posted by ciallost at 20:27| Comment(0) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

2007年11月20日

CodeIgniter:日本語URIでThe URI you submitted has disallowed characters.

前回はソート機能を作ってみましたが、今回は検索支援UI?として五十音パッド(なんじゃそりゃ?)を作ってみようとおもいました。んでいろいろいじっていたら、日本語(もちろんURLエンコード済み)が含まれているURLで"The URI you submitted has disallowed characters."というエラーが発生しました。

ただこの件に関しては予め?というか前に散々ネットを検索してCodeIgniterの情報を集めていた際に、
Code Igniter - goungoun技術系雑記帳」様
とか
price-change:blog CodeIgniter | URIで日本語を使うのだ」様
などで語られていたため、その当時は「まあ自分で必要になった時に(上記サイトの説明にあるように)設定すればいいや」などと簡単に考えていました。

で、その設定にしてもできない(--;。最終的に自分で使う分には問題ない程度までは解決しましたが、果たしてその解決方法が正しいのかどうかはわかりません(毎度のことながら)。そんなわけでここに書いてある方法はあてにはなりませんが、一応メモっておきます。

まず何をしていたかというと、上に書いたように五十音パッド?なるものを作成していました。要するに「あいうえお〜」を並べておいて、例えば「あ」をクリックしたら、「あ」で始まる旧市町村を検索する〜みたいな感じです。(参項↓こんなやつ)
Wikipedia - Category:鎌倉時代の武士

でformは使わずanchorにしたかったので当然URIは
http://localhost/class/method/parameter

みたいになるわけですが、どういうロジックにせよparameterの部分にはどうしたって、[あ-わ]の平仮名五十音が入りそうな予感だったので、まずは適当なメソッドをでっちあげて
http://localhost/gappei/tekitou/あ(※実際は"%E3%81%82")

のように送信?してみました。

すると案の定
The URI you submitted has disallowed characters.

となり正常に動作しません。

ただここまでは上述の通り想定の範囲内だったので、まずはgoungoun技術系雑記帳様で解説されていたようにconfigの$config['uri_protocol']をAUTOからREQUEST_URIに変更してみました。

すると送信とか以前にメインページ自体が表示できなくなってしまいました。おそらくは自分のところの環境がcodeigniterをサブディレクトリ(アパッチのDocumentRootより深い場所)に置いていたりとかなんとか…そんなのが影響してるっぽかったので、この方法は却下しました。
(※誤解を招く恐れがあるので明言しておきますが、決して上記サイト様の情報が誤っているとか、上記サイト様に文句を言っているとかいうわけではありません。あくまでもウチの環境ではできなかったという話です。)

で、仕方がないので次にprice-change:blog CodeIgniter | URIで日本語を使うのだで解説されていた方法を試してみました。

$config['permitted_uri_chars'] の正規表現?みたいな部分に日本語を付け足す?という感じの方法なのですが、自分の場合は漢字は必要なく今回は平仮名だけが通ればいいので、「ぁ-ん」だけを付け足してみました。

すると
preg_match() [function.preg-match]: Compilation failed: range out of order in character class at offset 5

というエラーがでました。

そこでよくよく考えて…というかフト思ったのが、実はうちの環境は文字コードは全てUTF-8に統一する方向でApacheもPHPもMySQLも設定していたのですが、codeigniterのファイルの文字コードってなんだったっけかな?と。それでconfig.phpの文字コードを見てみたらSJISになっていました!まあ元ファイルには日本語は一切含まれていないので何でもよかった?んでしょうが、どうやら「ぁ-ん」←コレを記述したことでアレな感じだったので、UTF-8で保存し直しました。

すると今度はすんなり通りました!よしよしと思いつつ「あ」から始まってランダムに「き」とか「せ」「ぬ」「ほ」…とクリックして、ほぼOKだろなどと考えつつ「む」をクリックした瞬間…
The URI you submitted has disallowed characters.

(TT(TT(TT(TT(TT

わけがわからず、とりあえず気を取り直してもう一度、今度はちゃんと「あ」から一文字づつ順番にクリック。でも「み」までは大丈夫なのに「む」は相変わらずダメ。その後に続く「め」から「わ」はOK。

さっぱりわからないので、まずは手がかりとして文字コードと思い、「む」のURLエンコードを見てみると「%E3%82%80」。むむむむ、なんか臭い。特に末尾の%80ってのが気になる、非常に気になる。なんで気になったかというと、その昔SJISのダメ文字が確か80がどーのこーの…。

後で調べてみるとダメ文字は0x5Cが含まれている文字で、80は関係ありませんでしたが、それでもこの%80が怪しいと睨んだのは、あながち的外れなことでもなく、「あ」から「わ」の文字の中に%80という部分を含む文字は「む」だけだったわけです。

で、試しに
「%E3%83%80」=「ダ」カタカナのダ
「%E3%80%82」=「。」句読点
「%E3%80%83」=「〃」繰返しの記号?
の三文字についても同じように調べてみたところ、全部同一のエラーがでました。隣の「ヂ」とか「、」などはOKでした。

そこで今度は$config['permitted_uri_chars'] の「ぁ-ん」の隣に「む」と記述してみました。「ぁ-んむa-z 0-9~%.:_-」

すると今度は「む」もすんなりと通りました。不思議なことに「ダ」「。」「〃」も通ってしまいました。

う〜む、謎は深まるばかり…。なんとなく%80がダメっぽい気もしないでもないですが、どうも100%そうとも言い切れないような、なんかモヤモヤした気分。そこでUTF-8についてもう少し調べていると、なんかpreg_matchに関して書いてあって、preg系の関数にUTF-8を通すには/uオプションをつけることとかなんとかなってる。

なんだ「/u」って、とおもいPHPのマニュアルを読む。
パターン修飾子 -- 正規表現パターンに使用可能な修飾子
u (PCRE_UTF8)
この修正子は、Perl 非互換な PCRE の機能を有効にします。パターン 文字列は、UTF-8 エンコードされた文字列として処理されます。 この修正子は、UNIX では PHP 4.1.0 以降、Win32 では PHP 4.2.3 以降で 使用可能です。 また、PHP 4.3.5 以降では、パターンの UTF-8 としての妥当性も確認されます。

ふむふむ…ってこんなのプロプログラマじゃなきゃ知らんがな(--;。ってか自分preg_matchとか使ってないし、使ったのはcodeigniter書いた人じゃん。などとぶつぶつ言いながら、↑の方でpreg_matchのエラーが出ていたファイル=/system/libraries/Router.phpの408行目付近を見てみる。

するとなにやらpermitted_uri_charsをごにょごにょしてるっぽいところでpreg_matchがあった。んですでにi修飾子が書かれてる。なのでおもむろにその隣にuって書いて保存。
if ( ! preg_match("|^[".preg_quote($this->config->item('permitted_uri_chars'))."]+$|i", $str))
     ↓
if ( ! preg_match("|^[".preg_quote($this->config->item('permitted_uri_chars'))."]+$|iu", $str))

こんなことしていいのかどうかは全く定かではない。でもとりあえず上で「ぁ-んむ」としていた個所を「ぁ-ん」に戻して再度テスト。

「あ」:もちろんOK
「む」:なんとOK!
「ダ」:エラー
「。」:エラー
「〃」:エラー

というわけで、一応「ぁ-ん」と指定していて、その文字は通ってそれ以外はエラーになったので、一応期待された動作ってことで終了!UTF-8で運用?している人には参考になるかもしれない。漢字も通したい場合は上記のサイト様のところの書式が参考になるとおもいます。但しおそらくその場合もpreg_matchには/u修飾子は必要そうな気がする(漢字は試してないっす)。だけどコアファイル?をいじってるのでくれぐれも自己責任で〜。
ラベル:codeigniter
posted by ciallost at 22:54| Comment(1) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

2007年11月19日

CodeIgniter:ソート機能を付けてみる

前回はチェックボックスの実装?をしてみましたが、今回はテーブル出力された表にソート機能を加えてみたいとおもいます。

できあがりイメージとしては、表の項目部分(つまり<th>タグで出力されているところ)をクリックするとその項目での昇順/降順でソートされる…のようなよくあるパターンのやつです。

手順的には、まずクリックされた項目などからデータベースのどのフィールドをどの方向にソートするかという情報をセッションデータに格納し、index()ではセッションデータを読み出して、データベースクエリにORDER句を用いてソートされたデータを出力…といった感じにします。

そこでまずModelのControllerから呼ばれるgetCountとgetDataにソート項目とソート方向の要素ををもつ配列$sort_keyという引数を一つ増やします。
function getCount($criteria, $sortkey)
function getData($offset, $limit, $sortkey)

次にクエリ組立部分に$sortkeyを使ってORDER句を加えます。
$this->db->orderby($sortkey['field'], $sortkey['order']);

次にControllerにsort()メソッドを追加します。
function sort($field='nid')
{
//ソート方向セット
$order='asc';
$now_order = unserialize($this->phpsession->get('sort_key'));
if ($field == $now_order['field'] && $now_order['order'] == 'asc')
{
$order = 'desc';
}
//ソート条件セット
$this->phpsession->save("sort_key", serialize(array('field'=>$field, 'order'=>$order)));
redirect('gappei/index/');
}

基本的にはsort_keyというセッションデータを格納しているだけですが、ソート方向を決めている部分が若干汚い感じになってしまいました。ロジックとしては基本的には昇順でフィールド名が前回ソートされたフィールド名と同じで向きが昇順だった場合のみ降順に設定します。つまり同じ項目を続けて2回クリックした場合は降順になるということです。そしてindex()で読み出します。
//ソート条件取得
$data['sort_key'] = array('field' => 'nid', 'order' => 'asc');

if ($this->phpsession->get('sort_key') <> '')
{
$data['sort_key'] = unserialize($this->phpsession->get('sort_key'));
}

次にViewですがこれまでは<th>タグを直に打っていましたが、アンカーやソート方向のマークなどを出力しなければならないので、別途関数を作って出力する形に書き直してみました。
function setTableHeading($th, $sort)
{
$th_tag = '';

foreach ($th as $field => $column)
{
$th_tag .= "<th>";
$th_tag .= anchor('gappei/sort/'.$field, $column);
if ($field == $sort['field'])
{
$order_mark = $sort['order']=='asc' ? '▽' : '△';
$th_tag .= " <span id=\"order_mark\">".$order_mark."</span>";
}
$th_tag .= "</th>\n";
}

return $th_tag;
}

引数は実際のデータベースのフィールド名をキーにテーブル出力する際の項目名を値とした連想配列、及びソートフィールドとソート方向を持った連想配列の2つの配列になります。

基本的にはCodeIgniterのanchorメソッドを使ってsort()メソッドを呼び出すリンク付きの項目名を出力しているだけです。ソートに使用されている項目名の場合はソート方向を表すマークも出力します。

そして今まで<th>タグを書いていた所から、上の関数を呼び出して出力する…みたいな感じです。
setTableHeading(array('pref'=>'都道府県',
'nc'=>'新市町村',
'oc'=>'旧市町村',
'gdate'=>'合併日'),
$sort_key);

で、これで一応動作はしますが、旧市町村をソートキーにした場合はうまくありません。これは発行しているクエリの問題で、要するに内側のサブクエリ時点でLIMITをかけている為ORDER句も基本的にはサブクエリ内に書く必要がありますが、旧市町村はサブクエリ時点では拾ってきていないので、その場合だけ外側のクエリでもORDERをかける必要があるということです。なので…

Model(外側のクエリ組立部分)
//旧市町村でソートする場合
if ($sortkey['field'] == 'oc')
{
$this->db->orderby($sortkey['field'], $sortkey['order']);
}

という感じでソートキーが旧市町村の時だけORDER句を生成するようにします。

あとここまでのコードだと一度項目をクリックしてしまうとリセットをかける手段がないので、新たに検索ボタンが押された場合はソート条件をリセットするようにsearch()メソッドに…
//ソート条件初期化(ORDER nid asc)
$this->phpsession->save("sort_key", serialize(array('field'=>'nid', 'order'=>'asc')));

を追加します。これは実際にはリセットというよりnid(=新市町村のID)で昇順にソートしています。

表示結果
sort.png

なんとなく駆け足で実装してしまいましたが、果たしてこんなのでいいのか?という不安はどこまでいってもついて回ります(--;特にViewファイルにfunctionを作ってそれを呼び出しているところなんかは、本当にここに書いてもいいのか?感がひとしおです。でも一応こんな適当な実装でも動くってことで…。
ラベル:codeigniter
posted by ciallost at 20:45| Comment(2) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

2007年11月18日

CodeIgniterでcheckboxを作ってみる

前回までで検索フィールド×3、表示件数変更ドロップダウンメニュー、検索結果のページングを実装した、なんちゃって検索アプリ風味(謎)なモノができあがりました。前回と前々回は多少詰め込みすぎた?というかBlogが若干長めになってしまいましたが、これには一応わけがあって、実は前回までで実際に作っている(手続き型スクリプトをCodeIgniterで書き直している)プログラムにBlogが追いついたのです。

なので今回からはネタにしているこのサンプルプログラム的には新機能の追加ということになります。新機能といっても、大元の(本来こっちをとっととCodeIgniterで書き直したい謎の)スクリプトには実装されているようなごく一般的なもので、CodeIgniterで書くにはどうしたらいいだろう?といったことを、今回からはスクリプト作成と平行してBlogに綴っていく予定です。

ということで今回はcheckboxです。DropDownFieldと同じくGUI部品だけ作ってもアレなんで、今回は表示モード?を切替えてみたいとおもいます。

これまでは都道府県・新市町村・旧市町村・合併日というカラムのテーブルを表示していましたが、新たに新市町村・合併日だけのカラムにした簡易表示?みたいなモノを出力することにして、それと通常表示?詳細表示(つまり今までの表示ですが)を切替える機能を加えます。

表示イメージ
disp_simple.png
まあこの表示がユーザにとって必要かどうかということはさておき(サンプルですから)、とにかくやりたいことは、表示状態?をcheckboxで切替えるってことです(^^;

そこでまず「表示」ということで前回の表示件数変更を切替えた際のformを利用しようと考えました。利用というか同じformにcheckbox作って、検索条件をセッションに格納した時と同じように配列にしてcookieに入れて…と考え実際に書いてみましたが、結論から言うとそのやり方では二重に失敗?しました(--;

失敗した経緯を詳しく書こうともおもったのですが、無駄に長くなるので、どこがダメだったかだけメモしておきます。

まずセッションで配列を格納する際にはserialize()を使っていましたが、cookieではこれがどうもうまく機能しなかったのです。具体的にはpost配列をserialize化することは強引に?できるみたいなのですが、それを読み出す時点で、もちろんunserialize()を使いますが、何故かうまくいきませんでした。

またPHPのマニュアルによると…
クッキー名で配列を記述することにより、 クッキーの配列を設定することも可能ですが、複数のクッキー がユーザーのシステム上に保存されることになります。 explode() を使用して ひとつのクッキー上に複数の名前と値をセットすることも 考慮してください。serialize() の使用はセキュリティーホールになり得るため、 この目的のために使用することは推奨されません。

ということなので、いずれにしろserializeは却下されました。

そして単独で、つまり表示件数の数値とは別に、cookieに保存してやればまあいいだろうとおもい、そのようなコードを書いたのですが、今度はcheckbox特有の動作によってうまくいきませんでした(--;。

この「checkbox特有の動作」はおそらくwebのプログラムを書いた人は一度はハマルんではないかとおもいますが、要するに…
checkboxがチェックされていないときはpostデータが送られてこない

という仕様のことです。

もう少し具体的にいえば、例えばテキストフィールドの場合だと<input type="text" name="hoge"〜のようになっていれば例え入力が空でも$_POST['hoge']という変数自体は存在するわけですが、これがチェックボックスだと<input type="checkbox" name="hage"〜のようになっていても、チェックされていない場合は$_POST['hage']という変数自体がないわけです。

なので不用意に if ($_POST['hage'] == TRUE)〜のようなコードを書いてしまうと、そもそも$_POST['hage']がないので、hageって何?みたいなエラーになってしまうわけです。

そして性質が悪い?というかやっかいなのは、↑のようになる場合はあくまでもチェックが外された状態でサブミットされた時であって、チェックを付けてサブミットされた時は何の問題もなかったかのように動いてしまったりすることです。

チェックボックスのこのような挙動は一見不合理なように見えますが、実は複数のチェックボックスをラジオボタン的に使う場合などは便利かもしれません。ラジオボタンだとどれか一つは必ず選択状態になりますが、チェックボックスだとどれも選択しないということができるからです。

ただ今回の場合のように単に真偽値だけを受取りたい場合は、その他のform部品とはちょっと違った挙動だということを考慮しないと、はまる原因になったりします。(ってか実際はまってます)

というわけで冗長的な感がないでもありませんが、メソッドを一つ作ることにして、前回の表示件数とは別に処理するような形にしました。またViewをヘッダ部・フォーム部・メイン部・フッタ部に分けて、共通部分はView内で別のViewを呼び出す形にし、新たにメイン部を2つ作りそれを切替えるようにControllerに記述しました。

View
<?$this->load->view('header');?>
<?$this->load->view('form');?>
<hr>
<?if($count > 0):?>
※この部分でデータをテーブルタグで出力
※gappei_detail.phpとgappei_simple.phpの2通りのViewを作成
<?else:?>
<p>該当データがありませんでした。</p>
<?endif;?>
<hr>
<?=$pagination?>
<?$this->load->view('footer');?>


Controller
function changeDispMode()
{
set_cookie('dispmode', $this->input->post('dispmode'), Gappei::COOKIE_LIFE);
redirect('gappei/index/');
}

結果的にchangeDispNumber()とほとんど同じになっていますが、挙動はかなり違います。ポイントは$_POST['dispmode']を直接受取らないで、CodeIgniterのライブラリ経由にすることで変数が存在しなくても(チェックが外されてpostされた場合)エラーにはならないという部分です。さらに(チェックが外されてpostされた場合は)空のデータをcookieにセットしているので、cookie自体が消去されます。つまりtureとかfalseとかの値がdispmodeというcookieに格納されるのではなく、falseの場合はそもそもcookieが削除されるということです。
//表示モード取得
$data['dmChecked'] = (get_cookie('dispmode') <> '') ? get_cookie('dispmode') : FALSE;

//表示viewロード
if ($data['dmChecked'] == TRUE)
{
$this->load->view('gappei_detail', $data);
} else {
$this->load->view('gappei_simple', $data);
}
ラベル:codeigniter
posted by ciallost at 15:02| Comment(0) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

2007年11月17日

CodeIgniter:DropDownFieldとcookie

前回は検索フィールドを複数化するとともに、細かい部分を修正しました。で、エラーは出ないなぁと思っていたところ、サーバ側に保存されているセッションデータを手動で消去して、ブラウザをリロードしてみたら…

error.png

こんなエラーが!要はセッションデータがないので$data['criteria']がワカンネーヨ!ということなので、初期化することにしました。

Controller
$data['criteria'] = array('nc'=>'', 'oc'=>'');

これで一応は大丈夫なのですが、配列のキーを指定してやらなければならないところがちょっと自分的には気に入らないっつーか、なんか他にどうにかならないもんですかねぇ?

それはさておき、前回は旧市町村名で検索できるようにしたわけですが、ついで?に都道府県名でも検索できるようにしたいとおもいます。

View
都道府県:<?=form_input('condition[pref]', $criteria['pref'])?><br />

Controller
$data['criteria'] = array('pref'=>'', 'nc'=>'', 'oc'=>'');

これだけで動きます(^^)v が、↑の初期化コードさえなくなれば、formにテキストフィールド作っただけでOKだったのにな…。

pref.png

まあ当初の状態よりかなりマシになってきたので、↑の問題はおいおい考えることとして、今回はDropDownFieldを作ります。

で、DropDownFieldってなんぞ?なんですが、ユーザガイドにそう書いてあったのであえて使ってます。これって一般名称ないんですかね?PullDownList?DropDownMenu?PullDownMenu?なんかわかんないけどそんな感じのやつです。要するにselectとoption羅列みたいな…。

んでGUI部品だけ作ってもしょうーもないんで、1ページの表示件数をユーザが選択できるようにしてみたいとおもいます。ユーザガイドによると…
form_dropdown(名前, 選択項目リスト, 選択済み項目, javascript)
みたいな感じらしいので、とりあえずViewに
<?=form_open('gappei/changeDispNumber')?>
<?=form_dropdown('dispnum', $dnOptions, $dnSelect, 'onChange="submit();"');?>
<noscript><?=form_submit('changeDisp', '変更')?></noscript>
</form>

新たなformとして作成します。formの送信先?はchangeDispNumberというメソッドを呼ぼうって魂胆です(^^;一応javascriptで選択されたらサブミットという動きにしましたが、非javascript環境も考慮して<noscript>タグにサブミットボタンを配置しました。

で、Controllerを作る前に悩んだんですが、結局この表示件数というパラメータもどこかに保存しておかないと、検索の時に散々悩んだ検索条件の保存と同じような現象(つまり選択された直後はその表示件数になっても、ページングリンクとか検索サブミットがなされた瞬間リセットされてしまう)が起きるだろうということです。

んで、検索条件はPHPのセッション機構でやりましたが、この表示件数というパラメータを考えたときに、セッションよりもクッキーの方が向いてる?というかユーザ側で保存(しかも比較的長期)した方がいいんじゃないの?と考えました。実際多くの検索エンジンはそうなってると思います。

というわけでCodeIgniterのcookieヘルパーをざっと調べて、ほとんどPHPの組込だな!とかおもって、おもむろにController
$this->load->helper('cookie');

まずヘルパーロード。これはコンストラクタの中に。そして新しいメソッド…
function changeDispNumber()
{
set_cookie('dispnum', $this->input->post('dispnum'), Gappei::COOKIE_LIFE);
redirect('gappei/index/');
}

このメソッドは表示件数のDropDownFieldが選択されると呼び出されcookieにその値を保存します。COOKIE_LIFEはclassの上のほうで
const COOKIE_LIFE = 2592000;

のように定義しました。果たしてこんなところに書いていいのかどうかは毎度のことながら不明(不安)。

んで、後はindex()の方にcookie読み出しと選択項目リストを定義します。
//表示件数取得
$data['dnSelect'] = (get_cookie('dispnum') <> '') ? get_cookie('dispnum') : 10;

デフォルトは10件ということになります。そしてリスト…
//表示件数メニューオプション
$data['dnOptions'] = array('10'=>'10',
'20'=>'20',
'30'=>'30',
'50'=>'50',
'100'=>'100'
);

表示結果
dispnum.png

上の方はDropDownFieldで20件を選択した様子です。データもきちんと20件表示されています(画面ではわかりませんが)。んで下のHTMLソースみたいなものはページングリンクの部分です。見てのとおり20件毎にリンクが生成されているのがわかるとおもいます。
ラベル:codeigniter
posted by ciallost at 02:59| Comment(3) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

2007年11月16日

CodeIgniter:複数の検索フィールドの作成

前回、表示をテーブルタグで整形できたところで、ようやくアプリケーションらしくなってきましたが、今回はいままで新市町村名でしか検索できなかったformを旧市町村や都道府県でも検索できるように改造?してみたいとおもいます。

まず旧市町村名で検索できるようにVIEWでテキストフィールドを作ります。

VIEW
新市町村:<?=form_input('newcity', $newcity)?><br />
旧市町村:<?=form_input('oldcity', $oldcity)?>

見てのとおり新市町村のフィールドをごっそりコピーしただけです。

次にCONTROLLERのsearch()でも同じように新市町村のpostデータをセッションに格納しているところをコピペします。
$this->phpsession->save('newcity', $this->input->post('newcity'));
$this->phpsession->save('oldcity', $this->input->post('oldcity'));

さらにindex()でセッションデータを受け取って検索条件に入れているところも同様にコピペします。
$data['criteria']['newcity'] = ($this->phpsession->get('newcity') <> "") ? $this->phpsession->get('newcity') : "";
$data['criteria']['oldcity'] = ($this->phpsession->get('oldcity') <> "") ? $this->phpsession->get('oldcity') : "";

ただしこの部分は以前は$data['newcity']として受取っていましたが、配列化して$data['criteria']['newcity|oldcity']みたいな感じにしておきます。そのため下の件数取得の部分も$data['criteria']を渡すように変更しておきます。
//件数取得
$count = $this->Gdb->getCount($data['criteria']);

次にMODELのgetCount()で検索条件を受け取ってクエリを組み立てている部分を下のように修正します。これもとりあえずコピペです。
$this->db->like('nc', $criteria['newcity']);
$this->db->like('oc', $criteria['oldcity']);

これでとりあえずブラウザをリロードしてみます。おっとVIEWでエラーがでちゃいました。$data['newcity']だったものを$data['criteria']['newcity']にしたのを忘れてました。ということで…

VIEW
新市町村:<?=form_input('newcity', $criteria['newcity'])?><br />
旧市町村:<?=form_input('oldcity', $criteria['oldcity'])?>

に変更します。

oldcity.pngこれでとりあえずリロードするとエラーもなくフィールドも2つ表示されています。試しに「旧市町村=椴法華」みたいに検索してみた様子です。ちなみに新市町村「市」旧市町村「町」などのようにするとAND検索になります。ページングも問題ありませんでした。で、完成!といきたいところですが問題がなくもないです。

例えば次に都道府県名で検索したいといった時に、まあVIEWにテキストフィールドを付け足すぐらいはやるとしても、また同じようにコードを修正していかなければなりません。

そこで何かうまい考えはないものかと、もう一度上でやったことを反芻してみると、ModelクラスのgetCount()に渡す検索条件を配列にしたことに思い当たります。受取った方はまたぞろ同じようなコードをコピペしているわけですが、このあたりを修正していくことでなんかできそうな感じです。

そこでgetCount()の中で実際にWHERE句を組み立てている$this->db->like();をユーザガイドで調べてみると2番目に連想配列を使用する方法として引数に連想配列を渡しています。そしてこの場合は配列のキーが検索対象フィールド名になるわけです。

ということでまず
$this->db->like('nc', $criteria['newcity']);
$this->db->like('oc', $criteria['oldcity']);

を単に
$this->db->like($criteria);

に変更します。

でこのままだとnewcity like '%検索条件%'のようなWHERE句になってしまうので、この配列のキーを設定しているControllerの部分を変更します。
$data['criteria']['nc'] = ($this->phpsession->get('newcity') <> "") ? $this->phpsession->get('newcity') : "";
$data['criteria']['oc'] = ($this->phpsession->get('oldcity') <> "") ? $this->phpsession->get('oldcity') : "";

Viewも同様に変更します。
新市町村:<?=form_input('newcity', $criteria['nc'])?><br />
旧市町村:<?=form_input('oldcity', $criteria['oc'])?>
これでModelのgetCount()の部分のコードは多少すっきりしましたが、まだControllerは冗長的な印象です。そこでよくよく見るとセッションデータに格納しているデータが配列ではないために、いちいち冗長な記述になっているような気がします。

そこでセッションデータ格納時に配列で格納して、取り出し時も配列をforeachなどで処理するように書き換えられれば、もうすこしすっきりしそうな感じです。

そこでまずControllerのsearch()でセッションデータに格納しているところですが、postデータを受け取っているのでこれは配列になっていません。なのでまずはpostデータを配列として受取れるようにViewを変更します。
新市町村:<?=form_input('condition[nc]', $criteria['nc'])?><br />
旧市町村:<?=form_input('condition[oc]', $criteria['oc'])?>

つまりformのテキストフィールドのnameを配列にしてしまうわけです。

※尚、通常PHPで連想配列を書く場合は$array['name']のようにシングルコーテーションで括るとおもいますが、↑のコードで"condition['nc']"のように書いてしまうと、Disallowed characterみたいなエラーが出てしまうので上記のようになっています。理由はわかりません(--;

次にセッションデータに格納している部分を
$this->phpsession->save('search_key', serialize($this->input->post('condition')));

のようにpostの配列を渡すようにします。ですがそのままだとまずいのでserialize化します。

さらにControllerでセッションデータを読み出している部分を以下のように変更します。
if ($this->phpsession->get('search_key') <> '')
{
$condition = unserialize($this->phpsession->get('search_key'));

foreach ($condition as $key => $value)
{
$data['criteria'][$key] = $value;
}
}

これでブラウザで確認すると以下のように新市町村および旧市町村での検索ができています。
array_session.png
で、大筋では?これでいいとおもうんですが、例えば検索結果が0件だった場合の処理とか、新市町村の検索条件はあるけど旧市町村が空だった場合にも oc like '%%' というクエリが組み立てられていたりとか、微妙に気になったところを修正します。

Controllerで$countだったものを$data['count']に変更してViewで読み出せるようにします。Viewでは$countを調べて0件より多ければテーブルを出力するようにします。
<?if($count>0):?>

これで0件だった時はforeachに$datasが渡されませんので、Invalid argument supplied for foreach()みたいなエラーは出なくなります。

次にModelのクエリ組立部分でdb->like()に検索条件配列を渡していたところを
$this->db->like(array_diff($criteria, array('', NULL)));

のように変更し値がない要素は出力しないようにします。

array_diff.pngこの画面では旧市町村=函館市という検索条件でサブミットしていますが、新市町村は空です。以前までだとクエリには nc like '%%' のような不要な部分も組み立てられていましたが、画の通りWHERE句は oc like '%函館市%' だけになっています。

これでだいぶスッキリしてきたとおもいます。次回は1ページ毎の表示件数をユーザが変更できるようにしてみたいとおもいます。
ラベル:codeigniter
posted by ciallost at 14:45| Comment(0) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

CodeIgniterのHTMLテーブルクラス

前回はModelを作成してコードを分離しましたが、力尽きて長くなりそうだったので、VIEWでテーブルに整形するところまでは書けませんでした。今回はデータをTableタグで整形表示してみようとおもいます。

単純にTableタグをベタ打ちしてもいいのですが、せっかくHTMLテーブルクラスなるものがあるので、それを試して見ます。

まずgenerateというそのものずばりみたいなものがあるので、前回までのコードで$datas(データ内容が多次元配列化されたモノ)を単純にgenerateに渡してみます。

VIEW
$this->table->generate($datas);

表示結果
generate.png
おお!ちゃんとテーブルだ!(当たり前か?)で、よく見ると、データの1行目が見出しタグ<th>になってしまっています。これはページ移動しても必ず1行目はそうなってます。

それとまあ当然といえば当然ですが、旧市町村のところがArrayになってしまっています。

というわけでまずthを指定してみました。

VIEW
$this->table->set_heading('ID', '都道府県', '新市町村', '旧市町村', '合併日');

表示結果
table_header.png
今度はきちんとテーブルヘッダが出力されました。

で、旧市町村部分ですが、結論から言うとCodeIgniterのHTMLテーブルクラスでは対応が困難なため、foreachで自力?で出力することにしました(--;

まあ<td rowspan="5">とかやらなきゃいけないので、汎用的なクラスではできなくてもしょうがないとおもいますが、もしかしたらテーブルクラスを使って作る方法があるかもしれません。

ユーザガイドを一通り見たところでは、テンプレートを使ってごにょごにょすればなんとなくいけそうかなぁ?というところまでは見えましたが、どうもこのデフォルトのテンプレートは、よくある奇数行と偶数行の交互に色違いみたいなのを前提としているようで、そうした用途以外でテンプレートを使おうとするとクラスを継承して〜とかなんか難しそうなので止めました(--;

で、自力出力
<table border="1" cellspacing="0" cellpadding="2" width="600">
<tr>
<th>都道府県</th>
<th>新市町村</th>
<th>旧市町村</th>
<th>合併日</th>
</tr>
<?foreach($datas as $row):?>
<?$rowspan=count($row['ocity'])?>
<tr>
<td rowspan="<?=$rowspan?>"><?=$row['pref']?></td>
<td rowspan="<?=$rowspan?>"><?=$row['ncity']?></td>
<td><?=($ocity=array_shift($row['ocity']))?$ocity['oc']:NULL?></td>
<td rowspan="<?=$rowspan?>"><?=$row['date']?></td>
</tr>
<?foreach($row['ocity'] as $oc):?>
<tr>
<td><?=$oc['oc']?></td>
</tr>
<?endforeach;?>
<?endforeach;?>
</table>

最初のテーブルヘッダまでの部分は単なるHTMLタグです。この部分だけはHTMLテーブルクラスで作れるかな?ともおもったのですが、最終的にgenerateを呼ばなきゃならないみたいなのでダメでした。なのでベタ打ちです。

次の部分からが内容出力です。foreachを入れ子にして旧市町村部分を出力する形になっています。まず$rowspanに旧市町村の数を入れて、都道府県・新市町村・合併日のカラムではその値でrowspan(上下の行をくっつけて表示)しています。

一般的にはこの$rowspanは0や1の値になる可能性があるかとおもいますが、今回使用しているこの合併データベースでは絶対に2以上になるので、その辺のロジックは端折ってます。

ちなみにHTMLタグ上でrowspan=1とやってもFirefox2では問題なくレンダリングされました。おそらく他のブラウザも問題ないと思います。0の場合はどうなんでしょうか?試してないのでわかりません(--;

んでこの次が問題なのですが、HTMLのテーブルタグの仕様ではrowspanされてないカラム(つまり旧市町村)も最初の一行はココに書かなきゃならないんです。(って文章だと全然わかりませんね)
<tr>
<td rowspan="5">(都道府県)</td>
<td rowspan="5">(新市町村)</td>
<td>ココに旧市町村の1行目を書く必要がある</td>
<td rowspan="5">(合併日)</td>
</tr>

しかも2行目からはこの下に続けて<tr>タグで書いていかなければならないわけです。

この仕様はプログラム的にはめんどくさいというか、複雑になりやすいというか…。例えば↑のココに旧市町村の1行目を書く必要があるという部分に入れ子の<tr>みたいな(なんじゃそりゃ?)形で書けたりとか、逆にその部分にはとりあえずダミーっつーかマーカ?っつーかなんかそんな感じのものだけ書いておいて、本データは下に続けて書くとかいう仕様だったら簡単だったんですが…。

まあ今更HTMLの仕様に文句いっても始まらないんで、苦肉の策というのか、正直言えばやり方がわからなかったんで、array_shiftです(--;。

array_shiftはPHPの組込関数?で配列の先頭の要素を取ってきて、元の配列からはそれを取り除きます。つまり配列が短くなるわけです。なんかこの手の関数ってのはきちんと使ってやらないとバグの温床になりそうな感じなんで、あんまり好きじゃないんですけど(何故ならきちんと使えないから…)。

ただこのおかげ(元の配列が短くなる)で、下のほうではforeachで回せるわけです。これが元の配列に影響がないと最初の1行を飛ばしてとかなんとかさらにメンドイ作業が…。

このデータベースの場合は旧市町村の数は絶対2以上(合併ですから)なことが保証されているのでこれでも動きますが、一般的な場合だとちゃんと値が入っているかとか配列なのかとかチェックしないとならないでしょうね(あ〜メンドイ)。

ということで表示結果です。
comp_table.png
ラベル:codeigniter
posted by ciallost at 12:10| Comment(0) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

CodeIgniterでModelを作ってみる

前回まででようやく検索結果のページング表示ができましたが、Controllerクラスが若干ぐちゃぐちゃになってきたので、ここらでそろそろModelクラスを作って、データベース関連の部分はそっちにコードを移行しようとおもいます。

で、ユーザガイドのModelのところを見ると、なにやらModelクラスを継承してはいるようですが、特に取得系や保存系?のメソッドがあるわけでもなく、特定のテーブルと結びついてるというわけでもなく、継承元のModelクラスみたいな?のがlibrariesフォルダにあったので(Model.php)チラ見してみましたがMagicメソッドがどーのこーのでわけがわからず(--;

仕方がないので(謎杉)modelsフォルダの中にgappei_model.phpというファイルを作ってみました。
class Gappei_model extends Model {
function Gappei_model()
{
parent::Model();
}
}

果たしてこんなのでいいのか?という毎度の不安の中、とりあえずdbから件数を取得するコードとデータを取得するコードを適当なメソッドをでっちあげて書きました。

件数取得
function getCount($criteria)
{
$this->db->select('DISTINCT nid, pref, nc, yomi, gdate');
$this->db->from('cities');
$this->db->like('nc', $criteria);

$count = $this->db->get()->num_rows();

return $count;
}

検索条件を引数に件数を返すメソッドです。検索条件は今のところ新市町村のみですのでlike節?は決め打ちっつーか'nc'で固定です。

次に結果データ取得メソッドですが、VIEWの方もいい加減print_rでは見難いので、多次元配列に格納して、VIEWでテーブル表示にしてみたいとおもいます。ということでまず結果を多次元配列に格納して返すプライベートメソッドを定義します。
private function formatData($result)
{
$data = '';

//nidをキーに多次元配列化
if ($result->num_rows() > 0) {
foreach ($result->result_array() as $row)
{
$data[$row['nid']]['nid'] = $row['nid'];
$data[$row['nid']]['pref'] = $row['pref'];
$data[$row['nid']]['ncity'] = $row['nc'];
$data[$row['nid']]['ocity'][$row['oid']]['oid'] = $row['oid'];
$data[$row['nid']]['ocity'][$row['oid']]['oc'] = $row['oc'];
$data[$row['nid']]['date'] = $row['gdate'];
}

$data = array_values($data); //歯抜けキーを連続に
}

return $data;
}

次にControllerから呼ばれる(予定の)データ取得メソッド
function getData($offset, $limit)
{
//サブクエリ組立
$subQuery = "(".$this->db->last_query()." LIMIT $offset, $limit) AS vn";

//結果取得SQL
$this->db->select('nid, pref, nc, o.id oid, o.city oc, gdate');
$this->db->from($subQuery);
$this->db->join('newcities_oldcities j', 'vn.nid = j.newcity_id');
$this->db->join('oldcities o', 'o.id = j.oldcity_id');

//結果取得
$formattedData = $this->formatData($this->db->get());

return $formattedData;
}

オフセットとリミットを引数にして多次元配列に格納したデータを返します。先に作ったPrivateFunction(=formatData)を途中で呼び出しています。

FROM句のサブクエリを組み立てる際に$this->db->last_query()というメソッドを使っていますが、これは直前のクエリ(SQL文)を返します。流れとしては先に件数を取得してそれとほぼ同じ(LIMIT句を加えただけの)クエリをデータ取得で使うので、なんとなくこの二つのメソッドの間に何か別のクエリが実行されたらヤバゲじゃん?な感じですが、とりあえずこれでよしとしておきます(^^;。

次にControllerですが、まずmodelクラスをロードして、dbのクエリ組立部分をごっそり削除します。そして新たにmodelクラスのメソッドを呼び出すようにします。
class Gappei extends Controller {
function Gappei()
{
parent::controller();
$this->load->library('phpsession');
$this->load->helper('form');
$this->load->helper('url');
}

function search()
{
$this->phpsession->save('newcity', $this->input->post('newcity'));
redirect('gappei/index/');
}

function index($offset=0)
{
//モデルロード
$this->load->model('Gappei_model', 'Gdb');

//ページャーロード
$this->load->library('pagination');

$data['title'] = "平成市町村合併";
$data['heading'] = "平成市町村合併";

//セッションから検索条件取得
$data['newcity'] = ($this->phpsession->get('newcity') <> "") ? $this->phpsession->get('newcity') : "";

//件数取得
$count = $this->Gdb->getCount($data['newcity']);

//データ取得
$data['datas'] = $this->Gdb->getData($offset, 10);

//ページング
$config['base_url'] = 'http://localhost/mount_u/codeigniter/index.php/gappei/index/';
$config['total_rows'] = $count;
$config['per_page'] = '10';

$this->pagination->initialize($config);

//ビューロード
$this->load->view('gappei_view', $data);

//デバッグ
$this->output->enable_profiler(TRUE);
}
}

Modelをロードしている所の第2引数はModelの別名です。$this->Gdb->getCount()のように使用できます。セッションから検索条件を取得している部分は三項演算子で書き直しています。

またデータ取得の部分で、以前は$data['query']に格納してVIEW側で$query->result_array()みたいにしていましたが、すでにModelで多次元配列を返すようにしましたので、$data['datas']に格納することにしてVIEW側では単に表示処理(具体的にはHTMLのTableタグ付け)だけにします。

VIEW
<pre>
<?foreach($query->result_array() as $row):?>
<?=print_r($row, 1)?>
<?endforeach;?>
</pre>
 ↓
<pre><?=print_r($datas, 1)?></pre>

そして表示結果
model.png
ってか結局まだprint_rじゃん(--; ってなわけで次回はテーブルに整形するところから…。
ラベル:codeigniter
posted by ciallost at 01:34| Comment(0) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

2007年11月15日

CodeIgniter:function search()の作成

前回なんとか検索結果をページングするところまで辿り付きましたが、セッションを利用すればいいということがわかったのもつかの間、また落とし穴がありました(--;。

前回のスクリプトで例えば「市」と検索します。検索結果が10件表示されます。ページングもバッチリOKです。次に「町」と検索します。これも検索結果表示・ページングともOKです。次にリセットしようとおもい、テキストフィールドの「町」の文字を消去してサブミットします。あうあう(--; なんと「町」で検索されちゃってます。

この動作はうまくない。まあテキストフィールドが一個の場合はこれでもいいような気もしますが、この後旧市町村とか都道府県とか検索フィールドが複数になってくると、もはやこれではアレな気がします。

ちなみにここのBlogの右上にある検索フィールドで同じようにフィールドを空欄にしてサブミットしてみました。あうあう(--; 以前の検索語句が検索されるようなことはありませんでしたが、レイアウトが…。こんなんでいいのか?>seesaaさん

まあ他人のことをとやかく言う前に自分の方をなんとかしましょう。ということで問題点を考えてみます。
$newcity = $this->input->post('newcity');
if ($newcity <> "")
{
$this->phpsession->save('newcity', $newcity);
}

前回のこの部分のコード、つまりpostデータを調べて空でなければセッションデータに格納しているところです。

空のpostがサブミットされた(なんか言い回しおかしい?)場合は、上記のif文によってセッションデータには格納されません。なので次行で検索語句をセッションから読み取っている部分では、以前の検索語句が読み取られてしまい、上述のような結果になってしまったわけです。

ではpostの有無に関わらず(つまり空の場合でも)セッションデータに格納してしまえばいいのか?というとそういうわけにもいきません。

何故なら最初に検索語句をサブミットされた時点ではそれでもよいのですが、次にページングリンクをクリックされた時にはpostデータは空なわけですから、if文がないと空のデータがセッションデータに格納され、次行でそれを読み出されて…結局ページングのためにセッション使ってる意味ないじゃん!な状態になってしまうわけです(--;

ではどうすればいいか?ここで問題をもう少し整理すると…
  • 最初に検索語句がサブミットされた場合は、postデータがあるので、セッションデータに格納され、それを読み出して正常に動作します。

  • この状態でページングリンクをクリックすると、postデータは空なので、セッションデータには格納されず、以前のセッションデータを読み出して正常に動作します。

  • 空のpostデータがサブミットされた場合は、postデータが空なので、セッションデータには格納されず、以前のセッションデータを読み出してしまいます。これは期待された動作ではありません。


この3番目の場合は空のpostデータをセッションデータに格納して欲しいわけです。ですがプログラム側ではpostが空かどうかだけで判断しているので、ページングリンクをクリックした結果postが空なのか、空のデータをサブミットした結果postが空なのかわかりません。

ということはpostの有無で判断するのではなく、サブミットの有無(サブミットボタンが押された場合)で判断すればうまくいきそうです。といってもif (submit_button = true)なんて書けません。そこでそもそもサブミットされた時には何が起きているか見てみます。

VIEW
form_open('gappei/index')

VIEWにこのように書いてあります、これはformヘルパーを使っていますが最終的に出力されるHTMLでは
<form action="http://localhost/codeigniter/index.php/gappei/index" method="post">

つまりCodeIgniterのルーティング機能によって?GappeiクラスのIndex()メソッドが呼ばれているわけです。

ということはGappeiクラスに別のメソッド(例えばsearch())を作ってそのメソッドを呼び出すようにしてやれば、サブミットボタンが押された場合と、ページングリンクがクリックされた場合(この場合はindex()を呼び出す)とを分けることができそうです。

というわけで…
function search()
{
$this->phpsession->save('newcity', $this->input->post('newcity'));
$this->index();
}

まず↑の方に書いたpostの有無を調べてセッションデータに格納している部分を消去します。そして新たにsearch()というメソッドを作り、そちらにセッションデータに格納のコードを書きます、ただし今度はpostの有無は調べずに空だろうとなんだろうとセッションデータに格納します。んで表示しなきゃなんないのでindex()を呼び出します。

VIEWの方も
form_open('test/index') //これを↓のように…
form_open('test/search') //に書換え

あと前回のコードではライブラリをロードするコードをindex()の中に書いていたので、それをコンストラクタの中に書くようにします。
class Gappei extends Controller {
function Gappei()
{
parent::controller();
$this->load->library('phpsession');
$this->load->helper('form');
}

というわけでブラウザで確認。「市」で検索してみる。検索結果・ページングOK。「町」で検索してみる。検索結果・ページングOK。んでフィールドを消去してサブミット。全件表示!(フィルタリングなし)ということで一応できあがり。

ちなみに↑のsearch()の中で$this->index()とやっている部分は、なんとなくダサかったっつーか、それなりの機能があるってことで
URLヘルパーをロードして
$this->load->helper('url');

リダイレクトしてやります
redirect('gappei/index/');

これでOK!
ラベル:codeigniter
posted by ciallost at 17:00| Comment(0) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

CodeIgniterのセッションクラス

前回は愚痴ばかりでしたが、未だにそれが標準的な実装なのかどうかはわからないものの、CodeIgniterで検索結果をページネーションクラスを使ってページングするためにはおそらくセッションを使うしか方法がない、というところに辿り付きました。前回も書きましたがこれ以外に何かよい方法、普通はこうするだろみたいな方法がありましたら是非教えて欲しいです。

で、今回はセッションクラスを使ってみようとおもいます。自分はだいたい新しいことを試す時はユーザガイドを読むよりも先に、ネットで検索します。そうすると意外に早く理解できたりといったことが多いので…。

CodeIgniterのセッションも検索してみました。するといきなりこんな情報が…CIの標準のsession機能は、何とcookieに全てのデータを入れる仕組み。う〜む、これってどうなんでしょう?

試しに今まで作ってきたスクリプトではなく単にセッションだけのスクリプトを書いてテストしてみました。そしたら確かにクッキーに全情報が入ってました。まあシリアライズ?というのかぱっと見ではよくわからなかったりもしますが。

すると上記ページにPHPのセッションを使ったものがWikiに置いてあると書いてあったので今度はそれを試してみます。

まずFILESというところのコードをコピーしてファイルを作り指定のディレクトリへ…、ってapplication/init/ってないじゃん(--;わからないけど作っちゃえ。んでそこにinit_phpsession.phpとして保存。phpsession.phpはlibirary(これはある)フォルダに保存。

あとは普通のライブラリのように
$this->load->library('phpsession');

でロードして、Exampleのように使う。

すると、今までクッキーに全部入っていたデータが、クッキーにはセッションIDだけになりサーバ側の指定ディレクトリ(デフォルトだとtmpとかか?XAMPPなのでイマイチわからない)にそのセッションIDと同じファイルができてる。中を見るとちゃんとデータが入ってます。

ってこれはPHPのセッション動作そのものですね。自分は前に使ったことがあったので、なるほどこれでできるのかと思いましたが、セッション機構そのものがはじめての人はわかりづらいかもしれませんね。

それはともかくこれで普通のPHPセッションが使えることが判明。これを作ってくれた人に感謝。そしてこれで検索条件が保存できるはず。ということで実装。

POSTデータを直接受取っていた部分
$data['newcity'] = $this->input->post('newcity');

を一旦セッションに格納してから取ってくるように変更
$newcity = $this->input->post('newcity');
if ($newcity <> "")
{
$this->phpsession->save('newcity', $newcity);
}

$session_newcity = $this->phpsession->get('newcity');
if ($session_newcity <> "")
{
$data['newcity'] = $session_newcity;
} else {
$data['newcity'] = "";
}

これで一応検索結果をページングできるようになりました。が、また別の落とし穴が…(--;
ラベル:codeigniter
posted by ciallost at 13:08| Comment(1) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

2007年11月14日

CodeIgniter(に限らず):検索結果とページングの嫌な関係

前回、ページングが実装できた!とおもいきや実は重大な落とし穴がある、と書きましたが、今回はその辺の話を書きたいとおもいます。今回はコードは一切でてきません(愚痴ですから…)。

まずその落とし穴ですが、前回までのスクリプトでなにがしかの検索をしてみます、まあページングされないと困るので、わざと比較的多くのデータが引っかかるような語句、例えば「市」などで検索してみます。

いちいち画像は貼りませんが、一見大丈夫そうに見えます。てゆーかこの時点(1ページ目表示)ではなんの問題もありません。きちんと「市」でフィルタリングされた結果が返っています。

総件数も、ページングリンクのLASTの部分にポインタを合わせてみるとわかりますが、フィルタリングなしの時は590だったオフセット値が430に減っているので、きちんと数えられているとおもいます。

そこで次へのリンクをクリックします。表示が2ページ目に移動します。一見大丈夫そうです。が、よく見ると「日高町」とか「枝幸町」「安平町」など「市」でフィルタリングした場合は引っかからないはずの新市町村の名前が見えます。

あれれ?とおもってデバッグ表示を見てみると、POSTデータはありませんなどと書いてあるし、SQLもWHERE nc like '%%'になっています。なんでだ?とおもってもう一度リロードしたり何度もページングリンクをクリックしたり…。

でも結局検索フォームをサブミットした直後のページはよいのですが、ページングリンクを一度でもクリックしてしまうとダメです。要するに検索語句がリセットされちゃっているんですね。

まあここまで読んで「そんなの当たり前だろ!」と言える人はすでに相当プログラミングに慣れている方か、プロの方でしょう。私のように趣味でやってるような素人プログラマはこれって結構ハマルポイントなんじゃないでしょうか?(オレダケ?)

というのも…(ここから先は純粋に愚痴です)、そもそもページングする時ってどういう時なんでしょうか?静的なHTMLであれば長くなりそうだったら当然そこでファイルを分けるなりなんなりするでしょう。つまりページングが必要な場合というのはなんらかの動的なデータ、しかも条件によって長さ(というか数)が変化するようなデータを表示する時なのではないでしょうか?

そしてそうした状況は、もうほとんど検索結果表示画面なんじゃないでしょうか?それ以外だと例えば情報系?(ITMediaとかAllAboutとかCodeZineとか…)のサイトによくある一つの記事を数ページにわけて表示したりとか、Blogなどでも長い記事を数ページに分けて表示したりしているところもあるようですが、結局これらもデータベースには元々の長い記事があって、それを表示時にページングしているという点では、ある種の検索結果だとおもうわけです。

んでそういう検索結果をページングする際に、まあいろいろな方法があるとおもいますが、共通していることは検索に用いた語句なりIDなり、いわゆる検索条件といったものを保持していなければならないということです。

例えばこのBlogでエサにしている自作の汚い手続き型スクリプトでは、ページング部分で生成しているURLリンクにgetクエリ文字列として渡しているわけです。
http://localhost/gappei.php?nc=市&page=3
のような感じです。(※もちろん日本語はURLエンコードしたりしてますが…)

フレームワークと言ってよいのかどうか知りませんがPHPにはPEARというライブラリ?があります。その中のPagerなどは上記のようにformデータをgetで受け取ってる場合は特に何もしなくても上のようにクエリ文字列を付加したリンクを生成してくれます。

もちろんPEARのPagerでもformデータがpostになっている場合は、上の状況と同じようになります(--;こうした場合(formがpost)通常のHTMLだけではAリンクからformをsubmitすることができないので、<a href="javascript:document.form1.submit();">のようにjavascriptを使ったりします。

いずれにしろ検索条件を次に移動するページに伝えてあげないとならないわけですが、ここからがハマリ所でして…。

CodeIgniterで上記の方法を再現しようとすると、まずgetでformデータを送ることはできません。これはユーザガイドのセキュリティに明記されています。もちろんここにあるようにクエリ文字列オプションを有効にすれば使えますが、オフにした場合その他の影響が大きすぎて普通はしないでしょう。

そうなると次はjavascriptとなるわけですが、paginationのconfig配列のbase_urlのところにjavascript:document.form1.submit();などと書いても無意味ですし、そもそも仮に↑のように書けたとしても今度はページングに使用するページ数なりオフセット値なりを渡すことができません。

通常そうした場合はhiddenフィールドなどを使うとおもうのですが、ページネーションの生成するリンクはどう見てもpostデータをサブミットするような構造にはなっていない、というかmod_rewriteを通すことを前提としたようなURI(/class/method/parameterのような)構造を生成してしまうわけです。

ではいったい検索条件を次ページに伝えるにはどうすればよいのでしょうか?一般的にページを跨いで何かを保持するにはセッションを使います。ショッピングカートの作成例なんかでよく説明されています。

ですがここからが今回の本題なんですが「検索条件ごときにいちいちセッションなんか使う」ものなんでしょうか?というのもセッションなどという大袈裟なモノを持ち出さなくても、上記のようにgetでクエリ文字列のリンクを生成したり、javascriptでsubmitしたり…というようにやり方はいろいろ考えられるわけです。

しかもCodeIgniterはデフォルト状態ではgetが使えないわけです。んでページネーションクラスを使用するとpostを自動的にサブミットしてくれるわけでもなく、単にoffsetパラメータ付きのURIを生成するだけです。ページネーションを必要とする場面がほとんどなんらかの検索結果の表示にも関わらずです。

結論から言うとCodeIgniterで検索条件を維持しつつページングする場合は、セッション(またはクッキー)を使うしかありません(※たぶん、ってか他の方法があったら是非教えて欲しいです)。これは別にCodeIgniterに限った話じゃないかもしれません。例えばCakePHPでもデフォルトのページャーは同じようなものですし、Pearも上述のとおりです。

ですがCakeの場合はURIが/class/method/parameterのような形になっていても?hoge=hage&foo=barみたいなクエリ文字列を付け足せますし、Pearの場合はgetの時は自動的にクエリ文字列付きのURIを生成してくれます。

でもCodeIgniterのページネーションは何もやってくれません。そしてクエリ文字列付きのURIはデフォルトでは許可されていません。残るはセッションなんですが、果たしてページングのために検索条件ごときをセッションデータとして保持するという実装は正しいものなんでしょうか?

もしこれが標準的なごく当たり前のプログラム作法?ならば、一言どこかに書いておいて欲しかったなぁとおもうわけです。プロの方はいざしらず素人はそういうことがわからないわけです。つまりこの問題に限らず、なんか実装できるし動いてるかもしれないけどこれってこんなんでいいの?的なことってなかなか情報がないんですよね。

せめてページネーションクラスのところに一言
検索結果等をページングする場合はセッションを使用し検索条件等を保持するようにします

とだけ書いておいて欲しかった。それだけで素人は安心するんです。それだけでセッション機構というなにやら複雑そうな&大袈裟そうなモノにも手を出してみようか?と考えられるんです。

以上長い愚痴でした(でもハマッたんです)。
ラベル:愚痴 codeigniter
posted by ciallost at 22:57| Comment(3) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

CodeIgniterでページングを実装してみる

前回はActiveRecordクラスを使ってまがりなりにも検索できるところまで書きましたが、今回はいよいよ懸案のページングを実装してみたいとおもいます。

そもそもフレームワークを使ってみようとおもったきっかけは、元の手続き型で書いた汚いスクリプトにSQLのLIMITを使ったページングを実装したい、というのが主な動機だったので、これがうまくいけば個人的にはほぼ満足というか目標達成!みたいなところでもあります。

で、さっそくユーザガイドを丸写しっと。まずControllerのIndex()の先頭付近に(別に先頭じゃなくてもいいとはおもいますが…)
$this->load->library('pagination');

としてページネーションクラスをロードします。

次にページネーションに渡すパラメータを設定します。ユーザガイドには最低限3つの項目とありますのでその通りに…
$config['base_url'] = 'http://localhost/codeigniter/index.php/gappei/index/';
$config['total_rows'] = (とりあえず保留);
$config['per_page'] = '10';

total_rowsはDBから総件数を取得してこないとならないのでとりあえず保留。per_pageはゆくゆくはユーザが指定できるようにしたいですが、これもとりあえず10件で、base_urlは“完全なURL”と書いてありますが、例えばうちの設定だとhttp://localhost/codeigniter/http://localhost/codeigniter/index.php/gappei/index/が呼び出されるようなルーティング設定にしてますが、この部分には後者の完全なURLを書かないとまずいみたいです。

そしてconfig配列をページネーションに渡します。
$this->pagination->initialize($config);

次にVIEWに生成されたページングリンクを出力するコードを…
$this->pagination->create_links();

次に保留にしておいた総件数の取得ですが、まず前回のクエリを組み立てている部分のコード…
//結果取得SQL
$this->db->select('nid, pref, nc, o.id oid, o.city oc, gdate');
$this->db->from( "(SELECT DISTINCT nid, pref, nc, yomi, gdate
FROM cities
WHERE nc like '%".$data['newcity']."%'
LIMIT 0, 10) AS vn");
$this->db->join('newcities_oldcities j', 'vn.nid = j.newcity_id');
$this->db->join('oldcities o', 'o.id = j.oldcity_id');

普通総件数を取得したい場合は、この後…
$count = $this->db->get()->num_rows();

でよいとおもいますが、前回までで散々述べてきたとおり、欲しい件数は新市町村の数になるので、このままではいけません。

具体的にはFROM句のサブクエリ部分でのLIMITを適用する前の件数が欲しいわけです。なのでまずサブクエリ部分(LIMITの前まで)を抽出してみます。
SELECT DISTINCT nid, pref, nc, yomi, gdate
FROM cities
WHERE nc like '%".$data['newcity']."%'

で、これをActiveRecord風に書き直します。
$this->db->select('DISTINCT nid, pref, nc, yomi, gdate');
$this->db->from('cities');
$this->db->like('nc', $data['newcity']);

db->like()はキーと値を渡してやると自動的にWHERE句を作ってくれます。こういうのはSQL直書きよりもイイですね。

そしてSQLを発行して件数を数えます。
$count = $this->db->get()->num_rows();

で、countの値を先ほど保留にしておいたtotal_rowsに設定します。
$config['total_rows'] = $count;

これで準備ができたのでブラウザをリロードしてみます。

paging.pngこの画像は3ページ目を表示しているところです。クエリーも総件数取得とデータ取得の2回実行されているのがわかるとおもいます。でもよく見るとページングのリンク部分は確かに3という数字がボールドリンクなしになっていて現在の表示ページが3ページ目だということを表していますが、データをみると1ページ目のデータが出力されています。

とおもってよく見るとLIMIT句のoffset値をちゃんと設定してませんでした(--;ここはページネーションで生成されたURLの第3セグメントを入れないとなりません。ということでデータ取得クエリのFROM句の部分を
$this->db->from(  "(SELECT DISTINCT nid, pref, nc, yomi, gdate
FROM cities
WHERE nc like '%".$data['newcity']."%'
LIMIT ".$offset.", 10) AS vn");

に変更します。そしてindex()が引数を取るように…
function index($offset=0)

と書き換えます。

paging2.pngこんどはきちんと3ページ目が表示されているようです。SQLもちゃんとLIMIT 20, 10になっているのがわかるとおもいます。これで一応ページングは実装できた・・・・・かのように見えますが、実は重大な落とし穴があります(--;

続きは次回
ラベル:codeigniter
posted by ciallost at 18:45| Comment(2) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

CodeIgniterのActiveRecordクラス

前回formヘルパーを使ってシンプルなformを作成しましたが、今回はPOSTで受取ったデータを元にクエリを組立てデータを取得してみたいとおもいます。

まずCodeIgniterのActiveRecordクラスですが、これがかなりイイ感じです。何がイイってこんなに緩くて、ってゆーかこれってSQL直書きしてるのと何が違うんでしょうかね〜?まあたぶん素人にはわからない部分で何かすごく高尚なことをやっているんでしょう(たぶんw)。

それはともかく前に晒した我流SQLを詰め込んでいきます。
$this->db->select('nid, pref, nc, o.id oid, o.city oc, gdate');
$this->db->from("(SELECT DISTINCT nid, pref, nc, yomi, gdate
FROM cities
WHERE nc like '%".$data['newcity']."%'
LIMIT 0, 10) AS vn");
$this->db->join('newcities_oldcities j', 'vn.nid = j.newcity_id');
$this->db->join('oldcities o', 'o.id = j.oldcity_id');

$data['query'] = $this->db->get();

テーブル名が若干変わっていますが、これはCakePHPで試行錯誤した時の名残です。Cakeはテーブル名を原則複数形にしなければならず、さらにHABTM関連の関連付けてるテーブルは両方のテーブル名をアルファベット順に並べたモノにしなければなりません。

なのでnewcities_oldcitiesは前のSQLだとjoinedというテーブルだったものと同じです。citiesも前はcityという名前だったMySQLのVIEWです。

で、問題なのはFROM句ですがここはサブクエリを書かないとならないので、よくわかりませんでしたがとりあえずダラダラと全部書いちゃいました。外側のJOIN部分も以前はWHERE句で書いていたものをせっかくなので(謎)JOINで書いてみました。

最後のdb->get()の部分は前は'cities'テーブルを指定していましたが、ActiveRecordクラスを使ってクエリを組み立てているので、パラメータなしでget()を読んでいます。そして結果…
activeRecord.png
う〜む、ちゃんとデータ取れてますね。ちなみに前にstdClass Objectと表示されていた部分は、VIEWで
foreach($query->result() as $row)

としていたところを
foreach($query->result_array() as $row)

にすることで普通の配列になりました。

あとデバッグ用に
$this->output->enable_profiler(TRUE);

とControllerの最後に記述しました。実行されたSQL文が見えるので便利です。データが取れているので大丈夫だとおもいましたが、一応これを見てもちゃんと意図どおりのSQLが組み立てられているようです。LIMITも新市町村の件数で取ってこれているし、もちろん新市町村名で検索も可能です。

次回はいよいよ懸案の?ページングを実装してみたいとおもいます。
posted by ciallost at 03:49| Comment(0) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

2007年11月13日

CodeIgniter:formの作成とpostデータ

前回、一応表示できた(print_rですが…)ところで、今回は検索アプリっぽく?formを作ってみました。

formといえば…、大昔まだブラウザがモザイクとか呼ばれていた頃の話、HTMLをエディタで手打ちしだした頃、pタグとかbとかiとかh1〜6等を一通り覚え、参考書で初めてform関連のタグ(とそのサンプル)を目にして、「おお、テキストフィールドだ!、サブミットボタンだ!チェックボックスやラジオボタンもあるぞ!これでGUIアプリが簡単に作れる!」とマジでおもって、その本片手に覚えたてのタグを一所懸命打ち、ブラウザで表示させて、いざsubmit!とかやった時の感動とそれに続く失望を思い出します(長っ)。

当時はCGIという言葉すら知らず、なにやらGUI部品を並べれば勝手にアプリケーションができあがるというアホな幻想を抱いていましたから、ボタンをクリックして何も起きない時の失望感は絶大でした(--;

で、アホな前置きはこれぐらいにして、CodeIgniterでは他の多くのフレームワークと同様form関連のヘルパーがあります。まあチュートリアルとかでも使ってましたが、なんか閉じタグ</form>だけはHTML直打ちってのが微妙にダサかったりしますが(^^;でもまあ直接タグ打つよりはいろいろな面で楽できそうですね。というわけで、Controllerのコンストラクションに…
$this->load->helper('form');

の一行を追加してホームヘルパーを呼びます(って介護じゃないんだから)フォームヘルパーをロードします。

そしてViewの方に…
<?=form_open('gappei/index')?>
新市町村:<?=form_input('newcity', '新市町村')?>
<?=form_submit('search', '検索')?>
</form>

とりあえずこれで表示されます。

form.pngテキストフィールドが一つとサブミットボタンの一番シンプルな形でしょうか。form_inputの第2パラメータに'新市町村'と書いてありますが、ユーザガイドによるとここはvalue="****"の部分になるようで、とりあえず必須っぽいので暫定です。(注:実は書かなくても大丈夫みたいですが…)

そんで次にformからのデータを受取る部分をコントローラに追加します。
$data['newcity'] = $this->input->post('newcity');

CodeIgniterではformのgetメソッドは使用できないので、フォームヘルパーで作ったformは必ずpostになるようです。postで指定してる'newcity'はinputテキストフィールドの名前になります(HTML:name="newcity")。そして受取ったデータを$data配列にnewcityとして格納します。この$dataは
$this->load->view('gappei_view', $data);

によって一括してVIEWに渡されますので、VIEW側からは$newcityとすることで送信したpostデータを受取ることができます。なので上で暫定的に'新市町村'としていた部分は…
form_input('newcity', $newcity)

に変更すれば検索ボタンを押して送信した後にも直前に送信した検索語が反映されるようになります。

そしてpostで受取ったデータを元にSQLを組立てデータベースにクエリ発行〜データ取得〜表示という流れになるとおもいますが、それは次回ということで。
ラベル:codeigniter
posted by ciallost at 09:50| Comment(0) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

2007年11月12日

CodeIgniterの基本:まずは表示

PHPに限らずプログラムを作成するときに、基本的な設計みたいなことって皆さんするんでしょうか?もちろんプロの方はそんなの当たり前だとはおもうのですが、私のような素人?で趣味のプログラミングみたいな場合に、例えばクラス図ぐらいは書くよ!とかフローチャートみたいなものは一応メモぐらいはしてから始めるよ!なんて人たちはどのくらいいるんでしょうか?

自分の場合は思い立ったらいきなりエディタ立ち上げて<?phpとか書き始めちゃいますが、皆さんどんな感じなんでしょうねぇ?んで次に自分のプログラミングスタイル(というほど確立されたものではありませんが)ではどーするかというと、例えば前回のスクリプトであれば…
$max_disp = preg_replace("/[^0-9]/", "", $_GET['disp']);

みたいにいきなり書いちゃうわけです。そんでもって次にすることは…
(まあこの場合だとgetデータを取得してるわけですから一応HTMLのformはできてると仮定して)
echo $max_disp;

なわけです(^^;

そんでformから「0」とか「あうあう」とか入力してみて、いちいち確かめるわけです。要するにこの場合だと正規表現の部分に自信がないんですね。で、ようやく$max_dispに入ってる値がまあ正しかろうというところで、echoの行を消してまたぞろぞろ書き始めるわけです。

こんなやり方なんで普通のコンパイル系の言語とかだとやってられないっつーか、とにかくすぐ結果が見えてこないと次が考えられないんですね。そんな程度なんでまさに手続き型というか、一行一行確認しながらぼそぼそとやっていくみたいな…。

で、話はフレームワークなんですが、これがまたやっかいというか、要するに上記の手法?がフレームワークによっては通用したりしなかったりするわけです。上の例では変数が配列じゃないのでechoでいいんですが、配列だとprint_r()とかしないと単にARRAYとか表示されて、まさに「あれ〜?」状態になるじゃないですか(--;。
echo "<pre>".print_r($hairetsu, 1)."</pre>";

みたいなpreタグ併用してよく出力してますけど、フレームワークだとなんか適当なところに書いちゃうと出力されなかったり、エラーでちゃったりするわけなんです。

で、本題ですがCodeIgniterはその辺かなりアバウトというか融通が利くというか、上記の手法(注:手法じゃね〜ョ)が通用するんですね!その後いろいろとわかってくるにしたがってvar_dump()とか$this->output->enable_profiler(TRUE);みたいな技(注:だから技じゃね〜ョ)も覚えましたが、CodeIgniterで開発?するにあたってこの緩さ加減は、自分にとって最大のメリットでした。

そんでチュートリアルにあるblogのコードをほんのちょっとだけいじって、とりあえず表示成功っす。(って全然前置きと関係ないじゃん)

Controller
<?php
class Gappei extends Controller {
function Gappei()
{
parent::controller();
}

function index()
{
$data['title'] = "平成市町村合併";
$data['heading'] = "平成市町村合併";
$data['query'] = $this->db->get('city');
$this->load->view('gappei_view', $data);
}
}
?>

View
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>TEST</title>
</head>
<body>
<pre>
<?foreach($query->result() as $row):?>
<?=print_r($row, 1)?>
<?endforeach;?>
</pre>
</body>
</html>

まあとりあえず得意のprint_rで…。controllerのdb->getで読み込んでいるcityというテーブルは前述したMySQLのView、つまりnewcityとかoldcityとかprefとかをJOINした仮想?のテーブルです。

表示結果
stdClass Object
(
[nid] => 1
[pref] => 北海道
[nc] => 函館市
[yomi] => はこだてし
[oid] => 1
[oc] => 函館市
[gdate] => 2004-12-01
)
stdClass Object
(
[nid] => 1
[pref] => 北海道
[nc] => 函館市
[yomi] => はこだてし
[oid] => 2
[oc] => 戸井町
[gdate] => 2004-12-01
)
stdClass Object
(
[nid] => 1
[pref] => 北海道
[nc] => 函館市
[yomi] => はこだてし
[oid] => 3
[oc] => 恵山町
[gdate] => 2004-12-01
)
   :
   :
stdClass Object
(
[nid] => 592
[pref] => 沖縄県
[nc] => 八重瀬町
[yomi] => やえせちょう
[oid] => 2021
[oc] => 具志頭村
[gdate] => 2006-01-01
)

なんかstdClass Objectとかいう謎の文字列が出力されていますがまあちゃんとデータは取れてるみたいですね。
ラベル:codeigniter
posted by ciallost at 01:09| Comment(0) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

2007年11月11日

CodeIgniterで作り直す前の手続き型のスクリプト

今回からようやくCodeIgniterの構築記(メモ)を書こうとしているわけですが、その前にやはりこれまで散々ネタにしてきた 手続き型で書かれたぐちゃぐちゃの・汚い・見るに耐えないスクリプトを公開しておこうとおもいます。

<?php
//GETデータの処理
if (isset($_GET['disp'])) {
$max_disp = preg_replace("/[^0-9]/", "", $_GET['disp']);
} else {
$max_disp = 10;
}

$q['disp'] = $max_disp;

if (isset($_GET['p'])) {
$p = preg_replace("/[^0-9]/", "", $_GET['p']);
} else {
$p = 1;
}

//データベース
try {
$dbh = new PDO('sqlite:c:/gappei.db');

$sql = "SELECT newcity.n_id NID,
pref,
newcity.city NC,
oldcity.o_id OID,
oldcity.city OC,
birth
FROM newcity, oldcity, joined
WHERE NID = joined.n_id AND oldcity.o_id = joined.o_id";

if ($_GET['snc'] != "") {
$sql .= " AND NC LIKE :ncity";
$exec_array[":ncity"] = "%".$_GET['snc']."%";
$q['snc'] = $_GET['snc'];
}

if ($_GET['soc'] != "") {
$sql .= " AND OC LIKE :ocity";
$exec_array[":ocity"] = "%".$_GET['soc']."%";
$q['soc'] = $_GET['soc'];
}

$stmt = $dbh->prepare($sql);

if ($stmt->execute($exec_array)) {
while ($row = $stmt->fetch()) {
$data[$row['NID']]['ID'] = $row['NID'];
$data[$row['NID']]['PREF'] = $row['pref'];
$data[$row['NID']]['NCITY'] = $row['NC'];
$data[$row['NID']]['OCITY'][$row['OID']] = $row['OC'];
$data[$row['NID']]['BIRTH'] = $row['birth'];
}
}

$dbh = null;

} catch (PDOException $e) {
print "エラー!: " . $e->getMessage() . "<br/>";
die();
}
//添字配列に変換
$data = array_values($data);

//件数
$hits = count($data);

//最終ページ
$lastpage = ceil($hits / $max_disp);

//表示ページ
if ($p != "") {
$disp_page = (0<$p && $p<=$lastpage)?$p:1;
} else {
$disp_page = 1;
}

?>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>平成市町村合併</title>
</head>
<body>
<h1>平成市町村合併</h1>
<form id="search" action="<?=$_SERVER['PHP_SELF']?>" method="get">
新市町村:<input type="text" name="snc" size="20" value="<?=htmlspecialchars($_GET['snc'])?>" /><br />
旧市町村:<input type="text" name="soc" size="20" value="<?=htmlspecialchars($_GET['soc'])?>" /><br />
<select name="disp" onChange="submit();">
<option value="10" <?=$max_disp==10?'selected="selected"':''?>>10</option>
<option value="20" <?=$max_disp==20?'selected="selected"':''?>>20</option>
<option value="30" <?=$max_disp==30?'selected="selected"':''?>>30</option>
</select>
<input type="submit" value="検索" />
</form>
<hr />
<table border="1" cellpadding="2" cellspacing="0" width="600">
<tbody>
<th>都道府県</th><th>新市町村</th><th>旧市町村</th><th>合併日</th>
<?php
$cnt = 0;
//データ出力用カウンタ
$index = $max_disp * ($disp_page - 1);

while (isset($data[$index]['NCITY']) && ($cnt < $max_disp)) {
$cnt++;
$rspan=count($data[$index]['OCITY']);
?>
<tr>
<td<?=$rspan>1?' rowspan="'.$rspan.'"':''?>><?=$data[$index]['PREF']?></td>
<td<?=$rspan>1?' rowspan="'.$rspan.'"':''?>><?=$data[$index]['NCITY']?></td>
<td><?=array_shift($data[$index]['OCITY'])?></td>
<td<?=$rspan>1?' rowspan="'.$rspan.'"':''?>><?=$data[$index]['BIRTH']?></td>
</tr>
<?php
if($rspan>1){
foreach($data[$index]['OCITY'] as $ocity){
echo "<tr><td>$ocity</td></tr>";
}
}
$index++;
}
?>
</tbody>
</table>
<hr />
<?php
//ページングリンク出力
if (isset($q)) {
//GETクエリ文字列生成
$common_q = http_build_query($q, '', '&');
}

echo "<p>\n";

//前ページリンク
if (1 < $disp_page) {
$prev = $disp_page - 1;
echo "<a href=\"".$_SERVER['PHP_SELF']."?".$common_q."&p=".$prev."\">< 前の".$max_disp."件</a> \n";
}

//他ページリンク
for ($j=1; $j<=$lastpage; $j++) {
if ($disp_page == $j) {
echo "<strong>$j</strong>\n";
} else {
echo "<a href=\"".$_SERVER['PHP_SELF']."?".$common_q."&p=".$j."\">".$j."</a> \n";
}
}

//次ページリンク
if ($disp_page < $lastpage) {
$next = $disp_page + 1;
$next_disp = min($max_disp, $hits - ($max_disp * $disp_page));
echo "<a href=\"".$_SERVER['PHP_SELF']."?".$common_q."&p=".$next."\">次の".$next_disp."件 ></a> \n";
}
echo "</p>\n";
?>
</body>
</html>

この時点ではデータベースがSQLiteだったのでPDOを使っています。その関係でDB接続〜クエリ送信〜データ取得あたりの処理は意外とすっきり見えるかもしれませんが、ひとえにPDOのおかげです。

ページングも自前で実装しています。formからのデータはgetで受けています。$qという配列はページングのリンクに付け足すクエリ文字列の元?を連想配列で格納してあり、それを直前で展開して&でつなげています。

あとはとり立てて見るべきところもない、ごく一般的なしょーもないスクリプトです。次回からはこれをCodeIgniterで書き直していく過程をメモっていきます。
ラベル:PHP
posted by ciallost at 21:45| Comment(0) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする