CodeIgniterの学習 73 - 標準のValidation機能を拡張し、Javascriptとphpの両方でバリデーションチェックしてみるその2(Form_validation化対応コード、暫定版)

こんにちは!

今日は以前のエントリー

http://d.hatena.ne.jp/dix3/20081013/1223862786 -
CodeIgniterの学習 21 - 標準のValidation機能を拡張し、Javascriptphpの両方でバリデーションチェックしてみる(ClientServer Validation の改造)


で俺俺改造した、ClientServer Validation の Form_validation 化版コードを貼っておく。
説明しだすと長いので、今日は画面ダンプと、ライブラリーのコードと、ポイントのみ。
使用例とかは次回のエントリーにでも。


以前のエントリー記述時のCodeIgniter(1.6系)では、Form_validationライブラリは存在せず、Validationライブラリのみが存在していた。
ValidationライブラリはCI 1.7.1でも存在するが、Validationライブラリは過去のものとなり、Form_validationの使用が推奨されていたはず。

以前の俺のエントリーも過去のものに化しているので、俺俺改造ソースもForm_validation上で何となく動くように更新。


画面


こんな感じ。POST前にjavascriptで必須チェック等を実行する。

簡易バリデーションチェックをクライアント側で、厳密なバリデーションチェックをサーバ側で行う二段構え。




コード

こんな感じ。一応動くけどテストが足りない。故に暫定版。

但し、<input name='hoge[]' /> みたいに配列で渡したときに動くかは未テスト。
テストが足りないコードを貼るなよというツッコミには、お好きにいじくり回して改造してください。・改良したらまた貼るよ。という事で逃げておく。


ちなみに元ネタは、http://codeigniter.com/wiki/ClientServer_Validation/ です。かなり変わっているけど。


ライブラリ:application/libraries/MY_Form_validation.php

<?php if ( !defined( 'BASEPATH' ) ) exit( 'No direct script access allowed' );
// 日本語 wikiから
// ------------------------------------------------------------------------
/**
 * Validation Class extension
 *
 * @package CodeIgniter
 * @subpackage Libraries
 * @category Validation
 * @author AK
 * @link http://codeigniter.com/user_guide/libraries/validation.html
 */

/**
 * modified : http://d.hatena.ne.jp/dix3 at 2008/10/13
 * link : http://d.hatena.ne.jp/dix3/20081013/1223862786
 */
/**
 * modified : http://d.hatena.ne.jp/dix3 at 2009/09/12
 * link : http://d.hatena.ne.jp/dix3/20090912
 */

class MY_Form_validation extends CI_Form_validation {
  private $_tmp_field_data = array();
  private $_defaultbgcolor = '#fff'; //input領域の背景色
  private $_alertbgcolor = '#ffbbcc'; //アラートの背景色
  public function __construct()
  {
    parent :: __construct();
  }

  public function set_rules( $field, $label = '', $rules = '' )
  {

    // If an array was passed via the first parameter instead of indidual string
    // values we cycle through it and recursively call this function.
    if ( is_array( $field ) ) {
      foreach ( $field as $row ) {
        // Houston, we have a problem...
        if ( ! isset( $row['field'] ) OR ! isset( $row['rules'] ) ) {
          continue;
        }
        // If the field label wasn't passed we use the field name
        $label = ( ! isset( $row['label'] ) ) ? $row['field'] : $row['label'];
        // Here we go!
        $this -> set_rules( $row['field'], $label, $row['rules'] );
      }
      return;
    }
    // No fields? Nothing to do...
    if ( ! is_string( $field ) OR ! is_string( $rules ) OR $field == '' ) {
      return;
    }
    // If the field label wasn't passed we use the field name
    $label = ( $label == '' ) ? $field : $label;
    // Is the field name an array?  We test for the existence of a bracket "[" in
    // the field name to determine this.  If it is an array, we break it apart
    // into its components so that we can fetch the corresponding POST data later
    if ( strpos( $field, '[' ) !== FALSE AND preg_match_all( '/\[(.*?)\]/', $field, $matches ) ) {
      // Note: Due to a bug in current() that affects some versions
      // of PHP we can not pass function call directly into it
      $x = explode( '[', $field );
      $indexes[] = current( $x );

      for ( $i = 0; $i < count( $matches['0'] ); $i++ ) {
        if ( $matches['1'][$i] != '' ) {
          $indexes[] = $matches['1'][$i];
        }
      }

      $is_array = TRUE;
    }else {
      $indexes = array();
      $is_array = FALSE;
    }
    // Build our master array
    $this -> _tmp_field_data[$field] = array( 'field' => $field,
      'label' => $label,
      'rules' => $rules,
      'is_array' => $is_array,
      'keys' => $indexes,
      'postdata' => NULL,
      'error' => ''
      );
    parent :: set_rules( $field, $label, $rules);
  }

  /**
   * 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
   */

  public function javascript( $alert_msg = '', $alert_func = 'null' )
  {
    if ( !$this -> _tmp_field_data ) {
      return $this -> _get_js_null_validation_run();
    }
    // client-side javascript implementation of CI built-in validation rules.
    // default alert message
    if ( $alert_msg == '' ) {
      $alert_msg = addslashes( 'Please fix the following errors:' );
    }
    // Load the language file containing error messages
    $this -> CI -> lang -> load( 'form_validation' );
    $params = null;
    foreach ( $this -> _tmp_field_data as $field => $v ) {
      // Explode out the rules!
      $ex = explode( '|', $v['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 = '';
            // エラー文言が見つからないものは処理素通り
            continue;

          }
        }else {
          $line = $this -> _error_messages[$rule];
        }
        // Build the error message
        $mparam = isset( $v['field'] ) ? $v['field'] : $field ;
        $mfield = isset( $v['label'] ) ? $v['label'] : $v['label'];
        $messages[] = sprintf( $line, $mfield, $mparam );

      }
      if(!$messages){
        continue;
      }
      $params[] = '{input:"' . $field . '",rule:"' . $v['rules'] . '",name:"' . ( isset( $v['field'] ) ? addslashes( $v['field'] ) : $field ) . '",msg:"' . join( ' ', $messages ) . '"}';
    }
    $join_params = join( ",\n\t\t", $params ) ;
    return $this -> _get_js_validation_run( $this -> _defaultbgcolor, $this -> _alertbgcolor, $alert_msg, $join_params, $alert_func );
  }
  // ------------------------------------------
  private function _get_js_null_validation_run()
  {
    $script = <<<___END___
<script type="text/javascript">
// <![CDATA[
var validation_run = function(f) {}
// ]]>
</script>

___END___;
    return $script;
  }
  // ------------------------------------------
  private function _get_js_validation_run( $defaultbgcolor, $alertbgcolor, $alert_msg, $join_params, $alert_func )
  {
    $script = <<<___END___
<script type="text/javascript">
// <![CDATA[
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;//個別エラー表示用フラグ追加
    var focus_flg = 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) &&  ( res == false ) ){
                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="{$alertbgcolor}";
                    if(!focus_flg){
                      obj_input.focus();
                      focus_flg = true;
                    }
                }
                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="{$defaultbgcolor}";
            }
        }
    }
  } 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;
}
var display_alert = function(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 = '{$alert_msg}' + str ;
    }

  }else{
    for (var i in errors){
      str += "\\n- " + errors[i][0];
      window.alert('{$alert_msg}' + str);
    }

  }
  return false;
}
var  validation_run = function(f) {
  var rules = [{$join_params}];
  return new Validator(f).validate(rules,{$alert_func});
}
// ]]>
</script>

___END___;

    return $script;
    # 元々はこっち
    // default implementation of the validation action
    // 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;
     * }
     * ';
     */

  }
} //endofclass
/**
 * End of file MY_Form_validation.php
 */
/**
 * Location: ./application/libraries/MY_Form_validation.php
 */
?>


使い方

次回に書く予定。今日はポイントのみ。


1)適当なコントローラとかでform_validationライブラリをloadした後、

2)コントローラ内等で

<?php
//上略
// 編集画面のバリデーション
$this -> form_validation -> set_rules( 'tmp_name' , 'テンプレート名', 'trim|required|xss_clean' );
$this -> form_validation -> set_rules( 'tmp_body' , 'テンプレート', 'trim|required' );
$this -> form_validation -> set_rules( 'memo' , 'メモ', 'trim|xss_clean' );
//下略
?>

みたいにルールの設定をする。(ココまではマニュアル通り。)
このフォームバリデーションの設定に従って、javascript側の簡易バリデーションチェック用ソースを作ってくれるわけです。



3)上のルールを設定した後にビュー側に、

<?php
//上略
$data['validation_js'] = $this -> form_validation -> javascript() ;
//下略
?>

とかで、今回のライブラリ拡張で作られるjavascriptを生成して渡してあげる。


4)ビューのhead部分に、

<?php if(isset($validation_js)){echo $validation_js ;} ?>

とかで3)で生成したjavascriptを貼りつける。


5)ビュー側のsubmitボタンを、

<input type="submit" onclick="javascript:try{if(!validation_run(this.form)){return false;}}catch(e){window.alert(e);return false;}" name="submit" value="保存" />

みたいに、onclick時に、validation_run(this.form)
を呼んで、falseならPOSTしないようにする。


6)ビュー側の各inputタグ周りを、

<label>テンプレート名<em>(必須)</em></label>
<span id="tmp_name_error"><?=form_error('tmp_name')?></span>
<input type="text" name="tmp_name" id="tmp_name" value="<?=set_value('tmp_name',$tmp_name) ?>" size="60" />

みたいにする。


エラー表示文字列部分を、


id="hogehoge_error"
とidを付けて囲い、

inputタグ部分を、


type="text" name="hogehoge" id="hogehoge"
とname,idを付けるところがキモ。

あとは、Form_validationのマニュアル通り、

  • エラー表示部分は、form_error
  • value部分は、set_value

を使う。





今日はここまで。


いろいろやらなくてはならないことが沢山あって忙しいです。
OpenCartをCodeIgniterに移植して、日本の商慣習に合わせたいとか、やりたいことも色々。