Laravelのルートモデルバインディング

概要

ルートモデルバインディングとは、URLパラメータからEloquentモデルのインスタンスを自動的に解決する仕組みです。
コントローラで手動に find() を呼び出す冗長なコードをフレームワーク側に委譲できます。

暗黙的バインディング

最もシンプルな形式です。
コントローラのメソッド引数の型ヒントとルートパラメータ名が一致するだけで、Laravelが自動的にモデルを解決します。

// routes/web.php
Route::get('/posts/{post}', [PostController::class, 'show']);

// PostController.php
public function show(Post $post): Response
{
    return response()->json($post);
}

デフォルトでは主キー(id)を使ってモデルを検索します。
レコードが見つからない場合は自動的に404レスポンスを返します。

解決するキーのカスタマイズ

スラッグやUUIDなど、id 以外のカラムで解決したい場合は2通りの方法があります。

ルート定義での指定

Route::get('/posts/{post:slug}', [PostController::class, 'show']);

モデルでのオーバーライド

// Post.php
public function getRouteKeyName(): string
{
    return 'slug';
}

明示的バインディング

RouteServiceProvider または AppServiceProviderboot() メソッドでバインディングを明示的に定義します。
カスタムクエリや複雑なロジックが必要な場合に有効です。

// AppServiceProvider.php
public function boot(): void
{
    Route::model('post', Post::class);

    // または任意のロジックでモデルを解決する
    Route::bind('post', function (string $value): Post {
        return Post::where('slug', $value)->firstOrFail();
    });
}

スコープ付きバインディング

ネストしたルートで親子関係を強制したい場合に使います。

Route::get('/users/{user}/posts/{post}', [PostController::class, 'show'])
    ->scopeBindings();

これにより、{post}{user} に属するものだけが解決対象になります。
別ユーザーのリソースへのアクセスは404になるため、認可ロジックの一部を宣言的に表現できます。

ルートグループ全体に適用することも可能です。

Route::scopeBindings()->group(function () {
    Route::get('/users/{user}/posts/{post}', [PostController::class, 'show']);
});

ソフトデリートされたモデルの解決

デフォルトでは、ソフトデリートされたモデルは解決されません。
withTrashed() をルートに付与することで解決対象に含められます。

Route::get('/posts/{post}', [PostController::class, 'show'])
    ->withTrashed();

カスタム解決ロジック(resolveRouteBinding

モデル側でバインディングロジックを完全に制御したい場合は、resolveRouteBinding をオーバーライドします。

// Post.php
public function resolveRouteBinding(mixed $value, ?string $field = null): ?Model
{
    return $this->where($field ?? $this->getRouteKeyName(), $value)
        ->where('is_published', true)
        ->firstOrFail();
}

この方法はモデルにドメインロジックを閉じ込められる一方、解決ロジックがルート定義から見えなくなるため、チームの規模や可読性のトレードオフを考慮してください。

まとめ

方法 ユースケース
暗黙的バインディング 標準的なCRUD操作
{param:column} 記法 スラッグ・UUIDなど主キー以外での解決
明示的バインディング 複雑なクエリやカスタムロジック
スコープ付きバインディング 親子関係の強制・認可の宣言的表現
resolveRouteBinding モデル側にロジックを閉じ込めたい場合

ルートモデルバインディングを適切に活用することで、コントローラはモデルの取得処理から解放され、ビジネスロジックに集中できます。

LaravelにRepositoryパターンは不要か

Laravelにおいて、Repositoryパターンは多くの場合不要です。その理由は、Eloquentが採用するActive Recordパターンにあります。

Active Recordはデータとロジックを統合したパターンであり、モデル自身がデータアクセスの責務を担っています。その上にさらにRepositoryレイヤーを重ねるのは責務の重複であり、本質的な価値をもたらしません。Repositoryパターンが自然に機能するのは、ドメインモデルとデータアクセスが分離されたData Mapperパターン(Doctrineなど)との組み合わせでしょう。

また、Laravelの多くの機能はEloquentに深く統合されています。リレーション、Eager Loading、イベント、Observerといった機能群は、Eloquentを前提として設計されています。Eloquentを使わないなら、Laravelを選択する理由そのものが希薄になります。

Laravelの標準的なアーキテクチャは、ControllerからService層やActionクラスを呼び出し、その中でEloquent Modelを利用する形を基本とします。クエリロジックはクエリスコープやモデルメソッドでカプセル化します。より複雑なクエリが必要なら、Query ObjectsやQuery Buildersで専用クラスに分離すればよいでしょう。単一責任のビジネスロジックには、ActionsやCommandsといったパターンが有効です。

ただし例外もあります。複数のデータソース(データベース、API、キャッシュ等)を統一インターフェースで扱う必要がある場合や、チーム規約として厳密なレイヤー分離が求められる場合には、Repositoryパターンが有用な選択肢となりえます。重要なのは、パターンを盲目的に適用するのではなく、実際の要件に基づいて判断することです。

Macの開発環境のメンテナンス用シェルスクリプト

開発マシンのメンテナンスは、やるべきだと分かっていながら後回しにしがちな作業の一つです。

Homebrewのアップデート、Dockerの肥大化したキャッシュ、積み上がったDerivedData——個別には些細でも、放置すれば確実にディスクとパフォーマンスを蝕みます。

本記事では、私が普段使用しているシェルスクリプトを紹介します。

設計方針

メンテナンススクリプトには独特の要件があります。
一部のコマンドが失敗しても、残りは実行を続けてほしい。set -e を使わない理由はここにあります。

set -uo pipefail

-u で未定義変数の参照を防ぎ、pipefail でパイプ中の失敗を検知します。
-e を外すことで、例えば brew doctor が警告を返しても後続の処理は止まりません。

構成

スクリプトは5つのセクションから成ります。

Homebrewの更新とクリーンアップ

brew update && brew upgrade && brew upgrade --cask --greedy
brew cleanup && brew autoremove
brew doctor

--cask --greedy は、自動更新機能を持つアプリ(Chrome、Firefox等)も含めて強制的にアップグレードします。
brew cleanupbrew autoremove で古いバージョンと不要な依存を除去し、brew doctor で環境の整合性を確認します。

Mac App Store

mas upgrade

mas を使い、App Store経由でインストールしたアプリもCLIから更新します。

Xcode関連

xcrun simctl delete unavailable

使われなくなったシミュレータランタイムを削除します。
これだけで数十GBの空きが生まれることもあります。

DerivedDataについては、Xcodeの起動状態を確認してから削除します。

if ! pgrep -q Xcode; then
  rm -rf ~/Library/Developer/Xcode/DerivedData/*
fi

ビルドキャッシュを消すだけなので、次回ビルド時に再生成されます。
起動中のXcodeを壊す心配を避けるためのガードです。

パッケージマネージャのキャッシュ整理

npm と pnpm のキャッシュを整理します。
いずれも command -v で存在確認してから実行するため、インストールしていない環境でもエラーにはなりません。

npm cache verify    # 整合性チェック。全削除はしない
pnpm store prune    # 未参照パッケージの削除

npm cache clean --force ではなく verify を選んでいるのは、壊れたエントリだけを除去し、有効なキャッシュは維持するためです。

Docker クリーンアップ

if command -v docker &>/dev/null && docker info &>/dev/null; then
  docker system prune -f --volumes --filter "until=168h"
  docker builder prune -f --filter "until=168h"
fi

docker info でデーモンの起動確認を行い、停止中なら何もしません。
--filter "until=168h" により、1週間以内に使用したリソースは保持されます。
直近の作業を壊さない安全策です。

キャッシュ容量の可視化

最後に ~/Library/Caches の上位10ディレクトリを表示します。

du -sh ~/Library/Caches/* 2>/dev/null | sort -rh | head -n 10

自動削除はしません。
アプリケーションキャッシュの削除判断は人間に委ねるべきです。

使い方

適当な場所に保存して実行権限を付与します。

chmod +x ~/scripts/maintenance.sh

手動で定期実行してもいいですし、launchd でスケジュールしても構いません。
ただし、Homebrewやmasは対話的な認証を求めることがあるため、完全な無人実行には向かない点に留意してください。

スクリプト全文

#!/bin/bash

# -------------------------------------------
# エラーハンドリング方針:
#   メンテナンススクリプトの性質上、一部失敗しても続行したいため
#   set -e は使わず、重要なコマンドのみ個別にハンドリングする。
#   未定義変数の参照とパイプ途中の失敗は検知する。
# -------------------------------------------
set -uo pipefail

# ===========================================
# Homebrewパッケージ更新とクリーンアップ
# ===========================================
brew update && brew upgrade && brew upgrade --cask --greedy
brew cleanup && brew autoremove
brew doctor

# ===========================================
# Mac App Storeアプリのアップデート
# ===========================================
mas upgrade

# ===========================================
# Xcode関連の不要ファイル削除
# ===========================================
xcrun simctl delete unavailable

# Xcodeが起動中でなければDerivedDataを削除
if ! pgrep -q Xcode; then
  rm -rf ~/Library/Developer/Xcode/DerivedData/*
else
  echo "⚠ Xcodeが起動中のためDerivedDataの削除をスキップしました"
fi

# ===========================================
# パッケージマネージャのキャッシュ/ストア整理
# ===========================================
# npm キャッシュの検証(整合性チェックのみ、全削除はしない)
if command -v npm &>/dev/null; then
  echo "--- npm cache verify ---"
  npm cache verify
fi

# pnpm ストアの最適化(未参照の依存パッケージを削除)
if command -v pnpm &>/dev/null; then
  echo "--- pnpm store prune ---"
  pnpm store prune
fi

# ===========================================
# Docker クリーンアップ
# ===========================================
if command -v docker &>/dev/null && docker info &>/dev/null 2>&1; then
  echo "--- Docker system prune ---"
  # 1週間以上前の未使用リソース + 未使用ボリュームを削除
  docker system prune -f --volumes --filter "until=168h"

  echo "--- Docker builder prune ---"
  docker builder prune -f --filter "until=168h"
fi

# ===========================================
# キャッシュ容量の確認(削除は手動判断)
# ===========================================
echo ""
echo "=== ~/Library/Caches 容量 Top 10 ==="
du -sh ~/Library/Caches/* 2>/dev/null | sort -rh | head -n 10
echo ""
echo "※ 不要なキャッシュがあれば手動で削除してください"
echo "  例: rm -rf ~/Library/Caches/<アプリ名>"

まとめ

このスクリプトが行うのは「安全に削除できるものだけを削除し、判断が必要なものは報告する」ことです。
定型作業を自動化するだけでも、ディスク容量とビルド環境の健全性は改善します。

Laravel 8以降は foreignIdFor() を使う

Laravel 8で foreignIdFor() メソッドが導入されました。
外部キーカラムの定義がよりシンプルで安全になる機能です。

foreignIdFor() とは

foreignIdFor() は、モデルクラスから外部キーカラムを自動生成するメソッドです。

Before: 従来の書き方

$table->unsignedBigInteger('user_id');
$table->foreign('user_id')
    ->references('id')
    ->on('users')
    ->onDelete('cascade');

After: foreignIdFor() を使った書き方

$table->foreignIdFor(User::class)
    ->constrained()
    ->onDelete('cascade');

わずか1行で、カラムの作成から外部キー制約の設定まで完結します。

constrained() メソッドについて

constrained() は外部キー制約を自動的に設定するメソッドです。

$table->foreignIdFor(User::class)->constrained();

このコードは以下を自動的に行います:
– モデルクラスから参照先のテーブル名を推測(Userusers
– 主キーカラム id への参照を作成
– 外部キー制約を設定

constrained() を呼ばなければ、カラムのみが作成され、外部キー制約は設定されません。

// カラムのみ作成(制約なし)
$table->foreignIdFor(User::class);

// カラム + インデックスのみ(制約なし)
$table->foreignIdFor(User::class)->index();

主なメリット

1. 型の安全性が向上

参照先のテーブルの主キー型と自動的に一致します。
users.idbigInteger なら、外部キーも unsignedBigInteger として作成されます。

2. タイプミスを防止

// モデルクラスから自動生成されるため、タイプミスが起きない
$table->foreignIdFor(User::class); // → user_id

// 従来の方法だとタイプミスのリスク
$table->unsignedBigInteger('usr_id'); // うっかりミス

3. コードの意図が明確

一目で「これは User モデルへの外部キーだ」と分かります。

注意点

モデルクラスが必要

当然ですが、モデルクラスが存在しないと使えません。

// モデルがない外部サービスのテーブルなど
$table->unsignedBigInteger('external_service_id');

カスタムテーブル名に注意

モデル名とテーブル名が規則通りでない場合は、明示的に指定が必要です。

class Person extends Model {
    protected $table = 'users';
}

// persons テーブルを探してしまうため、明示的に指定
$table->foreignIdFor(Person::class)->constrained('users');

カラム名のカスタマイズ

デフォルトでは {model}_id という名前になります。
カスタマイズする場合は第2引数を使います。

$table->foreignIdFor(User::class, 'author_id')->constrained('users');

複合主キーには非対応

複合主キーへの参照には従来の方法を使う必要があります。

よく使うパターン

// nullable な外部キー
$table->foreignIdFor(User::class)->nullable()->constrained();

// カスケード削除
$table->foreignIdFor(User::class)
    ->constrained()
    ->onDelete('cascade');

// SET NULL
$table->foreignIdFor(User::class)
    ->nullable()
    ->constrained()
    ->onDelete('set null');

// インデックスのみ(外部キー制約なし)
$table->foreignIdFor(User::class)->index();

まとめ

foreignIdFor() は、外部キー定義をより安全で保守しやすくする優れた機能です。
特別な理由がない限り、Laravel 8以降のプロジェクトでは積極的に使用することをお勧めします。