On a Laravel travel-agency site, a SetLocale middleware appended to the web group runs after SubstituteBindings, so localized destination slugs fail to bind; how do you fix the order?
answer
- route middleware is sorted, globals are not
- the kernel's default priority list
- prependToPriorityList(before:, prepend:)
- priority() replaces the whole list
- route:list -v shows the sorted result
basics
~10 sAdd SetLocale to the middleware priority list ahead of SubstituteBindings with $middleware->prependToPriorityList(before: SubstituteBindings::class, prepend: SetLocale::class). The router then moves it above binding while keeping it after StartSession; avoid priority([...]), which replaces the default list.
solid answer
~40 s`web(append:)` puts `SetLocale` after `SubstituteBindings`, which resolves the `{destination}` slug before the locale is set. Route middleware is sorted by the kernel's priority list after it is gathered, but only entries on that list are moved, and `SetLocale` is not on it. Adding it with `$middleware->prependToPriorityList(before: SubstituteBindings::class, prepend: SetLocale::class)` makes the sorter lift it above binding, while `StartSession`, which sits earlier on the list, still runs first so the session locale is readable. Prepending to the web group or the global stack would run it before the session starts. Calling `$middleware->priority([...])` with a short list is the classic mistake: it replaces the whole default list, dropping the guarantees that sessions start before authentication and authentication before authorization. Confirm the order with `php artisan route:list -v`.
code
php · 15 lines<?php
use App\Http\Middleware\SetLocale;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Routing\Middleware\SubstituteBindings;
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware): void {
$middleware->web(append: [SetLocale::class]);
$middleware->prependToPriorityList(
before: SubstituteBindings::class,
prepend: SetLocale::class,
);
})go deeper
Recall that middleware order matters and that bootstrap/app.php has a priority list you can extend.
Explain that the router sorts gathered route middleware by the priority list and that only listed entries move.
Diagnose the ordering bug from symptoms, fix it with prependToPriorityList rather than priority(), and verify with route:list -v.
Weigh relying on the priority list against designing groups so order is explicit, and decide who owns the list when packages also add middleware.
## The failure A travel-agency site serves destination pages under localized slugs: `/destinations/lisbon` in English and `/destinations/lisboa` in Portuguese. The `Destination` model resolves `{destination}` by looking up the slug for the **current application locale**. A `SetLocale` middleware reads the visitor's language from the session and calls `App::setLocale()`. It was registered like this: ```php $middleware->web(append: [SetLocale::class]); ``` The `web` group already ends with `SubstituteBindings`, the middleware that resolves route parameters into models. Appending places `SetLocale` after it, so binding runs with the default locale, the Portuguese slug is not found, and the visitor gets a 404. The middleware that sets the locale runs after the one that needs it. ## How Laravel orders route middleware The router builds a route's middleware list from the route's groups, the route itself and its controller, removes exclusions and duplicates, and then **sorts** the result with `Illuminate\Routing\SortedMiddleware` against the kernel's **priority list**. Key properties of that sort: - Only middleware that appear on the priority list are moved. Entries that are not on it are never moved themselves, so their relative order is the order you assigned. - A listed middleware that appears **after** another listed middleware with a later priority is moved in front of it; the process repeats until the listed entries follow the list's order. - Matching strips parameters and also checks the class's interfaces and parent classes, which is why the default list can name `Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests` and still catch `auth`. - The **global stack is never sorted**; it runs in registration order in the kernel pipeline. The framework's default list, in `Illuminate\Foundation\Http\Kernel`, places the precognition, cookie and session middleware first, then the `AuthenticatesRequests` contract, the throttle middleware, the `AuthenticatesSessions` contract, `SubstituteBindings` and finally `Authorize`. ## The fix: extend the list, do not replace it ```php ->withMiddleware(function (Middleware $middleware): void { $middleware->web(append: [SetLocale::class]); $middleware->prependToPriorityList( before: SubstituteBindings::class, prepend: SetLocale::class, ); }) ``` - `prependToPriorityList(before:, prepend:)` inserts `SetLocale` into the list immediately ahead of `SubstituteBindings`. The sorter now lifts it above binding. - `StartSession` stays earlier on the list, so the session is still started before `SetLocale` reads from it. - `appendToPriorityList(after:, append:)` is the mirror image; `appendToPriorityList(after: StartSession::class, append: SetLocale::class)` would work equally well here. - `before:` and `after:` accept arrays. If none of the named classes is on the list, the new entry is simply added at the end, where it rarely achieves anything; a typo in the class name fails silently this way. ## The alternatives, and why they fail | Attempt | Result | |---|---| | `web(prepend: [SetLocale::class])` | Runs before `StartSession`, so the session locale cannot be read | | `$middleware->prepend(SetLocale::class)` (global) | Runs before routing and before the session; never sorted | | `$middleware->priority([SetLocale::class, SubstituteBindings::class])` | Replaces the whole default list; session, auth and authorization ordering guarantees are gone | | `prependToPriorityList(before: SubstituteBindings::class, ...)` | Correct: after the session, before binding | The `priority()` trap deserves emphasis. `priority()` stores the array you pass as the complete list. With only two entries, `Authorize` (the `can` alias) is no longer guaranteed to run after binding, and `auth` is no longer guaranteed to run after `StartSession`. Nothing errors; routes with middleware assigned in an unlucky order simply start behaving differently. Use `priority()` only when you intend to restate the whole list. ## When the priority list is the wrong tool The priority list exists for the case the documentation calls rare: middleware assigned from several places whose order you do not control. When you control every assignment, explicit order is clearer: - Put related middleware in one named group in the order they must run. - Keep a custom middleware that depends on another next to it, rather than appending it to a distant group. - Reserve the priority list for cross-cutting classes, such as a locale or tenant resolver, that must precede framework middleware like `SubstituteBindings` wherever they are assigned. ## Verifying the order `php artisan route:list -v` prints each route's middleware after exclusion and sorting, one per line. After the fix, the destination route should list `StartSession` above `SetLocale` above `SubstituteBindings`. Checking this output belongs in the review of any change that touches middleware registration, because ordering bugs rarely surface as errors, only as wrong behaviour on some routes.
- Would the priority list help if SetLocale were registered on the global stack instead?No. The priority list only sorts the middleware the router gathers for a route. Global middleware runs in registration order in the kernel pipeline, before routing and before the web group starts the session, so a global `SetLocale` could not read the session locale anyway.
- What happens if prependToPriorityList() names a before: class that is not on the priority list?The kernel looks for each `before:` class on the list; if none is found, it appends the new middleware at the end of the list. It then sorts after everything else listed, which usually changes nothing useful, and no error tells you the anchor was wrong.
- Why can the default priority list name an interface rather than the auth middleware class?`SortedMiddleware` matches an entry by its class name with parameters stripped, then by every interface it implements and every parent class. `Authenticate` implements `AuthenticatesRequests`, so listing the contract covers it and any custom authenticator that implements the same interface.
saying these in an interview costs you the question
- Calls priority() with two entries, believing it merges into the defaults
- Prepends SetLocale to the web group and expects the session to be available
- Believes the priority list also reorders the global stack
- Thinks unlisted middleware are moved to the end by the sorter
- Assumes registration order in routes always equals execution order