CodeIgniterの学習 21 - 標準のValidation機能を拡張し、Javascriptとphpの両方でバリデーションチェックしてみる(ClientServer Validation の改造)
今日は 先日のエントリ ( http://d.hatena.ne.jp/dix3/20081011/1223705482 ) のXMLでのフォーム生成の試みをやめて、
標準のCodeigniterのフォームバリデーションチェック処理を拡張する形で、バリデーション機能を簡略化してみる。
CodeIgniter標準のバリデーションチェックは、サーバー側のみの実装なのだが、
それだけを試すのだとマニュアル通りで全然面白くないので、Javascriptでの事前簡易バリデーションを使ってみる。
クライアント側で簡易チェックを行うことで若干の負荷削減と操作性の向上も期待できる。
wiki (http://codeigniter.com/wiki/) を探してみると、ClientServer_Validationなる実装を見つけた。(http://codeigniter.com/wiki/ClientServer_Validation/ 作ってくれた人ありがとう。)
これは、CodeIgniter の標準Validation を継承し、
CodeIgniter標準のバリデーションチェック処理を記述するだけで、
Javascriptのバリデーションチェック処理までもが同時に生成出来てしまう優れものだ。
Javascriptを使うのに、Javascriptの記述がほとんど要らないのが気に入った。
俺俺改造してみる
設置も利用方法も簡単なのだが、これだけでは気に入らない箇所が有ったので、ついでに俺俺適当改造を施してみた。
以下、俺俺適当改造版ソースを貼り付けているが、
元ネタは、http://codeigniter.com/wiki/ClientServer_Validation/ です。詳細はこちらを参照のこと。
(改造版も簡単にテスト的には動かしたけど、無保証です。あしからず。)
(2008/10/13追記、xss_cleanの指定など、javascript側で未知のファンクションある場合はクライアントサイドでのチェック行わず、サーバ側のみに任せるようにしている。クライアントサイドのjavascriptでもチェックを行いたいときは、Validator.prototype.hoge=function を増やしていき、エラー文言を追加するような更なる改造が必要だ。)
先に完成画面を貼っておく
俺俺改造で、個別エラー表示(以下A方式)と、一括エラー表示(以下B方式)の両方に一応対応してみた。
(コールバック関数は試してないのでうまくいくかは知らん。)
呼び出しかたはCodeIgniterのバリデーションの書き方に寄せているので、すぐわかると思う。
- submitの前に、一度javascript側でバリデーションチェックが走り、だめならここで寸止め。
- 1)がOKならさらにサーバー側でバリデーションチェックが走り、ここで寸止め。
となる。(エラー画面は、1 , 2 共に共通)
改造後のバリデーションエラーイメージは、こんな感じ。
submit(POST)してサーバ側に問い合わせする前に、
先にjavascriptで簡易チェックが入ってエラーが表示される。
POST後にサーバ側で、PHPでの強制チェックが走る。
ビュー側でmaxlengthとか、style ime-modeとかをきちんと設定してあげると、かなりいい感じになると思う。
ソース:application/libraries/MY_Validation.php
http://codeigniter.com/wiki/ClientServer_Validation/の記述をベースに、
application/libraries/MY_Validation.php を作成した。
これで、既存のValidation.phpを継承したバリデーションが使えるようになる。
改造ポイント:
- 元ソースだと、コールバック関数を指定しないときは、デフォルトが window.alert でアラートが出現していたのだが、これを、上の写真のように、各項目の直下に表示するようにし、入力項目の背景をピンク色に変えるようにした。(エラー発生項目を再入力して正常時になった時は、アラートも消え、元の背景色に戻る。)(パタンBの方式では、フォーム内に一括エラー表示)
- 任意項目で、長さ制限等を指定しているときに、値指定が無くても長さ制限のエラーメッセージが出現していたのを抑えた。
- trimが指定されている時は先にtrimを掛けてから、値判定するようにした。(同時に画面項目上の値もtrimされる。)
- xss_clean等、javascript上では未知のファンクションが指定されているときの挙動を修正し、javascript側では、未知のファンクションは無視するようにした。(未知のファンクションの場合のエラーは表示されない。最終的にサーバー側のphpの処理が走るからいいよね。)
application/libraries/MY_Validation.php
(08/10/13 14:15 再修正済み)
<?php if ( !defined( 'BASEPATH' ) ) exit( 'No direct script access allowed' ); // ------------------------------------------------------------------------ /** * Validation Class extension * * @package CodeIgniter * @subpackage Libraries * @category Validation * @author AK * @link http://codeigniter.com/user_guide/libraries/validation.html */ /** * Set Fields * * This function takes an array of field names as input * and generates class variables with the same name, which will * either be blank or contain the $_POST value corresponding to it * * @access public * @param string $ * @param string $ * @return void */ /** * modified : http://d.hatena.ne.jp/dix3 at 2008/10/13 * link : http://d.hatena.ne.jp/dix3/20081013/1223862786 */ class MY_Validation extends CI_Validation { function MY_Validation() { parent :: CI_Validation(); } /** * JavaScript * * This function provides default implementation of the built-in CI validation rules in javascript. * The function generates a client-side js script, complete with <script>...</script> html tags, * suitable for inclusion in the document header. Additionally, custom rules can be added by * defining global js functions, or extending Validation js object. * * @access public * @param string $ - custom error message (optional) * @param string $ - name of a js error callback function (optional) * @return string - js */ function javascript( $alert_msg = '', $alert_func = 'null' ) { if ( !$this -> _fields || !$this -> _rules ) return '<script type="text/javascript">function validation_run(f) {}</script>'; // client-side javascript implementation of CI built-in validation rules. $script = ' <script type="text/javascript"> String.prototype.trim = function() { return this.replace(/^\s\s*/, "").replace(/\s\s*$/, ""); } var Validator = function(f) { this.form = f; } Validator.prototype.required = function(str) { return str.search(/\S/) > -1; } Validator.prototype.matches = function(str, field) { return (str == this.form.elements[field].value); } // FIX! change "field" from input name to input ref? Validator.prototype.max_length = function(str, val) { return (str.length <= val); } Validator.prototype.min_length = function(str, val) { return (str.length >= val); } Validator.prototype.exact_length = function(str, val) { return (str.length == val); } Validator.prototype.valid_email = function(str) { return str.search(/^([\w\+\-]+)(\.[\w\+\-]+)*@([a-z\d\-]+\.)+[a-z]{2,6}$/i) > -1; } Validator.prototype.valid_ip = function(ip) { var segments = ip.split("."); for (var i in segs) if(segs[i].length>3 || segs[i]>255 || segs[i].search(/\D/)>-1) return false; return true; } Validator.prototype.alpha = function(str) { return str.search(/[^a-z]/i) == -1; } Validator.prototype.alpha_numeric = function(str) { return str.search(/[^a-z0-9]/i) == -1; } Validator.prototype.alpha_dash = function(str) { return str.search(/[^\w-]/i) == -1; } Validator.prototype.numeric = function(str) { return ! isNaN(str); } Validator.prototype.integer = function(str) { return ! (isNaN(str) || str.indexOf(".") > -1); } Validator.prototype.valid_base64 = function(str) { return str.search(/[^a-zA-Z0-9\/\+=]/) == -1; } Validator.prototype.validate = function (rules, callback) { try { if (!rules.length) return true; var res, errors=[]; var disperr = false;//個別エラー表示用フラグ追加 for (var i in rules) { var item = rules[i]; var field = this.form.elements[item.input]; var rule_list = item.rule.split("|"); var required = false;//必須かどうかのフラグ for (var r in rule_list) { required = (item.rule.search(/required/) == -1) ? required : true; if(rule_list[r].search(/trim/)){ //trimの時は画面の値をまずtrimして判定する field.value = field.value.replace(/^\s\s*/, "").replace(/\s\s*$/, ""); } var re = /(callback_|validate_)?(\w+)(?:\[(.+)\])?/i.exec(rule_list[r]); var func = re[2]; if (!this[func]) { //try { func = eval(func); } catch (e2) { } //javascript側で未知のファンクション(xss_clean 等があるときは、クライアントサイド側では無視する。 //サーバーサイド側でもう一度走るのであまり問題ないはず。 try{ func = eval(func); res = (typeof(func) == "function") ? func(field.value, re[3]) : false; }catch(e2){} } else { res = this[func](field.value, re[3]); } //任意項目で且つ空白の時はエラー文言は表示しない if( (false == required) && ( "" ==field.value) && ( false == res ) ){ res = true; } } var obj_input = document.getElementById( item.input ); var obj = document.getElementById( item.input + "_error" ); if (!res && item.msg) { if(obj){//もし id="input項目_error" が存在しているならば、そこに個別にエラー表示する obj.innerHTML = item.msg ; if(obj_input){//もし入力項目に同名のidが振っているならば、背景色をピンク色にする obj_input.style.background="#ffbbcc"; } disperr = true; errors.push([item.msg, item.input]); }else{ //元ソースではこっち errors.push([item.msg, item.input]); } }else{ if(obj){ obj.innerHTML = "" ; } if(obj_input){ obj_input.style.background="#fff"; } } } } catch (e) { alert(e);return false; } if (errors.length) { // show errors if(disperr){//個別エラー表示時 //display_alert(errors);//画面上部にも同時にエラーを出したい時 return callback ? callback(errors) : false ; }else{ return callback ? callback(errors) : display_alert(errors); } } return true; } '; // default alert message if ( $alert_msg == '' ) $alert_msg = 'Please fix the following errors:'; // default implementation of the validation action $script .= ' function display_alert(errors) { var str = ""; var obj = document.getElementById("error_string"); //もしid="error_string" が存在しているときはこのinnerHTML上に出す、それ以外はアラートで出す if(obj){ for (var i in errors){str += "<br>" + errors[i][0];} obj.style.display = "block"; obj.innerHTML = "' . addslashes( $alert_msg ) . '" + str ; }else{ for (var i in errors){str += "\n- " + errors[i][0];} window.alert("' . addslashes( $alert_msg ) . '" + str); } return false; } '; // Original display_alert /** * $script .= ' * function display_alert(errors) { * var str = ""; * for (var i in errors) * str += "\n- " + errors[i][0]; * alert("' . addslashes( $alert_msg ) . '" + str); * return false; * } * '; */ // Load the language file containing error messages $this -> CI -> lang -> load( 'validation' ); $params = null; foreach ( $this -> _rules as $field => $rules ) { // Explode out the rules! $ex = explode( '|', $rules ); $messages = array(); foreach ( $ex as $rule ) { $param = false; if ( preg_match( "/(.*?)\[(.*?)\]/", $rule, $match ) ) { $rule = $match[1]; $param = $match[2]; } if ( ! isset( $this -> _error_messages[$rule] ) ) { if ( false === ( $line = $this -> CI -> lang -> line( $rule ) ) ) { // エラー文言が見つからないものはとりあえずメッセージは空白にしておく // $line = 'Unable to access an error message corresponding to your field name.'; $line = ''; } } else { $line = $this -> _error_messages[$rule]; } // Build the error message $mfield = ( ! isset( $this -> _fields[$field] ) ) ? $field : $this -> _fields[$field]; $mparam = ( ! isset( $this -> _fields[$param] ) ) ? $param : $this -> _fields[$param]; $messages[] = sprintf( $line, $mfield, $mparam ); } $params[] = '{input:"' . $field . '",rule:"' . $rules . '",name:"' . ( isset( $this -> _fields[$field] ) ? addslashes( $this -> _fields[$field] ) : $field ) . '",msg:"' . join( ' ', $messages ) . '"}'; } $script .= "\nfunction validation_run(f) {\n\tvar rules = [\n\t\t" . join( ",\n\t\t", $params ) . "\n\t];\n\treturn new Validator(f).validate(rules," . $alert_func . ");\n}\n</script>"; return $script; } }//endofclass /* End of file MY_Validation.php */ /* Location: ./application/libraries/MY_Validation.php */ ?>
使い方
基本は http://codeigniter.com/wiki/ClientServer_Validation/ を参照のこと。ポイント:
1)コントローラ側の記述方法
標準のバリデーション処理とほとんど同一。
$this -> load -> library( 'validation' );
で、バリデーションライブラリのロードを行い、
$fields = array( 'email' => 'Email','password' => 'パスワード',…
で、対象フィールドと画面表示用フィールド名のセット
$rules = array( 'email' => 'trim|required|valid_email|xss_clean','password' => 'required|min_length[5]|xss_clean',…
で、バリデーションのルールを記述
$this -> validation -> set_fields( $fields ); $this -> validation -> set_rules( $rules );
で上記ルールを有効化している。
違うところは、ルールを有効化した後に、
で、$data['validation_js']に、クライアントサイドでのチェック用javascriptが自動生成されるので、これをヘッダに貼り付ける点のみ。
$data['validation_js'] = $this -> validation -> javascript();
2)外側ビューの記述方法
ヘッダ(<head>〜</head>)部分に、
<?php if(isset($validation_js)){echo $validation_js ;} ?>
を貼り付けて、自動生成されたjavascriptを展開している。
3)内側ビューの記述方法
標準のバリデーションのエラー記述方法とほとんど一緒。
標準と違うところは、
A(個別エラー表示)方式:
や、
<p id="email_error"><?= $this->validation->email_error; ?></p>
<p id="firstname_error"><?= $this->validation->firstname_error; ?></p>
のように赤字部分のid="要素名_error" 付きのタグで、$this->validation->email; 等の個別エラーメッセージを囲ってあげるだけ。
ついでに、
<input type="text" name="email" id="email" value="<?=$this->validation->email;?>" size="50" /> <p id="email_error"><?=$this->validation->email_error; ?></p>
のようにして input要素に id="email" id="firstname" 等を付けてあげると、エラー時にはinput要素の背景色がピンク色になる。
B(一括エラー表示)方式:
<p id="error_string" class="none"><?=$this->validation->error_string; ?></p>
の赤字部分のid="error_string"付きのタグで、$this->validation->error_string のエラーメッセージを囲ってあげるだけ。
あとは、submitボタンを
<input type="submit" onclick="javascript:if(!validation_run(this.form)){return false;}" value="送信" />
みたいにして、先にvalidation_run(this.form) でクライアントサイドバリデーションを走らせればOK。
(クライアントサイドバリデーションOKの時は、
さらにサーバーサイドでCodeigniter標準のバリデーションが走り、エラーならPOST値をinput項目に再度格納してくれる。)
サンプルソース
変更点のみ抜粋
1)バリデーションライブラリ:application/libraries/MY_Validation.php
上記のソースを使用
2)コントローラ:application/controllers/tasklist.php
_setup_validation() にバリデーション設定を寄せている。
(過去のエントリで試したページネーション処理とかはここでは省略している。)
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); // タスクリストというかいろいろ実験 class Tasklist extends Controller { // コンストラクタ function Tasklist() { parent :: Controller(); // ログイン認証、ログインしてなければログイン画面に飛ばされる // $this -> freakauth_light -> check(); // 開発中はscaffoldingを有効に if ( config_item( 'my_debugger' ) ) { $this -> load -> scaffolding( 'tasklist' ); // テーブル名とクラス名は同じでなくてもいい } // urlヘルパ $this -> load -> helper( 'url' ); // フォームヘルパ $this -> load -> helper( 'form' ); // モデルの読み込み todo:後でモデルに寄せる // $this -> load -> model( 'tasklist_mdl' ); // バリデーションライブラリのロード $this -> load -> library( 'validation' ); $this -> validation->set_error_delimiters('','');//バリデーションエラー埋め込みの<p></p>囲みを解除 } // 登録更新処理、まだ枠だけ // ClientServer Validation を使用した例 // todo:後でxajaxを使用したValidationチェックの実験もやってみる function edit() { $data['title'] = "ClientServer Validationを使ったバリデーションの実験、まだ枠組みだけ、"; //バリデーションのセットアップ $this -> _setup_validation( "edit" ); // クライアント側バリデーションのセット //2番目の引数にコールバックするjavascriptを指定できるようだ $data['validation_js'] = $this -> validation -> javascript('なんかエラーがあるじょ'); // ビューの生成 $this -> _setTpl( "tasklist_edit" , $data ); } // サブミット時の処理(別に名前はsubmitじゃなくても、formのポスト側と合っていればなんでもいい。) function submit() { // todo:トークンキーの判定を設置する // todo:キャプチャの判定を設置する // todo:呼び出し元編集画面による条件分岐 //バリデーションのセットアップ $this -> _setup_validation( "edit" ); if ( $this -> validation -> run() === false ) { // サーバーサイドでのバリデーションエラーに引っかかった return $this -> edit(); } // バリデーションチェックOKの時はこっち $data['title'] = "サブミット後"; //$str = "本当はここでデータの登録更新処理して別画面にリダイレクトするか、登録確認画面を出す<br/>"; $str = "POSTデータ<br/>" . print_r( $_POST, true ); //$str = "hoge"; $data["msg"] = $str; // ビューの生成 $this -> _setTpl( "tasklist_success" , $data ); } // ClientServer Validation を使用したバリデーション例 function _setup_validation( $mode ) { // todo:トークンキーの設置 // todo:キャプチャの設置 switch ( $mode ) { case "edit" : // 編集画面のバリデーション // エラー表示用フィールド名のセット // ウチの場合は多言語化は必要ない、 // 多言語対応CMSを作るのなら定数読み込みに変える必要有り $fields = array( 'email' => 'Email', 'password' => 'パスワード', 'passconf' => 'パスワード(確認)', 'firstname' => '氏', 'lastname' => '名', 'city' => '市町村名', 'zipcode' => '郵便番号', 'address1' => '住所1', ) ; // バリデーションルールのセット // Javascriptでの事前バリデーションと共有化される $rules = array( 'email' => 'trim|required|valid_email|xss_clean', 'password' => 'required|min_length[5]|xss_clean', 'passconf' => 'trim|required|matches[password]|xss_clean', 'firstname' => 'trim|required|xss_clean', 'lastname' => 'trim|required|xss_clean', 'city' => 'trim|min_length[2]|xss_clean', 'zipcode' => 'trim|exact_length[7]|xss_clean', 'address1' => 'trim|required|xss_clean', ); break; default: break; } if ( isset( $rules ) ) { $this -> validation -> set_fields( $fields ); $this -> validation -> set_rules( $rules ); } } // ビューの生成 一カ所にまとめた function _setTpl( $viewName, $data = array() ) { // 大枠のテンプレート側の変数(ヘッダ部分のタイトルと、xajax用javascriptタグ) $tpl["page_title"] = "タスクリストにようこそ"; // タスクリスト(実験ソース)のテンプレートにデータをセット $tpl["main_content"] = $this -> load -> view( $viewName, $data , true ); // 大枠のテンプレートに、タスクリストのビューをはめ込む $this -> load -> view( 'base_view', $tpl ); } } //Endofclass /** * End of file tasklist.php */ /** * Location: ./application/controllers/tasklist.php */ ?>
3)外側ビュー:application/views/base_view.php
ヘッダ部分のみ変更、後はこれまでと同一
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja"> <head> (途中省略) <?php if(isset($validation_js)){echo $validation_js ;}?> (下省略)
4)中身ビュー:application/views/tasklist_edit.php
こんな感じ。(数値のみチェックや、max-length、ime-mode:disabledやらの細かい制御はまだ入れていない)
肝は、
<p id="email_error">
とかでidを振ってエラーを囲ってあげることと、
<input type="submit" onclick="javascript:if(!validation_run(this.form)){return false;}" value="送信">
で、サブミットの前に、validation_run(this.form) のチェックを行い、エラーならここでサブミットしないで寸止めすること。
<h3><?= $title ?></h3> <?=form_open('/tasklist/submit'); ?> <fieldset> <legend>クライアント側、サーバ側のバリデーションの共用実験</legend> <!-- 一括でエラーを表示したいとき --> <!-- <p id="error_string" class="none"><?=$this->validation->error_string; ?></p> --> <label>Email<em>*</em></label> <input type="text" name="email" id="email" value="<?=$this->validation->email;?>" size="50" /> <p id="email_error"><?=$this->validation->email_error; ?></p> <label>パスワード<em>*</em></label> <input type="password" name="password" id="password" value="<?=$this->validation->password;?>" size="40" /> <p id="password_error"><?=$this->validation->password_error; ?></p> <label>パスワード(確認)<em>*</em></label> <input type="password" name="passconf" id="passconf" value="<?=$this->validation->passconf;?>" size="40" /> <p id="passconf_error"><?=$this->validation->passconf_error; ?></p> <label>氏<em>*</em></label> <input type="text" name="firstname" id="firstname" value="<?=$this->validation->firstname;?>" size="40" /> <p id="firstname_error"><?=$this->validation->firstname_error; ?></p> <label>名<em>*</em></label> <input type="text" name="lastname" id="lastname" value="<?=$this->validation->lastname;?>" size="40" /> <p id="lastname_error"><?=$this->validation->lastname_error; ?></p> <label>市町村名</label> <input type="text" name="city" id="city" value="<?=$this->validation->city;?>" size="30" /> <p id="city_error"><?=$this->validation->city_error; ?></p> <label>郵便番号</label> <input type="text" name="zipcode" id="zipcode" value="<?=$this->validation->zipcode;?>" size="10" /> <p id="zipcode_error"><?=$this->validation->zipcode_error; ?></p> <label>住所1<em>*</em></label> <input type="text" name="address1" id="address1" value="<?=$this->validation->address1;?>" size="60" /> <p id="address1_error"><?=$this->validation->address1_error; ?></p> <div><input type="submit" onclick="javascript:if(!validation_run(this.form)){return false;}" value="送信" /></div> </fieldset> <?=form_close(); ?>
5)中身ビュー:application/views/tasklist_success.php
適当、まだ何も作っていない。
<!-- 成功時 --> <h3><?= $title ?></h3> <?= $msg ?>
6)cssファイル : ドキュメントルート/css/style.css
変更点のみ抜粋、
/*画面非表示*/ .none{ display:none; } /*フォーム内エラー文言は赤字*/ form fieldset p{ margin:0; padding:0px; color:#ff0000; font-size:0.875em; /*display:inline;*/ } form fieldset{ border:1px solid #ccc; } form em{ color:#ff0000; } form input , form textarea{ font-size:12px; }
こんな感じでフォームの作り方+バリデーション方法を大体確立した。
理想はこれをさらに改造して、
- wiki上の他のバリデーション拡張も取り込んでみるとか、
- 追加ロジック無しでのxajax対応とか、
- フォームヘルパーを改造し、バリデーションルールをビュー側のフォームヘルパの記述内に持たせるようにするとか、
- 電話番号、郵便番号、NG文字列等のバリデーションルールを新設するとか、
いろいろやると楽しそう。
(2008/10/13追記、xss_cleanの指定など、javascript側で未知のファンクションある場合はクライアントサイドでのチェック行わず、サーバ側のみに任せるようにしている。クライアントサイドのjavascriptでもチェックを行いたいときは、Validator.prototype.hoge=function を増やしていき、エラー文言を追加するような更なる改造が必要だ。)
次は、DBへの格納周り・既存データの更新とか、トークンを仕込むとかの処理を実験するつもり。
疲れたので今日はここまで。
(関連するエントリー:http://d.hatena.ne.jp/dix3/20081111/1226350226 を追加しました。)