Laravel 13 表单请求验证深度解析
表单请求验证是 Laravel 提供的一种强大的验证机制,它将验证逻辑从控制器中分离出来。本文将深入探讨 Laravel 13 中表单请求验证的高级用法。
表单请求基础
创建表单请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreUserRequest extends FormRequest { public function authorize(): bool { return true; }
public function rules(): array { return [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'unique:users,email'], 'password' => ['required', 'string', 'min:8', 'confirmed'], ]; } }
|
在控制器中使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreUserRequest; use App\Models\User;
class UserController extends Controller { public function store(StoreUserRequest $request) { $user = User::create($request->validated());
return response()->json($user, 201); } }
|
授权验证
基本授权
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdatePostRequest extends FormRequest { public function authorize(): bool { $post = $this->route('post');
return $this->user()->can('update', $post); } }
|
条件授权
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateOrderRequest extends FormRequest { public function authorize(): bool { $order = $this->route('order');
if ($order->status === 'completed') { return false; }
return $this->user()->id === $order->user_id || $this->user()->isAdmin(); } }
|
验证规则
条件规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule;
class UpdateUserRequest extends FormRequest { public function rules(): array { $userId = $this->route('user')->id;
return [ 'name' => ['required', 'string', 'max:255'], 'email' => [ 'required', 'email', Rule::unique('users', 'email')->ignore($userId), ], 'password' => Rule::when( $this->filled('password'), ['required', 'string', 'min:8', 'confirmed'] ), 'role' => Rule::when( $this->user()->isAdmin(), ['required', Rule::in(['admin', 'user', 'moderator'])] ), ]; } }
|
复杂规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule;
class StoreProductRequest extends FormRequest { public function rules(): array { return [ 'name' => ['required', 'string', 'max:255'], 'sku' => [ 'required', 'string', Rule::unique('products', 'sku')->where(function ($query) { return $query->where('store_id', $this->input('store_id')); }), ], 'price' => ['required', 'numeric', 'min:0', 'max:999999.99'], 'category_id' => [ 'required', Rule::exists('categories', 'id')->where(function ($query) { return $query->where('is_active', true); }), ], 'tags' => ['array', 'max:10'], 'tags.*' => ['string', 'max:50'], 'images' => ['array', 'max:5'], 'images.*' => ['image', 'max:2048', 'mimes:jpeg,png,webp'], ]; } }
|
自定义错误消息
消息方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreUserRequest extends FormRequest { public function rules(): array { return [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'unique:users'], 'password' => ['required', 'min:8', 'confirmed'], ]; }
public function messages(): array { return [ 'name.required' => '请输入用户名', 'name.max' => '用户名不能超过255个字符', 'email.required' => '请输入邮箱地址', 'email.email' => '请输入有效的邮箱地址', 'email.unique' => '该邮箱已被注册', 'password.required' => '请输入密码', 'password.min' => '密码至少需要8个字符', 'password.confirmed' => '两次输入的密码不一致', ]; } }
|
属性名称
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreProductRequest extends FormRequest { public function rules(): array { return [ 'name' => ['required', 'string'], 'price' => ['required', 'numeric', 'min:0'], 'stock' => ['required', 'integer', 'min:0'], ]; }
public function attributes(): array { return [ 'name' => '商品名称', 'price' => '价格', 'stock' => '库存', ]; } }
|
准备输入数据
prepareForValidation() 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| <?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Str;
class StorePostRequest extends FormRequest { public function rules(): array { return [ 'title' => ['required', 'string', 'max:255'], 'slug' => ['required', 'string', 'unique:posts,slug'], 'content' => ['required', 'string'], ]; }
protected function prepareForValidation(): void { $this->merge([ 'slug' => Str::slug($this->input('title')), 'author_id' => $this->user()->id, ]);
if ($this->has('tags')) { $this->merge([ 'tags' => array_map('trim', $this->input('tags')), ]); } } }
|
passedValidation() 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreOrderRequest extends FormRequest { public function rules(): array { return [ 'items' => ['required', 'array', 'min:1'], 'items.*.product_id' => ['required', 'exists:products,id'], 'items.*.quantity' => ['required', 'integer', 'min:1'], ]; }
protected function passedValidation(): void { $items = collect($this->input('items'))->map(function ($item) { $product = \App\Models\Product::find($item['product_id']); return array_merge($item, [ 'price' => $product->price, 'total' => $product->price * $item['quantity'], ]); });
$this->merge([ 'items' => $items->toArray(), 'subtotal' => $items->sum('total'), 'tax' => $items->sum('total') * 0.1, 'total' => $items->sum('total') * 1.1, ]); } }
|
验证后钩子
withValidator() 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| <?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Validator;
class StoreBookingRequest extends FormRequest { public function rules(): array { return [ 'room_id' => ['required', 'exists:rooms,id'], 'check_in' => ['required', 'date', 'after:today'], 'check_out' => ['required', 'date', 'after:check_in'], 'guests' => ['required', 'integer', 'min:1'], ]; }
public function withValidator(Validator $validator): void { $validator->after(function ($validator) { if ($this->hasDateConflict()) { $validator->errors()->add( 'check_in', '该房间在所选日期已被预订' ); }
if ($this->exceedsRoomCapacity()) { $validator->errors()->add( 'guests', '入住人数超过房间容量' ); } }); }
protected function hasDateConflict(): bool { return \App\Models\Booking::where('room_id', $this->input('room_id')) ->where(function ($query) { $query->whereBetween('check_in', [$this->input('check_in'), $this->input('check_out')]) ->orWhereBetween('check_out', [$this->input('check_in'), $this->input('check_out')]); }) ->exists(); }
protected function exceedsRoomCapacity(): bool { $room = \App\Models\Room::find($this->input('room_id')); return $this->input('guests') > $room->capacity; } }
|
自定义验证规则
规则对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| <?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class StrongPassword implements Rule { protected int $minLength = 8; protected array $requirements = [];
public function passes($attribute, $value): bool { if (strlen($value) < $this->minLength) { $this->requirements[] = "至少 {$this->minLength} 个字符"; return false; }
if (!preg_match('/[A-Z]/', $value)) { $this->requirements[] = '至少一个大写字母'; return false; }
if (!preg_match('/[a-z]/', $value)) { $this->requirements[] = '至少一个小写字母'; return false; }
if (!preg_match('/[0-9]/', $value)) { $this->requirements[] = '至少一个数字'; return false; }
if (!preg_match('/[!@#$%^&*]/', $value)) { $this->requirements[] = '至少一个特殊字符'; return false; }
return true; }
public function message(): string { return '密码必须包含: ' . implode(', ', $this->requirements); } }
|
使用自定义规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest; use App\Rules\StrongPassword;
class StoreUserRequest extends FormRequest { public function rules(): array { return [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'unique:users'], 'password' => ['required', 'confirmed', new StrongPassword()], ]; } }
|
闭包规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Facades\Auth;
class TransferRequest extends FormRequest { public function rules(): array { return [ 'amount' => [ 'required', 'numeric', 'min:0.01', function ($attribute, $value, $fail) { if ($value > Auth::user()->balance) { $fail('余额不足'); } }, ], 'to_account' => [ 'required', 'exists:accounts,number', function ($attribute, $value, $fail) { if ($value === Auth::user()->account->number) { $fail('不能转账给自己'); } }, ], ]; } }
|
条件验证
sometimes() 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateProfileRequest extends FormRequest { public function rules(): array { return [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email'], ]; }
public function withValidator($validator): void { $validator->sometimes('email', 'unique:users,email', function ($input) { return $input->email !== $this->user()->email; });
$validator->sometimes('password', ['required', 'min:8', 'confirmed'], function ($input) { return $input->filled('new_password'); }); } }
|
错误响应
自定义错误响应
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Http\Exceptions\HttpResponseException; use Illuminate\Contracts\Validation\Validator;
class ApiRequest extends FormRequest { protected function failedValidation(Validator $validator): void { throw new HttpResponseException( response()->json([ 'success' => false, 'message' => '验证失败', 'errors' => $validator->errors(), ], 422) ); } }
|
授权失败响应
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Http\Exceptions\HttpResponseException;
class AdminRequest extends FormRequest { public function authorize(): bool { return $this->user()->isAdmin(); }
protected function failedAuthorization(): void { throw new HttpResponseException( response()->json([ 'success' => false, 'message' => '您没有权限执行此操作', ], 403) ); } }
|
测试表单请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <?php
namespace Tests\Unit\Requests;
use Tests\TestCase; use App\Http\Requests\StoreUserRequest; use Illuminate\Support\Facades\Validator;
class StoreUserRequestTest extends TestCase { public function test_valid_data_passes(): void { $request = new StoreUserRequest(); $validator = Validator::make([ 'name' => 'John Doe', 'email' => 'john@example.com', 'password' => 'password123', 'password_confirmation' => 'password123', ], $request->rules(), $request->messages());
$this->assertFalse($validator->fails()); }
public function test_invalid_data_fails(): void { $request = new StoreUserRequest(); $validator = Validator::make([ 'name' => '', 'email' => 'invalid-email', 'password' => 'short', ], $request->rules(), $request->messages());
$this->assertTrue($validator->fails()); $this->assertArrayHasKey('name', $validator->errors()->toArray()); $this->assertArrayHasKey('email', $validator->errors()->toArray()); $this->assertArrayHasKey('password', $validator->errors()->toArray()); } }
|
最佳实践
1. 保持规则简洁
1 2 3 4 5 6 7 8 9 10 11 12
| <?php
class SimpleRequest extends FormRequest { public function rules(): array { return [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email'], ]; } }
|
2. 使用自定义规则类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?php
use App\Rules\PhoneNumber; use App\Rules\PostalCode;
class AddressRequest extends FormRequest { public function rules(): array { return [ 'phone' => ['required', new PhoneNumber()], 'postal_code' => ['required', new PostalCode()], ]; } }
|
3. 分离复杂验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <?php
class ComplexRequest extends FormRequest { public function rules(): array { return array_merge( $this->basicRules(), $this->addressRules(), $this->paymentRules() ); }
protected function basicRules(): array { return [ 'name' => ['required', 'string'], ]; }
protected function addressRules(): array { return [ 'address.street' => ['required'], 'address.city' => ['required'], ]; }
protected function paymentRules(): array { return [ 'payment.method' => ['required'], ]; } }
|
总结
Laravel 13 的表单请求验证提供了一种强大的方式来处理输入验证。通过合理使用表单请求,可以创建清晰、可维护的验证逻辑,同时保持控制器的简洁。