验证规则写在控制器里,起初只有三条,半年后变成三十条,还夹着权限判断与数据准备。把验证搬进 FormRequest,控制器就能回到「只处理业务」的状态。

Laravel 表单验证进阶:FormRequest 与自定义规则的正确用法 示意图

用 FormRequest 收拢验证

class StorePostRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()?->can('create', Post::class) ?? false;
    }

    public function rules(): array
    {
        return [
            'title' => ['required', 'string', 'max:80', 'unique:posts,title'],
            'slug' => ['nullable', 'string', 'max:120', 'alpha_dash'],
            'category_id' => ['required', 'integer', 'exists:categories,id'],
            'content' => ['required', 'string', 'min:20'],
            'tags' => ['array'],
            'tags.*' => ['string', 'max:20'],
        ];
    }
}

控制器里只需要类型提示,Laravel 会在进入方法前完成验证与授权:

public function store(StorePostRequest $request)
{
    $post = Post::create($request->validated());

    return redirect()->route('posts.show', $post);
}

validated() 只返回规则里声明过的字段,这一点很重要:它能防止用户多提交 status、views 这类不该由前端决定的字段。

自定义规则对象

复杂逻辑(例如「日期必须在另一个字段之后」或「编码必须符合内部规范」)写闭包会越来越乱。用规则类可以复用、可以单元测试。

class ValidPostSlug implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (preg_match('/^\d+$/', $value)) {
            $fail('网址别名不能是纯数字,否则会和文章 ID 冲突。');

            return;
        }

        if (in_array($value, ['admin', 'api', 'login'], true)) {
            $fail('这个网址别名是系统保留词,请换一个。');
        }
    }
}

然后在规则数组里写 new ValidPostSlug 即可。规则类的好处是错误信息与判断逻辑放在一起,不会散落在控制器里。

字段名与错误信息本地化

默认错误提示里出现的是英文字段名,用户看不懂。用 attributes() 换成中文:

public function attributes(): array
{
    return [
        'title' => '标题',
        'category_id' => '分类',
        'content' => '正文',
    ];
}

需要整站统一时,把映射放到 lang/zh_CN/validation.php 的 attributes 里,不用每个 FormRequest 重复写。

页面与 API 的错误格式

同一个 FormRequest 同时服务页面与接口时,错误响应格式应该分开。Laravel 默认会根据请求头判断,但自定义格式更可控:

protected function failedValidation(Validator $validator): void
{
    if ($this->expectsJson()) {
        throw new HttpResponseException(response()->json([
            'message' => '参数校验未通过',
            'errors' => $validator->errors(),
        ], 422));
    }

    parent::failedValidation($validator);
}

这样前端拿到的永远是 { message, errors } 结构,页面则继续走「回跳 + 闪存错误」的默认流程。

前后端规则保持一致

  • 前端只做「即时反馈」,后端规则才是最终约束,不要只依赖前端;
  • 把长度、格式这类规则同步到前端提示文案里,避免用户提交后才看到错误;
  • 接口文档里写清必填项与取值范围,比让调用方猜要省很多沟通。

处理用户粘贴的长文本时,先用站内的文本处理工具去掉多余空行与首尾空格,能减少不少「长度超限」的误报。验证规则写好后,也建议为关键的规则类补几个单元测试。

小结

FormRequest 的价值不只是「少写几行」,而是把「什么样的输入是合法的」变成一处可读、可测、可复用的定义。规则一旦集中,排查「为什么这个请求被拒绝」就只需要看一个文件。