Laravel FormRequest 与验证器使用指南
前言#
Laravel 的表单验证是日常开发中使用频率极高的功能,但官方文档往往只覆盖最常用的部分。这篇文章汇总了我在实际开发中整理的 Validator 非完全用法、FormRequest 表单请求类、闭包 / 行内验证器、统一错误响应、错误背包(MessageBag) 以及 Precognitive 预认知 等进阶用法,并附上完整可运行的示例代码,方便随时查阅。
本文基于 Laravel 10.x / PHP 8.1+ 编写。
目录#
- Validator 非完全用法汇总
- FormRequest 表单请求类示例
- 闭包验证器与行内验证器
- 统一处理验证错误响应
- 验证器错误背包(MessageBag)用法
- 独立表单验证类的自动调用流程
- Precognitive 预认知
- 参考资料
Validator 非完全用法汇总#
以下均为 官方文档中没有完全提及 的用法,基于
Illuminate\Support\Facades\Validator与Illuminate\Validation\Validator。
<?php
use Illuminate\Support\Facades\Validator;
// 参数一:待验证数据;参数二:验证规则;validateWithBag 绑定错误背包
$validator = Validator::make($request->all(), [
'title' => 'required|between:8,50',
'email' => 'required|email',
])->validateWithBag('myBag');
// 返回验证失败的数据(数组),validated() 的反逻辑
$validator->invalid();
// 获取验证通过的数据(数组),invalid() 的反逻辑
$validator->validated();
// 确定数据是否通过验证规则(bool),fails() 的反逻辑
$validator->passes();
// 确定数据是否没有通过验证规则(bool),passes() 的反逻辑
$validator->fails();
// 验证数据,有错时抛出异常
// 不想抛出可通过 passes() / fails() 自行处理
// @throws \Illuminate\Validation\ValidationException
$validator->validate();
// 返回有效的数据(数组)
$validator->valid();
// 获取失败的验证规则(数组)
$validator->failed();
// 根据闭包向给定字段添加条件规则,非常有用
$validator->sometimes($attribute, $rules, callable $callback);
// 指示验证器在第一个规则失败后即停止验证,默认 false,非常有用
$validator->stopOnFirstFailure($stopOnFirstFailure = true);
// 获取验证器的消息容器(后两者是 messages() 的马甲)
$validator->messages();
$validator->errors();
$validator->getMessageBag();
// 获取要验证的数据,getData() 的别名
$validator->attributes();
// 获取给定属性的值
// 编写自定义规则、且一个字段依赖另一个字段时十分有用
$validator->getValue($attribute);
// 确定给定属性在给定集合中是否已有规则
$validator->hasRule(string $attribute, string|array $rules);
// 获取给定属性的规则及其参数
$validator->getRule(string $attribute, string|array $rules);
// 获取全部验证规则
$validator->getRules();
// 追加验证规则
$validator->addRules(array $rules);
// 将失败的规则和错误消息添加到集合中
$validator->addFailure(string $attribute, string $rule, array $parameters = []);
// 注册自定义验证器扩展
$validator->addExtension($rule, $extension);
$validator->addImplicitExtension($rule, $extension); // 隐式扩展
$validator->addDependentExtension($rule, $extension); // 依赖扩展
$validator->addExtensions(array $extensions);
$validator->addImplicitExtensions(array $extensions); // 隐式扩展
$validator->addDependentExtensions(array $extensions);
// 注册自定义验证器消息替换器
$validator->addReplacer($rule, $replacer);
// 自定义消息与属性名
$validator->setCustomMessages(array $messages);
$validator->setAttributeNames(array $attributes);
FormRequest 表单请求类示例#
<?php
namespace App\Http\Requests;
use Closure;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
class TestRequest extends FormRequest
{
/**
* 验证失败的重定向地址。
* 四选一设置即可,权重依次递减;也可以重写 getRedirectUrl() 方法。
* 默认重定向回来源页,可在 failedValidation() 中禁止重定向。
*/
protected $redirect;
protected $redirectRoute;
protected $redirectAction;
protected $redirector;
/**
* 错误背包。
* 如果页面有多个独立表单,可通过错误背包来区分。
*
* @link https://learnku.com/docs/laravel/10.x/validation/14856#named-error-bags
* @var string
*/
protected $errorBag = 'default';
/**
* 表示验证器是否应在第一个规则失败时停止。
* 注意:此处是停止所有属性与规则,与仅停止单个属性的 bail 规则不同。
*
* @var bool
*/
protected $stopOnFirstFailure = false;
/**
* 确定用户是否有权提出此请求。
* 可在此处做权限判断,例如用户发表帖子前判断其是否有发帖权限。
*
* @return bool
*/
public function authorize(): bool
{
// 例子:注册时间不足 24 小时,禁止发表新帖子
$registerTimestamp = strtotime(Auth::user()->created_at);
if ((time() - $registerTimestamp) < 86400) {
return false;
}
return true;
}
/**
* 授权失败处理:当 authorize() 存在且返回 false 时调用,可省略。
*/
protected function failedAuthorization()
{
throw new \Illuminate\Auth\Access\AuthorizationException;
}
/**
* 要验证的数据,可省略,默认取 request()->all()。
*/
public function validationData()
{
return [
'title' => '不像我只会心疼哥哥',
'content' => '哥哥,你女朋友要是知道我俩吃同一个棒棒糖,你女朋友不会吃醋吧!',
'password' => '88888888',
];
}
/**
* 验证规则。
*/
public function rules(): array
{
return [
'title' => 'required|between:8,50',
'content' => 'required|min:100',
];
}
/**
* 自定义属性别名,可省略。
*/
public function attributes()
{
return [
'title' => '标题',
'content' => '内容',
];
}
/**
* 自定义错误消息,可省略。
*/
public function messages()
{
return [
'required' => ':attribute不能为空, 请填写后再提交',
'title.between' => '帖子标题长度限制在:min至:max个字之间',
'password.between' => '密码错误',
];
}
/**
* 准备验证数据:在验证开始前对请求数据进行预处理。
*/
protected function prepareForValidation(): void
{
$this->merge([
/**
* rules() 验证的是这里修改过后的数据。
* 例如 'www.domain.com' 被改写成 'http://www.domain.com' 后进行验证,
* 且会影响 $this->all() 与 $this->validated() 的结果。
*/
'website' => 'http://' . $this->website,
]);
}
/**
* 验证完成后对任何请求数据进行规范化,可省略。
*/
protected function passedValidation(): void
{
/**
* 例如 'http://www.domain.com' 被改写成 'http://www.domain.com?code=123456'。
* 会影响 $this->all()、$this->input('website') 等;
* 但不会影响 $this->validated() 的结果,其中仍是 'http://www.domain.com'。
*/
$this->replace([
'website' => $this->website . '?code=123456',
]);
}
/**
* 配置验证器实例。
*
* @param \Illuminate\Validation\Validator $validator
* @return void
*/
public function withValidator(\Illuminate\Validation\Validator $validator)
{
// 验证器的后置操作
$validator->after(function (\Illuminate\Validation\Validator $validator) {
/**
* errors 具体内容可参考 Illuminate\Support\MessageBag。
*/
if ($validator->errors()->isEmpty()) {
/**
* 运行到 isEmpty() 时,表示 rules() 中的验证规则都已通过。
* 此时密码只通过了 rules() 里的规则验证,
* 还需要继续验证密码是否与持久存储中的一致。
*/
$password = $this->input('password');
if ($password !== '123456') {
$validator->errors()->add('password', '数据库比对密码错误');
}
}
});
}
/**
* 自定义验证失败后的错误处理。
*
* 注意:
* 1. 必须抛出一个 Exception,否则业务代码会继续执行;抛出后由框架捕捉处理。
* 直接 return 一个 response 对象是无效的。
* 2. 父类 FormRequest 默认抛出 Illuminate\Validation\ValidationException:
* - 表单请求(application/x-www-form-urlencoded)会重定向回来源页面,并把错误写入 flash session;
* - 非表单请求则返回一个固定格式的 JSON 响应。
*
* 父类默认的 failedValidation 不能满足以下需求:
* 1. 有时希望验证失败时始终返回 JSON 而不跳转,甚至返回 XML;
* 2. 默认返回的 JSON 格式固定,而可能需要自定义一些字段。
*/
protected function failedValidation(Validator $validator)
{
// 具体内容可参考 Illuminate\Support\MessageBag
$error = $validator->errors();
// 自定义 response 对象 Illuminate\Http\Response
$response = response()->json([
// ... 自定义的错误响应内容
'code' => 10001,
'message' => $error->first(),
'errors' => $error->errors(),
]);
/**
* 抛出一个 HttpResponseException 异常类,
* 这将阻止验证器调用之后的后续代码,直接把 response 发送到浏览器。
*/
throw new HttpResponseException($response);
}
}
闭包验证器与行内验证器#
<?php
use Closure;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Validator;
class TestRequest extends FormRequest
{
public function rules(): array
{
return [
// 闭包验证器写法 1:引用类方法,避免 rules() 过于臃肿
'title' => [
'required',
'string',
Closure::fromCallable([$this, 'validateTitle']),
],
// 闭包验证器写法 2:直接内联闭包
'content' => [
'string',
function ($attribute, $value, $fail) {
if (...) {
$fail("参数 {$attribute} 不符合要求.");
}
},
],
// 使用自定义验证器 phoneNumber,在 prepareForValidation 里注册
'mobile' => 'required|phoneNumber',
];
}
protected function prepareForValidation(): void
{
Validator::extend('phoneNumber', [$this, 'validatePhoneNumber']);
}
/**
* 自定义验证器 phoneNumber。
* 自定义验证器参数比闭包多,可以完成更多事情。
*/
public function validatePhoneNumber($attribute, $value, $parameters, $validator)
{
// 可以通过 $validator 取到其他字段的值
$areaCode = $validator->getValue('area-code');
if ($areaCode == '1') {
// 美国手机号格式验证
} elseif ($areaCode == '86') {
// 中国手机号格式验证
if (...) {
$validator->errors()->add($attribute, "{$attribute}不是中国大陆手机号");
}
}
return true;
}
/**
* 闭包验证器写法 1 对应的方法。
*/
public function validateTitle($attribute, $value, $fail)
{
if (...) {
$fail("参数 {$attribute} 不符合要求.");
}
return true;
}
}
统一处理验证错误响应#
当项目较多使用 $request->validate() 或未自定义 failedValidation() 的验证类时,可以在全局异常处理器中统一兜底返回,保证接口错误响应格式一致。
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class Handler extends ExceptionHandler
{
// ...
public function register(): void
{
// 为表单验证统一兜底处理返回。
// 包含没有自定义 failedValidation 的独立验证类和 $request->validate()
$this->renderable(function (ValidationException $e, Request $request) {
return response()->json([
'code' => 2,
'message' => $e->getMessage(),
'error' => $e->validator->errors(),
], $e->status);
});
$this->reportable(function (Throwable $e) {
//
});
}
}
验证器错误背包(MessageBag)用法#
<?php
use Illuminate\Support\Facades\Validator;
$validator = Validator::make($request->all(), [
'title' => 'required|unique:posts|max:255',
'body' => 'required',
]);
if ($validator->fails()) {
// Illuminate\Support\MessageBag
$errors = $validator->errors();
// 最常用
$errors->first($key = null, $format = null); // 获取给定 key 的第一条消息
$errors->get($key, $format = null); // 获取给定 key 的所有消息
$errors->all($format = null); // 获取全部消息
$errors->isEmpty(); // bool 是否没有错误
$errors->isNotEmpty(); // bool 是否包含错误
$errors->count(); // int 错误总数
$errors->add($key, $message); // this 添加错误
// 非常用
$errors->keys(); // 返回包含错误的 keys
$errors->addIf($boolean, $key, $message); // 当参数 1 为 true 时添加消息
$errors->isUnique($key, $message); // 判断 key 与 message 的组合是否已存在
$errors->merge($messages); // 将新消息数组合并到消息包中
$errors->has($key); // 确定是否存在给定 key 的消息
$errors->hasAny($keys = []); // 确定是否存在任意给定 key 的消息,可传非数组
$errors->missing($key); // 确定是否不存在给定 key 的消息
$errors->unique($format = null); // 获取全部消息,并去重
$errors->forget($key); // 删除给定 key 的消息
$errors->toArray(); // 转为数组
$errors->toJson(); // 转为 JSON
}
独立表单验证类的自动调用流程#
<?php
use App\Http\Requests\TestRequest;
class HomeController extends Controller
{
// 用 FormRequest 代替 Illuminate\Http\Request 作为控制器的参数注入
public function myaction(TestRequest $request)
{
// 使用独立验证类时,不需要手动调用 fails()
// 验证通过后可直接取用已验证的数据
return $request->validated();
}
}
整个调用链如下:
- 实现自定义验证类
MyRequest。 MyRequest继承Illuminate\Foundation\Http\FormRequest。FormRequest中 Trait 了ValidatesWhenResolvedTrait。ValidatesWhenResolvedTrait的validateResolved()方法调用了MyRequest->fails(),失败时调用failedValidation()方法。Illuminate\Foundation\Providers\FormRequestServiceProvider中启用你的MyRequest。
Precognitive 预认知#
当浏览器请求头包含 Precognition-Validate-Only 时,Laravel 会在响应头中自动加入 Precognition-Success=true。预认知允许前端在真正提交前即时校验字段(常用于 Inertia / Vue 场景),此时可以通过 $this->isPrecognitive() 判断请求是否为预认知请求,从而跳过开销较大的验证规则。
<?php
use Illuminate\Validation\Rules\Password;
public function rules(): array
{
return [
'password' => [
'required',
// 预认知请求只做轻量校验,跳过高开销规则(如 uncompromised)
$this->isPrecognitive()
? Password::min(8)
: Password::min(8)->uncompromised(),
],
];
}