自定义验证类须继承ci_form_validation并命名为my_form_validation;is_unique不适用加密字段,需手动解密比对;过滤逻辑须按上下文区分;构造函数需验证依赖服务状态。

自定义类必须继承 CI_Form_validation 才能复用回调规则
CodeIgniter 3/4 的 form_validation 类对 callback_ 规则有硬性限制:它只在当前控制器实例中查找方法,不搜索 Library、Helper 或模型。直接在 app/Libraries/MyValidation.php 里写 check_phone(),然后规则里写 callback_check_phone,run() 会静默失败,返回 FALSE 却不报错。
正确做法是创建继承类:
class My_Form_validation extends CI_Form_validation
{
public function check_phone($str)
{
if (empty($str) || !preg_match('/^1[3-9]\d{9}$/', $str)) {
$this->set_message('check_phone', '手机号格式不正确');
return FALSE;
}
return TRUE;
}
}
文件路径必须为 app/Libraries/MY_Form_validation.php(注意前缀 MY_),CI 会自动加载。之后在控制器里调用 $this->form_validation->set_rules('phone', '手机号', 'callback_check_phone') 就能生效。
is_unique 不能直接用于加密字段校验
如果数据库里手机号是加密存储的(比如用 service('encrypter')->encrypt()),is_unique[users.phone] 会拿明文去查加密后的值,永远返回“不存在”,导致重复注册漏洞。
必须手动查库比对:
- 在自定义回调里先解密已有记录:
$encrypted = service('encrypter')->decrypt($existing_row->phone) - 再用
hash_equals()安全比对(避免时序攻击) - 不要用
==或===直接比较解密结果 - 注意:解密失败时要捕获
EncrypterException,防止因脏数据导致整个验证崩溃
过滤方法要区分上下文,不能一招鲜
同一个字段在不同场景下需要不同处理:
- 入库前:用
trim()+ 正则校验(如手机号、身份证号),再加密存入TEXT字段 - 查询展示:解密后用
esc($phone, 'html')输出到页面,防 XSS - API 返回:用
esc($phone, 'json'),避免 JSON 注入或转义错误 - 日志记录:禁止直接写解密后明文,应记录脱敏值(如
138****1234)
别把所有过滤逻辑塞进一个 filter_phone() 方法里——传参类型、输出目标、是否需脱敏,这些条件不明确,就容易漏掉关键环节。
构造函数里必须验证依赖对象状态
如果你的自定义验证类需要访问数据库或加密服务,别只靠类型提示:
public function __construct(\CodeIgniter\Database\BaseConnection $db)
{
$this->db = $db;
// ❌ 错误:没检查连接是否可用
}
要加上运行时验证:
- 调用
$db->isConnected()确保连接活跃 - 对加密服务,检查
service('encrypter')是否返回有效实例,而非null - 失败时抛出
InvalidArgumentException,带具体信息如"Encrypter service not initialized"
否则在 CLI 环境或某些路由下,类初始化成功但后续方法调用直接报 Call to a member function on null,问题难定位。











