วันจันทร์ที่ 21 มกราคม พ.ศ. 2556

Cakephp กับ Form Validation ด้วย Javascript


       วันนี้ได้ลองศึกษาการทำ Form Validation หรือการตรวจสอบข้อมูลในฟอร์มกรอกข้อมูลด้วย Javascript ใน CakePHP ดูครับ โดยใช้ Plugin Validation ของ JQuery ซึ่งรองรับทุกตัวครับ แต่ที่ผมสนใจเป็นพิเศษคือจากบทความนี้ครับ http://bakery.cakephp.org/articles/mattc/2008/10/26/automagic-javascript-validation-helper แต่ก็มีปัญหาคือเป็นเวอร์ชั่นเก่า (อีกแล้ว) ก็เลยค้นหาต่อไป และไปเจอในเว็บนี้ https://github.com/mcurry/js_validate ปรากฏว่า ใช้ได้เลยครับ เลยขอยืมมาปรับใช้ซะหน่อย ความสามารถของมันคือ มันจะไปดึง Validation จากใน Model ใน CakePHP ของเรามาแปลงให้เป็น Validation ของ Javascript ได้เลย แต่จะมีปัญหากับการใช้ Regular  Expression เช่นถ้าใช้คำสั่ง ‘rule’=>’/^[0-9]+/i’  มันจะErrorทันที เพราะมันจะรองรับคำสั่งตรวจสอบพื้นฐานเท่านั้นครับ แต่ก็ไม่เป็นไร (พูดปลอบใจตัวเอง ^_^) เพราะคำสั่งตรวจสอบพื้นฐาน เช่น ตรวจสอบอีเมล,ตัวเลข,วันที่,อื่นๆ มีให้ค่อนข้างครบครันเลยทีเดียว

ผมได้ออกแบบฐานข้อมูลชื่อ db_cakephp และสร้างเทเบิล products กับ categories ดังรูป


มาดูโค๊ดกันเลยครับ
1.สร้างไฟล์ ValidationHelper.php ไว้ใน View/Helper ให้ใช้โค๊ดดังนี้

<?php
/*
 * CakePHP jQuery Validation Plugin
 * Copyright (c) 2009 Matt Curry
 * www.PseudoCoder.com
 * http://github.com/mcurry/cakephp_plugin_validation
 * http://sandbox2.pseudocoder.com/demo/validation
 *
 * @author      mattc <matt@pseudocoder.com>
 * @license     MIT
 *
 */

//feel free to replace these or overwrite in your bootstrap.php
if (!defined('VALID_EMAIL_JS')) {
  define('VALID_EMAIL_JS', '/^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/');
}
//I know the octals should be capped at 255
if (!defined('VALID_IP_JS')) {
  define('VALID_IP_JS', '/^[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}$/');
}

class ValidationHelper extends Helper {
  var $helpers = array('Javascript');

  //For security reasons you may not want to include all possible validations.
  //In your bootstrap you can define which are allowed
  //Configure::write('javascriptValidationWhitelist', array('alphaNumeric', 'minLength'));
  var $whitelist = false;

  public function bind($modelNames, $options=array()) {
    $defaultOptions = array('form' => 'form', 'inline' => true, 'root' => Router::url('/'), 'watch' => array(), 'catch' => true);
    $options = am($defaultOptions, $options);
    $pluginOptions = array_intersect_key($options, array('messageId' => true, 'root' => true, 'watch' => true));

    //load the whitelist
    $this->whitelist = Configure::read('javascriptValidationWhitelist');

    $validation = array();
    if (!is_array($modelNames)) {
      $modelNames = array($modelNames);
    }

    //filter the rules to those that can be handled with JavaScript
    foreach($modelNames as $modelName) {
      $model = classRegistry::init($modelName);
      $arr=explode('.',$modelName);
      $realModelName=$arr[0];

      foreach ($model->validate as $field => $validators) {
        if (array_intersect(array('rule', 'allowEmpty', 'on', 'message', 'last'), array_keys($validators))) {
          $validators = array($validators);
        }

        foreach($validators as $key => $validator) {
          $rule = null;

          if (!is_array($validator)) {
            $validator = array('rule' => $validator);
          }

          //skip rules that are applied only on created or updates
          if (!empty($validator['on'])) {
            continue;
          }

          if (!isset($validator['message'])) {
      $message = sprintf(__($key, true), __($field, true));
      if($key != $message) {
       $validator['message'] = $message;
      } else {
       $validator['message'] = sprintf('%s %s',
                       __('There was a problem with the field', true),
                       Inflector::humanize($field)
                       );
      }
          }

          if (!empty($validator['rule'])) {
            $rule = $this->__convertRule($validator['rule']);
          }

          if ($rule) {
            $temp = array('rule' => $rule,
                          'message' => __($validator['message'], true)
                         );

            if (isset($validator['last']) && $validator['last'] === true) {
              $temp['last'] = true;
            }

            if (isset($validator['allowEmpty']) && $validator['allowEmpty'] === true) {
              $temp['allowEmpty'] = true;
            }

            if (in_array($validator['rule'], array('blank'))) {
              //Cake Validation::_check returning true is actually false for blank
              //add a "!" so that JavaScript knows
              $temp['negate'] = true;
            }

            $validation[$realModelName . Inflector::camelize($field)][] = $temp;
          }
        }
      }
   
   if(!empty($pluginOptions['watch'])) {
    $pluginOptions['watch'] = $this->__fixWatch($modelName, $pluginOptions['watch']);
   }   
    }
  
  if ($options['form']) {
   if($options['catch']) {
    $js = sprintf('$(function() { $("%s").validate(%s, %s) });',
           $options['form'],
           $this->Javascript->object($validation),
           $this->Javascript->object($pluginOptions));
   } else {
    $js = sprintf('$(function() { $("%s").data("validation", %s) });',
           $options['form'],
           $this->Javascript->object($validation)); 
   }
    } else {
      return $this->Javascript->object($validation);
    }

    if ($options['inline']) {
      return sprintf($this->Javascript->tags['javascriptblock'], $js);
    } else {
      $this->Javascript->codeBlock($js, array('inline' => false));
    }

    return;
  }

  public function __convertRule($rule) {
    $regex = false;

    if ($rule == '_extract') {
      return false;
    }

    $params = array();
    if (is_array($rule)) {
      $params = array_slice($rule, 1);
      $rule = $rule[0];
    }
  
    if (is_array($this->whitelist) && !in_array($rule, $this->whitelist)) {
      return false;
    }

    if ($rule == 'comparison') {
      $params[0] = str_replace(array(' ', "\t", "\n", "\r", "\0", "\x0B"), '', strtolower($params[0]));
      switch ($params[0]) {
        case 'isgreater':
          $params[0] = '>';
          break;
        case 'isless':
          $params[0] = '<';
          break;
        case 'greaterorequal':
          $params[0] = '>=';
          break;
        case 'lessorequal':
          $params[0] = '<=';
          break;
        case 'equalto':
          $params[0] = '==';
          break;
        case 'notequal':
          $params[0] = '!=';
          break;
      }
    }

    //Certain Cake built-in validations can be handled with regular expressions,
    //but aren't on the Cake side.
    switch ($rule) {
      case 'alphaNumeric':
        return '/^[0-9A-Za-z]+$/';
      case 'between':
        return sprintf('/^.{%d,%d}$/', $params[0], $params[1]);
      case 'boolean':
        return array('rule' => 'boolean');
      case 'date':
        //Some of Cake's date regexs aren't JavaScript compatible. Skip for now
        if (!empty($params[0])) {
          $params = $params[0];
        } else {
          $params = 'ymd';
        }
        return array('rule' => 'date', 'params' => $params);
      case 'email':
        return VALID_EMAIL_JS;
      case 'equalTo':
        return sprintf('/^%s$/', $params[0]);
      case 'extension':
        if (empty($params[0])) {
          $params = array('gif', 'jpeg', 'png', 'jpg');
        } else {
          $params = $params[0];
        }
        return sprintf('/\.(%s)$/', implode('|', $params));
      case 'inList':
        return array('rule' => 'inList', 'params' => $params[0]);
      case 'ip':
        return VALID_IP_JS;
      case 'minLength':
        return sprintf('/^[\s\S]{%d,}$/', $params[0]);
      case 'maxLength':
        return sprintf('/^[\s\S]{0,%d}$/', $params[0]);
      case 'money':
        //The Cake regex for money was giving me issues, even within PHP.  Skip for now
        return array('rule' => 'money');
      case 'multiple':
        $defaults = array('in' => null, 'max' => null, 'min' => null);
        $params = array_merge($defaults, $params[0]);
        return array('rule' => 'multiple', 'params' => $params);
     case 'notEmpty':
        return array('rule' => 'notEmpty');
      case 'numeric':
        //Cake uses PHP's is_numeric function, which actually accepts a varied input
        //(both +0123.45e6 and 0xFF are valid) then what is allowed in this regular expression.
        //99% of people using this validation probably want to restrict to just numbers in standard
        //decimal notation.  Feel free to alter or delete.
        return '/^[+-]?[0-9|.]+$/';
      case 'range':
      case 'comparison':
        //Don't think there is a way to do this with a regular expressions,
        //so we'll handle this with plain old javascript
        return array('rule' => $rule, 'params' => array($params[0], $params[1]));
    }

    //try to lookup the regular expression from
    //CakePHP's built-in validation rules
    $Validation =& Validation::getInstance();
    if (method_exists($Validation, $rule)) {
      $Validation->regex = null;
      call_user_func_array(array(&$Validation, $rule), array_merge(array(true), $params));

      if ($Validation->regex) {
        $regex = $Validation->regex;
      }
    }

    if ($regex) {
      //special handling
      switch ($rule) {
        case 'postal':
        case 'ssn':
          //I'm not a regex guru and I have no idea what "\\A\\b" and "\\b\\z" do.
          //Is it just to match start and end of line?  Why not use
          //"^" and "$" then?  Eitherway they don't work in JavaScript.
          return str_replace(array('\\A\\b', '\\b\\z'), array('^', '$'), $regex);
      }
      return $regex;
    }
    // If not rule is selected handle with a regular expression
    return($rule);
}

 public function __fixWatch($modelName, $fields) {
  foreach($fields as $i => $field) {
   if (strpos($field, '.') !== false) {
    list($model, $field) = explode('.', $field);
    $fields[$i] = ucfirst($model) . ucfirst($field);
   } else {
    $fields[$i] = $modelName . ucfirst($field);
   }
  }
  
  return $fields;
 }
}
?>

2.สร้างไฟล์ validation.js ใน webroot/js ให้ใช้โค๊ดดังนี้
/*
 * CakePHP jQuery Validation Plugin
 * Copyright (c) 2009 Matt Curry
 * www.PseudoCoder.com
 * http://github.com/mcurry/cakephp_plugin_validation
 * http://sandbox2.pseudocoder.com/demo/validation
 *
 * @author      mattc <matt@pseudocoder.com>
 * @license     MIT
 *
 */

(function($) {
 $.fn.validate = function(rules, opts) {
  options = $.extend({watch: []}, opts);

  $.each(options.watch,
  function(fieldId) {
   $("#" + options.watch[fieldId]).change(function() {
    $.fn.validate.ajaxField($(this));
   });
  });

  return this.each(function() {
   $this = $(this);
   $this.submit(function() {
    return $.fn.validate.check(rules);
   });
  });
 };
 
 $.fn.validate.check = function(rules) {
  var errors = [];
  var val = null;

  $.fn.validate.beforeFilter();

  $.each(rules,
  function(field) {
   $field = $("#" + field);
   if ($field.attr("type") == "checkbox") {
    if ($field.filter(":checked").length > 0) {
     val = $field.filter(":checked").val();
    } else {
     val = "0";
    }
   } else {
    val = $field.val();
   }

   var fieldName = $field.attr('name');
   if (typeof val == "string") {
    val = $.trim(val);
   }

   $.each(this,
   function() {
    //field doesn't exist...skip
    if ($("#" + field).attr("id") == undefined) {
     return true;
    }

    if (this['allowEmpty'] && typeof val == "string" && val == '') {
     return true;
    }

    if (this['allowEmpty'] && typeof val == "object" && val == null) {
     return true;
    }

    if (!$.fn.validate.validateRule(val, this['rule'], this['negate'], fieldName)) {
     errors.push(this['message']);
     $.fn.validate.setError(field, this['message']);

     if (this['last'] === true) {
      return false;
     }
    }
   });
  });

  $.fn.validate.afterFilter(errors);

  if (errors.length > 0) {
   return false;
  }

  return true;
 }

 $.fn.validate.validateRule = function(val, rule, negate, fieldName) {
  if (negate == undefined) {
   negate = false;
  }

  //handle custom functions
  if (typeof rule == 'object') {
   if ($.fn.validate[rule.rule] != undefined) {
    return $.fn.validate[rule.rule](val, rule.params, fieldName);
   } else {
    return true;
   }
  }

  //handle regex rules
  if (negate && val.match(eval(rule))) {
   return false;
  } else if (!negate && !val.match(eval(rule))) {
   return false;
  }

  return true;
 };

 $.fn.validate.boolean = function(val) {
  return $.fn.validate.inList(val, [0, 1, '0', '1', true, false]);
 };

 $.fn.validate.comparison = function(val, params) {
  if (val == "") {
   return false;
  }

  val = Number(val);
  if (val == "NaN") {
   return false;
  }

  if (eval(val + params[0] + params[1])) {
   return true;
  }

  return false;
 };

 $.fn.validate.inList = function(val, params) {
  if (params != null) {
   if ($.inArray(val, params) == -1) {
    return false;
   }
  }

  return true;
 };

 $.fn.validate.multiple = function(val, params) {
  if (typeof val != "object" || val == null) {
   return false;
  }

  if (params.min != null && val.length < params.min) {
   return false;
  }
  if (params.max != null && val.length > params.max) {
   return false;
  }

  if (params["in"] != null) {
   for (i = 0; i < params["in"].length; i++) {
    if ($.inArray(params["in"][i], val) == -1) {
     return false;
    }
   }
  }

  return true;
 };

 $.fn.validate.notEmpty = function(val, params) {
  if (typeof val == "string" && val == "") {
   return false;
  }

  if (typeof val == "object" && val.length == 0) {
   return false;
  }

  return true;
 }

 $.fn.validate.range = function(val, params) {
  if (val < parseInt(params[0])) {
   return false;
  }
  if (val > parseInt(params[1])) {
   return false;
  }

  return true;
 };

 $.fn.validate.ajaxField = function($field) {
  $.fn.validate.clearError($field);
  $.fn.validate.ajaxBeforeFilter($field);

  var data = new Object;
  data[$field.attr("name")] = $field.val();
  $.post(options.root + "js_validate/field/" + $field.attr("id"), data,
  function(validates) {
   $.fn.validate.ajaxAfterFilter($("#" + validates.field));
   if (!validates.result) {
    $.fn.validate.setError(validates.field, validates.message);
   }
  },
  "json");
 }

 $.fn.validate.ajaxBeforeFilter = function($field) {
  $field.after("<img class=\"ajax-loader\" src=\"" + options.root + "js_validate/img/ajax-loader.gif\">");
 }

 $.fn.validate.ajaxAfterFilter = function($field) {
  $field.siblings(".ajax-loader").remove();
 }

 $.fn.validate.clearError = function($field) {
  if (typeof $field == "string") {
   $field = $("#" + field);
  }

  $field.removeClass("form-error").parents("div:first").removeClass("error").children(".error-message").remove();
 }

 $.fn.validate.setError = function(field, message) {
  $("#" + field).addClass("form-error").parents("div:first").addClass("error").append('<div class="error-message">' + message + '</div>');
 };

 $.fn.validate.beforeFilter = function() {
  if (options.messageId != null) {
   $("#" + options.messageId).html("").slideDown();
  }

  $(".error-message").remove();
  $("input").removeClass("form-error");
  $("div").removeClass("error")
 };

 $.fn.validate.afterFilter = function(errors) {
  if (options.messageId != null) {
   $("#" + options.messageId).html(errors.join("<br />")).slideDown();
  }
 };
 
 var options = [];
})(jQuery);

แค่นี้เราก็ได้ Validation Javascript ไว้ใช้งานแล้วครับ ต่อมาก็เป็นวิธีใช้ครับ

3.สร้างไฟล์ ProductsController.php ในโฟลเดอร์ Controller ให้พิมพ์โค๊ดดังนี้
<?php

class ProductsController extends AppController {

    public $name = 'Products';
    public $helpers = array('Html', 'Form', 'Validation'); //เรียกใช้ Helpers ให้ทำงานในส่วนของ View
    public $uses = array('Product', 'Category'); //เรียกใช้ Model Product,Category

    public function add() {
  $this->set('title_for_layout', 'เพิ่มรายการสินค้า');
        if ($this->request->is('post')) {//ตรวจว่ามีการส่งค่าแบบ post เข้ามา
            $data = $this->data['Product'];
            if ($this->Product->save($data)) {//Validationและบันทึกข้อมูลและReturnค่ากลับมาในคำสั่งนี้
                //บันทึกข้อมูลเรียบร้อยแล้ว
            }
        }
        $categories = $this->Category->getCategory(); //ไปดึงข้อมูลหมวดสินค้าจากเมธอด getCategoy() ใน Class Category จากไฟล์ Category.php
        $this->set('category', $categories);
    }

}

?>

4.ในส่วนของ Model ให้สร้างไฟล์ Product.php ไว้ในโฟลเดอร์ Model ครับ และพิมพ์โค๊ดดังนี้
<?php

class Product extends AppModel {

    public $name = 'Product';
    var $validate = array(
        'name' => array(
            'notEmpty' => array(
                'rule' => 'notEmpty',
                'required' => true,
                'message' => 'กรุณากรอกชื่อสินค้า'
            ),
            'between' => array(
                'rule' => array('minLength', 5),
                'message' => 'กรุณาระบุชือสินค้าไม่ต่ำกว่า 5 ตัวอักษร'
            )
        ),
        'category_id' => array(
            'rule' => 'notEmpty',
            'message' => 'กรุณาเลือกประเภทสินค้า'
        ),
        'quantity' => array(
            'rule' => 'numeric',
            'message' => 'กรุณาระบุจำนวนเป็นตัวเลข'
        ),
        'price' => array(
            'rule' => 'numeric',
            'message' => 'กรุณาระบุราคาเป็นตัวเลข'
        ),
        'image' => array(
            'rule' => 'notEmpty',
            'message' => 'กรุณาเลือกรูปสินค้า'
        )
    );

}

?>

5.Model อีกตัวนึงคือ Category เพื่อสำหรับไว้แสดงหมวดสินค้า ให้สร้างไฟล์ชื่อ Category.php ไว้ในโฟลเดอร์ Model และให้ใช้โค๊ดังนี้
<?php
class Category extends AppModel{
 public $name='Category';
 public $validate =array(
  'name'=>array(
     'notEmpty'=>array(
        'rule'=>'notEmpty','message'=>'กรอกชือหมวดสินค้าด้วยครับ'
     )
  )
 );
 public function getCategory(){
  return $this->find('list',array('fields'=>array('Category.id','Category.name'),'order'=>'Category.name ASC')     
  );
 }
 
}
?>

6.สร้างไฟล์ add.php ใน View/Products/ ครับ และให้พิมพ์โค๊ดดังนี้
<?php
echo $this->Html->script('validation');//เรียกใช้ validation.js
echo $this->Html->script('ckeditor/ckeditor');//เรียกใช้ ckeditor/ckeditor.js
echo $this->Form->create('Product', array('action' => 'add'));//สร้างฟอร์ม
echo $this->Validation->bind('Product');//เรียกใช้ Validation จาก Model "Product"
?>

<table width="780" border="0" align="center" cellpadding="4" cellspacing="0">
    <tr>
        <td colspan="2" align="center" ><h2>เพิ่มรายการสินค้า</h2></td>
    </tr>
    <tr>
        <td align="right"><strong>ชื่อสินค้า</strong></td>
        <td><?php echo $this->Form->input('name', array('label' => false, 'style' => 'width:450px')); ?></td>
    </tr>
    <tr>
        <td align="right"><strong>ประเภทสินค้า</strong></td>
        <td><?php echo $this->Form->input('category_id', array('label' => false, 'type' => 'select', 'empty' => 'เลือกประเภท', 'options' => $category)); ?></td>
    </tr>
    <tr>
        <td align="right"><strong>จำนวน</strong></td>
        <td><?php echo $this->Form->input('quantity', array('label' => false, 'style' => 'width:80px')); ?></td>
    </tr>
    <tr>
        <td align="right"><strong>ราคา</strong></td>
        <td><?php echo $this->Form->input('price', array('label' => false, 'style' => 'width:100px')); ?></td>
    </tr>
    <tr>
        <td align="right"><strong>เลือกรูปสินค้า</strong></td>
        <td><?php echo $this->Form->input('image', array('label' => false, 'type' => 'file')); ?></td>
    </tr>
    <tr>
        <td align="right"><strong>รายละเอียด</strong></td>
        <td><?php
echo $this->Form->textarea('detail', array('label' => false, 'cols' => '70', 'rows' => '20'));
echo $this->Ckeditor->loadcustom('Product.detail'); //วิธีเลือกใช้ ชือโมเดล.ชื่อฟิลด์
?></td>
    </tr>
    <tr>
        <td width="150"> </td>
        <td><?php echo $this->Form->submit('บันทึกข้อมูล'); ?> </td>
    </tr>
</table>
<?php echo $this->Form->end(); ?>

จากนั้นให้ลองรันทดสอบ โดยพิมพ์ว่า http://localhost/cakephp/products/add

ไม่มีความคิดเห็น :

แสดงความคิดเห็น