概觀
Laravel 13 中,可透過 PHP Attributes 宣告 Controller 的中介層指派與授權檢查。取代原本的middleware() 方法及 can 中介層,只需在類別或方法上直接加入 attribute 即可完成設定。
use Illuminate\Routing\Attributes\Controllers\Authorize;
use Illuminate\Routing\Attributes\Controllers\Middleware;
#[Middleware('auth')]
class PostController
{
#[Middleware('subscribed')]
#[Authorize('create', Post::class)]
public function store(Request $request): Response
{
// ...
}
}
Controller 的 attributes 皆位於
Illuminate\Routing\Attributes\Controllers 命名空間下。#[Middleware] — 指派中介層
套用於類別
在類別層級加上#[Middleware],該中介層將套用至該 Controller 的所有 action。
use Illuminate\Routing\Attributes\Controllers\Middleware;
#[Middleware('auth')]
class UserController
{
public function index(): View { /* ... */ }
public function show(User $user): View { /* ... */ }
public function store(Request $request): Response { /* ... */ }
}
#[Middleware('auth')]
#[Middleware('verified')]
class ProfileController
{
// ...
}
套用於方法
方法層級標註的中介層會與類別層級的中介層合併。#[Middleware('auth')]
class UserController
{
// 全部僅套用 auth
public function index(): View { /* ... */ }
// auth + subscribed 皆套用
#[Middleware('subscribed')]
public function create(): View { /* ... */ }
}
以 only / except 限定
在類別層級的 attribute 中指定 only 或 except,可限定套用對象的方法。
#[Middleware('auth')]
#[Middleware('subscribed', only: ['create', 'store', 'edit', 'update'])]
class ArticleController
{
// 僅 auth
public function index(): View { /* ... */ }
public function show(Article $article): View { /* ... */ }
// auth + subscribed
public function create(): View { /* ... */ }
public function store(Request $request): Response { /* ... */ }
public function edit(Article $article): View { /* ... */ }
public function update(Request $request, Article $article): Response { /* ... */ }
// 僅 auth(等同於 except 排除的效果)
public function destroy(Article $article): Response { /* ... */ }
}
Closure 中介層
Attribute 也可接受 closure。適合想以行內方式撰寫處理的場合。use Closure;
use Illuminate\Http\Request;
use Illuminate\Routing\Attributes\Controllers\Middleware;
class ReportController
{
#[Middleware(static function (Request $request, Closure $next) {
if (! $request->user()->hasRole('analyst')) {
abort(403);
}
return $next($request);
})]
public function generate(): Response
{
// ...
}
}
與傳統 middleware() 方法的比較
// 傳統寫法(實作 HasMiddleware 介面)
use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Controllers\Middleware;
class UserController implements HasMiddleware
{
public static function middleware(): array
{
return [
'auth',
new Middleware('log', only: ['index']),
new Middleware('subscribed', except: ['store']),
];
}
}
// 使用 attribute 的寫法
#[Middleware('auth')]
#[Middleware('log', only: ['index'])]
#[Middleware('subscribed', except: ['store'])]
class UserController
{
// 不需實作 HasMiddleware
}
使用 attribute 時無需實作
HasMiddleware 介面。但將 middleware() 方法與 attribute 混用可能導致預期外的行為,建議統一擇一使用。#[WithoutMiddleware] — 排除中介層
將於類別層級套用的中介層,從特定方法或整個類別排除。
套用於方法
use App\Http\Middleware\EnsureTokenIsValid;
use Illuminate\Routing\Attributes\Controllers\Middleware;
use Illuminate\Routing\Attributes\Controllers\WithoutMiddleware;
#[Middleware('auth')]
#[Middleware(EnsureTokenIsValid::class)]
class ApiController
{
// 套用 auth 與 EnsureTokenIsValid
public function show(Resource $resource): JsonResponse { /* ... */ }
// 套用 auth,但排除 EnsureTokenIsValid
#[WithoutMiddleware(EnsureTokenIsValid::class)]
public function index(): JsonResponse { /* ... */ }
}
套用於類別及 only / except
若於類別層級加上 #[WithoutMiddleware],則包含子類別在內的所有 action 都會排除該中介層。可以 only / except 限定排除範圍。
#[Middleware('auth')]
#[WithoutMiddleware('subscribed', except: ['index'])]
class AdminController
{
// 套用 subscribed(因 except: ['index'] 而不被排除)
public function index(): View { /* ... */ }
// 排除 subscribed
public function dashboard(): View { /* ... */ }
public function settings(): View { /* ... */ }
}
#[WithoutMiddleware] 僅對路由中介層有效,無法排除註冊於 app/Http/Kernel.php 的全域中介層。#[Authorize] — 透過 Policy 進行授權
作為 can 中介層的語法糖,可以 attribute 宣告透過 Policy 的授權檢查。
基本用法
use App\Models\Post;
use Illuminate\Routing\Attributes\Controllers\Authorize;
class PostController
{
// 檢查 'viewAny' 能力(Post Policy 的 viewAny 方法)
#[Authorize('viewAny', Post::class)]
public function index(): View { /* ... */ }
// 檢查 'view' 能力(傳入路由參數 'post')
#[Authorize('view', 'post')]
public function show(Post $post): View { /* ... */ }
// 檢查 'create' 能力
#[Authorize('create', Post::class)]
public function create(): View { /* ... */ }
#[Authorize('create', Post::class)]
public function store(Request $request): Response { /* ... */ }
// 檢查 'update' 能力(傳入路由參數)
#[Authorize('update', 'post')]
public function edit(Post $post): View { /* ... */ }
#[Authorize('update', 'post')]
public function update(Request $request, Post $post): Response { /* ... */ }
#[Authorize('delete', 'post')]
public function destroy(Post $post): Response { /* ... */ }
}
Policy 的引數
第 2 個引數可傳入以下形式。| 傳入值 | 說明 |
|---|---|
Post::class | 模型類別(適用於 viewAny 等不需要模型實例的情況) |
'post' | 路由參數名稱(會傳入被模型繫結後的實例) |
[Comment::class, 'post'] | 多個引數(以陣列形式傳入 Policy 方法) |
use App\Models\Comment;
use App\Models\Post;
use Illuminate\Routing\Attributes\Controllers\Authorize;
class CommentController
{
// 將 $post(路由參數)與 Comment class 傳入 Policy
#[Authorize('create', [Comment::class, 'post'])]
public function store(Post $post, Request $request): Response
{
// ...
}
#[Authorize('delete', 'comment')]
public function destroy(Comment $comment): Response
{
// ...
}
}
與 can 中介層的比較
// 傳統寫法(Route Facade)
Route::get('/posts', [PostController::class, 'index'])->middleware('can:viewAny,App\Models\Post');
Route::put('/posts/{post}', [PostController::class, 'update'])->middleware('can:update,post');
// 使用 attribute 的寫法
class PostController
{
#[Authorize('viewAny', Post::class)]
public function index(): View { /* ... */ }
#[Authorize('update', 'post')]
public function update(Request $request, Post $post): Response { /* ... */ }
}
實務範例
套用於 Resource Controller
use App\Models\Article;
use Illuminate\Routing\Attributes\Controllers\Authorize;
use Illuminate\Routing\Attributes\Controllers\Middleware;
#[Middleware('auth')]
class ArticleController
{
#[Authorize('viewAny', Article::class)]
public function index(): View
{
return view('articles.index', [
'articles' => Article::paginate(),
]);
}
#[Authorize('view', 'article')]
public function show(Article $article): View
{
return view('articles.show', compact('article'));
}
#[Authorize('create', Article::class)]
public function create(): View
{
return view('articles.create');
}
#[Authorize('create', Article::class)]
public function store(StoreArticleRequest $request): RedirectResponse
{
$article = Article::create($request->validated());
return redirect()->route('articles.show', $article);
}
#[Authorize('update', 'article')]
public function edit(Article $article): View
{
return view('articles.edit', compact('article'));
}
#[Authorize('update', 'article')]
public function update(UpdateArticleRequest $request, Article $article): RedirectResponse
{
$article->update($request->validated());
return redirect()->route('articles.show', $article);
}
#[Authorize('delete', 'article')]
public function destroy(Article $article): RedirectResponse
{
$article->delete();
return redirect()->route('articles.index');
}
}
套用於 API Controller
use Illuminate\Routing\Attributes\Controllers\Middleware;
use Illuminate\Routing\Attributes\Controllers\WithoutMiddleware;
#[Middleware('auth:sanctum')]
#[Middleware('throttle:api')]
class ApiPostController
{
// 列表取得不需驗證
#[WithoutMiddleware('auth:sanctum')]
public function index(): JsonResponse
{
return response()->json(Post::paginate());
}
public function store(Request $request): JsonResponse
{
// 僅限已驗證使用者
}
}
Attribute 的處理順序
當有多個 attribute 存在時,處理順序如下。#[Authorize] 內部作為 can 中介層運作,因此於相同的 Pipeline 中處理。總結:該使用哪一種?
| 情境 | 建議做法 |
|---|---|
| 新的 Controller(Laravel 13) | 使用 attribute |
既有具備 middleware() 方法的 Controller | 逐步遷移至 attribute |
| 需要動態的中介層設定 | 繼續使用 middleware() 方法 |
| 想在路由檔案集中管理 | 繼續使用 Route::middleware() |
下一步
PHP Attributes(Queue、Eloquent)
解說可用於 Queue Job 及 Eloquent 模型的 PHP Attributes。
中階:Controller
學習 Laravel Controller 的基本用法。