ThinkPHP迁移到Laravel不完全指北:多场景验证器
前言
ThinkPHP提供了内置的多场景验证器,可以通过scene
方法指定验证场景,而Laravel并没有类似的方法,官方文档中给到的方案是使用FormRequest来使得验证规则和控制器逻辑分离,但同一个控制器中的每一个不同方法都需要定义一个FormRequest类,这样过于麻烦,我们希望把验证器逻辑统一到同一个类文件中。接下来我们将封装一个多场景验证器。
ThinkPHP多场景验证器
下面是Likeadmin框架(基于ThinkPHP 8.0)中的一个多场景验证器,他负责校验管理员账号的增删改查API请求入参,先看下面的代码:
<?php
namespace app\adminapi\validate\auth;
use app\common\validate\BaseValidate;
use app\common\model\auth\Admin;
/**
* 管理员验证
*/
class AdminValidate extends BaseValidate
{
protected $rule = [
'id' => 'require|checkAdmin',
'account' => 'require|length:1,32|unique:'.Admin::class,
'name' => 'require|length:1,16|unique:'.Admin::class,
'password' => 'require|length:6,32|edit',
'password_confirm' => 'requireWith:password|confirm',
'role_id' => 'require',
'disable' => 'require|in:0,1|checkAbleDisable',
'multipoint_login' => 'require|in:0,1',
];
protected $message = [
'id.require' => '管理员id不能为空',
'account.require' => '账号不能为空',
'account.length' => '账号长度须在1-32位字符',
'account.unique' => '账号已存在',
'password.require' => '密码不能为空',
'password.length' => '密码长度须在6-32位字符',
'password_confirm.requireWith' => '确认密码不能为空',
'password_confirm.confirm' => '两次输入的密码不一致',
'name.require' => '名称不能为空',
'name.length' => '名称须在1-16位字符',
'name.unique' => '名称已存在',
'role_id.require' => '请选择角色',
'disable.require' => '请选择状态',
'disable.in' => '状态值错误',
'multipoint_login.require' => '请选择是否支持多处登录',
'multipoint_login.in' => '多处登录状态值为误',
];
public function sceneAdd()
{
return $this->remove(['password', 'edit'])
->remove('id', true)
->remove('disable', true);
}
public function sceneDetail()
{
return $this->only(['id']);
}
public function sceneEdit()
{
return $this->remove('password', 'require|length')
->append('id', 'require|checkAdmin')
->remove('role_id', 'require')
->append('role_id', 'checkRole');
}
public function sceneDelete()
{
return $this->only(['id']);
}
public function edit($value, $rule, $data)
{
if (empty($data['password']) && empty($data['password_confirm'])) {
return true;
}
$len = strlen($value);
if ($len < 6 || $len > 32) {
return '密码长度须在6-32位字符';
}
return true;
}
public function checkAdmin($value)
{
$admin = Admin::findOrEmpty($value);
if ($admin->isEmpty()) {
return '管理员不存在';
}
return true;
}
public function checkAbleDisable($value, $rule, $data)
{
$admin = Admin::findOrEmpty($data['id']);
if ($admin->isEmpty()) {
return '管理员不存在';
}
if ($value && $admin['root']) {
return '超级管理员不允许被禁用';
}
return true;
}
public function checkRole($value, $rule, $data)
{
$admin = Admin::findOrEmpty($data['id']);
if ($admin->isEmpty()) {
return '管理员不存在';
}
if ($admin['root']) {
return true;
}
if (empty($data['role_id'])) {
return '请选择角色';
}
return true;
}
}
上述代码有几个优点与不足:
- 每个场景的验证规则通过
append
、only
等方法来添加或删除规则,好处是分开了每个场景的验证规则,不会混杂在一起,但是这样写起来比较繁琐,不够直观。 - 验证规则中的
checkAdmin
、checkAbleDisable
、checkRole
等方法是自定义的验证规则写在同一个文件里,方便管理。
改写成Laravel
接下来我们改写成Laravel11的多场景验证器:
<?php
namespace App\Adminapi\Validate\Auth;
use App\Common\Model\Auth\Admin;
use App\Common\Validate\BaseValidate;
use Closure;
use Illuminate\Support\Facades\Config;
use Illuminate\Validation\Rule;
class AdminValidate extends BaseValidate
{
public function rules($scene = '')
{
$rules = [
'add' => [
'account' => 'required|string|between:1,32|unique:admin,account',
'name' => 'required|string|between:1,16|unique:admin,name',
'password' => 'required|string|between:6,32',
'password_confirm' => 'required_with:password|same:password',
'role_id' => 'required',
'multipoint_login' => 'required|in:0,1',
'disable' => 'required|in:0,1',
],
'detail' => [
'id' => 'required|exists:admin,id',
],
'edit' => [
'id' => 'required|exists:admin,id',
'account' => [
'required', 'string', 'between:1,32',
Rule::unique('admin', 'account')->ignore(request()->id)
],
'name' => [
'required', 'string', 'between:1,16',
Rule::unique('admin', 'name')->ignore(request()->id)
],
'password' => 'nullable|string|between:6,32',
'password_confirm' => 'nullable|same:password',
'role_id' => [
function ($attribute, $value, Closure $fail) {
$admin = Admin::find(request()->id);
if (!$admin) {
return $fail('管理员不存在');
}
if ($admin->root) {
return;
}
if (empty($value)) {
return $fail('请选择角色');
}
},
],
'disable' => [
'required',
'in:0,1',
function ($attribute, $value, Closure $fail) {
$admin = Admin::find(request()->id);
if (!$admin) {
return $fail('管理员不存在');
}
if ($value && $admin->root) {
return $fail('超级管理员不允许被禁用');
}
},
],
'multipoint_login' => 'required|in:0,1',
],
'delete' => [
'id' => 'required|exists:admin,id',
],
'editSelf' => [
'name' => 'required|string|between:1,16',
'avatar' => 'required',
'admin_id' => 'required|exists:admin,id',
'password' => [
'nullable',
'string',
'between:6,32',
function ($attribute, $value, Closure $fail) {
if (empty(request()->password_old)) {
return $fail('请填写当前密码');
}
$admin = Admin::find(request()->attributes->get('adminId'));
if (!$admin) {
return $fail('管理员不存在');
}
$passwordSalt = Config::get('project.unique_identification');
$oldPassword = create_password(request()->password_old, $passwordSalt);
if ($admin->password !== $oldPassword) {
return $fail('当前密码错误');
}
},
],
'password_confirm' => 'required_with:password|same:password',
],
];
return $rules[$scene] ?? [];
}
protected $messages = [
'id.required' => '管理员id不能为空',
'account.required' => '账号不能为空',
'account.between' => '账号长度须在1-32位字符',
'account.unique' => '账号已存在',
'password.required' => '密码不能为空',
'password.between' => '密码长度须在6-32位字符',
'password_confirm.required_with' => '确认密码不能为空',
'password_confirm.same' => '两次输入的密码不一致',
'name.required' => '名称不能为空',
'name.between' => '名称须在1-16位字符',
'name.unique' => '名称已存在',
'role_id.required' => '请选择角色',
'disable.required' => '请选择状态',
'disable.in' => '状态值错误',
'multipoint_login.required' => '请选择是否支持多处登录',
'multipoint_login.in' => '多处登录状态值错误',
];
public function messages()
{
return $this->messages;
}
}
上述代码使用Closure闭包函数来实现自定义验证规则,把自定义验证规则统一到同一个类文件中。另外,不同场景的验证规则通过rules
方法来返回, 每个场景的规则全部写出,不通过append
、only
等方法动态生成,这样写起来更加直观,易于维护。
当然还存在一些问题,比如同一个字段在不同场景的验证规则会有重复的代码,我们可以写一个自动匹配逻辑来简化代码,类似这样:
$scenes = [
'id' => ['id'],
'rename' => ['id', 'name'],
'addCate' => ['type', 'pid', 'name'],
'editCate' => ['id', 'name'],
'move' => ['ids', 'cid'],
'delete' => ['ids'],
];
if (isset($scenes[$scene])) {
return array_intersect_key($rules, array_flip($scenes[$scene]));
}
return $rules; // 默认返回所有规则
未完待续
本文章出自开源项目likeadmin_laravel的实施经验,后续会继续更新,欢迎关注。
技术栈:
- PHP 8.0 => PHP 8.2
- ThinkPHP 8 => Laravel 11
- 管理后台:Vue3 + TypeScript + ElementPlus UI + TailwindCSS
- 小程序:Vue3 + TypeScript + Uniapp + TailwindCSS
- PC端:Vue3 + Nuxt
项目进度:
██████░░░░ 60%
欢迎加入开源共建~
项目地址:likeadmin_laravel
本作品采用《CC 协议》,转载必须注明作者和本文链接
插个眼
可以参考下【让你的validate支持场景验证】希望可以让你的改造更加精彩,这个方案我已经在生产环境用了很多年了。
插个眼
我是这样实现的, 自己封装一个自定义的FormRequest, Request 继承自己的 FormRequest 类就行了
优点: 1.是几乎是无感的, 因为就改变了继承类 2. 用方法更灵活
类1 为了优化, Validater两个循环验证很多时候都是没必要的
类2 实现多场景
一个Request类 例如叫 AuthRequest 只需要继承 EfficientSceneFormRequest 类 控制器端只需要注入 AuthRequest 对象
authorize
方法暂时没处理 也是类似的这个思路是来自于
MineAdmin
最近有项目,正好跟着项目在学习likeamdin,感谢作者
还好没用过,立省100%