php 79 lines · 3 tabs

Scoped Route-Model Binding for Nested Resources in Laravel

Shared by codesnips Aug 2026
3 tabs
<?php

use App\Http\Controllers\CommentController;
use Illuminate\Support\Facades\Route;

// The ->scoped() call forces {comment} to resolve through $post->comments,
// so /posts/{post}/comments/{comment} 404s on a mismatched pair.
Route::resource('posts.comments', CommentController::class)
    ->scoped(['comment' => 'slug'])
    ->only(['index', 'show', 'store', 'update', 'destroy']);

// Without ->scoped(), this equivalent explicit binding would be needed everywhere:
// Route::get('/posts/{post}/comments/{comment:slug}', ...)->scopeBindings();
3 files · php Explain with highlit

This snippet shows how Laravel's scoped route-model binding prevents a common IDOR-style bug in nested REST routes: a comment that belongs to one post being fetched through the URL of a different post. When routes are nested like /posts/{post}/comments/{comment}, naive binding resolves each model independently, so a mismatched pair still returns a 200 and leaks data. Scoping ties the child's lookup to the parent's relationship, so an unrelated {comment} yields a 404 automatically.

In routes/web.php, the resource is registered with Route::resource(...)->scoped([...]). The scoped call is the key line: it tells the router that the comment parameter must be resolved through the post model's relationship rather than as a top-level query. Because the relationship name (comments) is inferred from the plural of the parameter, Laravel calls $post->comments()->where(...)->firstOrFail() under the hood. Passing an explicit column (['comment' => 'slug']) also switches the child to slug-based binding scoped to the parent.

The Comment model declares the inverse belongsTo side and, importantly, overrides getRouteKeyName to bind on slug. That method is what the router consults when building the scoped query, so URLs read /posts/laravel-routing/comments/great-point instead of exposing sequential integer ids, which reduces enumeration risk.

In CommentController, the show and update methods type-hint both Post $post and Comment $comment. By the time the controller runs, the framework has already guaranteed the comment belongs to the post, so no manual where('post_id', $post->id) check is needed. This keeps the controller thin and moves the invariant into the routing layer where it cannot be forgotten. The update method still runs a validate call and an explicit authorize for per-user ownership, since binding enforces structural ownership but not permission.

The trade-off is a small extra query per request and reliance on correctly named relationships. When a relationship name differs from the parameter, the scoped(['comment' => 'column']) form or a custom resolveChildRouteBinding is required. This pattern is the idiomatic choice whenever a nested resource must never be addressable outside its parent.


Related snips

Share this code

Here's the card — post it anywhere.

Scoped Route-Model Binding for Nested Resources in Laravel — share card
Link copied