こんにちは、かつコーチです。
前回はNamed Routeについて解説しました。
今回は、URLのパラメータを扱うコントローラをもっとシンプルに書ける「Route Model Binding」を紹介します。
「IDを受け取って、モデルを検索して、なければ404を返す」というお決まりの処理を、Laravelがほぼ自動でやってくれる便利な機能です。
Route Model Bindingとは?
IDからモデルを取得する定型処理
投稿詳細ページのような「IDを指定してデータを1件取得する」処理を、Route Model Bindingを使わずに書くと次のようになります。
Route::get('/posts/{id}', [PostController::class, 'show']);
class PostController extends Controller
{
public function show($id)
{
$post = Post::findOrFail($id);
return view('posts.show', ['post' => $post]);
}
}
{id} で受け取ったIDを使って、コントローラの中で Post::findOrFail($id) を呼び出す、という流れです。
Route Model Bindingを使うと、この「IDからモデルを検索する」処理をLaravelが肩代わりしてくれます。
Route::get('/posts/{post}', [PostController::class, 'show']);
class PostController extends Controller
{
public function show(Post $post)
{
return view('posts.show', ['post' => $post]);
}
}
ルートの {id} を {post} に変え、コントローラの引数に Post $post と型を指定するだけで、Laravelが自動的にURLの値からPostモデルを検索し、インスタンスとして渡してくれます。
仕組みのポイント:パラメータ名とモデル名を揃える
Route Model Bindingが動くための条件は、ルートのパラメータ名({post})とコントローラの引数名($post)を一致させることです。
内部では、パラメータの値を主キー(通常は id)としてモデルを検索し、見つからなければ自動的に404エラーを返してくれます。
findOrFail() を毎回書かなくてよくなる分、コントローラのコードがぐっとすっきりします。
実際に使ってみる
基本の実装例
コメント編集画面を例に、実際の使い方を見てみましょう。
// routes/web.php
use App\Http\Controllers\CommentController;
Route::get('/comments/{comment}/edit', [CommentController::class, 'edit'])->name('comments.edit');
Route::put('/comments/{comment}', [CommentController::class, 'update'])->name('comments.update');
// app/Http/Controllers/CommentController.php
namespace App\Http\Controllers;
use App\Models\Comment;
use Illuminate\Http\Request;
class CommentController extends Controller
{
public function edit(Comment $comment)
{
return view('comments.edit', ['comment' => $comment]);
}
public function update(Request $request, Comment $comment)
{
$validated = $request->validate([
'body' => 'required|string|max:1000',
]);
$comment->update($validated);
return redirect()->route('comments.edit', $comment)->with('status', '更新しました');
}
}
update() のように Request $request と Comment $comment の両方を受け取るときは、モデルの引数はルートパラメータの後ろに置くのが基本です。
redirect()->route('comments.edit', $comment) のように、モデルのインスタンスをそのまま route() に渡せる点も地味に便利です。
Laravelが自動的に $comment->id を取り出してURLに埋め込んでくれます。
存在しないIDへのアクセスはどうなるか
存在しないIDでアクセスされた場合、Route Model Bindingは自動的に ModelNotFoundException を投げます。
Laravelはこの例外をキャッチして、標準の404エラーページを表示する仕組みになっています。
つまり、開発者が「存在チェック」を書かなくても、安全に404が返る状態がデフォルトで実現されているということです。
つまずきやすいポイント
パラメータ名を揃え忘れる
私が最初にRoute Model Bindingを使ったとき、しばらくハマったのがこのパターンです。
❌ Before:ルートのパラメータ名とコントローラの引数名が食い違っている
Route::get('/posts/{id}', [PostController::class, 'show']);
public function show(Post $post)
{
return view('posts.show', ['post' => $post]);
}
このコードを実行すると、Route Model Bindingが働かず、$post にはPostモデルではなく単なる文字列のIDが渡ってきます。
その結果、Bladeテンプレート側で $post->title のようにプロパティへアクセスした瞬間、「文字列に対してプロパティを呼び出そうとした」というエラーになりました。
エラーメッセージが「モデルが見つからない」ではなく型に関するものだったため、原因がRoute Model Bindingの仕様だと気づくまでに時間がかかったのを覚えています。
✅ After:パラメータ名と引数名を同じにする
Route::get('/posts/{post}', [PostController::class, 'show']);
public function show(Post $post)
{
return view('posts.show', ['post' => $post]);
}
{id} のような汎用的な名前ではなく、{post} のようにモデル名を意識したパラメータ名にする習慣をつけておくと、この種の食い違いを防げます。
論理削除されたデータを取得したいとき
SoftDeletes(論理削除)を使っているモデルの場合、標準のRoute Model Bindingは削除済みのデータを自動的に除外します。
管理画面などで、削除済みデータも含めて表示したい場合は、withTrashed() を使うようにルート側で指定します。
Route::get('/admin/posts/{post}', [Admin\PostController::class, 'show'])
->withTrashed();
このオプションをつけ忘れると、「削除済みの投稿を復元したいのに、詳細画面が404になる」という現象が起きるので、論理削除を扱う機能を実装する際は覚えておくとよいでしょう。
一歩先の使い方:主キー以外のカラムで検索する
スラッグでの取得に対応する
ブログ記事のURLを /posts/how-to-use-laravel のようなスラッグ(URLに使う人が読みやすい識別子)にしたい場合、主キー(id)ではなく別のカラムで検索させたいことがあります。
そんなときは、モデル側に getRouteKeyName() を定義するだけで対応できます。
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/**
* Route Model Bindingで使うカラムを指定する
*/
public function getRouteKeyName(): string
{
return 'slug';
}
}
こうしておけば、ルート定義やコントローラのコードは一切変更せずに、{post} の値が slug カラムで検索されるようになります。
Route::get('/posts/{post}', [PostController::class, 'show']);
public function show(Post $post) // URLの値がslugカラムで検索される
{
return view('posts.show', ['post' => $post]);
}
特定のルートだけスラッグで検索させたい場合は、ルート定義側で {post:slug} のように指定することも可能です。
Route::get('/posts/{post:slug}', [PostController::class, 'show']);
まとめ
この記事のポイント
- Route Model Bindingを使うと、コントローラで
findOrFail()を書かずにモデルを直接受け取れる - 動作させるには、ルートのパラメータ名とコントローラの引数名を一致させる必要がある
- 該当データが存在しない場合は自動的に404エラーが返る
SoftDeletesを使うモデルでは、withTrashed()を明示しないと削除済みデータが取得できないgetRouteKeyName()を定義すれば、idではなくslugなど別カラムで検索させることもできる
次に読むべき記事
ここまででルーティングとコントローラの基本的な応用を押さえました。
次回からはデータベース編に入り、まずはテーブルをコードで管理する「Migration」の基本を解説します。
→ 次の記事:Migrationの基本:テーブルをコードで管理する