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) | 日記 | このブログの読者になる | 更新情報をチェックする