In a Laravel 13 controller, how does implementing HasMiddleware assign middleware, and how do Middleware only and except narrow it?
answer
- interface, not a base class
- public static function middleware(): array
- new Middleware('log', only: ['index'])
- read without building the controller
- replaces $this->middleware() in the constructor
basics
~10 sA controller implementing HasMiddleware defines a static middleware() method returning middleware names, closures or Middleware objects; new Middleware('log', only: ['index']) limits one to listed methods and except excludes methods. It replaces constructor $this->middleware() calls.
solid answer
~40 sIn Laravel 13 a controller declares its own middleware by implementing `Illuminate\Routing\Controllers\HasMiddleware` and adding `public static function middleware(): array`. Each entry is a middleware name or alias (`'auth'`, `'throttle:10,1'`), a closure `function (Request $request, Closure $next)`, or an `Illuminate\Routing\Controllers\Middleware` object; `new Middleware('log', only: ['index'])` applies only to the listed action methods, `except: ['store']` skips the listed ones, and the fluent `->only()` / `->except()` do the same. The method is **static** so the router can read the list without building the controller: the controller is constructed at the end of the middleware pipeline. This replaced calling `$this->middleware()` in the constructor, which only works when a controller extends `Illuminate\Routing\Controller`; the base controller of a Laravel 11+ skeleton is empty.
code
php · 25 lines<?php
namespace App\Http\Controllers;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Controllers\Middleware;
class RentalController implements HasMiddleware
{
public static function middleware(): array
{
return [
'auth',
new Middleware('throttle:10,1', only: ['store']),
(new Middleware('verified'))->except(['index', 'show']),
function (Request $request, Closure $next) {
return $next($request);
},
];
}
// index, show, store ...
}go deeper
Know that a controller can list its own middleware by implementing HasMiddleware and returning them from a static middleware() method.
Explain only/except against method names, closure middleware, and why the static method lets the router skip building the controller until the pipeline ends.
Migrate constructor-based middleware from older apps, spot the fatal error of mixing styles, and decide what belongs on routes versus on the controller.
Set one convention for where middleware is declared (route groups, HasMiddleware or attributes) so a reviewer can see an endpoint's protection in one place.
## Where controller middleware comes from **Middleware** are layers that wrap a request before (and after) it reaches an action: authentication, throttling, logging. You can attach them in the routes file with `->middleware('auth')`, or you can let the **controller** declare what its actions need. In Laravel 13 the interface-based way to do that is `HasMiddleware`. ```php use Illuminate\Routing\Controllers\HasMiddleware; use Illuminate\Routing\Controllers\Middleware; class RentalController implements HasMiddleware { public static function middleware(): array { return [ 'auth', new Middleware('log', only: ['index']), new Middleware('verified', except: ['index', 'show']), ]; } } ``` ## What each entry can be | Entry | Meaning | |---|---| | `'auth'`, `'throttle:10,1'`, a class name | Applies to every action of the controller | | `new Middleware('log', only: ['index'])` | Applies only when the action method is in the list | | `new Middleware('verified', except: ['show'])` | Applies to every action except the listed methods | | `(new Middleware('log'))->only('index')` | Fluent form of the same filter; a single string is wrapped into an array | | `function (Request $request, Closure $next) { ... }` | An inline middleware, no class needed | The `Middleware` constructor accepts a closure, a string or an array of middleware, plus the optional `only` and `except` lists. ## How the filter decides The router's check is short and worth knowing exactly: 1. If `only` is set and the action's method name is **not** in it, the middleware is dropped. 2. If `except` is non-empty and the method name **is** in it, the middleware is dropped. 3. Otherwise it stays. Consequences: - The comparison is against the **method name**, not the route name or URI; for an invokable controller that name is `__invoke`. - `only: []` is still "set", so it excludes the middleware from every action. - Controller middleware is appended after the route's own middleware and duplicates are removed, so declaring `'auth'` both in the routes file and in the controller runs it once. ## Why the method is static The router gathers a route's middleware **before** it runs the pipeline. With `HasMiddleware` it calls `RentalController::middleware()` statically, so no controller instance exists yet; the container builds the controller only at the end of the pipeline, when the action is about to run. The constructor therefore executes **after** authentication and session middleware have done their work. The older style worked differently: a controller extending `Illuminate\Routing\Controller` called `$this->middleware('auth')->only('index')` inside its constructor. To read that list the router had to **instantiate the controller first**, so the constructor ran before any route middleware, and code in it could not rely on the authenticated user or the session. ## The legacy path in Laravel 13 - The framework class `Illuminate\Routing\Controller` still exists, with `middleware()` and `getMiddleware()`, and the router still honours it. - The skeleton's `App\Http\Controllers\Controller` is an **empty abstract class** that does not extend it, so `$this->middleware()` is an undefined method in a fresh app. - You cannot mix the two on one class: `Illuminate\Routing\Controller::middleware()` is an instance method, and PHP refuses to redeclare it as static, so implementing `HasMiddleware` on a subclass of it is a fatal error. - PHP attributes (`#[Middleware]`, `#[WithoutMiddleware]`) are a third option in Laravel 13 and merge with the `HasMiddleware` list. ## Choosing where middleware lives - Put middleware in the **routes file or a route group** when it describes the URL space (an admin prefix, an API group). - Put it on the **controller** when it describes the actions themselves and should follow the class wherever it is routed. - Keep authorization decisions on policies and their dedicated attributes or middleware, rather than hand-written closures in `middleware()`.
- Why can't a controller that extends Illuminate\Routing\Controller implement HasMiddleware?`Illuminate\Routing\Controller` defines an instance method `middleware($middleware, array $options = [])`. `HasMiddleware` requires `public static function middleware()`, and PHP refuses to redeclare an inherited non-static method as static, so the class fails to load with a fatal error. Drop the parent class or keep the legacy constructor style.
- What does new Middleware('log', only: []) do?It applies to no action at all. The router treats `only` as set whenever it is not null, and an empty list contains no method name, so every action is excluded. An empty `except` list, by contrast, is ignored.
- In what order do route-file middleware and HasMiddleware entries run?The route's own middleware (including its group) comes first, then the controller's `HasMiddleware` list, then any middleware attributes; duplicates are removed. The combined list is then sorted by the application's middleware priority, so a few framework middleware can move ahead of where they were declared.
saying these in an interview costs you the question
- HasMiddleware::middleware() is an instance method you call in the constructor.
- only and except match route names such as rentals.index.
- $this->middleware('auth') works in a fresh Laravel 13 controller out of the box.
- The controller constructor runs before any route middleware when using HasMiddleware.
- Declaring auth in both the routes file and the controller runs it twice.