Controller Middleware via PHP Attributes

+15 Mana ✨

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

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

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

StyleWhen to use
AttributesYou want policy colocated with actions — ideal for new projects
HasMiddleware interfaceYou 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

  1. Mixing all three styles in one controller — Middleware execution order becomes hard to reason about. Be consistent.
  2. Forgetting the use statement — #[Middleware] without the import silently does nothing because PHP treats it as a generic attribute.

Best Practices

  1. Colocate policy with the action — A reader should see the ability check next to the method, not in a separate routes file.
  2. 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

php
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');
    }
}
✓ Completed