Laravel 表单验证与中间件
Laravel 提供了强大的表单验证系统和中间件机制。Form Request 将验证逻辑从控制器中分离,让代码更加清晰;中间件则提供了请求过滤的优雅方式。本文详细介绍两者的使用方法。
Form Request 验证
// 创建 Form Request
php artisan make:request StoreArticleRequest
php artisan make:request UpdateArticleRequest
// app/Http/Requests/StoreArticleRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreArticleRequest extends FormRequest
{
public function authorize(): bool
{
// 返回 false 会抛出 403,可在此做权限判断
return auth()->check();
}
public function rules(): array
{
return [
"title" => [
"required",
"string",
"max:200",
"unique:articles,title", // articles 表的 title 字段唯一
],
"slug" => [
"nullable",
"string",
"max:200",
"unique:articles,slug," . $this->article?->id, // 更新时排除自身
],
"content" => ["required", "string", "min:10"],
"category_id" => ["required", "integer", "exists:categories,id"],
"tags" => ["array"],
"tags.*" => ["integer", "exists:tags,id"],
"cover_image" => ["nullable", "image", "max:2048"], // 最大 2MB
"is_published" => ["sometimes", "boolean"],
"published_at" => ["nullable", "date", "after:now"],
];
}
public function messages(): array
{
return [
"title.required" => "请填写文章标题",
"title.unique" => "该标题已存在",
"content.min" => "文章内容至少 10 个字符",
"category_id.exists" => "选择的分类不存在",
];
}
public function attributes(): array
{
return [
"category_id" => "分类",
"cover_image" => "封面图片",
];
}
protected function prepareForValidation(): void
{
// 验证前预处理
if ($this->has("slug") && empty($this->slug)) {
$this->merge(["slug" => \Illuminate\Support\Str::slug($this->title)]);
}
}
protected function failedValidation(\Illuminate\Contracts\Validation\Validator $validator)
{
// 自定义验证失败响应格式
throw new \Illuminate\Validation\ValidationException($validator, response()->json([
"success" => false,
"message" => "验证失败",
"errors" => $validator->errors(),
], 422));
}
}
控制器中使用 Form Request
// app/Http/Controllers/ArticleController.php
namespace App\Http\Controllers;
use App\Http\Requests\StoreArticleRequest;
use App\Http\Requests\UpdateArticleRequest;
use App\Models\Article;
use Illuminate\Http\Request;
class ArticleController extends Controller
{
public function store(StoreArticleRequest $request)
{
// $request->validated() 获取已验证的数据
$data = $request->validated();
// 处理多对多关系
$tags = $data["tags"] ?? [];
unset($data["tags"]);
$article = Article::create($data);
if (!empty($tags)) {
$article->tags()->attach($tags);
}
return redirect()
->route("articles.show", $article->slug)
->with("success", "文章创建成功");
}
public function update(UpdateArticleRequest $request, Article $article)
{
$data = $request->validated();
$tags = $data["tags"] ?? null;
unset($data["tags"]);
$article->update($data);
if ($tags !== null) {
$article->tags()->sync($tags);
}
return redirect()
->route("articles.show", $article->slug)
->with("success", "文章更新成功");
}
// 手动验证(简单场景,不需要单独文件)
public function contact(Request $request)
{
$validated = $request->validate([
"name" => "required|string|max:100",
"email" => "required|email",
"message" => "required|string|min:10",
], [
"name.required" => "请填写姓名",
]);
// 验证通过后继续...
}
}
常用验证规则
[
// 存在性验证
"field" => "required", // 不能为空
"field" => "nullable", // 可以为空
"field" => "sometimes", // 仅在字段存在时才验证
// 类型验证
"field" => "string",
"field" => "integer",
"field" => "boolean",
"field" => "array",
"field" => "email",
"field" => "url",
"field" => "ip",
"field" => "date",
"field" => "file",
"field" => "image",
// 数值验证
"field" => "numeric|min:0|max:100",
"field" => "integer|min:1",
"field" => "between:1,100",
// 字符串验证
"field" => "string|max:255",
"field" => "regex:/^[a-zA-Z]+$/",
"field" => "starts_with:foo,bar",
"field" => "ends_with:foo,bar",
"field" => "in:foo,bar,baz",
// 确认字段
"field" => "password|confirmed", // 需要 field_confirmation 字段
// 文件验证
"file" => "file|mimes:pdf,doc,docx|max:10240", // 最大 10MB
"file" => "image|mimes:jpg,png,gif|dimensions:min_width=100,min_height=100",
// 数组验证
"items" => "array",
"items.*" => "string|max:100", // 数组每个元素验证
// 条件验证
"email" => "required_if:type,personal", // type=personal 时必填
"field" => "required_unless:type,guest",
"field" => "required_with:field1,field2",
"field" => "required_without:field1,field2",
"field" => "exclude_if:type,demo|boolean", // type=demo 时排除验证
// 唯一性
"slug" => "unique:articles,slug," . $article->id,
"slug" => Rule::unique("articles")->ignore($article),
// 自定义规则
"field" => ["required", new AlphaSpaceRule()],
]
中间件
// 创建中间件
php artisan make:middleware CheckAge
php artisan make:middleware AdminMiddleware
// app/Http/Middleware/CheckAge.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class CheckAge
{
public function handle(Request $request, Closure $next, int $minAge = 18)
{
if ($request->user() && $request->user()->age < $minAge) {
if ($request->expectsJson()) {
return response()->json(["error" => "年龄不符合要求"], 403);
}
return redirect()->route("home")->with("error", "年龄不符合要求");
}
return $next($request); // 继续处理
}
}
// app/Http/Middleware/AdminMiddleware.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class AdminMiddleware
{
public function handle(Request $request, Closure $next)
{
if (!$request->user() || !$request->user()->isAdmin()) {
abort(403, "无权访问");
}
return $next($request);
}
}
// app/Http/Middleware/CountRequests.php(记录请求次数)
class CountRequests
{
public function handle(Request $request, Closure $next)
{
$key = "requests:" . $request->ip();
$count = cache()->increment($key);
cache()->put($key, $count, now()->addMinutes(1));
if ($count > 100) {
return response()->json(["error" => "请求过于频繁"], 429);
}
return $next($request);
}
}
注册中间件
// app/Http/Kernel.php
namespace App\Http;
class Kernel extends HttpKernel
{
// 全局中间件(每个请求都会执行)
protected $middleware = [
// ...
];
// 路由中间件(需要时在路由中指定)
protected $middlewareAliases = [
"auth" => \App\Http\Middleware\Authenticate::class,
"auth.basic" => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
"admin" => \App\Http\Middleware\AdminMiddleware::class,
"throttle" => \Illuminate\Routing\Middleware\ThrottleRequests::class,
"verified" => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
"age" => \App\Http\Middleware\CheckAge::class, // 带参数
];
// 中间件组
protected $middlewareGroups = [
"web" => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class, // CSRF 保护
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
"api" => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.":api",
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
}
// 路由中使用
Route::middleware(["auth", "admin"])->group(function () {
Route::get("/admin/dashboard", fn () => view("admin.dashboard"));
});
Route::middleware(["age:21"])->group(function () {
// 21 岁以上才能访问
});
常见问题
- Q: Form Request 和手动 $request->validate() 哪个更好?
A:Form Request 将验证逻辑从控制器分离,适合复杂验证规则和复用;手动验证适合简单的一次性验证。两者验证能力相同。 - Q: 路由中间件和全局中间件的区别?
A:全局中间件在每个请求中都执行;路由中间件只在特定路由或路由组上执行。登录验证(auth)通常是路由中间件,不登录的用户也可以访问公开页面。 - Q: 中间件如何传递额外参数?
A:在 Kernel 的 middlewareAliases 中定义时加参数(如 "age" => CheckAge::class.":21"),中间件 handle 方法的 $next 之后接收(如 handle(Request $request, Closure $next, int $minAge = 18))。 - Q: 为什么 POST 表单提交报 419 错误?
A:419 是 CSRF 验证失败。在表单中加 @csrf 指令生成隐藏 Token,或在中间件中排除该路由:Route::post("webhook", ...)->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class])。 - Laravel 中间件官方文档
- Laravel 认证系统:Sanctum 与 Passport 详解