前言#

Laravel 的表单验证是日常开发中使用频率极高的功能,但官方文档往往只覆盖最常用的部分。这篇文章汇总了我在实际开发中整理的 Validator 非完全用法、FormRequest 表单请求类、闭包 / 行内验证器、统一错误响应、错误背包(MessageBag) 以及 Precognitive 预认知 等进阶用法,并附上完整可运行的示例代码,方便随时查阅。

本文基于 Laravel 10.x / PHP 8.1+ 编写。

目录#

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();
    }
}

整个调用链如下:

  1. 实现自定义验证类 MyRequest。
  2. MyRequest 继承 Illuminate\Foundation\Http\FormRequest。
  3. FormRequest 中 Trait 了 ValidatesWhenResolvedTrait。
  4. ValidatesWhenResolvedTrait 的 validateResolved() 方法调用了 MyRequest->fails(),失败时调用 failedValidation() 方法。
  5. 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(),
        ],
    ];
}

参考资料#