RESTful controller conventions — index, show, create, store, edit, update, destroy
Concept
Route model binding automatically resolves Eloquent models from route parameters. When a type-hinted parameter in a controller method matches a route parameter name, Laravel queries the database and injects the model — throwing a 404 if not found.
Implicit binding: Laravel infers the binding from the type hint. {user} in the route + User $user in the method → User::findOrFail($user) automatically. The route parameter name must match the method parameter name.
Custom key binding: By default, implicit binding uses the model's primary key. Use {user:email} in the route to bind by a different column: Route::get('/users/{user:email}', ...) → resolves User::where('email', $value)->firstOrFail().
getRouteKeyName(): Override in the model to change the default binding key: public function getRouteKeyName(): string { return 'slug'; }. All implicit bindings for this model will use the slug.
Scoped bindings: Route::resource('posts.comments', PostCommentController::class)->scoped(['comment' => 'slug']) — comments are scoped to the parent post AND resolved by slug: Comment::where('slug', $slug)->where('post_id', $post->id)->firstOrFail().
Explicit binding (RouteServiceProvider): Route::bind('user', fn($value) => User::where('username', $value)->firstOrFail()) — full control over resolution for all {user} parameters. Or use model's resolveRouteBinding() method.
404 behavior: Model not found → \Illuminate\Database\Eloquent\ModelNotFoundException → caught by exception handler → 404 response. Customize: $this->renderable(fn(ModelNotFoundException $e) => response()->json([...], 404)) in Handler.
Code Example
<?php
// routes/web.php
Route::get('/users/{user}', [UserController::class, 'show']); // implicit: by primary key
Route::get('/users/{user:username}', [UserController::class, 'show']); // implicit: by username
Route::get('/posts/{post:slug}/comments/{comment}', [PostCommentController::class, 'show']); // nested
// Controller — model auto-injected
class UserController extends Controller
{
// $user is auto-resolved: User::findOrFail($id) — 404 if not found
public function show(User $user): View
{
return view('users.show', compact('user'));
}
}
class PostCommentController extends Controller
{
// Both models auto-resolved and scoped (comment must belong to post)
public function show(Post $post, Comment $comment): View
{
// With scoped binding: comment->post_id === post->id is verified
return view('comments.show', compact('post', 'comment'));
}
}
// Custom binding key via model method
class Post extends Model
{
public function getRouteKeyName(): string
{
return 'slug'; // /posts/{slug} resolves by slug
}
}
// Explicit binding in RouteServiceProvider (or AppServiceProvider)
Route::model('order', Order::class); // standard model binding
Route::bind('order', function(string $value, \Illuminate\Routing\Route $route) {
// Only resolve orders belonging to authenticated user
return Order::where('id', $value)
->where('user_id', auth()->id())
->firstOrFail();
});
// Soft-deleted models — include trashed
Route::get('/posts/{post}', [PostController::class, 'show'])
->withTrashed(); // resolves soft-deleted models too