Introduction
Laravel 13 formalised support for PHP attributes on controllers. Instead of implementing HasMiddleware or registering middleware in routes, you can decorate a controller class or method with #[Middleware(...)] and #[Authorize(...)]. The result is a declarative, readable, type-safe way to express policy next to the code it protects.
Key Concepts
- PHP attributes: Native PHP 8 metadata syntax (
#[Attribute]) Laravel now respects for controller middleware. #[Middleware]: Applies a middleware to the class or a single method.#[Authorize]: Runs a policy ability before the method executes.- Still compatible with
HasMiddleware: Attributes and the interface both work; pick one style per project.
Real World Context
In a controller with 10 methods where only 3 need admin access, attributes let you mark those three methods individually without repeating yourself in routes or juggling only/except arrays. The policy intent lives next to the action it protects.
Deep Dive
Class-Level Middleware
phpuse Illuminate\Routing\Attributes\Controllers\Middleware; #[Middleware('auth')] class PostController extends Controller { // Every action requires authentication public function index() { /* ... */ } public function show(Post $post) { /* ... */ } }
The attribute behaves exactly like ->middleware('auth') on every route that maps to this controller, but the intent is visible on the class itself.
Method-Level Middleware
php#[Middleware('auth')] class CommentController { public function index(Post $post) { /* public */ } public function show(Comment $comment) { /* public */ } #[Middleware('subscribed')] public function store(Post $post) { // Only subscribed users can comment } #[Middleware('can:delete,comment')] public function destroy(Comment $comment) { /* ... */ } }
Class-level attributes apply to every action; method-level attributes add on top. Laravel merges them in order.
Authorization Attributes
phpuse Illuminate\Routing\Attributes\Controllers\Authorize; class CommentController { #[Authorize('create', [Comment::class, 'post'])] public function store(Post $post) { // CommentPolicy::create($user, $post) must return true } #[Authorize('delete', 'comment')] public function destroy(Comment $comment) { // CommentPolicy::delete($user, $comment) must return true } }
#[Authorize(ability, model)] runs the policy ability before the method body. A failure throws AuthorizationException just like $this->authorize() would.
Choosing Between Styles
| Style | When to use |
|---|---|
| Attributes | You want policy colocated with actions — ideal for new projects |
HasMiddleware interface | You want every middleware declaration in one list at the top of the class |
Route-level ->middleware() | Middleware varies per route even for the same controller method |
All three work in Laravel 13. Pick one style per controller and stick with it.
Common Pitfalls
- Mixing all three styles in one controller — Middleware execution order becomes hard to reason about. Be consistent.
- Forgetting the
usestatement —#[Middleware]without the import silently does nothing because PHP treats it as a generic attribute.
Best Practices
- Colocate policy with the action — A reader should see the ability check next to the method, not in a separate routes file.
- Prefer
#[Authorize]over$this->authorize()— It runs before the method body, so you cannot forget to call it.
Summary
- Laravel 13's
#[Middleware]and#[Authorize]attributes let you declare policy on controllers and methods. - Class-level attributes cover every action; method-level attributes stack on top.
- Pick one style (attributes,
HasMiddleware, or route-level) per controller. - Attribute-based checks run before the method body, so they cannot be skipped.
Code Examples
use App\Models\Comment;
use App\Models\Post;
use Illuminate\Routing\Attributes\Controllers\Authorize;
use Illuminate\Routing\Attributes\Controllers\Middleware;
#[Middleware('auth')]
class CommentController extends Controller
{
#[Authorize('create', [Comment::class, 'post'])]
public function store(Post $post)
{
$post->comments()->create(request()->validate([
'body' => 'required|string|max:2000',
]));
return back()->with('success', 'Comment posted');
}
}