Laravel: Laravel表单验证
最后更新:2026-08-26
验证是 Laravel 的"安检门"——所有进入系统的数据都必须通过验证,脏数据一条也混不进去。
1. 你将学到
- 验证规则:required/email/unique/exists/file/image/custom
- Form Request 验证类:make:request
- 自定义验证规则:Closure 规则与 Rule 对象
- 错误消息处理:$errors 视图共享与 API JSON 响应
- 条件验证与验证 Bail/Sometimes 规则
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) 验证管道流程
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 规则更新时怎么排除自身?
A 用
Rule::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 提前终止、或缓存验证结果。大多数场景性能不是问题。
📖 小节
- Laravel 验证在数据进入系统前拦截,规则丰富且可组合
- Form Request 类封装验证逻辑,控制器保持干净
- Closure 规则适合一次性逻辑,Rule 对象适合复用逻辑
- Web 验证失败重定向并 flash errors,API 返回 422 JSON
- sometimes/nullable 处理可选字段,bail 提前终止
- prepareForValidation 在验证前修改输入数据
📝 作业
-
基础题(⭐):为 ShopMetrics 商品创建编写 StoreProductRequest,包含 name/sku/price/stock/category_id 的验证规则,确保 sku 唯一、价格为正数。
-
进阶题(⭐⭐):创建 ValidCouponCode Rule 对象,验证优惠券未过期且有剩余次数,在 StoreOrderRequest 中使用该规则,测试有效和无效优惠券的响应。
-
挑战题(⭐⭐⭐):实现 SufficientStock Rule(验证订单数量不超过库存),在 StoreOrderRequest 的 items.*.quantity 上使用,处理多商品同时下单的库存竞争问题。