为什么验证逻辑不应该散落在控制器里
CodeIgniter 3 自带的表单验证类(Form_validation)已经提供了 required、valid_email、min_length 等常用规则,但实际业务中总会遇到框架没有覆盖的场景:手机号归属地校验、身份证号码合法性、密码强度等级、用户名是否被占用等。如果这些逻辑都写在控制器的回调方法里,项目规模一旦扩大,同一条验证规则往往会在多个控制器中重复出现,修改一处规则就得全局搜索替换,维护成本非常高。
更好的做法是把所有自定义规则集中到一个继承自 CI_Form_validation 的扩展类中,这样验证规则可以在任何控制器里像内置规则一样直接引用,错误提示也统一由语言文件管理。CodeIgniter 3 提供了非常干净的扩展机制,只要遵循命名约定,框架会自动加载你的扩展类,完全不需要手动 include。

创建 MY_Form_validation 扩展类的完整步骤
第一步是在 application/libraries 目录下新建文件 MY_Form_validation.php。注意类名前缀 MY 是由 application/config/config.php 中的 subclass_prefix 配置项决定的,默认就是 MY。类必须继承 CI_Form_validation,构造函数需要把配置参数透传给父类:
<?php
if (!defined('BASEPATH')) exit('No direct script access allowed');
class MY_Form_validation extends CI_Form_validation {
public function __construct($config = array())
{
parent::__construct($config);
}
/**
* 验证中国大陆手机号
*/
public function valid_phone($str)
{
if (preg_match('/^1[3-9]\d{9}$/', $str)) {
return TRUE;
}
$this->set_message('valid_phone', '{field} 格式不正确');
return FALSE;
}
/**
* 验证身份证号码(18位,含校验位验证)
*/
public function valid_idcard($str)
{
if (!preg_match('/^\d{17}[\dXx]$/', $str)) {
$this->set_message('valid_idcard', '{field} 格式不正确');
return FALSE;
}
$weights = array(7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2);
$codes = array('1','0','X','9','8','7','6','5','4','3','2');
$sum = 0;
for ($i = 0; $i < 17; $i++) {
$sum += (int)$str[$i] * $weights[$i];
}
if (strtoupper($str[17]) === $codes[$sum % 11]) {
return TRUE;
}
$this->set_message('valid_idcard', '{field} 校验位不正确');
return FALSE;
}
}扩展类就位后,框架在加载 form_validation 库时会自动识别并加载这个子类,不需要在 autoload.php 中做任何额外配置。如果确实想让它在全站自动可用,也可以把 form_validation 加入 autoload.php 的 libraries 数组,效果等同于自动加载扩展后的版本。
有一个细节容易被忽略:扩展类的文件名必须与类名完全一致(区分大小写),在 Linux 服务器上 MY_Form_validation.php 与 my_form_validation.php 会被视为不同文件,命名不一致会直接报找不到类的错误。
在控制器中像内置规则一样调用自定义规则
扩展类写好之后,使用方式与内置规则没有任何区别,直接在 set_rules 中引用方法名即可。下面是一个典型的注册表单验证示例:
public function register()
{
$this->load->library('form_validation');
$this->form_validation->set_rules('phone', '手机号', 'required|valid_phone');
$this->form_validation->set_rules('idcard', '身份证号', 'required|valid_idcard');
$this->form_validation->set_rules('password', '密码', 'required|min_length[8]');
if ($this->form_validation->run() == FALSE) {
$this->load->view('user/register');
} else {
// 验证通过,处理注册业务
}
}除了直接写在字符串规则里,CodeIgniter 3 还支持把规则集中写在配置文件中。在 application/config 下创建 form_validation.php,框架运行 run() 时会自动读取,控制器里的代码会进一步精简,非常适合规则较多的中大型项目:
$config = array(
'signup' => array(
array(
'field' => 'phone',
'label' => '手机号',
'rules' => 'required|valid_phone|is_unique[users.phone]'
),
array(
'field' => 'idcard',
'label' => '身份证号',
'rules' => 'required|valid_idcard'
)
)
);配置文件方式还支持分组,控制器调用 run('signup') 即可执行指定分组的规则集。这样同一个控制器里多个表单(注册、绑定手机、实名认证)各自维护独立的规则组,互不干扰,可读性比在控制器里堆砌 set_rules 调用要好得多。
回调方法与类方法的区别及常见错误排查
CodeIgniter 3 支持两种自定义验证方式:写在控制器里的 callback_ 前缀方法,以及写在扩展类中的普通方法。控制器回调的规则写法是 callback_check_username,而扩展类方法直接写方法名。两者最大的区别在于作用范围:回调方法只能在当前控制器使用,扩展类方法全局可用。新项目建议统一使用扩展类方式,旧代码里的回调方法可以在重构时逐步迁移。
排查自定义规则不生效的问题时,可以按以下几个方向检查。第一,确认方法是否返回了布尔值,验证方法必须显式返回 TRUE 或 FALSE,返回 0、1 或 null 都会导致判断异常。第二,确认错误信息是否通过 set_message 设置,如果方法返回 FALSE 但没有设置错误消息,页面只会显示默认的 Unable to access an error message 提示。第三,如果规则带有参数,例如 greater_than[18],方法签名必须接收第二个参数:
/**
* 验证日期必须早于指定日期,用法:before_date[2006-01-01]
*/
public function before_date($str, $param)
{
$time = strtotime($str);
if ($time === FALSE) {
$this->set_message('before_date', '{field} 不是有效的日期');
return FALSE;
}
if ($time < strtotime($param)) {
return TRUE;
}
$this->set_message('before_date', '{field} 必须早于 ' . $param);
return FALSE;
}关于错误提示的维护,更规范的做法是把提示文本放进语言文件。在 application/language/chinese 目录下建立 form_validation_lang.php,然后扩展类中通过 $this->CI->lang->line('valid_phone') 读取,set_message 中也可以直接传语言键名。这样项目需要支持多语言,或者产品经理要求统一修改文案时,只需要改语言文件一处即可,所有规则提示同步生效。
总结一下,把自定义验证规则沉淀到 MY_Form_validation 扩展类中,配合配置文件式规则分组和语言文件管理错误提示,是 CodeIgniter 3 项目中处理表单验证最稳妥的组织方式。它让验证逻辑具备可复用、可测试、可集中维护三个特性,项目越复杂,这种结构带来的收益就越明显。
CodeIgniter 3表单验证自定义规则库修改时间:2026-09-14 13:35:51