CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Laravel 实战入门/课时/验证与异常
RawBlame
课时目录12 节

Laravel 实战入门

  1. 01Laravel 概览
  2. 02目录结构
  3. 03路由
  4. 04控制器
  5. 05Blade 模板
  6. 06Eloquent ORM
  7. 07认证与授权
  8. 08验证与异常
  9. 09队列与事件
  10. 10运维与工具
  11. 11测试
  12. 12综合练习

验证与异常

表单验证与 HTTP 异常。

30 分钟

表单验证与错误处理

这一课你将能做到

  1. 在控制器中用 validate() 快速校验输入,并读懂自动返回的 422 与错误包
  2. 用 FormRequest 类把规则、授权、错误消息集中管理,并处理嵌套数组与文件校验
  3. 编写自定义验证规则,学会用 Rule 对象表达唯一值、存在性、枚举等复杂约束

为什么要学

「永远不要信任客户端输入」是后端第一课。Spring Boot 里你用 Bean Validation(@NotBlank、@Min、@Valid),由 Hibernate Validator 执行,失败后在 BindingResult 或全局异常里处理。Laravel 的验证层覆盖同样的场景,但规则以数组或字符串表达,开箱即用近百条规则,且能自动生成字段级错误消息——中文提示也只需发布语言文件。

验证还有两个工程价值:它与授权(FormRequest 的 authorize())、旧输入回填(old())无缝衔接;它是异常 API 统一响应的第一个真实案例。本课把「下单」接口的规则完整写透。

先建立图像

请求进入
   │
   ▼
FormRequest::authorize()  ──false──► 403
   │true
   ▼
rules() 逐条校验
   │全部通过      └──► 控制器方法内 $request->validated() 只拿干净数据
   ▼
ValidationException(422)
   ├─ web:back() + errors + old 输入(Blade @error 回显)
   └─ api:{ message, errors: { field: [msgs...] } } JSON

规则对照:

LaravelBean Validation
required@NotNull / @NotBlank
string / integer / numeric类型绑定 + @Pattern
min:3 / max:20(字符串看长度,数字看大小)@Size / @Min/@Max
email@Email
unique:users,email需手写查询校验
exists:orders,id需手写
confirmed(需有 xxx_confirmation 字段)手写两次比对
in:a,b / 枚举 Rule@Pattern 或自定义

跟着做

步骤 1:控制器内快速验证

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class OrderController extends Controller
{
    public function store(Request $request)
    {
        $validated = $request->validate([
            'channel'          => ['required', 'string', 'in:alipay,wechat,mock'],
            'address_id'       => ['required', 'integer', 'exists:addresses,id'],
            'remark'           => ['nullable', 'string', 'max:200'],
            'items'            => ['required', 'array', 'min:1'],
            'items.*.product_id' => ['required', 'integer', 'exists:products,id'],
            'items.*.quantity'   => ['required', 'integer', 'min:1', 'max:99'],
            'coupon_code'      => ['nullable', 'string', 'size:8'],
        ]);

        // 只有通过校验的字段会出现在这里
        return response()->json(['received' => $validated], 201);
    }
}

验证失败:API 请求直接收到 422:

curl -i -X POST http://127.0.0.1:8000/api/orders \
  -H "Content-Type: application/json" \
  -d '{"channel":"xxx","items":[]}'
# HTTP/1.1 422 ...
# {"message":"The selected channel is invalid.","errors":{"channel":[...],"items":[...]}}

步骤 2:用 FormRequest 抽离规则

php artisan make:request StoreOrderRequest

app/Http/Requests/StoreOrderRequest.php:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreOrderRequest extends FormRequest
{
    // 授权:未登录直接 403,规则根本不执行
    public function authorize(): bool
    {
        return $this->user() !== null;
    }

    public function rules(): array
    {
        return [
            'channel'            => ['required', 'string', 'in:alipay,wechat,mock'],
            'address_id'         => ['required', 'integer',
                Rule_exists_address($this->user()->id)],
            'items'              => ['required', 'array', 'min:1', 'max:50'],
            'items.*.product_id' => ['required', 'integer', 'exists:products,id'],
            'items.*.quantity'   => ['required', 'integer', 'min:1', 'max:99'],
            'remark'             => ['nullable', 'string', 'max:200'],
        ];
    }

    // 自定义字段名(中文提示里更好读)
    public function attributes(): array
    {
        return [
            'channel' => '支付渠道',
            'address_id' => '收货地址',
            'items' => '商品明细',
            'items.*.product_id' => '商品',
            'items.*.quantity' => '数量',
        ];
    }

    // 自定义消息(不依赖全局语言包时)
    public function messages(): array
    {
        return [
            'items.required' => '至少选择一件商品',
            'items.*.quantity.max' => '单件商品最多购买 99 件',
        ];
    }
}

其中「地址必须属于当前用户」用闭包规则或 Rule 对象表达(防止用别人的地址下单):

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

// 简单写法:闭包规则
'address_id' => ['required', 'integer', function (string $attribute, mixed $value, Closure $fail) {
    $belongs = \App\Models\Address::where('id', $value)
        ->where('user_id', $this->user()->id)
        ->exists();

    if (! $belongs) {
        $fail('收货地址无效');
    }
}],

控制器方法直接声明类型,校验通过后拿干净数据:

public function store(StoreOrderRequest $request)
{
    $data = $request->validated(); // ['channel'=>..., 'items'=>[...], ...]
    // 交给 OrderService
}

步骤 3:中文化错误提示

Laravel 11 默认只带英文语言文件。安装社区中文语言包:

composer require laravel-lang/common --dev
php artisan lang:add zh_CN

之后在 config/app.php 中设 'locale' => 'zh_CN',验证失败的 message 即为中文;也可按请求动态切换 app()->setLocale('zh_CN')。字段中文名由 FormRequest 的 attributes() 提供。

步骤 4:文件与条件规则

头像上传 + 按渠道条件校验:

public function rules(): array
{
    return [
        'avatar'   => ['required', 'file', 'image', 'mimes:jpg,png,webp', 'max:2048'], // KB
        'channel'  => ['required', 'in:alipay,wechat'],
        // 支付宝需要支付宝流水号占位:复杂条件用 Rule::requiredIf
        'trade_no' => [
            Rule_required_if_alipay($this),
            'string', 'size:32',
        ],
    ];
}

// 上面两个辅助位的真实写法:
use Illuminate\Validation\Rule;

'address_id' => ['required', 'integer',
    Rule::exists('addresses', 'id')->where('user_id', $this->user()->id)],

'trade_no' => [
    Rule::requiredIf(fn () => $this->channel === 'alipay'),
    'nullable', 'string', 'size:32',
],

常用条件规则还有 exclude_if、sometimes(字段存在才校验)、prohibited(多租户场景禁止客户端传租户字段)。

步骤 5:唯一值与更新场景

注册与更新资料都要校验 email 唯一,但更新时必须排除自己:

public function rules(): array
{
    return [
        'name'  => ['required', 'string', 'max:50'],
        'email' => [
            'required', 'email',
            Rule::unique('users', 'email')->ignore($this->user()->id),
            // 软删除场景默认会忽略已删除行;需要限定作用域:
            // ->where(fn ($q) => $q->where('tenant_id', $this->user()->tenant_id))
        ],
    ];
}

注册接口(无排除):Rule::unique('users', 'email')。

步骤 6:自定义规则类

业务规则「优惠券码当前用户可用」值得封装复用:

php artisan make:rule UsableCoupon

app/Rules/UsableCoupon.php:

<?php

namespace App\Rules;

use App\Models\Coupon;
use Closure;
use Illuminate\Contracts\Validation\DataAwareRule;
use Illuminate\Contracts\Validation\ValidationRule;

class UsableCoupon implements ValidationRule, DataAwareRule
{
    protected array $data = [];

    public function setData(array $data): static
    {
        $this->data = $data;
        return $this;
    }

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $coupon = Coupon::where('code', $value)->first();

        if (! $coupon) {
            $fail('优惠券不存在');
            return;
        }

        if (! $coupon->usableBy($this->data['user_id'] ?? null)) {
            $fail('该优惠券不适用于当前账户');
        }
    }
}

使用:'coupon_code' => ['nullable', 'string', new UsableCoupon()]。Laravel 10 后推荐这种实现 ValidationRule 的轻量写法,旧版 passes/message 接口仍然支持。

讲清楚要点

  1. 校验在控制器之前发生:FormRequest 的解析由容器在调用方法前完成,失败就抛异常,控制器里不必写 if。
  2. validated() 只含通过规则的字段:配合 prohibited 可防止客户端偷偷传 is_admin、paid_at 等内部字段——这属于纵深防御。
  3. min/max 的语义随规则变化:对字符串是字符数,对数字是数值大小,对数组是元素个数,对文件是 KB。写金额校验时建议明确 numeric 或 integer 再配 min。
  4. 嵌套数组用点号:items.*.quantity;错误也按路径出现在 errors['items.0.quantity'],前端可据此定位每一行。
  5. 数据库规则(exists/unique)会查库:批量循环校验要注意查询次数;Rule::exists 每次校验一条查询是正常设计,但对超大数组可考虑先 pluck 一次再用 in 规则。
  6. 异常响应可定制:在 bootstrap/app.php 的 withExceptions 中捕获 ValidationException 可调整 JSON 结构;网页端自动回填依赖 web 中间件的 Session。
  7. 正则规则能不用就不用:regex: 可读性差且容易写错,优先用内置规则组合;PHP 8.2 的 filter_var / enums 也可结合。

你可能会懵的地方

  • 字符串写法 'required|email' 和数组写法 ['required','email'] 有什么区别? 规则内要传含管道符的值(如正则里有 |)时只能用数组;团队统一用数组更安全,功能完全一致。
  • 校验 nullable 的字段,空字符串算不算通过? nullable 表示允许 null;提交空字符串仍会继续走后续规则。想把空字符串转成 null,用中间件 ConvertEmptyStringsToNull。
  • 为什么 unique 更新时一直报「已被占用」? 忘了 ignore($id),把自己的记录也当成冲突。
  • FormRequest 的 authorize 和 Policy 重复吗? authorize 适合粗粒度门槛(登录、基础角色);资源级规则(这条地址是否属于本人)交给 Policy 或规则类。简单项目二者合一也常见。
  • 密码字段用什么规则? required(注册)/ confirmed(两次输入)/ min:8;不要限制过死的字符集(会降低密码多样性),也不要给密码加 max 过低。泄露检查可接 password 规则(比对常见泄露密码库)。

错法对照

错法问题正解
User::create($request->all()) 不做校验脏数据 + 提权风险FormRequest 白名单后 validated()
前端校验通过就认为安全接口可被直接调用规则全部在服务端
校验失败手动 return ['code'=>1] 且状态码 200语义混乱、客户端难处理保留 422 + errors 结构
金额允许小数且随意 round对账误差分为单位存整数,规则用 integer
更新 unique 不排除自身用户无法保存自己的资料ignore($model->id)
自定义规则里静默 return 不给错误用户不知道为何失败调用 $fail('原因')

卡点急救

  1. 规则好像没执行:确认 FormRequest 已作为控制器参数类型声明(不是 Request);缓存过期用 optimize:clear;规则文件命名空间在 App\Http\Requests。
  2. 文件校验总失败:确认表单带 enctype="multipart/form-data";post_max_size / upload_max_filesize 要大于文件;mimes 按扩展名判断,想验真实内容用 mimetypes:。
  3. 中文消息不显示:已 lang:add zh_CN 且 locale 正确;自定义 attributes() 提供字段名;跑过 config 缓存则重新生成。

面试常问

  • Laravel 的验证在请求流程的哪一步执行?失败后发生什么? 路由器解析控制器方法时,容器先实例化 FormRequest:先 authorize 后 rules;失败抛 ValidationException。网页请求被异常处理器转为重定向回上一页,错误与旧输入闪存进 Session;JSON 请求返回 422 与 message/errors。控制器本体在全部通过后才执行。

  • 如何防止批量赋值与字段伪造(如伪造 is_admin)? 三层:FormRequest 只声明业务字段并 validated();对危险字段加 prohibited 规则或直接不在规则中出现;模型 $fillable 白名单兜底。即使绕过前两层,模型也不会写入未授权字段。

  • exists / unique 规则在高并发或大表上要注意什么? 被校验列要有索引(唯一列本身有索引);大数组明细逐项 exists 会产生大量查询,可改为一次性查合法 ID 集合再比对;唯一冲突在最终 INSERT 时仍可能发生(并发窗口),代码要捕获唯一键异常做兜底。

自测三问

  1. 下单接口要求「地址属于当前用户、明细至少一条、每件数量 1-99」,你会在 FormRequest 里写哪些规则?
  2. 用户更新邮箱时 unique 规则怎么写才能不把自己判重?
  3. 网页表单和 JSON API 在校验失败时分别收到什么?错误信息在前端分别怎么取用?

小结一句话

验证是数据进入系统的安检门:FormRequest 定规则与授权,422 与字段级错误反馈客户端,自定义 Rule 沉淀业务约束,所有写入从此只信任干净数据。

延伸

  • 验证文档
  • FormRequest 表单请求
  • 内置规则速查表
  • Bean Validation 对照
  • 下一课:队列、任务调度、事件与缓存——学习把慢操作移出请求,以及异步解耦与缓存加速。
加载本节练习…
← 上一课认证与授权下一课 →队列与事件