skip to content

In Laravel, how does URL::defaults() let route() fill a {locale} segment automatically, and where should you call it?

level: middleimportance: nice to knowfreq 20%

answer

  1. request-wide default for URL generation
  2. set in a route middleware
  3. used only when the parameter is missing
  4. does not affect incoming route matching
  5. prioritise before SubstituteBindings

basics

~20 s

URL::defaults(['locale' => 'fr']) registers request-wide default values that the URL generator uses whenever route() or action() is called without that parameter. Call it in a route middleware that knows the user or request, ordered before SubstituteBindings.

solid answer

~40 s

`URL::defaults()` stores parameter values on the URL generator for the rest of the request. When `route('posts.index')` targets `/{locale}/posts` and no `locale` is passed, the generator fills the segment from those defaults; an explicitly passed value still wins. It affects **generation only** — incoming requests must still carry the segment. Because the value usually depends on the request (the user's language, a tenant slug), set it in a **route middleware**, not in a provider's `boot()`, where no user is resolved yet. The docs also advise giving that middleware priority over `SubstituteBindings` with `$middleware->prependToPriorityList()` in `bootstrap/app.php`, because URL defaults can interfere with implicit model binding.

go deeper

for a junior

Recall that URL::defaults saves you from passing the same route parameter, like a locale, to every route() call.

for a middle

Explain that the defaults fill only missing parameters, apply to generation and not to matching, and belong in a route middleware.

for a senior

Order the defaults middleware before SubstituteBindings with the priority list, and guard against links generated before the middleware ran.

for a principal

Choose between locale-prefixed URLs with generator defaults and locale by domain or cookie, weighing SEO, caching and link-generation complexity.

## The problem A localised app prefixes most routes with a language segment: ```php Route::get('/{locale}/posts', [PostController::class, 'index'])->name('posts.index'); Route::get('/{locale}/posts/{post}', [PostController::class, 'show'])->name('posts.show'); ``` Without help, every link in every view must pass the locale: `route('posts.show', ['locale' => app()->getLocale(), 'post' => $post])`. Forget it once and `route()` throws, because a required parameter is missing. **`URL::defaults()`** removes that repetition. ## What `URL::defaults()` does `URL::defaults(array $defaults)` merges the given key-value pairs into the **URL generator's default parameters**. From then on, while the generator builds a route URL: 1. It looks for each route parameter in the values you passed explicitly. 2. If a parameter is missing (or empty), it uses the default registered under that name. 3. If neither exists and the parameter is required, generation fails as usual. So after `URL::defaults(['locale' => 'fr'])`, `route('posts.show', ['post' => $post])` returns a URL ending in `/fr/posts/15`, and `route('posts.show', ['locale' => 'de', 'post' => $post])` still returns one ending in `/de/posts/15`. The same defaults apply to `action()`, which resolves to a route too. ## What it does not do | Area | Affected by `URL::defaults`? | |---|---| | `route()` and `action()` output | Yes | | Signed links built on a named route | Yes, they go through the same generator | | `url('/some/path')` with a literal path | No — there is no parameter to fill | | Matching an incoming request | No — `/posts` still does not match `/{locale}/posts` | | Other requests under PHP-FPM | No — each request boots a fresh generator | The last row matters: defaults are runtime state on the generator, not configuration. Whatever you set applies to URLs generated later in the same process — under PHP-FPM that means the rest of the request, while a long-running process such as a queue worker keeps them until something overwrites them. ## Where to call it: a route middleware The value usually depends on the request, so the docs' pattern is a middleware: ```php class SetDefaultLocaleForUrls { public function handle(Request $request, Closure $next): Response { URL::defaults(['locale' => $request->user()?->locale ?? 'en']); return $next($request); } } ``` - A **service provider's `boot()`** runs before routing and before authentication, so the user and the route are not available there yet. - A **controller** would be too late for links built by middleware or by shared view data. - A middleware attached to the localised route group runs for exactly the requests that need it. ## Ordering against `SubstituteBindings` The URL-generation docs warn that setting URL defaults "can interfere with Laravel's handling of implicit model bindings" and recommend running the defaults middleware **before** `SubstituteBindings`. Route middleware is sorted by a priority list, so an alphabetical or group order is not enough; adjust the list in `bootstrap/app.php`: ```php ->withMiddleware(function (Middleware $middleware): void { $middleware->prependToPriorityList( before: \Illuminate\Routing\Middleware\SubstituteBindings::class, prepend: \App\Http\Middleware\SetDefaultLocaleForUrls::class, ); }) ``` ## Outside the request: queued mail and jobs URL defaults are usually set by route middleware, and route middleware only runs for HTTP requests. A **queue worker** building a localised email — say a weekly digest with links to `posts.show` — never passes through `SetDefaultLocaleForUrls`, so the generator has no `locale` default and `route()` throws a missing-parameter `UrlGenerationException`. Two fixes are common: 1. Pass the locale explicitly when generating links in jobs, mailables and notifications (`route('posts.show', ['locale' => $user->locale, 'post' => $post])`). 2. Call `URL::defaults(['locale' => $user->locale])` at the start of the job's `handle()` method, before any link is built. A long-running worker keeps the same URL generator across jobs, so a default set by one job can still be present for the next; set it in every job that relies on it rather than assuming a clean slate. The same applies to signed links: `URL::signedRoute()` builds on the named route, so it uses whatever defaults are present at that moment and fails the same way without them. ## Details worth knowing - A default can target a **custom binding key**: for a route written `{post:slug}`, the generator also looks up a default stored under the key `post:slug`. - A route-level default, set with `->defaults('locale', 'en')` on the route definition, is a different feature: it supplies a value for the matched route's parameters, not for link generation. - Defaults set once stay for the rest of the request, so a middleware that switches locale halfway through must call `URL::defaults()` again. - Test it by generating a URL inside a request that passed through the middleware; a unit test that calls `route()` without it will see no default.

  • Why is AppServiceProvider::boot() the wrong place for URL::defaults(['locale' => ...])?
    Providers boot before the request is routed and before authentication runs, so the user's locale and the route are not known yet. A value computed there would be the same for every request. A route middleware runs per request, after the user can be resolved, which is why the docs place the call there.
  • Does URL::defaults(['locale' => 'fr']) let a request to /posts match a route defined as /{locale}/posts?
    No. URL defaults only feed the URL generator when `route()` or `action()` builds a link. Matching an incoming request still needs the segment in the path; a request to `/posts` does not match `/{locale}/posts`.

saying these in an interview costs you the question

  • URL::defaults overrides values passed explicitly to route()
  • URL defaults make incoming requests match without the segment
  • Call URL::defaults once in a service provider boot method
  • URL::defaults also rewrites paths passed to url()
  • Route order in the group decides middleware priority