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 的表单请求验证提供了一种强大的方式来处理输入验证。通过合理使用表单请求,可以创建清晰、可维护的验证逻辑,同时保持控制器的简洁。