Routing
Verbs, parameters and constraints, named routes, groups, model binding and fallbacks β plus how the router decides which route wins.
Verbs
<?php
use Illuminate\Support\Facades\Route;
Route::get('/items', fn () => 'list');
Route::post('/items', fn () => 'create');
Route::put('/items/1', fn () => 'replace');
Route::patch('/items/1', fn () => 'update');
Route::delete('/items/1', fn () => 'destroy');
Route::match(['get', 'head'], '/ping', fn () => 'pong');
Route::any('/anything', fn () => 'any verb');
foreach ([['GET', '/items'], ['POST', '/items'], ['GET', '/ping']] as [$verb, $uri]) {
echo $verb, ' ', $uri, ' β ', visit($uri, $verb)->getContent(), PHP_EOL;
}A URI that exists for a different verb gives 405 Method Not Allowed, not 404 β a useful distinction when debugging a form that posts to a get route.
<?php
use Illuminate\Support\Facades\Route;
Route::get('/only-get', fn () => 'ok');
echo visit('/only-get')->getStatusCode(), PHP_EOL; // 200
echo visit('/only-get', 'POST')->getStatusCode(), PHP_EOL; // 405
echo visit('/nothing-here')->getStatusCode(), PHP_EOL; // 404Parameters
<?php
use Illuminate\Support\Facades\Route;
Route::get('/users/{id}', fn (string $id) => "user $id");
Route::get('/posts/{post}/comments/{comment}', fn ($post, $comment) => "post $post, comment $comment");
Route::get('/files/{name?}', fn (?string $name = 'index') => "file: $name");
echo visit('/users/7')->getContent(), PHP_EOL;
echo visit('/posts/1/comments/2')->getContent(), PHP_EOL;
echo visit('/files')->getContent(), PHP_EOL;
echo visit('/files/readme')->getContent(), PHP_EOL;Parameters are matched to closure arguments by position, not by name β but injected classes are matched by type, so you can mix them freely.
Constraints
where() restricts what a segment may contain. A URI that fails the constraint does not match the route at all β it falls through to the next one, or 404s.
<?php
use Illuminate\Support\Facades\Route;
Route::get('/orders/{id}', fn ($id) => "numeric order $id")->whereNumber('id');
Route::get('/orders/{slug}', fn ($slug) => "slug order $slug");
Route::get('/u/{name}', fn ($name) => "name $name")->whereAlpha('name');
Route::get('/v/{v}', fn ($v) => "version $v")->where('v', '[0-9]+\.[0-9]+');
echo visit('/orders/42')->getContent(), PHP_EOL;
echo visit('/orders/black-friday')->getContent(), PHP_EOL;
echo visit('/v/1.2')->getContent(), PHP_EOL;
echo visit('/v/abc')->getStatusCode(), PHP_EOL;Named routes
Names decouple your links from your URLs.
<?php
use Illuminate\Support\Facades\Route;
Route::get('/profile/{user}', fn ($user) => "profile $user")->name('profile.show');
Route::get('/about', fn () => 'about')->name('about');
// Only needed here: these routes are added after the app booted.
app('router')->getRoutes()->refreshNameLookups();
echo route('profile.show', ['user' => 7]), PHP_EOL;
echo route('about'), PHP_EOL;
echo route('about', absolute: false), PHP_EOL;
echo route('profile.show', ['user' => 7, 'tab' => 'settings']), PHP_EOL; // extras become a query stringChange /about to /about-us and every route('about') still works. A hard-coded /about would not.
Groups
Groups share attributes across many routes.
<?php
use Illuminate\Support\Facades\Route;
Route::prefix('api')->name('api.')->group(function () {
Route::get('/status', fn () => 'ok')->name('status');
Route::prefix('v1')->name('v1.')->group(function () {
Route::get('/users', fn () => 'v1 users')->name('users.index');
});
});
app('router')->getRoutes()->refreshNameLookups();
echo visit('/api/status')->getContent(), PHP_EOL;
echo visit('/api/v1/users')->getContent(), PHP_EOL;
echo route('api.v1.users.index', absolute: false), PHP_EOL;Prefixes, names and middleware nest and accumulate; controllers and domains are replaced by the innermost value.
Route model binding
Type-hint an Eloquent model and Laravel fetches it β or 404s before your code runs.
<?php
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Eloquent\Model;
Schema::create('articles', function (Blueprint $t) {
$t->id();
$t->string('slug');
$t->string('title');
});
class Article extends Model
{
public $timestamps = false;
protected $guarded = [];
public function getRouteKeyName(): string { return 'slug'; }
}
Article::create(['slug' => 'hello-world', 'title' => 'Hello World']);
Route::get('/articles/{article}', fn (Article $article) => $article->title);
echo visit('/articles/hello-world')->getContent(), PHP_EOL;
echo visit('/articles/nope')->getStatusCode(), PHP_EOL; // 404, automaticallyπ‘ Tip: getRouteKeyName() switches binding from the primary key to any column β usually a slug or a UUID. For a single route, /{article:slug} does the same thing inline.
Fallback and redirects
<?php
use Illuminate\Support\Facades\Route;
Route::redirect('/old-home', '/new-home');
Route::permanentRedirect('/ancient', '/new-home');
Route::get('/new-home', fn () => 'the new home');
Route::fallback(fn () => response('nothing here, sorry', 404));
$r = visit('/old-home');
echo $r->getStatusCode(), ' β ', $r->headers->get('location'), PHP_EOL;
echo visit('/ancient')->getStatusCode(), PHP_EOL; // 301
echo visit('/who-knows')->getContent(), PHP_EOL; // the fallbackRoute::fallback() must be registered last β it matches everything.
Controllers
For anything beyond a line or two, routes point at controllers.
<?php
use Illuminate\Support\Facades\Route;
use Illuminate\Routing\Controller;
class ArticleController extends Controller
{
public function index(): string { return 'all articles'; }
public function show(string $id): string { return "article $id"; }
}
Route::get('/a', [ArticleController::class, 'index']);
Route::get('/a/{id}', [ArticleController::class, 'show']);
echo visit('/a')->getContent(), PHP_EOL;
echo visit('/a/3')->getContent(), PHP_EOL;Route::resource('articles', ArticleController::class) registers the seven conventional routes (index, create, store, show, edit, update, destroy) in one line.
Inspecting the route table
<?php
use Illuminate\Support\Facades\Route;
Route::get('/one', fn () => 1)->name('one');
Route::post('/two/{id}', fn () => 2)->name('two')->middleware('web');
foreach (Route::getRoutes() as $route) {
if (! str_starts_with($route->uri(), 'one') && ! str_starts_with($route->uri(), 'two')) {
continue;
}
printf("%-6s %-12s %-6s %s\n",
implode('|', $route->methods()),
$route->uri(),
$route->getName(),
implode(',', $route->gatherMiddleware()) ?: '-'
);
}php artisan route:list is the same information from the command line, and it is the fastest way to answer "why is this URL 404ing?"
Reference: Routing in the Laravel documentation.