skip to content

In Laravel 13, how do the #[Middleware] and #[WithoutMiddleware] controller attributes work at class and method level?

level: middleimportance: nice to knowfreq 22%

answer

  1. Illuminate\Routing\Attributes\Controllers
  2. repeatable, class or method
  3. method attributes merge with class ones
  4. parent class attributes inherited
  5. WithoutMiddleware skips route, not global, middleware

basics

~20 s

#[Middleware('auth')] on a controller class or method attaches route middleware, with optional only and except; method attributes merge with class ones and parents' class attributes are inherited. #[WithoutMiddleware] removes named route middleware, never global middleware.

solid answer

~40 s

Laravel 13 lets you declare controller middleware as PHP attributes from `Illuminate\Routing\Attributes\Controllers`. `#[Middleware('auth')]` is repeatable and allowed on the class or on a method; class-level entries accept `only:` and `except:` method lists, and method-level entries are **merged** with the class's, not substituted for them. Class attributes on parent controllers are collected too (inheritance support landed in 13.5.0), parents first. `#[WithoutMiddleware(EnsureTokenIsValid::class)]`, added in 13.20.0, takes a single middleware name and removes it from the route's gathered middleware, which covers route-file, group, `HasMiddleware` and attribute middleware, but not the global stack. Attributes work with or without `HasMiddleware`; when both exist the attribute list is appended to the static list.

code

php · 20 lines
php
<?php

namespace App\Http\Controllers;

use App\Http\Middleware\EnsureFleetManager;
use Illuminate\Routing\Attributes\Controllers\Middleware;
use Illuminate\Routing\Attributes\Controllers\WithoutMiddleware;

#[Middleware('auth')]
#[Middleware(EnsureFleetManager::class, except: ['index', 'show'])]
class FleetController
{
    public function index() { /* auth */ }

    #[Middleware('throttle:5,1')]
    public function store() { /* auth, fleet manager, throttle */ }

    #[WithoutMiddleware(EnsureFleetManager::class)]
    public function update() { /* auth only */ }
}

go deeper

for a junior

Recognise #[Middleware('auth')] above a controller or method as a way to protect actions, and know it comes from Illuminate\Routing\Attributes\Controllers.

for a middle

Explain merging of class and method attributes, only/except filters, parent inheritance, and what #[WithoutMiddleware] can and cannot remove.

for a senior

Review attribute-based exemptions for security impact, know the 13.x minor versions that introduced each behaviour, and the PHP 8.5 requirement for closures.

for a principal

Choose one declaration style for the codebase and back it with a route-listing test so no endpoint loses protection silently.

## Attributes as a third way to declare controller middleware A PHP **attribute** is structured metadata written as `#[Name(...)]` above a class or method, readable through reflection. Laravel 13 reads two of them on controllers, both in the `Illuminate\Routing\Attributes\Controllers` namespace: | Attribute | Arguments | Targets | Added | |---|---|---|---| | `#[Middleware]` | `Closure\|string $middleware`, `?array $only`, `?array $except` | class, method, repeatable | Laravel 13.0 | | `#[WithoutMiddleware]` | `string $middleware`, `?array $only`, `?array $except` | class, method, repeatable | Laravel 13.20.0 | (The related `#[Authorize]` attribute extends `Middleware` and is covered with policies.) ## How `#[Middleware]` is collected When a route's action is a controller method, the router builds the list like this: 1. Walk from the controller class **up through its parents**, collecting class-level `#[Middleware]` attributes. Ancestors' entries end up first, then the controller's own, each in declaration order. (Inheritance support arrived in 13.5.0.) 2. Append the **method's** own `#[Middleware]` attributes. 3. Drop any entry whose `only`/`except` excludes the dispatched method name. 4. Append the result after the `HasMiddleware::middleware()` list if the class implements that interface (or after the legacy `getMiddleware()` list), and after the route's own middleware, removing duplicates. So a method attribute **adds to** the class's middleware; there is no "override" semantics. The controller does not need to extend anything or implement an interface for attributes to be read. ```php #[Middleware('auth')] #[Middleware('log', only: ['index'])] class RentalController { #[Middleware('verified')] public function store() { /* auth + verified */ } public function index() { /* auth + log */ } } ``` ## How `#[WithoutMiddleware]` works `#[WithoutMiddleware]` names one middleware to **remove** for the matching actions. The router collects these attributes the same way (parents, class, method, `only`/`except`) and adds them to the route's **excluded** list, next to anything the route file excluded with `withoutMiddleware()`. Then, when it resolves names to classes, it rejects: - any gathered middleware whose resolved class name is in the excluded list; - any whose class is a **subclass** of an excluded class. Limits to remember: - It acts on **route middleware**: middleware from the route, its groups (such as `web`), `HasMiddleware` and `#[Middleware]`. **Global middleware**, which runs for every request before routing, is outside its reach. - It takes a single **string**; closure middleware cannot be named, so it cannot be removed this way. - Class-level `#[WithoutMiddleware]` on a base controller is inherited by child controllers. ## Closures inside the attribute The docs show an inline middleware inside the attribute, `#[Middleware(static function (Request $request, Closure $next) { ... })]`. Attribute arguments are **constant expressions**, and PHP accepts closures there only from **PHP 8.5**, and only static ones. Laravel 13 itself requires PHP 8.3, so on 8.3 or 8.4 an inline closure middleware belongs in `HasMiddleware::middleware()` instead, where any closure is allowed. ## Attributes or `HasMiddleware`? - **Attributes** keep the middleware next to the method they protect, which is easy to read in review. - **`HasMiddleware`** keeps everything in one method and supports closures on every supported PHP version. - Both can coexist; the attribute list is merged after the static one. A team usually standardises on one style to avoid a reader missing half the protection. The model to hold onto: attributes are just another source feeding the same gather-then-exclude process the router already runs for every controller route.

  • Can #[WithoutMiddleware] remove a closure middleware declared in HasMiddleware?
    No. `#[WithoutMiddleware]` takes a single string, and when the router filters the gathered list it never rejects a closure entry. Only named middleware, matched by resolved class name or as a subclass of an excluded class, can be removed; a closure has to be taken out of the list that declares it.
  • What happens with a closure inside #[Middleware] on PHP 8.4?
    The file fails to compile, because attribute arguments are constant expressions and PHP permits (static) closures there only from 8.5. Laravel 13 supports PHP 8.3 and 8.4, so on those versions put inline closure middleware in HasMiddleware::middleware() instead.

saying these in an interview costs you the question

  • A method-level #[Middleware] replaces the class-level middleware for that method.
  • #[WithoutMiddleware] can switch off global middleware for one action.
  • Middleware attributes only work on controllers that implement HasMiddleware.
  • Class-level attributes on a parent controller are ignored by child controllers in Laravel 13.
  • Any PHP 8.3 app can pass a closure straight into #[Middleware].