skip to content

In a Laravel 13 controller, how does implementing HasMiddleware assign middleware, and how do Middleware only and except narrow it?

level: middleimportance: must knowfreq 50%

answer

  1. interface, not a base class
  2. public static function middleware(): array
  3. new Middleware('log', only: ['index'])
  4. read without building the controller
  5. replaces $this->middleware() in the constructor

basics

~10 s

A 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 s

In 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
<?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

for a junior

Know that a controller can list its own middleware by implementing HasMiddleware and returning them from a static middleware() method.

for a middle

Explain only/except against method names, closure middleware, and why the static method lets the router skip building the controller until the pipeline ends.

for a senior

Migrate constructor-based middleware from older apps, spot the fatal error of mixing styles, and decide what belongs on routes versus on the controller.

for a principal

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.