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