Laravel 11 数据验证与错误提示

用户输入的数据是不可信的,因此验证是 Web 应用安全性的重要防线。Laravel 提供了多种验证机制,让你能够轻松定义规则、自定义错误消息,并将验证逻辑与控制器分离。 本章将全面介绍 Laravel 的数据验证功能,以及如何在前端优雅地展示错误提示。

🎯 为什么需要数据验证?

  • 保证数据完整性和正确性(如邮箱格式、字段必填)。
  • 防止恶意输入(SQL注入、XSS等)。
  • 提升用户体验,即时反馈错误信息。
  • 符合业务规则(如唯一性、范围限制)。

📦 基本验证用法

在控制器中,使用 validate 方法对请求数据进行验证。如果验证失败,会自动重定向回上一页并携带错误信息。

use Illuminate\Http\Request;

public function store(Request $request)
{
    $validated = $request->validate([
        'title' => 'required|string|max:255',
        'content' => 'required',
        'email' => 'required|email|unique:users,email',
        'age' => 'nullable|integer|min:18',
    ]);

    // 验证通过,$validated 包含已验证的数据
    // 继续业务逻辑...
}
>

也可以使用数组形式定义规则,更易读:

$request->validate([
    'title' => ['required', 'string', 'max:255'],
    'content' => ['required'],
    'email' => ['required', 'email', 'unique:users,email'],
]);
>
💡 提示: 验证失败时,Laravel 会自动将用户重定向回来源页面,并将所有错误信息闪存到 Session,同时将用户输入(除密码外)闪存到 old()

🔧 常用验证规则

  • required — 字段必须存在且不为空。
  • required_if:another,value — 当另一个字段等于某值时该字段必填。
  • string — 字段必须是字符串类型。
  • integer — 必须是整数。
  • numeric — 必须是数值。
  • email — 必须是有效的邮箱格式。
  • url — 必须是有效的 URL。
  • date — 必须是有效的日期格式。
  • min:value — 数值最小值,或字符串最小长度。
  • max:value — 数值最大值,或字符串最大长度。
  • between:min,max — 数值或长度介于 min 和 max 之间。
  • unique:table,column — 在指定表中必须唯一(排除当前记录时可用 unique:users,email,{$id})。
  • exists:table,column — 必须存在于指定表的字段中。
  • confirmed — 必须与 _confirmation 字段匹配(如 password 与 password_confirmation)。
  • image — 上传文件必须是图片(jpg、png等)。
  • mimes:jpg,png — 限制文件 MIME 类型。

📝 自定义错误消息

你可以为每个规则自定义错误消息,支持字段占位符 :attribute:value 等:

$request->validate([
    'title' => 'required|max:255',
    'email' => 'required|email|unique:users',
], [
    'title.required' => '标题不能为空',
    'title.max' => '标题不能超过 255 个字符',
    'email.required' => '邮箱地址是必填项',
    'email.email' => '请输入有效的邮箱地址',
    'email.unique' => '该邮箱已被注册',
]);
>

还可以使用 attributes 数组自定义字段名称:

$request->validate([
    'user.email' => 'required|email',
], [
    'user.email.required' => '邮箱不能为空',
], [
    'user.email' => '用户邮箱',
]);
>

📋 表单请求验证(Form Request)

当验证逻辑变得复杂时,建议使用表单请求类将验证规则独立出来,保持控制器简洁。

创建表单请求类:

php artisan make:request StorePostRequest

生成的文件位于 app/Http/Requests,在其中定义验证规则和错误消息:

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StorePostRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true; // 可在此进行权限验证
    }

    public function rules(): array
    {
        return [
            'title' => 'required|string|max:255',
            'content' => 'required',
            'category_id' => 'required|exists:categories,id',
        ];
    }

    public function messages(): array
    {
        return [
            'title.required' => '标题不能为空',
            'content.required' => '内容不能为空',
            'category_id.exists' => '请选择有效的分类',
        ];
    }
}

在控制器中类型提示该请求,Laravel 会自动验证:

public function store(StorePostRequest $request)
{
    // 验证已通过,直接使用 $request 获取数据
    $post = Post::create($request->validated());
    return redirect()->route('posts.index');
}
💡 优势: 验证逻辑与控制器解耦,复用性强,且支持依赖注入(如通过 authorize() 进行权限检查)。

⚠️ 错误提示展示

在视图中,$errors 变量自动可用,它是一个 MessageBag 实例,包含所有验证错误信息。

全局错误列表
@if ($errors->any())
    <div class="alert alert-danger">
    <ul>
    @foreach ($errors->all() as $error)
        <li>{{ $error }}</li>
    @endforeach
    </ul>
    </div>
@endif
针对特定字段的错误
<input type="text" name="title" value="{{ old('title') }}">
@error('title')
    <div class="error-message">{{ $message }}</div>
@enderror
>
使用 Bootstrap 样式类
<input type="email" name="email" class="form-control @error('email') is-invalid @enderror" value="{{ old('email') }}">
@error('email')
    <div class="invalid-feedback">{{ $message }}</div>
@enderror
>

🌐 AJAX 验证

如果请求期望 JSON 响应(如 AJAX 请求),验证失败时会返回 JSON 格式的错误信息,状态码为 422。

前端示例(使用 Fetch API):

fetch('/api/posts', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
    },
    body: JSON.stringify(data)
})
.then(response => {
    if (!response.ok) {
        return response.json().then(err => { throw err; });
    }
    return response.json();
})
.catch(err => {
    if (err.errors) {
        // 处理验证错误
        Object.keys(err.errors).forEach(field => {
            console.log(`${field}: ${err.errors[field].join(', ')}`);
        });
    }
});
>

🎨 实战示例:完整的文章创建流程

1. 表单请求类 StorePostRequest.php

public function rules(): array
{
    return [
        'title' => 'required|string|max:255|unique:posts,title',
        'content' => 'required|string|min:10',
        'category_id' => 'required|exists:categories,id',
        'tags' => 'array',
        'tags.*' => 'exists:tags,id',
        'image' => 'nullable|image|max:2048',
    ];
}

public function messages(): array
{
    return [
        'title.unique' => '标题已存在,请更换',
        'content.min' => '内容至少需要 10 个字符',
        'category_id.exists' => '分类不存在',
        'tags.*.exists' => '所选标签无效',
        'image.image' => '请上传有效的图片文件',
        'image.max' => '图片不能超过 2MB',
    ];
}

2. 控制器方法

public function store(StorePostRequest $request)
{
    $data = $request->validated();

    if ($request->hasFile('image')) {
        $data['image'] = $request->file('image')->store('posts', 'public');
    }

    $post = Post::create($data);
    $post->tags()->sync($data['tags'] ?? []);

    return redirect()->route('posts.show', $post)->with('success', '文章发布成功!');
}

3. 视图表单(简化)

<form method="POST" action="{{ route('posts.store') }}" enctype="multipart/form-data">
    @csrf

    <div class="mb-3">
        <label>标题</label>
        <input type="text" name="title" class="form-control @error('title') is-invalid @enderror" value="{{ old('title') }}">
        @error('title')
            <div class="invalid-feedback">{{ $message }}</div>
        @enderror
    </div>

    <div class="mb-3">
        <label>内容</label>
        <textarea name="content" class="form-control @error('content') is-invalid @enderror">{{ old('content') }}</textarea>
        @error('content')
            <div class="invalid-feedback">{{ $message }}</div>
        @enderror
    </div>

    <div class="mb-3">
        <label>分类</label>
        <select name="category_id" class="form-select @error('category_id') is-invalid @enderror">
            <option value="">请选择</option>
            @foreach ($categories as $category)
                <option value="{{ $category->id }}" {{ old('category_id') == $category->id ? 'selected' : '' }}>
                {{ $category->name }}
                </option>
            @endforeach
        </select>
        @error('category_id')
            <div class="invalid-feedback">{{ $message }}</div>
        @enderror
    </div>

    <div class="mb-3">
        <label>图片</label>
        <input type="file" name="image" class="form-control @error('image') is-invalid @enderror">
        @error('image')
            <div class="invalid-feedback">{{ $message }}</div>
        @enderror
    </div>

    <button type="submit" class="btn btn-primary">发布文章</button>
</form>

❓ 常见问题

在控制器中使用 validate 时无法直接指定重定向地址,但可以在表单请求类中重写 $redirect 属性或 getRedirectUrl() 方法。或者捕获验证异常手动处理。

使用星号语法,如 'items.*.name' => 'required|string',验证 items 数组中每个子项的 name 字段。

使用 sometimes 规则:$request->validate(['field' => 'sometimes|required|...']); 当字段存在时才验证。

📝 小结

数据验证是保证应用质量的关键环节。Laravel 提供了灵活的验证方式,从简单的控制器验证到高级的表单请求类,配合强大的规则库和自定义错误消息,可以应对各种复杂场景。 配合视图中便捷的错误显示,能有效提升用户体验。下一章我们将学习会话管理,掌握用户状态维护的技巧。