skip to content

In Laravel, how do you let a route parameter capture slashes, such as a nested topic path, and what limits apply?

level: middleimportance: nice to knowfreq 20%

answer

  1. default: anything but a slash
  2. where('path', '.*')
  3. only in the last segment
  4. matched against the decoded path
  5. split the string in the handler

basics

~10 s

A route parameter matches everything except a slash by default; ->where('path', '.*') lets it capture slashes, and the routing docs support that only when the parameter is the last segment of the URI.

solid answer

~40 s

Laravel's router lets every character except `/` into a parameter, so `/news/topics/{path}` cannot match `/news/topics/politics/europe/elections` by default. Chaining `->where('path', '.*')` replaces the default with a pattern that allows slashes, and the handler receives the whole remainder as one string, `politics/europe/elections`, which it can split with `explode('/', $path)`. The docs support slashes, including encoded ones, only in the last route segment, so put the catch-all at the end. Matching runs on the decoded path, which is why an encoded `%2F` needs the same constraint. A tighter pattern such as `[a-z0-9/-]+` is safer than `.*`, and a greedy catch-all registered early will swallow routes that share its prefix.

code

php · 12 lines
php
<?php

// routes/web.php
use App\Http\Controllers\TopicController;
use Illuminate\Support\Facades\Route;

// Specific route first
Route::get('/news/topics/popular', [TopicController::class, 'popular']);

// Catch-all last, as the final segment, with a tight pattern
Route::get('/news/topics/{path}', [TopicController::class, 'show'])
    ->where('path', '[a-z0-9/-]+');

go deeper

for a junior

Know that parameters stop at slashes by default and that where('.*') lets a parameter capture them.

for a middle

Explain the last-segment limit, the decoded-path behaviour for %2F, and why the handler receives one string to split.

for a senior

Register specific routes before catch-alls, prefer tight patterns over .*, and treat captured paths as untrusted input.

for a principal

Choose between variable-depth paths, fixed-depth routes and query strings as a URL design decision with long-lived public consequences.

## The default: one segment per parameter Each route parameter matches **any characters except a forward slash**. That default is what makes URIs predictable: `/news/{year}/{slug}` has exactly three segments, and each parameter maps to exactly one of them. It also means a parameter cannot hold a nested path such as a topic hierarchy. ## Allowing slashes with where('.*') A news archive may organise topics as a tree: `politics/europe/elections`. To capture the tree in one parameter, replace the default pattern with one that allows slashes: ```php use Illuminate\Support\Facades\Route; Route::get('/news/topics/{path}', function (string $path) { $parts = explode('/', $path); // ['politics', 'europe', 'elections'] // ... })->where('path', '.*'); ``` What the handler receives: - A single **string** holding everything after `/news/topics/`. - Slashes are kept, so the handler splits the string itself. - The value is **decoded**: the router matches against the decoded request path, so an encoded `%2F` arrives as `/` and needs the same constraint to match at all. The framework uses the same technique itself: `Route::fallback()` registers `{fallbackPlaceholder}` with a `.*` constraint so it can match any path. ## The limits 1. **Only the last segment.** The routing docs state that encoded forward slashes are supported only within the last route segment. Put the catch-all parameter at the end of the URI; `/files/{path}/download` with a slash-capturing `{path}` is not a supported shape. 2. **Greedy matching shadows neighbours.** `.*` matches as much as it can. If `/news/topics/{path}` with `.*` is registered **before** `/news/topics/{path}/feed`-style routes or a static `/news/topics/popular`, the catch-all may win. Register specific routes first and the catch-all last. 3. **`.*` also matches unexpected input.** It accepts dots, spaces and `..`. When the value maps to storage keys, files or database lookups, validate it or use a tighter regex. ## Choosing a pattern | Pattern | Accepts | Use when | |---|---|---| | default (no where) | one segment, no slash | normal parameters | | `.*` | anything, including slashes | quick catch-alls, the docs' example | | `.+` | anything with at least one character | the segment must not be empty | | `[a-z0-9/-]+` | lowercase slugs separated by slashes | topic trees, nested categories | The tight pattern documents the URL shape and rejects junk before the handler runs, which also means malformed paths 404 at the router. ## A news-archive example ```php use App\Http\Controllers\TopicController; Route::get('/news/topics/popular', [TopicController::class, 'popular']); Route::get('/news/topics/{path}', [TopicController::class, 'show']) ->where('path', '[a-z0-9/-]+'); ``` `/news/topics/popular` hits the static route because it is registered first; `/news/topics/politics/europe` reaches `show()` with `$path = 'politics/europe'`. ## Alternatives worth naming - **Fixed depth**: if topics are always two levels deep, `/news/topics/{section}/{topic}` is clearer and gives each level its own constraint. - **Query strings**: filters such as `?tags=politics,europe` are often a better fit than path segments when order does not matter. - **Fallback routes**: for "anything unmatched", `Route::fallback()` is safer than a hand-written catch-all, because the router checks it last. ## Handling the captured value safely The captured string comes straight from the URL, so treat it as untrusted input: 1. Split it with `explode('/', $path)` and drop empty parts, which appear when the URL has doubled slashes. 2. Reject parts such as `..` or anything outside the expected slug alphabet, unless the regex already excludes them. 3. Map each part to a known record, for example a topic looked up by slug, rather than using the raw string as a storage path or cache key. 4. Return a 404 when a part does not resolve, so broken links behave like any other missing page. ## Pitfalls interviewers probe - **Expecting a normal parameter to capture `a/b`**: without the constraint the URL simply does not match. - **Placing the slash-capturing parameter in the middle** of the URI. - **Registering the catch-all before specific routes** that share its prefix. - **Trusting `.*` input** when it is later used as a path on disk.

  • Why does /news/topics/politics%2Feurope 404 on a Laravel route /news/topics/{path} without a constraint?
    The router matches against the decoded path, so `%2F` becomes a real slash before matching. The default parameter pattern excludes slashes, so the route does not match. Adding `->where('path', '.*')` (or a tighter slash-allowing regex) on a last-segment parameter makes it match.
  • When is a fixed-depth route better than a slash-capturing catch-all?
    When the structure has a known depth, such as section and topic. `/news/topics/{section}/{topic}` gives each level its own name and constraint, generates URLs cleanly with named parameters, and cannot swallow unrelated routes. Catch-alls suit genuinely variable depth.

saying these in an interview costs you the question

  • A normal route parameter already matches values that contain slashes
  • where('.*') makes slashes work in any segment of the URI
  • An encoded %2F slips through the default pattern because it is not a slash
  • The order of routes never matters once a catch-all exists