【Laravel】Route Model Bindingで自動的にモデルを取得する

laravelアイキャッチ Laravel

こんにちは、かつコーチです。

前回は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 $requestComment $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の基本:テーブルをコードで管理する

タイトルとURLをコピーしました