Laravel: Laravel表单验证

最后更新:2026-08-26

验证是 Laravel 的"安检门"——所有进入系统的数据都必须通过验证,脏数据一条也混不进去。

1. 你将学到


2. 一个安全审计员的真实故事

(1) 痛点:用户输入导致数据混乱

Alice 注册 ShopMetrics 时邮箱填了"abc"、手机号填了"123"、店铺名留空——系统全盘接收,数据库里一堆无效数据。Bob 的商店被恶意用户通过 API 提交了负数价格的商品,订单金额变成 -999 USD,财务报表直接崩了。Charlie 做安全审计时发现 17 个端点没有任何输入验证。

(2) Laravel 验证的解法

Laravel 验证在数据进入系统前拦截——一个规则数组就能覆盖所有校验逻辑,Form Request 类让控制器保持干净。

PHP
// Validation rules — 30 seconds to write, protects forever
$validated = $request->validate([
    'name' => 'required|string|max:255',
    'email' => 'required|email|unique:users',
    'price' => 'required|numeric|min:0',
]);

(3) 收益

Alice 加了验证后,无效注册降到 0;Bob 的商品价格再也不会出现负数;Charlie 的安全审计 17 个端点全部通过。


3. 验证规则速查

(1) 常用规则

规则 说明 示例
required 必填 'name' => 'required'
email 邮箱格式 'email' => 'required|email'
unique:table,column 唯一 'slug' => 'unique:shops,slug'
exists:table,column 存在 'shop_id' => 'exists:shops,id'
min:n 最小值/长度 'price' => 'numeric|min:0'
max:n 最大值/长度 'name' => 'string|max:255'
numeric 数字 'quantity' => 'required|numeric'
integer 整数 'page' => 'integer|min:1'
string 字符串 'name' => 'required|string'
boolean 布尔 'is_active' => 'boolean'
date 日期 'published_at' => 'date'
file 文件 'logo' => 'file|max:2048'
image 图片文件 'photo' => 'image|mimes:jpeg,png'
confirmed 二次确认 'password' => 'confirmed'
regex:pattern 正则 'phone' => 'regex:/^[0-9]{10}$/'
in:a,b,c 枚举值 'status' => 'in:active,suspended'

(2) 验证管道流程

100%
flowchart TD
    A[Request Input] --> B[Validate Rules]
    B -->|Pass| C[Sanitized Data]
    C --> D[Controller Logic]
    B -->|Fail| E[Redirect Back with Errors]
    E --> F["Display \$errors in View"]

▶ 示例:ShopMetrics 商品创建验证

PHP
// Inline validation in controller
public function store(Request $request): RedirectResponse
{
    $validated = $request->validate([
        'name' => 'required|string|max:255',
        'sku' => 'required|string|unique:products,sku',
        'price' => 'required|numeric|min:0.01|max:999999.99',
        'stock' => 'required|integer|min:0',
        'category_id' => 'required|exists:categories,id',
        'description' => 'nullable|string|max:5000',
        'is_active' => 'boolean',
    ]);

    $product = Product::create($validated);

    return redirect()->route('products.show', $product)
        ->with('success', 'Product created.');
}

输出:

TEXT 📖 仅展示
// 执行成功

4. Form Request 验证类

(1) 创建 Form Request

BASH
php artisan make:request StoreShopRequest
php artisan make:request UpdateShopRequest

(2) 定义规则和授权

PHP
// app/Http/Requests/StoreShopRequest.php
class StoreShopRequest extends FormRequest
{
    public function authorize(): bool
    {
        return auth()->check() && auth()->user()->can('create', Shop::class);
    }

    public function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'slug' => 'required|string|unique:shops,slug',
            'description' => 'nullable|string|max:5000',
            'domain' => 'nullable|url',
            'status' => 'in:active,suspended',
        ];
    }

    public function messages(): array
    {
        return [
            'name.required' => 'Shop name is required.',
            'slug.unique' => 'This URL slug is already taken.',
            'domain.url' => 'Please enter a valid URL.',
        ];
    }
}

(3) 在控制器中使用

PHP
public function store(StoreShopRequest $request): RedirectResponse
{
    // $request->validated() only contains validated data
    $shop = Shop::create($request->validated());
    return redirect()->route('shops.show', $shop);
}
维度 内联验证 Form Request
位置 控制器方法内 独立类文件
复用性 ✅ 多控制器复用
授权检查 手动 ✅ authorize()
控制器代码 较多 精简
适合 简单验证 复杂/复用验证

▶ 示例:ShopMetrics StoreShopRequest

PHP
// app/Http/Requests/StoreShopRequest.php
class StoreShopRequest extends FormRequest
{
    public function authorize(): bool
    {
        return auth()->user()->role === 'tenant_owner';
    }

    public function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'slug' => 'required|alpha_dash|unique:shops,slug,NULL,id,tenant_id,' . tenant()->id,
            'description' => 'nullable|string|max:5000',
            'status' => 'sometimes|in:active,suspended',
        ];
    }

    public function messages(): array
    {
        return [
            'slug.unique' => 'You already have a shop with this slug.',
            'slug.alpha_dash' => 'Slug can only contain letters, numbers, dashes.',
        ];
    }

    protected function prepareForValidation(): void
    {
        $this->merge([
            'tenant_id' => tenant()->id,
            'slug' => Str::slug($this->slug ?? $this->name),
        ]);
    }
}

输出:

TEXT 📖 仅展示
// 执行成功

5. 自定义验证规则

(1) Closure 规则

PHP
// Inline custom rule
$validated = $request->validate([
    'discount' => [
        'required',
        'numeric',
        'min:0',
        function (string $attribute, mixed $value, Closure $fail) {
            if ($value > request('subtotal')) {
                $fail('The discount cannot exceed the subtotal.');
            }
        },
    ],
]);

(2) Rule 对象

BASH
php artisan make:rule ValidCouponCode
PHP
// app/Rules/ValidCouponCode.php
class ValidCouponCode implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $coupon = Coupon::where('code', $value)
            ->where('expires_at', '>', now())
            ->where('usage_limit', '>', DB::raw('usage_count'))
            ->first();

        if (!$coupon) {
            $fail('This coupon code is invalid or expired.');
        }
    }
}

// Usage
public function rules(): array
{
    return [
        'coupon_code' => ['nullable', 'string', new ValidCouponCode()],
    ];
}
方式 适合场景 复用性
Closure 一次性简单逻辑
Rule 对象 复杂/复用逻辑
Rule::class 方法 数据库相关验证

(3) Rule 类方法

PHP
use Illuminate\Validation\Rule;

// Unique ignoring current model
'slug' => Rule::unique('shops', 'slug')->ignore($shop->id),

// In with dynamic values
'status' => Rule::in(['active', 'suspended', 'closed']),

// Exists with additional query
'shop_id' => Rule::exists('shops', 'id')->where(function ($query) {
    $query->where('tenant_id', tenant()->id);
}),

▶ 示例:ShopMetrics 订单验证规则

PHP
// app/Http/Requests/StoreOrderRequest.php
class StoreOrderRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'items' => 'required|array|min:1',
            'items.*.product_id' => [
                'required',
                'integer',
                Rule::exists('products', 'id')->where('is_active', true),
            ],
            'items.*.quantity' => 'required|integer|min:1|max:100',
            'coupon_code' => ['nullable', 'string', new ValidCouponCode()],
            'discount' => [
                'sometimes',
                'numeric',
                'min:0',
                function ($attribute, $value, $fail) {
                    if ($value > $this->input('subtotal', 0)) {
                        $fail('Discount cannot exceed subtotal.');
                    }
                },
            ],
        ];
    }
}

输出:

TEXT 📖 仅展示
// 执行成功

6. 错误消息处理

(1) Web 页面错误显示

HTML
<!-- Display all errors -->
@if ($errors->any())
    <div class="alert alert-error">
        <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

<!-- Display error for specific field -->
<input type="text" name="name" value="{{ old('name') }}"
       class="{{ $errors->has('name') ? 'border-red-500' : '' }}">
@error('name')
    <p class="text-red-500 text-sm">{{ $message }}</p>
@enderror

(2) API JSON 错误响应

JSON
{
    "message": "The given data was invalid.",
    "errors": {
        "name": ["The name field is required."],
        "email": ["The email must be a valid email address."]
    }
}
请求类型 验证失败行为 错误格式
Web 表单 重定向回表单 + flash errors $errors 视图变量
API JSON 返回 422 + JSON {"errors": {...}}
AJAX 返回 422 + JSON 同 API

▶ 示例:ShopMetrics API 验证错误处理

PHP
// app/Http/Controllers/Api/ShopController.php
public function store(StoreShopRequest $request): JsonResponse
{
    $shop = Shop::create($request->validated());

    return response()->json([
        'message' => 'Shop created successfully.',
        'data' => new ShopResource($shop),
    ], 201);
}

// Client receives 422 on validation failure:
// {
//   "message": "The slug has already been taken.",
//   "errors": {
//     "slug": ["The slug has already been taken."]
//   }
// }

输出:

TEXT 📖 仅展示
// 执行成功

7. 条件验证

(1) Sometimes 规则

PHP
// Only validate if field is present
$validated = $request->validate([
    'name' => 'required|string',
    'notes' => 'sometimes|nullable|string|max:5000',
]);

// Conditional rules based on another field
Validator::make($data, [
    'payment_method' => 'required|in:credit_card,bank_transfer',
    'card_number' => 'required_if:payment_method,credit_card|numeric',
    'bank_account' => 'required_if:payment_method,bank_transfer|numeric',
]);

(2) Bail 规则

PHP
// Stop validating after first failure on a field
$validated = $request->validate([
    'email' => 'bail|required|email|unique:users',
    // If required fails, email and unique won't run
]);
规则 作用 使用场景
sometimes 字段存在时才验证 可选字段
bail 首次失败后停止 昂贵验证
required_if 条件必填 支付方式→卡号
required_unless 条件非必填
required_with 伴随必填 密码确认
prohibited_if 条件禁止
exclude_if 条件排除 不写入 validated
nullable 允许 null 可选字段

▶ 示例:ShopMetrics 条件验证场景

PHP
// app/Http/Requests/UpdateSubscriptionRequest.php
class UpdateSubscriptionRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'plan_id' => 'required|exists:plans,id',
            'payment_method' => 'required|in:credit_card,paypal,bank_transfer',
            'card_number' => 'required_if:payment_method,credit_card|string|size:16',
            'card_cvv' => 'required_if:payment_method,credit_card|string|size:3',
            'paypal_email' => 'required_if:payment_method,paypal|email',
            'bank_account' => 'required_if:payment_method,bank_transfer|string',
            'coupon_code' => 'sometimes|nullable|string|max:50',
        ];
    }
}

输出:

TEXT 📖 仅展示
// 执行成功

8. 综合示例:ShopMetrics 完整验证流程

PHP
// ============================================
// Comprehensive: ShopMetrics Order Validation
// Covers: Form Request, custom rules, conditional, errors
// ============================================

// app/Rules/SufficientStock.php
class SufficientStock implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $productId = request()->input(str_replace('.quantity', '.product_id', $attribute));
        $product = Product::find($productId);
        if ($product && $value > $product->stock) {
            $fail("Only {$product->stock} units available for {$product->name}.");
        }
    }
}

// app/Http/Requests/StoreOrderRequest.php
class StoreOrderRequest extends FormRequest
{
    public function authorize(): bool
    {
        return auth()->check();
    }

    public function rules(): array
    {
        return [
            'items' => 'required|array|min:1|max:50',
            'items.*.product_id' => [
                'required',
                'integer',
                Rule::exists('products', 'id')->where('is_active', true),
            ],
            'items.*.quantity' => [
                'required',
                'integer',
                'min:1',
                'max:100',
                new SufficientStock(),
            ],
            'coupon_code' => 'sometimes|nullable|string|max:50',
            'notes' => 'sometimes|nullable|string|max:1000',
            'shipping_address.line1' => 'required|string|max:255',
            'shipping_address.city' => 'required|string|max:100',
            'shipping_address.zip' => 'required|string|max:20',
            'shipping_address.country' => 'required|string|size:2',
        ];
    }

    public function messages(): array
    {
        return [
            'items.required' => 'Your cart is empty.',
            'items.min' => 'Add at least one item to place an order.',
            'items.*.product_id.exists' => 'One of the selected products is unavailable.',
            'shipping_address.line1.required' => 'Street address is required.',
        ];
    }
}

// Controller stays clean
public function store(StoreOrderRequest $request): RedirectResponse
{
    $order = $this->orderService->createFromRequest($request);
    return redirect()->route('orders.show', $order)
        ->with('success', 'Order placed successfully!');
}

❓ 常见问题

Q validate() 和 Form Request 怎么选?
A 简单验证(3-5 条规则)直接用 validate();复杂验证(10+ 规则、需要复用、需要授权检查)用 Form Request。Form Request 是最佳实践。
Q unique 规则更新时怎么排除自身?
ARule::unique('shops', 'slug')->ignore($shop->id)unique:shops,slug,{shop} 格式。更新验证必须排除当前记录,否则永远验证失败。
Q API 验证失败返回什么格式?
A Laravel 自动返回 422 状态码 + JSON:{"message":"...","errors":{"field":["error message"]}}。前端应根据 422 状态码解析 errors 对象显示字段级错误。
Q prepareForValidation 有什么用?
A 在验证前修改输入数据,如自动生成 slug、格式化手机号、添加默认值。修改后的数据参与验证和 validated() 输出。
Q 如何在测试中测试验证规则?
A 使用 $this->postJson() 提交无效数据,断言返回 422 和错误消息:$response->assertJsonValidationErrors('name')。也可以用 Validator::make() 直接测试规则。
Q 验证规则太多影响性能吗?
A unique/exists 规则会查数据库,有性能开销。对高频 API,考虑用 bail 提前终止、或缓存验证结果。大多数场景性能不是问题。

📖 小节


📝 作业

  1. 基础题(⭐):为 ShopMetrics 商品创建编写 StoreProductRequest,包含 name/sku/price/stock/category_id 的验证规则,确保 sku 唯一、价格为正数。

  2. 进阶题(⭐⭐):创建 ValidCouponCode Rule 对象,验证优惠券未过期且有剩余次数,在 StoreOrderRequest 中使用该规则,测试有效和无效优惠券的响应。

  3. 挑战题(⭐⭐⭐):实现 SufficientStock Rule(验证订单数量不超过库存),在 StoreOrderRequest 的 items.*.quantity 上使用,处理多商品同时下单的库存竞争问题。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏