Laravel 13 表单处理完全指南

表单处理是 Web 应用开发的核心功能。本文将深入探讨 Laravel 13 中表单处理的各种技巧和最佳实践。

表单请求验证

创建表单请求

1
2
php artisan make:request StoreUserRequest
php artisan make:request UpdateUserRequest

表单请求类

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
55
56
57
58
59
60
61
62
63
64
65
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class StoreUserRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}

public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'email',
Rule::unique('users')->ignore($this->user?->id),
],
'password' => ['required', 'string', 'min:8', 'confirmed'],
'avatar' => ['nullable', 'image', 'max:2048'],
'role' => ['required', Rule::in(['admin', 'editor', 'user'])],
'preferences' => ['nullable', 'array'],
'preferences.theme' => ['nullable', Rule::in(['light', 'dark'])],
'preferences.language' => ['nullable', 'string', 'size:2'],
];
}

public function messages(): array
{
return [
'name.required' => '请输入用户名',
'email.unique' => '该邮箱已被使用',
'password.min' => '密码至少需要 :min 个字符',
];
}

public function attributes(): array
{
return [
'name' => '用户名',
'email' => '邮箱地址',
'password' => '密码',
];
}

protected function prepareForValidation(): void
{
$this->merge([
'email' => strtolower($this->email),
'name' => trim($this->name),
]);
}

protected function passedValidation(): void
{
$this->replace([
'password' => bcrypt($this->password),
]);
}
}

控制器中使用

基础用法

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
<?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 redirect()->route('users.show', $user)
->with('success', '用户创建成功');
}

public function update(UpdateUserRequest $request, User $user)
{
$user->update($request->validated());

return redirect()->route('users.show', $user)
->with('success', '用户更新成功');
}
}

手动验证

1
2
3
4
5
6
7
8
9
10
11
12
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|string|min:8|confirmed',
]);

$user = User::create($validated);

return redirect()->route('users.show', $user);
}

复杂验证规则

条件验证

1
2
3
4
5
6
7
8
9
10
11
12
13
public function rules(): array
{
return [
'type' => ['required', Rule::in(['personal', 'business'])],
'company_name' => ['required_if:type,business', 'string', 'max:255'],
'tax_id' => [
'required_if:type,business',
'regex:/^[0-9]{2}-[0-9]{7}$/',
],
'age' => ['required', 'integer', 'min:18'],
'parent_consent' => ['required_if:age,<,18', 'boolean'],
];
}

自定义验证规则

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
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class StrongPassword implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (strlen($value) < 12) {
$fail('密码长度至少12位');
}

if (!preg_match('/[A-Z]/', $value)) {
$fail('密码必须包含大写字母');
}

if (!preg_match('/[a-z]/', $value)) {
$fail('密码必须包含小写字母');
}

if (!preg_match('/[0-9]/', $value)) {
$fail('密码必须包含数字');
}

if (!preg_match('/[^A-Za-z0-9]/', $value)) {
$fail('密码必须包含特殊字符');
}
}
}

// 使用
public function rules(): array
{
return [
'password' => ['required', 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
public function rules(): array
{
return [
'coupon_code' => [
'required',
'string',
function ($attribute, $value, $fail) {
$coupon = Coupon::where('code', $value)->first();

if (!$coupon) {
$fail('优惠券不存在');
return;
}

if ($coupon->expired_at < now()) {
$fail('优惠券已过期');
return;
}

if ($coupon->usage_limit && $coupon->used_count >= $coupon->usage_limit) {
$fail('优惠券已用完');
}
},
],
];
}

表单构建

Blade 表单

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
55
56
57
58
59
60
61
62
63
64
<form action="{{ route('users.store') }}" method="POST" enctype="multipart/form-data">
@csrf

<div class="form-group">
<label for="name">用户名</label>
<input type="text"
name="name"
id="name"
value="{{ old('name') }}"
class="form-control @error('name') is-invalid @enderror"
required>
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>

<div class="form-group">
<label for="email">邮箱</label>
<input type="email"
name="email"
id="email"
value="{{ old('email') }}"
class="form-control @error('email') is-invalid @enderror"
required>
@error('email')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>

<div class="form-group">
<label for="password">密码</label>
<input type="password"
name="password"
id="password"
class="form-control @error('password') is-invalid @enderror"
required>
@error('password')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>

<div class="form-group">
<label for="password_confirmation">确认密码</label>
<input type="password"
name="password_confirmation"
id="password_confirmation"
class="form-control"
required>
</div>

<div class="form-group">
<label for="avatar">头像</label>
<input type="file"
name="avatar"
id="avatar"
class="form-control-file @error('avatar') is-invalid @enderror"
accept="image/*">
@error('avatar')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>

<button type="submit" class="btn btn-primary">提交</button>
</form>

表单组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

namespace App\View\Components;

use Illuminate\View\Component;

class FormGroup extends Component
{
public function __construct(
public string $name,
public string $label,
public string $type = 'text',
public ?string $value = null,
public bool $required = false
) {}

public function render()
{
return view('components.form-group');
}
}
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
{{-- resources/views/components/form-group.blade.php --}}
<div class="form-group">
<label for="{{ $name }}">
{{ $label }}
@if($required)
<span class="text-danger">*</span>
@endif
</label>

@if($type === 'textarea')
<textarea name="{{ $name }}"
id="{{ $name }}"
class="form-control @error($name) is-invalid @enderror"
{{ $required ? 'required' : '' }}
rows="3">{{ old($name, $value) }}</textarea>
@else
<input type="{{ $type }}"
name="{{ $name }}"
id="{{ $name }}"
value="{{ old($name, $value) }}"
class="form-control @error($name) is-invalid @enderror"
{{ $required ? 'required' : '' }}>
@endif

@error($name)
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
1
2
3
4
{{-- 使用 --}}
<x-form-group name="name" label="用户名" required />
<x-form-group name="email" label="邮箱" type="email" required />
<x-form-group name="bio" label="简介" type="textarea" />

文件上传处理

单文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function store(Request $request)
{
$request->validate([
'avatar' => 'required|image|max:2048',
]);

$path = $request->file('avatar')->store('avatars', 'public');

$user = User::create([
'name' => $request->name,
'avatar' => $path,
]);

return redirect()->route('users.show', $user);
}

多文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function store(Request $request)
{
$request->validate([
'photos.*' => 'image|max:5120',
'photos' => 'required|array|min:1|max:5',
]);

$paths = [];

foreach ($request->file('photos') as $file) {
$paths[] = $file->store('photos', 'public');
}

return redirect()->back()->with('success', '上传成功');
}

表单安全

CSRF 保护

1
2
3
4
5
6
7
8
9
10
<form method="POST" action="{{ route('users.store') }}">
@csrf
{{-- 表单字段 --}}
</form>

<form method="DELETE" action="{{ route('users.destroy', $user) }}">
@method('DELETE')
@csrf
<button type="submit">删除</button>
</form>

XSS 防护

1
2
3
4
5
6
7
8
{{-- 自动转义 --}}
<p>{{ $user->name }}</p>

{{-- 原始输出(确保已净化)--}}
<p>{!! Purifier::clean($user->bio) !!}</p>

{{-- 转义属性 --}}
<input type="text" value="{{ $user->name }}">

动态表单

动态字段验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function rules(): array
{
$rules = [
'title' => ['required', 'string', 'max:255'],
];

foreach ($this->input('fields', []) as $index => $field) {
$rules["fields.{$index}.name"] = ['required', 'string'];
$rules["fields.{$index}.type"] = ['required', Rule::in(['text', 'number', 'email'])];

if ($field['type'] === 'number') {
$rules["fields.{$index}.value"] = ['nullable', 'numeric'];
}
}

return $rules;
}

表单数组处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public function store(Request $request)
{
$request->validate([
'items' => ['required', 'array', 'min:1'],
'items.*.product_id' => ['required', 'exists:products,id'],
'items.*.quantity' => ['required', 'integer', 'min:1'],
'items.*.price' => ['required', 'numeric', 'min:0'],
]);

$order = DB::transaction(function () use ($request) {
$order = Order::create([
'user_id' => auth()->id(),
'total' => collect($request->items)->sum(fn($item) => $item['quantity'] * $item['price']),
]);

foreach ($request->items as $item) {
$order->items()->create($item);
}

return $order;
});

return redirect()->route('orders.show', $order);
}

表单重定向

带输入重定向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string',
'email' => 'required|email',
]);

// 处理逻辑

return redirect()->route('users.index')
->with('success', '用户创建成功');
}

public function update(Request $request, User $user)
{
if ($validationFails) {
return back()
->withInput()
->withErrors(['field' => '错误信息']);
}

return redirect()->route('users.show', $user)
->with('success', '更新成功');
}

表单测试

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
<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Models\User;

class UserFormTest extends TestCase
{
public function test_create_user_with_valid_data()
{
$response = $this->post(route('users.store'), [
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
]);

$response->assertRedirect(route('users.index'));
$response->assertSessionHas('success');

$this->assertDatabaseHas('users', [
'email' => 'john@example.com',
]);
}

public function test_create_user_with_invalid_data()
{
$response = $this->post(route('users.store'), [
'name' => '',
'email' => 'invalid-email',
'password' => 'short',
]);

$response->assertSessionHasErrors(['name', 'email', 'password']);
}
}

总结

Laravel 13 的表单处理提供了:

  • 强大的表单请求验证
  • 灵活的自定义验证规则
  • Blade 表单组件
  • 文件上传处理
  • CSRF 和 XSS 防护
  • 动态表单支持
  • 完善的测试支持

掌握表单处理技巧是构建安全可靠 Web 应用的基础。