2007年12月16日

日付フィールドの入力チェック

前回はDecoratorパターンもどき?で合併日の日付をその他のフィールド(都道府県、新市町村、旧市町村)と共にAND検索することができるようになりました。今回はその日付フィールドの入力チェックのメモです。

当初からこれはかなり複雑になることが予想されていましたが、そもそも何故そうなるのか?というと、これは個人的なUIの趣味?というか、要するに日付の入力を単体のフィールドでしかも範囲指定までするという仕様が一番の原因です。

個人的な趣味というのはどういうことか?というと、煩いUIが嫌なんです。入力チェックというと範囲外の入力があった場合にアラートがドンッ!と出てきて「違うぞゴルァ!・・・[OK]」みたいなのがありがちですが、アレほど使い勝手の悪いUIもないとおもいます。

Webアプリとかだと大抵はjavascriptのalert()かなんかで、上記のようなエラーメッセージ?が表示されたりするわけですが、こういうのはまあ百歩譲ったとしてもせいぜいデータベースにデータを入力(INSERT)するような、つまりバックエンドのRDBのデータが変更されるような時ぐらいはまあヨシとして、ここでやろうとしてるようなたかだか検索程度のことでアラートが出てきた日にはもう二度とそのアプリは使いたくなくなるでしょう。

そんなわけで極力そういうキモイUIは避けたく、入力チェックといってもスクリプトに渡す値が適切かどうか判断して、範囲外だった場合は適宜修正してから当該スクリプト(のメソッドとか)に渡す、という方式が好みだったりします。

ところがこれが単純な数値とか五十音パッドのような平仮名だけのように比較的限定範囲が明確なものならいいのですが、日付(しかも範囲指定)となるとなかなか容易ではありません。

容易ではなくなる要因のひとつには、ルールを単純化してしまうと感覚的な入力との差異が際立ってしまうというのがあります。例えば今回のように範囲指定を入力させる場合、2007.03.21-2007.04.15のようにフォーマットに則って正しく入力されればいいのですが、2007.03.21-04.15のように西暦部分が省略された場合など、人間が見れば「あ〜これは後半部分の2007が省略されているな」とすぐわかりますが、プログラム的には非常に面倒くさいことになります。

また日付というのは数字を使って表現されていて一見プログラミングと相性がいいような気もしますが、ご承知のとおりこれがかなりアナログチックな代物で、定型的に?処理しようとすると恐ろしく複雑になっていきます。そもそも0という概念がないし…。

で、愚痴はともかく、普段ならテキトーに誤魔化すところですが、今回はこうした複雑な部分をできるだけスクリプトで実現してみたい?などと考えてしまいました。といってもいきなり複雑なモノを作ろうとすると難しいので、まずはたたき台的に単純なルールでやってみました。

ルール(単純版)

1.日付は年月日(西暦)を[.]ドット区切りで表現する 2007.12.08
2.月日が一桁の場合は桁合わせの0は省略可
3.範囲指定は[-]ハイフンで繋げる 2007.3.21-2007.12.8
4.範囲指定は省略可(2個目の年月日はなくてもよい)
5.年月日のどれかが省略された場合は1を補う 2007.10->2007.10.1
6.不正な値はmktime()に任せる 2007.10.32->2007.11.1

たったこれだけですがスクリプト的にはなにやら複雑な感じになっています(--;。最後のmktime()や桁合わせ0省略ルールはPHPに100%依存しています。んで、とりあえずできあがったのがこれ↓
function _validDate($gdate)
{
//[.]ドットは年月日の区切り、[-]ハイフンは範囲指定
$Ymds = preg_replace("/[^0-9.-]/", "", $gdate);

//2個以上の[-]ハイフンは無視して配列に展開
$Ymd = explode('-', $Ymds, 2);

foreach ($Ymd as $date)
{
$tmpD = preg_replace("/[^0-9.]/", "", $date);
$tmpD = preg_replace("/\.+/", ".", $tmpD);

if ($tmpD <> '')
{
$tmpD = explode('.', $tmpD);
}

while (count($tmpD) < 3)
{
$tmpD[] = 1;
}

list($year, $month, $day) = $tmpD;

$timestamp[] = mktime(0, 0, 0, $month, $day, $year);
}

sort($timestamp);

foreach ($timestamp as $time)
{
$formattedDate[] = date("Y.m.d", $time);
}

$fDates = implode('-', $formattedDate);

return $fDates;
}

まず数値・ドット・ハイフン以外を問答無用で削除、ハイフンでexplodeします。explodeの3個目のパラメータは生成された配列の最大個数です。つまり3個以上にはなりません、また行頭にハイフンがあった場合は1個目の要素が空になります。

次のforeachブロックの最初のpreg_replaceはたぶん余計ですね、なんとなく心配性?(現実世界では全然心配性ではないが…)的に書いちゃいました。その次のpreg_replaceで連続ドットを1個にしています。これは「月」にあたる要素が空白だった場合「日」を前に詰めてしまうことになりますが、[..............]のような極端な場合を想定して、一応これも書いておきます。↓のlist()で4個目以降の要素があっても弾かれるので、この行もたぶん要りません。

次のif文とwhileブロックで空だろうとなんだろうととにかく要素3個(以上)の$tmpD配列を作ります。要素が空の場合は1で埋めています。そしてlist()で年月日変数に割り当てます。ここで4個目以降の要素があった場合でも無視されます。

次にそれをmktime()に渡してUNIXタイムスタンプ配列を得ます。この時点で13月25日とか5月48日のような不正な日付が解消されます。で、2個の要素のタイムスタンプ配列をソートしていますが、これは2個目が1個目より過去の場合を想定していて、SQL生成の際におかしなことになりそうなので必ず「古い日付〜新しい日付」になるようにします。

最後にタイムスタンプを再びdate()でドット区切りの日付に戻し、ハイフンで繋げて返しています。

これで一応最初に作ったルールには合致しているようです(もしかした穴があるかも???)。ルール2はmktime()を通している時点で勝手にIntegerにキャスト?されているとおもいます。

んで実際動かしてみると、まあ書式に則って入力した場合はいいとして、エラーはでないものの↑の感覚的な入力との差異という部分が気になってきます。例えば…

2007.5.21-6.30 =>
 2007.05.21-2007.06.30 となって欲しいところですが、
 2007.05.21-2008.06.01 となりますし、

2007.05-09 =>
 2007.05.01-2007.09.30 か、せめて
 2007.05.01-2007.09.01 と期待したいところですが、
 2007.05.01-2009.01.01 となってしまいます。

もちろんこれはプログラム的には正しい動作です。しかしどうしても感覚的には前者の方がしっくりきます。また…

2005 とだけ入力した場合は、
 2005.01.01-2005.12.31 になって欲しかったり、

2004.02 なら
 2004.02.01-2004.02.29のように閏年も考慮した上で範囲を返してくれたらいいなぁ?…ともおもいます。

さらにmktime()を使用したことでいわゆるUnixエポック(1970年1月1日 00:00:00 GMT)の縛り?が生じてしまいます。このアプリでは実際使われている日付データの範囲は2001年〜2007年まででしかないので、問題ないと言えばそうなのですが、範囲外の入力値があった場合に1970.01.01などとなるのは若干気持ち悪い気もしないでもありません。

2005.06.30-50.22 => 1970.01.01-2005.06.30

50.22という値はもはや日付というより謎の数値ですが、これを「月」とみなして、

2005.06.30-2009.02.22

のように変換されてもいいのではないか?…と。

まあそんなわけでとりあえず単純版のルールではなんとか実装できたっぽいので、次回は後半述べたような感覚的な入力との差異の吸収ということに関して考えてみたいとおもいます。
ラベル:codeigniter PHP
posted by ciallost at 04:49| Comment(0) | TrackBack(1) | 日記 | このブログの読者になる | 更新情報をチェックする

2007年12月10日

検索機能の強化:日付の検索その1

前回は同一検索フィールド内でのAND検索の実装のメモを書きましたが、今回は日付の検索機能の実装をメモっていきます。

このアプリでは市町村の合併日フィールドをキーに検索できるようにしたいのですが、これがかなり後回しになったのは正直メンドイからです(^^;。日付というある意味扱いにくいデータ?なこともさることながら、これまでの検索関係の実装に加えて合併日検索?を盛り込もうとしたときに、正直どのように書けばいいのか?迷ってしまったわけです。

まず一般的に日付データ?というか日付入力が扱いにくいのは、ネット上でも言及され尽くしている感がありますが、要はプログラム(マー)の意図した書式で如何にユーザに入力させるか?という部分で、これに関しては様々な方法がありUIの進化の過程?も垣間見ることができます。

テキストフィールド
 ↓
年月日個別のテキストフィールド
 ↓
年月日個別のプルダウンメニュー
 ↓
カレンダー

みたいな感じで進化してきているようです。おそらくは当初は単なるテキストフィールドで2007/12/08のように完全にフォーマットに従って入力させようと試みていたのが、区切り文字にバリエーションを持たせたり(2007-12-08)とか、区切りなし(20071208)で入力したいとか、0で埋めたくない(2007/12/8)とか…いろいろな要望にこたえる形でのこととおもわれます。

また意図しない書式で入力された場合の入力チェックの煩雑さというのも、このような進化に影響を与えていることとおもわれます。で、最近?はカレンダーでクリックするやつが流行ってる?ようですが、この方法は入力チェックの観点からすると楽といえば楽です。ただ個人的には日付入力ごときでカレンダーのような大袈裟な過去のアナログ時代の遺物みたいなものが出てくるのがどうしても我慢できなくてこの方法は採りたくなかったわけで…。理想的にはテキストフィールド一発でやりたい!というのがまず一つ目のメンドイ原因です。

メンドイ理由の2つ目は、検索オブジェクトなるモノを作ってしまった結果、無理矢理にでもオブジェクト指向的にならざるを得ず、かといってそんなの知らないよ!状態だったので、まずはその辺から勉強しなきゃならなくて時間はかかるし不安は残るし…で。結局なんちゃってデザインパターンのようなモノを書いたわけですが、たぶん1週間後にはきれいさっぱり忘れそうなのでまずはそれからメモっておきます。

まず合併日で検索ということでViewのformにテキストフィールドを追加します。

View/serach_form.phpに合併日フォーム追加
$inputFieldName = array('pref', 'nc', 'oc', 'gdate');
合併日<br />
<?=form_input($input['gdate'])?>

$inputFieldNameはテキストフィールドのname属性を配列にして処理しているので、最初に名前の配列を生成しています。その配列に新たにgdateを加えています。

とりあえずこれだけで合併日の検索?はできるようになります。が、これは以下のようなlike節を生成しているだけで、つまりは合併日フィールドを文字列として検索していることになります。
WHERE gdate like '%2007-12-08%'

また、このままでは検索タイトル(検索結果ページの上部に「新市町村:〜」の検索結果、のように表示している部分)を出力している部分のコードでエラーが出てしまいます。なので検索オブジェクトクラスファイルのNormalSearchクラスで検索タイトル(HTMLで表示するための文字列)を取得するgetSearchTitle()内のswitch文で分岐している部分に、新たに合併日(case 'gdate')を追加します。
function getSearchTitle()
{
$searchTitle = '';

foreach ($this->getCriteria() as $key => $word)
{
switch ($key) {
case 'pref':
$searchKey = '都道府県';
break;
case 'nc':
$searchKey = '新市町村';
break;
case 'oc':
$searchKey = '旧市町村';
break;
case 'gdate':
$searchKey = '合併日';
break;
}

$searchTitle .= "「".$searchKey.":".$word."」";
}

$searchTitle .= 'の検索結果';

return $searchTitle;
}

この部分のコードは元々あまり綺麗でなく自分でもなんとかしようとは思っているのですが、とりあえずはここに「合併日」を追加します。

これでエラーは出なくなりましたが、文字列検索ではどうしようもないので、ちゃんと日付として範囲検索などもできるように考えてみます。まず入力フォーマットをでっちあげ決めます。

・日付はYYYY.MM.DDのドット区切り
・範囲指定は[-]ハイフン
ex:2005.04.01 又は 2005.04.01-2005.12.31

たったこれだけなんですが、入力する際に完全に↑のフォーマットに従って入力されればよいものの、想定される範囲で考えても入力チェックはかなり複雑になりそうな予感がします。ま、とりあえずは「数値、[.]ドット、[-]ハイフン」以外は無条件で削除することにして、それをcontrollerに書きます。

Controller/function search()
//[.]ドットは年月日の区切り、[-]ハイフンは範囲指定
$conditions['gdate'] = preg_replace("/[^0-9.-]/", "", $conditions['gdate']);

日付のデリミッタを[.]ドットにしたのは、範囲指定の記号に[-]ハイフンを使いたかったからです。ただこのため現状の合併日フィールドの書式(YYYY-MM-DD)と異なってしまうために、これをドット区切り表記に変更することにしました。

データ取得クエリを生成している部分の外側のSELECTでフィールドを列記している部分を以下のように変更します。
SELECT ... gdate ...

SELECT ... DATE_FORMAT(gdate, '%Y.%m.%d') as gdate ...

MySQLのDATE_FORMAT関数を使って変換しているだけです。注意しなければならないのは、データ取得クエリにはFROM句でサブクエリを使用していますが、その部分やWHERE句では上記の変換をせずに、外側のSELECTに適用するということです。
SELECT〜
FROM (
SELECT〜
WHERE〜
)
WHERE〜

↑のような構造の?SQLの場合
内側のWHERE句>内側のSELECT>外側のWHERE句>外側のSELECTの順に解釈される?とおもわれ、仮に内側のSELECT部分などにDATE_FORMAT()を使用してしまうと、gdateをWHERE句に用いた場合にデータを取ってこれなくなってしまいます。

model/function getData()
//結果取得SQL
$this->db->select("pid, pref, nid, nc, yomi, oid, oc, oyomi, gdate");

$this->db->select("pid, pref, nid, nc, yomi, oid, oc, oyomi,
DATE_FORMAT(gdate, '%Y.%m.%d') as gdate");

これで合併日が(YYYY.MM.DD)の形式になりました。

で、ここからが本題なのですが、formからのテキストフィールドに入力された値をキーに検索する時はNormalSearchクラスの検索オブジェクトクラスがインスタンス化されています。これは五十音パッドの検索部分の実装(多態編)で作成したとおりなのですが、searchModeという抽象クラスを継承したそれぞれの検索クラスがあるわけです。
               searchMode
  ┌──────┼─────┐
NormalSearch CapSearch IdSearch

このような形になっていると、検索機能の強化:IDで検索できるようにしてみるで述べたように、新たに検索機能?を付け加える際にはsearchModeを継承して新しいクラスを作ればいいようになっています。

ところが今回のように合併日を検索しようとする場合、formが一緒になっていることからもわかるように、合併日を単体で検索するだけでなく都道府県、新市町村、旧市町村などと組み合わせてAND検索をしたいわけですが、上記のクラス構成でsearchModeを継承して新たにGdateSearchなどというクラスを作ってしまうと、NormalSearchと組み合わせた検索?ができなくなってしまいそうです。(※そのような構成でも組み合わせる方法があるのかもしれませんが???)

そこでいろいろ調べた結果Decoratorパターンを使えばなんとなくいいような気がしてきたので、見よう見真似でそれを実装してみることにしました。ちなみにその際に下記のサイト様を参項にさせていただきました。

まさるblog 越後在住ソフトウェアエンジニアの記録
デザインパターンを学ぶ〜その6:Decoratorパターン(1)〜


koshigoewiki:php:デザインパターン:decoratorパターン

あとデザインパターン全体に対しての下記サイト様の言及が非常に参考になりました。

Happie's page SE・PG入門 デザインパターンを読み解く

で、Decoratorパターンですが、自分が理解したところをぶっちゃけて言ってしまえば、これはいわゆるラッパーで、デコレートされるクラスと共通の抽象クラスを継承しているデコレートクラスを作り、呼び出し側では元クラスの替わりにそのデコレートクラスを呼び出す、といった感じだと思います。その際デコレートクラスではメンバに元クラスを保持するようにして、メソッド内ではデコレートする処理と共に元クラスのメソッドも呼び出すような格好で実装?する…みたいな感じでしょうか???

まあ詳しい説明&正しい説明は他のちゃんとしたサイトを参照してもらうとして、コードだけ晒しておきます。(このエントリかなり長くなっちゃったし…)

検索オブジェクトクラス(search.php)
abstract class SearchMode
{
private $criteria;

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

final public function getCriteria()
{
$criteria = array_diff($this->criteria, array('', NULL));
return $criteria;
}

abstract public function where();

abstract public function getSearchTitle();
}

abstract class OtherSearch extends SearchMode
{
protected $searchMode;

function __construct($searchMode, $condition)
{
parent::__construct($condition);
$this->searchMode = $searchMode;
}
}

class GdateSearch extends OtherSearch
{
public function __construct($searchMode, $condition)
{
parent::__construct($searchMode, $condition);
}

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

$criteria = $this->getCriteria();
$date = explode('-', $criteria['gdate']);

$CI->db->where('gdate >=', $date[0]);
$CI->db->where('gdate <=', $date[1]);

$this->searchMode->where();
}

function getSearchTitle()
{
$criteria = $this->getCriteria();
$newSearchTitle = "「合併日:".$criteria['gdate']."」の検索結果";
$oldSearchTitle = $this->searchMode->getSearchTitle();

$searchTitle = str_replace("の検索結果", $newSearchTitle, $oldSearchTitle);

return $searchTitle;
}
}

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

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

foreach ($this->getCriteria() as $field => $condition)
{
if ($field == 'gdate') {continue;}

$sameFieldConditions = explode(" ", $condition);

foreach ($sameFieldConditions as $cond)
{
$CI->db->like($field, $cond);
}
}
}

function getSearchTitle()
{
$searchTitle = '';

foreach ($this->getCriteria() as $key => $word)
{
if ($key == 'gdate') {continue;}

switch ($key) {
case 'pref':
$searchKey = '都道府県';
break;
case 'nc':
$searchKey = '新市町村';
break;
case 'oc':
$searchKey = '旧市町村';
break;
/***** GdateSearch内で処理 *****/
}

$searchTitle .= "「".$searchKey.":".$word."」";
}

$searchTitle .= 'の検索結果';

return $searchTitle;
}
}

このファイル内には他にCapSearchクラスとIdSearchクラスが書いてありますがそれは省略してあります。新たに付け加えた部分はOtherSearchとGdateSearchで、OtherSearchはデコレータのインタフェース?でコンストラクタに元クラスのオブジェクトと検索条件を引数とするようにして、上述したようにメンバとして検索オブジェクトを保持します。GdateSearchが今回のメイン?でOtherSearchを継承したデコレータクラス?になります。

where()メソッド内では範囲指定([-]ハイフン区切り)された検索条件からクエリを生成します。当然のことながらこのWHERE句はNormalSearchとは全然違ったモノになります。そして最後に元クラスのwhere()を呼んでいます。CodeIgniterのアクティブレコード的にはdb->where()を連続して呼ぶとANDで連結するようなWHERE句になります。

getSearchTitle()は単純に元クラスのgetSearchTitle()で検索タイトルを取得して、それに「合併日:〜」を付け足しているだけです。ただし「〜の検索結果」という文字列を最後にしたかったので、単純に+するのではなく(↑の方の牛丼の例ではそのような形になっていましたが)str_replaceで該当部分を置き換えています。この結果↑の方で書いたNormalSearchクラスのgetSearchTitle()内でswitch文で分岐していたcase 'gdate'の部分は削除しました。

最後にControllerのsearch()ファンクションでNormalSearchをインスタンス化している部分の後にデコレータとしてGdateSearchをインスタンス化するコードを加えます。
function search()
{


if (implode('', $conditions) <> '')
{
$this->cond = new NormalSearch($conditions);

if ($conditions['gdate'] <> '')
{
$this->cond = new GdateSearch($this->cond, $conditions);
}


}
}

GdateSearchでは上述のように検索オブジェクトと検索条件の2つのパラメータが必要なので、先にインスタンス化されてメンバとして保持されている$condを検索条件$conditionと一緒に渡します。

というわけでなんとか合併日で検索できるようになりました。

例によって表示結果w
gdate_search.png

次回はこのフィールドの入力チェックに関してのメモです。
ラベル:codeigniter PHP
posted by ciallost at 03:22| Comment(0) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

2007年12月07日

検索機能の強化:検索フィールド内でのAND検索

前回に引き続き検索機能の強化ということで、今回は検索フィールド内でのAND検索の実装についてメモしておきます。検索フィールド内でのAND検索というのは、同一フィールド内でスペース(半角スペース)で区切られた単語(この合併データベース検索アプリの場合はほとんど文字と同義)をデータベースに対してANDでフィルタリングしよう、というものです。

具体的には、都道府県フィールドなどに「県[SPACE]山」などと入力した場合に、これまでのモノはWHERE pref like '%県 山%'というクエリしか発行せず結果は0件になりますが、これをWHERE pref like '%県%' AND pref like '%山%'というようなクエリを発行するようにして「山形県」「山梨県」「岡山県」…などを検索できるようにしたい、ということです。

↑でもちらっと触れたように、この機能?はココの合併データベース検索アプリではほとんど必要ないものかもしれません。もしくはANDではなくORにした方がよいのではないか?という気もしないでもありません。が、まあ大元のスクリプトには実装されてることですし、とりあえず作ってみたいとおもいます。

まず入力値ですが、これは次のように規定します。

半角または全角スペースで区切られた単語(文字)をAND検索する
連続する半角・全角スペースは1個とみなす

で、これをそのままヴァリデーションとして実装します。
//pregに/uオプションを付けても、\s(空白文字)に全角SPはマッチしない
foreach ($this->input->post('condition') as $key => $str)
{
$conditions[$key] = trim(preg_replace("/[ \s]+/u", ' ', $str));
}

preg_replaceに/uオプション(unicode対応)をつけてホワイトスペース文字(\s)の連続を半角スペースに置き換えて両端をtrimしています。これをフィールド分(今のところ3つ:都道府県、新市町村、旧市町村)繰り返しているだけです。

但し全角スペースに関してはコメントにあるように、/uオプションでも空白文字としては認識していないようだったので、文字クラス内に記述しました。なので正規表現部分の空白は全角スペース、置換文字の方の空白は半角スペースです。

次に実際のクエリを組み立てている部分ですが、元々は連想配列をアクティブレコードのlike()に渡していた部分を書き換えます。

修正前 controller/search.php(検索オブジェクトクラスファイル)
function where()
{
$CI =& get_instance();

$CI->db->like($this->getCriteria());
}

修正後 controller/search.php(検索オブジェクトクラスファイル)
function where()
{
$CI =& get_instance();

foreach ($this->getCriteria() as $field => $condition)
{
$sameFieldConditions = explode(" ", $condition);

foreach ($sameFieldConditions as $cond)
{
$CI->db->like($field, $cond);
}
}
}

結局単に配列を渡していただけの部分がforeachの入れ子になってしまいました。やってることは見てのとおり単純なことなんですが、修正前と比べるとやはりかなりアレだなぁ?という印象が…。

でもまあ基本的にはこれだけの変更でフィールド内でのAND検索が実装できたわけで、こういう部分は手続き型でダラダラ書いているよりはわかりやすいと感じました。

検索画面
and_search.png
まずこんな感じで適当に半角・全角スペースを混ぜて検索してみます。
※赤枠:全角スペース、緑枠:半角スペース(300%拡大)

表示結果
and_search_result.png

↑のイメージのように、前後のスペースが削除され連続スペースも一つになっているのがわかるとおもいます。クエリも各フィールドごとにANDで出力されているようです。もちろん検索結果も正常です。

ということでおもっていたよりも意外にあっさりとできた感じでした。次回は合併日の検索について書こうとおもいます。
ラベル:codeigniter PHP
posted by ciallost at 22:49| Comment(1) | TrackBack(1) | 日記 | このブログの読者になる | 更新情報をチェックする

2007年12月03日

検索機能の強化:IDで検索できるようにしてみる

前回はMySQLの照合順序で右往左往してしまい、検索機能強化がどんどん後回しになっていたわけですが、今回から(何事もなければ…)何回かに分けて検索機能の強化について書いてみたいとおもいます。

検索機能の強化といっても大したことを考えているわけではなく、いわゆるAjaxでトレンドになったいんくりめんたるさあちとか、全文検索でファジー(死語?)にどーのこーのとかいうような高級なことをやろうとしているわけではありません。

一応ロードマップ?としてはまず今回はIDで検索できるようにスクリプトを改造します。次に検索フィールド内でAND検索ができるようにします。これはグーグルのようにスペースで区切った単語をAND検索するというものです。さらに合併日で検索できるようにします。

以上の検索方法は大元のスクリプトには、もちろん手続き型の例のアレですが、一応実装されていて、CodeIgniter?というかオブジェクト指向的?に実装したらどうなるのかな?という実験みたいなものです。で、まあこういうことを総称して検索機能の強化と言ってるだけでして、要するに自分用のメモです(^^;。

というわけで今回はIDでの検索です。これは↑の3つの中ではおそらく一番実装が楽だろう?という予想の元に、まず試してみることにしました。

IDでの検索というのはどういうことかというと、今まで実装してきた検索方法はNormalSearchにしてもKanaSearchにしても名称とかふりがな(読み)のような実体?というか現物?というかなんかそんな感じのフィールドを検索対象にしていましたが、IDでの検索というのは文字通りそれぞれのテーブルのプライマリーキーになっているようなフィールド(ほとんどidという名前がついている)で検索しようということです。

この検索方法がどのようなときに必要か?というと、例えば都道府県別で合併市町村の一覧を出力したいとします。UI的には都道府県をダダっと並べておいて、それをクリックしたらその都道府県での合併市町村の一覧を表示する〜といったようなものをイメージしています。

<都道府県別一覧>
北海道|青森|秋田|岩手|・・・|鹿児島|沖縄|

↑こんな感じで…

この時都道府県名にアンカーをつけて適宜メソッドを呼び出すわけですが、一見NormalSearch(検索フィールドからのサーチ)でも大丈夫そうな気がします。ですが例えば上記では北海道を除いて煩雑さを避けるために○○県の県の部分を省略しています。これをこのままアンカーに利用してNormalSearchに送信すれば事は足りる?とおもわれます。が!
anchor('gappei/search/'.urlencode('京都'), '京都')

のようなことを不用意にやってしまうと、この場合なら京都府の一覧とともに東“京都”の一覧も表示されてしまったりするわけです。

この例だと回避策としては「県」を省略せずに渡してやれば済む話ですが、前回の「伊達市」のように異都道府県同一市町村名?みたいなパターンだとNormalSearchでやろうとすれば北海道&伊達市、福島県&伊達市のようにANDで絞込検索?のような形にするしかありません。

まあそれでも言いといえばいいのですが、やはりダイレクトに検索?というか指定できたほうが便利なことは確かです。この市町村合併データベースのような例だとあまり必要性はないかもしれませんが、映画やCD・書籍などのデータベースだと同名異作品という場合は頻発するのではないかとおもわれます。

ということで前置きはこのぐらいにして、実装の過程をメモっておきます。

まず五十音パッドの検索部分の実装(多態編)で作成した検索オブジェクトクラスに新たにIdSearchというクラスを作ります。
class IdSearch extends SearchMode
{
function __construct(array $condition)
{
parent::__construct($condition);
}
}

他の検索オブジェクトクラスと同じようにSearchModeクラスを継承する形にします。次にwhere()の実装ですが、これは$condition(検索条件:コンストラクタに渡される)の形を、
参照するフィールド名 => ID番号

のように渡すことにするので、以下のようにほとんどNormalSearchのwhere()と同じようになります。
function where()
{
$CI =& get_instance();

$CI->db->where($this->getCriteria());
}

NormalSearchの場合はdb->like()でクエリを生成していましたが、今回は「=」でいいわけで、db->where()にそのまま検索条件配列を渡しています。次にgetSearchTitle()ですがこれはとりあえず
function getSearchTitle()
{
return 'ID Search';
}

とだけ書いて後回しにして、まずはControllerで新たにメソッドを定義します。
function idSearch($field, $id=0)
{
$this->cond = new IdSearch(array($field => $id));
$this->phpsession->save('searchObj', serialize($this->cond));
redirect('gappei/index/');
}

$fieldと$idをそれぞれURIの第3,4セグメントから受け取って、先ほど作ったクラスをインスタンス化してindex()にリダイレクトしているだけです。

これでとりあえずデータは受取れるはずですが、まだViewというかUIを作っていないのでさしあたってURL入力欄から
http://localhost/gappei/idSearch/pid/1

のように入力して試してみます。

それぞれのidのフィールド名は、

pid:都道府県ID
nid:新市町村ID
oid:旧市町村ID

のようになっているので、上記のURIだと北海道の一覧が出力されます。ここでいろいろパラメータを変えて試してみると、まあ当然ですがフィールド名に無いモノを指定したり、IDの部分(URIの第4セグメント)を文字列にしたりするとエラーが出るので、Controllerにヴァリデート(入力チェック)のコードを書きます。

Controller/idSearch()
function idSearch($field, $id=0)
{
/*$idは初期値を0にしてURIの第4セグメントが空の送信に対処する
$idは数値以外は削除
$fieldはpid, nid, oid以外は何もしないでindex()にリダイレクト
※$idの最大値のチェックは行わない、仮に送信された場合でもデータ件数0になるだけ*/
$id = preg_replace("/[^0-9]/", "", $id);

if (preg_match("/^(p|n|o)id$/", $field))
{
: (インスタンス作成等)

ほとんどヴァリデーションのコードを自力で書くことにした。でやったことと同じような感じです。これでとりあえず想定されるようなURIからのエラーは出なくなりました。

次に後回しにしていたgetSearchTitle()をちゃんと書きます。
function getSearchTitle()
{
$CI =& get_instance();

$criteria = $this->getCriteria();
$key = key($criteria);
$id = $criteria[$key];

$searchTitle = '';

switch ($key) {
case 'pid':
$CI->db->select('name')->from('prefs')->where(array('id' => $id));
break;
case 'nid':
$CI->db->select('city as name')->from('newcities')->where(array('id' => $id));
break;
case 'oid':
$CI->db->select('oc as name')->from('oldcities')->where(array('oid' => $id));
$searchTitle = '旧';
break;
}

$row = $CI->db->get()->row_array();
$searchTitle .= (count($row) > 0) ? $row['name'] : '';

return $searchTitle;
}

まあ見ての通りあんまり綺麗じゃない(ってか汚い!)コードです(--;。

単に名称を出力するだけなのに、何故かこんなに長くなっちゃいました。これはひとえにメソッドが引数を受け取っていないことからくるモノと言えそうです。

冒頭述べたように大抵の場合はUI?というか、要するにHTMLのアンカーの文字列イコール検索タイトルなわけです。なのでそれをメソッドの引数に渡すことができれば単純にそれをreturnするだけで済むはずなんですが、他のクラスとの兼ね合いから引数を取らない形でgetSearchTitle()が定義されているので、検索オブジェクトを元にいちいちDBに問い合わせしてタイトルを得ているわけです。

そもそも取ってくるテーブルのフィールド名がまちまちなため、まず検索オブジェクトのキーを元にswitch文で分岐して、それぞれのテーブルに合ったクエリを生成しています。旧市町村の場合は頭に「旧」と付けたかったのでこれはハードコーディングです(--;。

最後に$rowをcountしているのは結果セットが無い場合$row['name']を代入しようとするとエラーがでてしまうためです。これはidの値が最大値を超えた場合に起こり得ます。ここでこのように実装しておくことでController側のメソッドでは最大値のチェックをしていません。

まあ汚いですけど親クラスのgetSearchTitle()の定義に従って引数を受け渡さないように書くことで、Controller(のindex()メソッド)は一切変更せずに検索タイトルを受取ることが出来ます。
//検索タイトル取得
$data['searchTitle'] = htmlspecialchars($searchObj->getSearchTitle());

最後にViewを作ります。一応構想としては都道府県別・新市町村別・旧市町村別などの項目でワンクリックで目的の名称の一覧を表示できるようなモノを考えていましたが、この合併データベースでは新市町村別・旧市町村別などはずらずらと項目を並べてもほとんど意味がないため、都道府県別だけに留めました。

id_search.png

まあこんな感じのモノを五十音パッドの下に表示するということです。他の項目別メニュー?も階層的にしてクリックしたら展開する〜みたいなのまで作ろうとも考えましたが、あまり本筋とは関係ない部分なので止めました。

またこの出力も一応ファイル的にはlist.phpとして分離してgappei_detail.phpやgappei_simple.phpなどのメインViewからロードしていますが、list.php自体は単なるHTMLです。
<h3 class="ui_title">都道府県別</h3>
<p class="prefs">
<?=anchor('gappei/idSearch/pid/1', '北海道');?><br />
<?=anchor('gappei/idSearch/pid/2', '青森');?>|
<?=anchor('gappei/idSearch/pid/3', '岩手');?>|

:(中略)

<?=anchor('gappei/idSearch/pid/46', '鹿児島');?>|
<?=anchor('gappei/idSearch/pid/47', '沖縄');?>
</p>

HTMLヘルパを使ってul()メソッドで出力しようかな?とも考えたのですが、<ul>だと一行一都道府県になってしまい無駄に長くなるので却下しました。このようなある意味ランダム?な表示だとやはり手動でやるしかないですかね〜?まあとりあえず今回はUIよりもID検索が主なのでこれでヨシとしました。

何度も言うようですが、この合併データベースだとID検索はあまり意味がないように思えますが、データベースの内容によってはこれが結構重宝するんです。というか無いとヤバイ(謎)。

というわけで次回は検索フィールド内でのAND検索の実装について書いてみようとおもいます。
ラベル:codeigniter PHP
posted by ciallost at 05:15| Comment(0) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

2007年12月02日

MySQLの照合順序:utf8_unicode_ciってなんぞ?

前回はヴァリデートのコードをそれぞれのメソッドの頭に記述して、なんちゃって入力チェックのようなことをやった経緯を書きました。あんな実装でも変なエラーが出なくなってきたので、まあヨシとしてます。

んで、ようやく今回は検索機能の強化?について書けるかな?と考えていたんですが、どうやろうか?などと思案しながらスクリプトをいじっていると奇妙な現象?に遭遇したので、検索機能の強化はまた後回しにして今回はそれについて書きます。

結論から言うと自分がMySQLの照合順序なるものを全く理解していなかったというだけの話です。「あ〜それね」「今更何言ってんの?」と言い切れる方は以下は読む必要はありません。なので以下は個人的なメモです(愚痴とも言う…マタカヨ)

まず奇妙な現象というのは以下のようなモノです。

例の五十音パッドで「た」で始まる新市町村を検索します。

伊達市 2006-03-01
大仙市 2005-03-22
田村市 2005-03-01
伊達市 2006-01-01
高崎市 2006-01-23
高崎市 2006-10-01
胎内市 2005-09-01
 :

簡易表示だと上記のような感じに結果が表示されます。

ここで伊達市や高崎市がダブって表示されているのは別段問題ではなく、合併日を見ればわかりますが要するに二段階で合併してる?というか、高崎市の場合だとまず2006-01-23に高崎市・倉渕村・箕郷町・群馬町・新町の5つの市町村が合併して高崎市になり、その後2006-10-01に榛名町が合併したということです。伊達市の場合は単純に同名の市が北海道と福島県にあったというだけです。

ん? 伊達市? だてし? 「だ」てし?

これが奇妙な現象で、「なんで伊達市が検索されてるんだ?」という話です。SQLは下記のようになっています。
SELECT pid, pref, nid, nc, yomi, oid, oc, oyomi, gdate FROM (SELECT DISTINCT pid, pref, nid, nc, yomi, gdate FROM cities WHERE SUBSTRING(yomi FROM 1 FOR 1) = 'た' ORDER BY yomi asc LIMIT 10, 10) AS vn JOIN newcities_oldcities j ON vn.nid = j.newcity_id JOIN oldcities o ON o.oid = j.oldcity_id WHERE SUBSTRING(yomi FROM 1 FOR 1) = 'た'

改行してないから見づらいですが、問題はWHERE句のところで…。

ちなみに外側のクエリとサブクエリに同じWHERE句が発行されていますが、これはバグだとか今回の件に関連することではなくて仕様です。詳細は五十音パッドの検索部分の実装(switch case編)の後半部分で述べているとおりですが、旧市町村が検索された場合だけでなく常に外側のクエリにもWHERE句を生成する実装にしてあります。どうしてかというと条件判断を書きたくなかったもんで(--;

で、それはともかく検索条件は
WHERE SUBSTRING(yomi FROM 1 FOR 1) = 'た'

なわけです。「だ」なんてどこにも書いてないじゃん!実はこの部分のクエリを生成しているコードは手抜き?というか、気づいてはいたんだけどとりあえず後回しにしていた部分でした。

具体的には「た」だけではなくて「た」or「だ」のように検索したかったのです。クエリを書くとすれば…
WHERE SUBSTRING(yomi FROM 1 FOR 1) = 'た' or SUBSTRING(yomi FROM 1 FOR 1) = 'だ'

みたいな感じでしょうか?当然半濁点の可能性のあるハ行などは「は」「ば」「ぱ」に関してorで繋げなければなりません。しかも濁点のつかない行とかもあるし(マ行、ラ行)。そんなわけで「こんな場合分けやってられるか!」と半ば放棄して後回しにしていたわけです。

ところがなんと!いざ検索してみれば実装もしてないのに期待通りの結果が表示されてるぅ?!これは寝ている間にPCの中の人が勝手に…ナワケナイ

そもそも自分の中では↑のように考えていたわけで、これは動作は期待通りですが、コード的にはバグのようなもんなわけです(個人的には…)。

なので五十音パッドを出力する際にもわざわざ半角カナに変換して、濁点・半濁点を取り去って、また平仮名に戻して、重複削除して〜のようなコードも書いていたわけです。
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;
}

このメソッドはデータベースから取ってくる時のコードなので、このようにmb系の関数を使ってなんとか濁点を吸収(濁点・半濁点などを清音の文字と同一視するの意)できていますが、上述のように(清音で)検索する際にはこれの逆変換?というか、とにかく面倒くさそうだなぁということで後回しにしていたのです。

んで、まあおおよそのところMySQLがなんかいたづら?してるんだろう?というところまでは想像したんですが、これがネットで検索してもなかなか情報がでてこない。(ってか検索の仕方が悪いという噂が…)

おそらくこういう事って、MySQL使いの方やPHPerの方々の中では常識に類するぐらい当たり前のことなんでしょうね。なのでいちいちBlogとかに書く人もいないし、質問すらする人もいない。当たり前すぎて知らない奴はアホ、なことなんだろうと思いました。

で、散々調べてようやく、どうも照合順序とかいうのが関係してるっぽいということが判明。それでもそのものズバリを説明してるところは発見できず、それでもなんとかutf8_unicode_ciのciの部分は大文字/小文字を同一視するってことだけはわかりました。

でも日本語の大文字/小文字ってなんだ???

こういう部分は本当に海外製っつーか1バイト圏のソフトなんだなぁ?と思い知らされる部分で、アルファベットのことしか考えてない、っつーかもちろんこういう機能が実装されているので何も考えてないわけではないとは思いますが、根底にある発想というかなんというか…。

日本語(の文字)の大文字/小文字といわれてピンときますか?自分は全くわからないです。「あ」と「ぁ」のことかと…。「ゃ」とか「ぃ」とか、「ι」とか…ってこれは日本語じゃね〜ヨ。

逆の立場で考えればアルファベットの楷書体/行書体とか、ローマ字の永字八法とか言われてピンとくるガイジンがいるのか?…と。

で、愚痴はこれぐらいにして、とにかくこのutf8_unicode_ciを指定していると濁点・半濁点はおろかカタカナもなんと半角カタカナも同一視してしまうということが実験で確かめられました(^^;実験…っていったい

なので例えば新市町村フィールドに「る」と入力して検索を実行すると…

うるま市 2005-04-01
つがる市 2005-02-11
つるぎ町 2005-03-01
南アルプス市 2003-04-01

う「る」ま市・つが「る」市・つ「る」ぎ町というトンデモなひらがな市町村に加えて、南ア「ル」プス市というさらに上をいくトンデモな市も検索されて和ませてくれます。

んで、もしこういうことならば↑の方で晒した五十音パッドの出力に関する部分のコードはほとんど必要ないんじゃないか?とおもい以下のようにしてみました。
function _getCap($field)
{
//濁点・半濁点を清音に吸収するため、半角仮名で処理した後に重複削除
foreach($this->Gdb->getCap($field)->result_array() as $row)
{
$kana[] = $row['cap']; //単純に代入してるだけ

/*以下は無駄な処理っぽかったのでコメントアウト
$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;
}

すると今度は五十音パッドがなんかおかしい!

error_50on_pad.png

「てねへめらりるれろ」は元々頭文字にない文字なのでディム化されていますが、修正前は存在していたはずの「た」「こ」が修正後ではディム化されています。これはどうしたことだろうか?とそもそものテーブル(というかビューですが)を調べてみると…

phpMyAdminで表示(SELECT * FROM ncap)
ncap.png

のように「こ」「た」の(あるべき)部分に「ご」「だ」と出力されています。そこで今度はこのビューを生成しているSQLを確認してみると…
CREATE VIEW ncap AS select distinct substr(newcities.yomi,1,1) AS cap from newcities order by newcities.yomi;

と、特に問題なさそうなのですが、実はここに照合順序が影響してきていて、まずORDER BY句であいうえお順に並び替えようとしているのですが、utf8_unicode_ciという照合順序だと「た」も「だ」も同じモノとして扱われてしまいます。つまり「た」と「だ」など同一視される文字間ではソートはされないで、DB格納順に出力されるということです。

さらにDISTINCTで重複を削除した際にDBに格納された順で重複削除が行われてしまい、たまたま「こ」と「た」で始まる市町村は「ご」や「だ」ではじまる市町村が先に格納されていたためこのような結果になったわけです。

そしてその結果を濁点・半濁点を含まない五十音(「あ〜わ」まで及び「を」「ん」を除くので正確には44文字)と比較しているので、最終的に「こ」と「た」はディム化されてしまったというわけです。

というわけで↑の五十音パッド出力のコードは、$kana配列をarray_uniqueで重複削除している部分だけが不必要で、半角カナに変換して濁点・半濁点を清音に吸収している部分は必要だったということになります。

では当初の予定?どおりに動作させるにはどうしたらよいか?というと、これはあっけないくらい簡単な話で、単に照合順序を変えてやればいいだけです。

ncap(およびocap)はビューなので、大元のnewcitiesとoldcitiesのyomi, oyomiフィールドの照合順序をutf8_binにすれば、修正前のコードの意図した通りの動作になります。(つまりビュー時点では濁点・半濁点・清音が違うものとして認識され、↑のコードではarray_uniqueでさらに重複削除しないと、例えば「た、た」とか「は、は、は」のように出力されてしまうということです)

但し照合順序をutf8_binにしてしまうと、五十音パッド出力のコードは↑の修正前でよいのですが、今度は検索する方のコードが上述したとおり非常に煩雑なものになりかねません。

というわけで結論としては

照合順序:utf8_unicode_ci
五十音パッド出力コード:array_unique部分を削除
検索コード:そのまま

ということにしました。

今回の例のようにふりがなの頭一文字のような場合はutf8_unicode_ciが便利かとおもいます。ただこの照合順序にした場合は濁点・半濁点・清音の区別をつけたい場合に不都合が生じます。その場合はutf8_binにするべきだとおもいます。

またutf8_binの場合でも今回の例のようなふりがなの頭一文字だけを取得したい(検索したい)場合は、もっと単純にふりがなの頭一文字だけのフィールドを作って、入力時点で濁点・半濁点を清音に統一してしまうという方法もあります(つまり伊達市でも「た」と予め入力しておく)。実際大元のスクリプトでは全部の読みではなく頭一文字だけのフィールドになっていました。

ということでもしかしたら意外と知られていないことだったりして?ナワケナイッスネ(--;
ラベル:MySQL
posted by ciallost at 06:38| Comment(2) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

2007年12月01日

ヴァリデーションのコードを自力で書くことにした。

前回、CodeIgniterのValidationクラスは、残念ながらちょっと使えないということを書きました。そうは言っても入力チェックはしないとならないので今回はその過程のメモです。

ただ自力で書いたといっても自作Validationクラスを書いたとかそういう高級なことではなく、単に今までのスクリプト(主にController)にチェックのためのコードを付け足したってだけなので、今回のメモはCodeIgniterとはほとんど関係ありません。

それと平行して?というか前後して?スクリプト中で気になっていた部分をちょこちょこ書き直していたりしたので、それも一応メモっとこうとおもいます。なので今回は公開用というよりは自分用のメモと言うべき内容ですので、予めご了承ください。

まず現時点で「入力」がどれだけあるのかを確認しておきます。

検索フォーム:フィールド×3(都道府県・新市町村・旧市町村)
五十音パッド:平仮名44文字
五十音パッド:タブ×2(新市町村・旧市町村)
表示モード:チェックボックス(真偽値)
表示件数:プルダウンリスト(10, 20, 30, 50, 100)
ソート:(都道府県・新市町村・旧市町村・合併日)
ページングのオフセット値:数値

そしてそれぞれの入力に対してほぼ1:1で対応するアクション?というか要するにControllerクラスのメソッドが定義されています。

search()
kanaSearch($cap)
changeKanaTab($field)
changeDisp()---*1
sort($field='nid')
index($offset=0)

*1:表示モードと表示件数に関しては元々はchageDispMode()とchangeDispNum()という別々のメソッドでしたが、changeDisp()にまとめました。

index()以外のメソッドは必ず最後にindex()にリダイレクトするようになっています。なのでページングのオフセット値($offset)以外の入力値は、全てそれぞれのメソッド内の先頭で入力チェック(ヴァリデート?)することにしました。

まずsearch()ですがこれは検索オブジェクト的?にはNormalSearchというインスタンスを作成している場所?で、チェックといっても数字のみとか英数のみのように限定することができません。

最終的にはここでの入力値がSQLのWHERE句に渡されますが、その際はアクティブレコードでクエリを組み立てているので、SQLインジェクションの心配はない?ということにしておきます(^^;。

なので基本的にはこの段階ではノーチェックなのですが、一つだけ気になることがあったので、そのチェックだけ施しています。

気になっていた事というのは、3つのフィールドが全部空白だった場合の処理で、今まではWHERE句なしつまり全データ表示という仕様?になっていましたが、さすがに全データというのはナンなんで、3フィールド共空白の場合は検索しないという仕様に変更しました。
function search()
{
//全フィールドが空の場合は、全件取得ではなく直前のクエリに戻す(つまり何もしない)
if (implode('', $this->input->post('condition')) == '')
{
redirect('gappei/index/');
}

}

次に五十音パッドの平仮名ですが、これはCodeIgniter:日本語URIでThe URI you submitted has disallowed characters.で書いたとおり、CodeIgniterの$config['permitted_uri_chars']で大方のやばそうな入力は弾かれます。

まあこれに引っかかった場合いきなりexit()という仕様はアレなんですが(^^;…。それはまあ置いといて、↑の機能だけでは英数などは弾かれないので、平仮名の「あ〜わ」以外の文字は弾くような設定にしました。
function kanaSearch($cap)
{
/*
平仮名以外の日本語文字はconfigで弾く設定になっているが、
それら以外の英数記号などをここで弾く
[参照]config/config.php-127行目$config['permitted_uri_chars']
*/

if (!preg_match("/[あ-わ]/u", $cap))
{
redirect('gappei/index/');
}

}

CodeIgniter:日本語URIでThe URI you submitted has disallowed characters.で学んだとおりUTF-8なので/uオプションをつけてpreg_matchを使っています。

次に五十音パッドのタブ選択ですが、これは単純にncapかocapの2通りしかないので、正規表現でそれら以外は全部 ncap(デフォルト)として処理しました。
function changeKanaTab($field)
{
//タブはncapかocapにマッチするもの以外はncap
$selectedTab = preg_match("/^(n|o)cap$/", $field) ? $field : 'ncap';

}

表示系(モード・件数)に関しては、件数は数値以外は強制削除にしましたが、モード(チェックボックスの値)に関してはあえてヴァリデートはしていません。
function changeDisp()
{
//表示件数は数値以外削除
$dispNum = preg_replace("/[^0-9]/", "", $this->input->post('dispnum'));
set_cookie('dispnum', $dispNum, Gappei::COOKIE_LIFE);

//表示モードは内容の如何を問わず存在すればTRUE、なのでヴァリデートしない
set_cookie('dispmode', $this->input->post('dispmode'), Gappei::COOKIE_LIFE);

}

これはindex()内で読み出す際に内容は読み取らずに単に存在するかどうか?だけで真偽値を判断するようにしたからです。
//表示モード取得
$data['dmChecked'] = (get_cookie('dispmode') <> '') ? TRUE : FALSE;

次にソートに関してですが、これはヴァリデートの前に多少?(かなり?)手直ししました。まず今までは単純に、新市町村のソートならばncフィールド(つまり新市町村名)をキーにソートする仕様でしたが、漢字の順番に並べてもあまり意味がないので、新旧市町村に関してはその読みのフィールド(yomi, oyomi)をキーにすることにしました。

また都道府県も都道府県名をキーにするより都道府県IDで並べた方が北から南〜のように整然とするので、DBのビューやクエリを修正してpref_idを取ってくるようにし、ソートキーにはそれ(pid=エイリアス)を使うようにしました。

さらに今まではソート方向を引数として取らずにメソッド内でチェックしてソート方向を決定していましたが、この実装だと煩雑になるので引数でソートキーとソート方向を受取るようにしました。

修正前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()
function sort($field, $order='asc')
{
$sortkey = array('field'=>$field, 'order'=>$order);

$this->phpsession->save('sort_key', serialize($sortkey));

redirect('gappei/index/');
}

これに伴いViewでテーブルヘディングを出力している部分も以下のように変更しました。
$th = array(
'pid' => '都道府県',
'yomi' => '新市町村',
'oyomi' => '旧市町村',
'gdate' => '合併日',
);

function setTableHeading($th, $sort)
{
$th_tag = '';

foreach ($th as $field => $column)
{
$th_tag .= "<th>";

if ($field == $sort['field'])
{
if ($sort['order']=='asc')
{
$order_mark = '▽';
$order = 'desc';
} else {
$order_mark = '△';
$order = 'asc';
}

$th_tag .= anchor("gappei/sort/$field/$order", $column);
$th_tag .= " <span id=\"order_mark\">".$order_mark."</span>";

} else {
$th_tag .= anchor("gappei/sort/$field", $column);
}

$th_tag .= "</th>\n";
}

return $th_tag;
}

今まではパラメータとしてフィールド(=ソートキー)しか渡していなかった部分を、ソートが行われているカラムの場合はURIの第4セグメントでソート方向を渡すようにしています。ソートが行われていないカラムは以前同様ソートキーしか渡していませんが、これはControllerのsort()でfunction sort($field, $order='asc')のようにソート方向がない場合はascを初期値としていることで解決?しています。

※修正前のテーブルヘディング出力コード
CodeIgniter:ソート機能を付けてみる

そしてヴァリデーションですが、これは単純に受取る予定のない文字列はデフォルト値(nid asc)にしてしまう設定です。
function sort($field, $order='asc')
{
$fld = preg_match("/^(pid|yomi|oyomi|gdate)$/", $field) ? $field : 'nid';
$odr = preg_match("/^(asc|desc|)$/", $order) ? $order : 'asc';

}

最後にページングのオフセット値ですが、これはまず数値に限定することと、もう一つは値の取る範囲を限定しています。
//offset値チェック:数値以外の送信、ブラウザで戻るボタンを押された場合の対処
$offset = preg_match("/[^0-9]/", $offset) ? 0 : $offset;
$offset = $data['count'] < $offset ? 0 : $offset;

$data['count']は文字通りデータの件数ですが、このチェックをしないとブラウザの戻るボタンをクリックした際に以下のような状況でエラーがでます。

・「か」で始まる旧市町村を検索する(142件)
 >表示件数は10件/頁
・最終ページを表示する(15ページ目)
 >この時のLIMIT句は LIMIT 140, 10
・「め」で始まる旧市町村を検索する(4件)
・ブラウザの戻るボタンをクリックする(エラー)
 >Invalid argument supplied for foreach()
 >この時のLIMIT句は LIMIT 140, 10 にも関わらず検索オブジェクトは「め」で始まる旧市町村になっているためエラー

つまり上記の例の場合だと「め」で始まる旧市町村の件数が4件しかないのにも関わらず、オフセットを140でデータを取ってこようとしている部分がまずいということです。

この場合SQLは文法的に正しい?(たぶん正しいとおもう)ので、DBではエラーは出ませんが、取得データはおそらくNULLだと考えられます。ただデータ件数はこのSQLの前に件数取得SQL(LIMITなしJOINなし)で取ってきているため、View側の<?if($count > 0):?>は真になってしまい、foreachブロックに入ってエラーが出ているというわけです。

ということでオフセット値をデータ件数と比較して、大きければ強制的に0、つまり1ページ目を表示ということにしました。文章で書くとわかりづらいですが、とにかくこれでエラーはでなくなりました。

というわけでValidationの実装はひとまず終了!これら以外にもちょこちょこスクリプトを修正しているので、ある時点でBlogで晒したコードとはかなり変化してきている部分もあるとおもいますが、そういう部分もおいおい解説できたらと考えています。

これで多少はコードが綺麗になってきたので、次回から懸案?となっている検索機能の強化を書いていきたいとおもいます。
ラベル:PHP
posted by ciallost at 04:52| Comment(3) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする