<?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();
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Comment extends Model
{
protected $fillable = ['body', 'slug'];
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}
public function getRouteKeyName(): string
{
return 'slug';
}
}
<?php
namespace App\Http\Controllers;
use App\Models\Comment;
use App\Models\Post;
use Illuminate\Http\Request;
class CommentController extends Controller
{
public function show(Post $post, Comment $comment)
{
// $comment is guaranteed to belong to $post by the scoped binding.
return view('comments.show', compact('post', 'comment'));
}
public function update(Request $request, Post $post, Comment $comment)
{
$this->authorize('update', $comment);
$data = $request->validate([
'body' => ['required', 'string', 'max:2000'],
]);
$comment->update($data);
return redirect()
->route('posts.comments.show', [$post, $comment])
->with('status', 'Comment updated.');
}
public function destroy(Post $post, Comment $comment)
{
$this->authorize('delete', $comment);
$comment->delete();
return redirect()->route('posts.comments.index', $post);
}
}
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
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
Django URL namespacing and reverse lookups
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["form"]
static values = { delay: { type: Number, default: 250 } }
Debounced live search with Stimulus + Turbo Streams
#!/usr/bin/env bash
set -euo pipefail
export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"
Secrets management with environment isolation and Vault
<?php
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
Laravel service container and dependency injection
Share this code
Here's the card — post it anywhere.