# 3.4 FormRequest 验证器 :::tip 概述 参数格式验证由 **FormRequest** 完成(Laravel 标准机制):控制器方法类型注入请求类,未通过校验时框架自动返回统一 JSON 错误响应。所有请求类继承本项目的 `BaseRequest`,子类只需声明 `rules()` 与 `messages()`。 ::: ## 请求类文件位置 ```text app/Http/Requests/ExampleRequest.php ``` ## 完整代码(来自项目实际代码) ```php 字段规则 */ public function rules(): array { return [ 'name' => 'required|string|max:255', // 案例名称:必填,最长 255 'avatar' => 'nullable|string|max:255', // 头像/图片地址:可空,最长 255 'type' => 'nullable|integer|in:1,2,3,4', // 类型:1=类型1,2=类型2,3=类型3,4=类型4 'status' => 'nullable|integer|in:1,2', // 状态:1=在用,2=停用 'sort' => 'nullable|integer', // 排序值:可空,整数 ]; } /** * 自定义错误消息 * * 为需要友好提示的规则提供中文消息,覆盖框架默认英文提示。 * * @return array 字段规则 => 错误消息 */ public function messages(): array { return [ 'name.required' => '案例名称不能为空', 'name.max' => '案例名称不能超过 255 个字符', 'avatar.max' => '图片地址不能超过 255 个字符', 'type.in' => '案例类型不合法,允许:1-类型1 2-类型2 3-类型3 4-类型4', 'status.in' => '状态值不合法,允许:1-在用 2-停用', 'sort.integer' => '排序值格式不正确', ]; } } ``` ## 控制器中的使用 ```php // 类型注入:请求到达方法前自动完成校验 public function add(ExampleRequest $request): JsonResponse { // validated() → 已校验数据,键名转换由 Service 写入入口统一完成 return $this->_add($this->exampleService, $request->validated()); } ``` :::tip validated() 是白名单 `validated()` 只返回 `rules()` 中声明过的键,未声明的字段在进入控制器前即被丢弃。业务需要传递给 Service 的字段(如修改接口的 `id`、用户表单的 `city` / `roles`)都应在规则中声明,否则即使前端传了也到不了 Service。 ::: ## BaseRequest 基类提供的能力 `app/Http/Requests/BaseRequest.php`: | 能力 | 说明 | |------|------| | `authorize()` 统一返回 true | 鉴权由路由中间件与 `#[Permission]` 注解负责,请求类不参与授权 | | `failedValidation()` 统一处理 | 校验失败返回固定 JSON:`{"code":1,"ok":false,"msg":"参数验证失败","data":{字段=>错误数组}}`(code=1 与 `Controller::fail()` 约定一致) | :::warning 两条校验失败路径 - **FormRequest 注入**(本章方式)→ 失败响应 `code=1`; - **代码内手动** `Validator::validate()` 抛出的 `ValidationException` → 由全局 Handler 渲染为 `code=422`。 前端两种都应兼容(以 `code !== 0` 判失败)。详见 [5.16 统一响应与异常抛出](/zh/core/exception.html)。 ::: ## 常用验证规则 | 规则 | 说明 | 示例 | |------|------|------| | `required` | 必填 | `'name' => 'required'` | | `string` | 字符串 | `'name' => 'string'` | | `max:n` | 最大长度 | `'title' => 'max:200'` | | `min:n` | 最小长度 | `'password' => 'min:6'` | | `integer` | 整数 | `'sort' => 'integer'` | | `in:a,b,c` | 枚举值 | `'status' => 'in:1,2'` | | `nullable` | 可空(不传则跳过其余规则) | `'avatar' => 'nullable\|string'` | | `email` | 邮箱格式 | `'email' => 'email'` | | `date` | 日期格式 | `'start_time' => 'date'` | | `regex:/.../` | 正则 | `'code' => 'regex:/^[A-Za-z]+$/'` | ## 分层职责:格式 vs 业务 | 层 | 职责 | 示例 | |----|------|------| | **FormRequest(本章)** | 参数**格式**:必填、类型、长度、枚举 | `name` 必填、`type` 只能 1-4 | | **Service 层** | 参数**业务规则**:唯一性、状态依赖、权限 | `name` 不能与已有记录重复 | :::tip 唯一性为什么不放 FormRequest? `uniqueFields` 校验在 Service 层完成(`HasUniqueCheck` Trait),因为软删除场景下数据库唯一索引会误伤(`is_delete=1` 的旧记录仍占索引),且需要排除自身(update 场景)。见 [3.5 Service 层开发](/zh/guide/logic.html)。 ::: ## 场景拆分(新增 vs 修改字段差异大时) FormRequest 可按方法区分规则: ```php class UserRequest extends BaseRequest { public function rules(): array { // 通过 $this->isMethod('post') 或自定义场景标识区分 $rules = [ 'realname' => 'required|string|max:50', 'mobile' => 'nullable|max:20', ]; if ($this->isMethod('post')) { // 新增时才要求用户名 $rules['username'] = 'required|string|min:3|max:50'; } return $rules; } } ```