Laravelのベストプラクティス記事は世の中にたくさんあります。FormRequestを使え、認可はPolicyへ、N+1はEager Loadingで——どれも正しい。ただ、私の環境ではコードの大半を書くのは人間ではなくClaude Codeです。そしてAIは、規約記事を読んでくれません。
放っておくと、AIはコントローラに所有者チェックのif文を書き、同じWHERE条件をあちこちにコピペし、既存の400行メソッドにさらに処理を足していきます。人間の新人なら一度レビューで指摘すれば直りますが、AIはセッションが変わるたびに同じ指摘を無限に繰り返す羽目になる。
そこでこの記事では、人間向けの「規約集」をAIコーディングに強制するための3層構成を、私が本業のLaravel案件(Laravel 10 / PHP 8.2 / Claude Code)で実際に運用している設定ごと紹介します。
- 第1層: CLAUDE.md — 規約を「AIが従える形式」に書き直す
- 第2層: レビュー観点化 — カスタムスラッシュコマンドで規約準拠をセルフレビューさせる
- 第3層: Hooks — Pint / PHPStan / 禁止パターンgrepで機械的にブロックする
なぜAIはベストプラクティスを破るのか
まず前提として、AIが規約を破るのは「知らないから」ではありません。「FormRequestを使うべきか」と聞けば模範解答を返します。問題は、既存コードの悪いパターンに引きずられることです。
私が保守しているある案件では、「自分が作成したデータのみ編集可能」というルールがコントローラのif文で実装されていて、人間の手でチェック漏れが3件発覚した過去がありました。これをPolicyクラスに集約して解消した——ここまでは人間向けベストプラクティスの話です。ところがその後Claude Codeに新機能を書かせると、リポジトリ内に残っていた古いif文パターンを「この プロジェクトの流儀」として学習し、せっかく統一したPolicyを迂回するコードを生成してきました。
AIは「一般論のベストプラクティス」より「目の前のコードベースの多数派」を優先します。だからこそ、プロジェクト側で明示的にルールを宣言し、破ったら機械的に弾く仕組みが要るわけです。
第1層: CLAUDE.md — 規約を「AIが従える形式」に書く
CLAUDE.mdはClaude Codeが毎セッション読み込むプロジェクト定義ファイルです。ここに規約を書くのが第一歩ですが、書き方にコツがあります。私が試行錯誤して落ち着いたのは 「NG/OKのコード対 + 一文の理由」 形式です。
## Laravelコーディング規約(必須・違反コードは生成しない)
### 認可はPolicyに集約する
- コントローラ内のif文による所有者チェックは禁止。
- NG: `if ($post->user_id !== auth()->id()) { abort(403); }`
- OK: `$this->authorize('update', $post);`
- 理由: if文方式で過去にチェック漏れ3件が発生。認可はPolicy以外に書かない。
### クエリ条件はEloquentスコープで共通化する
- 同じWHERE条件(有効フラグ・期間・ステータス)を2箇所以上に書かない。
- NG: `->where('is_active', true)->where('expired_at', '>', now())`
- OK: `->active()->inPeriod()`(Modelにローカルスコープを定義)
### 後処理はイベント・リスナーに分離する
- 完了処理(メール送信・在庫更新・ログ記録)をコントローラやServiceに追記しない。
- 既存のイベント(例: OrderCompleted)にリスナーを追加する形で拡張する。
### エラーハンドリング
- try-catchで握りつぶさない。例外は Handler.php の構造化ログ+通知に乗せる。
- ログにはユーザーID・リクエストURLの文脈を必ず含める。
ポイントは3つです。
- 抽象語で書かない。 「適切に認可すること」ではAIには効きません。NG/OKの実コード対で書くと、生成時のパターンマッチが効きます。
- 理由を一文添える。 理由付きルールのほうが、AIが例外ケースで勝手に緩める頻度が体感で明らかに減ります。
- ルールは実際に事故ったものから書く。 私は「過去に人間がやらかした失敗」から逆算してルール化しました。前述の認可漏れ3件のほか、WHERE条件のコピペで条件変更時の修正箇所が10箇所以上に膨らんだ件は、そのままスコープ規約になっています。
全部盛りの巨大な規約を書くのは逆効果です(CLAUDE.mdが長いほど個々のルールの遵守率は下がります)。私は「事故実績のあるルールだけ」に絞って15項目程度で運用しています。
第2層: レビュー観点化 — 規約準拠をセルフレビューさせる
CLAUDE.mdだけだと、長いセッションの終盤でルールがコンテキストから薄れて破られることがあります。そこで第2層として、実装後に規約チェック専用のレビューを走らせるカスタムスラッシュコマンドを用意しました。
.claude/commands/laravel-review.md を作っておくと、/laravel-review で呼び出せます。
---
description: 直前の変更をLaravel規約に照らしてセルフレビューする
---
直前に変更したPHPファイルを対象に、以下の観点で規約違反を検出してください。
違反があれば「ファイル:行番号 / 違反規約 / 修正案」の表で報告し、修正まで実施すること。
1. コントローラにバリデーションを直書きしていないか(FormRequestを使う)
2. 認可をif文で書いていないか(Policy + authorize()を使う)
3. 同一のWHERE条件を複数箇所に書いていないか(Eloquentスコープ化)
4. 完了系の後処理をメソッドに追記していないか(イベント・リスナーに分離)
5. 重い処理(PDF生成・外部API・集計)を同期実行していないか(キューへ)
6. 例外を握りつぶしていないか(Handler.php経由の構造化ログに乗せる)
このチェックリストの中身も体験ベースです。たとえば5番は、月次レポートのPDF生成を同期処理にしていてユーザーを30秒以上待たせ、タイムアウトまで起こしていた案件をキュージョブ化で解消した経験から入れました。AIも放っておくと平気で同期処理を書くので、レビュー観点に固定しています。
実際に運用してみると、実装直後の /laravel-review で違反の7〜8割はこの層で捕まります。「生成時に守らせる(第1層)」と「生成後に検査させる(第2層)」は別物として両方置くのが効きました。
第3層: Hooks — 破ったら機械的にブロックする
最後の砦が Claude Code の Hooks です。第1層・第2層は結局「AIへのお願い」なので、確率的にすり抜けます。Hooksはフックの対象にしたツール経由であれば必ず動きます。ここが人間向け規約集との違いで、規約を「文化」ではなく「CI」に寄せる層です(フックを通らない経路までは止められません)。
.claude/settings.json にPostToolUseフックを登録します。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": ".claude/hooks/check-laravel.sh" }
]
}
]
}
}
.claude/hooks/check-laravel.sh はこうです。exit 2 で返すと、stderr の内容がそのままClaude Codeにフィードバックされ、自動で修正を試みます。
#!/bin/bash
# PHPファイル編集のたびに Pint / PHPStan / 禁止パターンを機械チェックする
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // empty')
[[ "$file" != *.php ]] && exit 0
[[ "$file" == *"/vendor/"* ]] && exit 0
# 1. コードスタイル(Laravel Pint)
if ! vendor/bin/pint --test "$file" > /dev/null 2>&1; then
vendor/bin/pint "$file" > /dev/null 2>&1 # スタイルは自動修正で済ませる
fi
# 2. 静的解析(PHPStan/Larastan level 6)
if ! vendor/bin/phpstan analyse "$file" --level=6 --no-progress 2>&1; then
echo "PHPStan違反があります。エラーを解消してください: $file" >&2
exit 2
fi
# 3. 規約の禁止パターン(CLAUDE.mdの機械チェック版)
violations=$(grep -nE 'DB::raw\(|env\(|->user_id !== auth\(\)' "$file" | grep -v 'config/')
if [ -n "$violations" ]; then
echo "CLAUDE.md規約の禁止パターンを検出しました。修正してください:" >&2
echo "$violations" >&2
exit 2
fi
exit 0
3番のgrepが地味に効きます。DB::raw(、config以外での env() 呼び出し、if文認可の典型パターンといった「CLAUDE.mdに書いた禁止事項」をそのまま正規表現に落としているので、第1層をすり抜けた違反も、正規表現に引っかかる書き方であればここで止まり、Claude Codeが自分で書き直します。
副産物として、エラー通知の規約(前述のHandler.php構造化ログ)を守らせた案件では、本番エラーの検知が平均3時間から1分以内になりました。規約は守らせて初めて数字になる、を実感した瞬間です。
チートシート: どの規約をどの層で守らせるか
3層のどこに何を置くかの早見表です。私の運用の結論としては「意図が要るものは上の層、機械判定できるものは下の層」です。
| Laravel規約 | 第1層 CLAUDE.md | 第2層 レビュー観点 | 第3層 Hooks |
|---|---|---|---|
| コードスタイル(PSR-12) | — | — | ◎ Pint自動修正 |
| 型・未定義変数・戻り値 | — | — | ◎ PHPStan |
DB::raw / env() 直呼び禁止 |
○ NG/OK例 | — | ◎ grep |
| 認可はPolicyへ(if文禁止) | ◎ NG/OK例 | ○ | △ 典型パターンのみgrep |
| バリデーションはFormRequestへ | ◎ | ◎ | — |
| WHERE条件のスコープ化 | ◎ | ◎ | — |
| 後処理のイベント分離 | ○ | ◎ | — |
| 重い処理のキュー化 | ○ | ◎ | — |
| N+1の検出 | ○ | ○ | △ 別途Debugbar/CI |
◎=主担当、○=補助、△=部分的。設計判断が絡む規約(イベント分離・キュー化)はgrepでは判定できないので第2層が主担当、機械判定できるもの(スタイル・静的解析・禁止API)は第3層に落とす、という分担です。
前提条件・注意点
- 検証環境: Laravel 10 / PHP 8.2 / Claude Code(MAXプラン)/ Laravel Pint / Larastan(level 6)
- Hooksのexit 2ブロックはPostToolUseで使うと「生成→検出→自動修正」のループになります。修正ループが3回を超えるようなら、ルール自体が既存コードと矛盾していないかを疑ってください(私は一度、既存コードの大半が違反しているルールを入れてループ地獄になりました)
- PHPStanをファイル単位で回すとキャッシュが効かない場合があるので、遅いと感じたら対象をappディレクトリ配下に限定するなど調整してください
まとめ
- 規約集を書くだけではAIには効かない。「既存コードの多数派」に引きずられるため
- 第1層 CLAUDE.md: 事故実績ベースの規約をNG/OKコード対+理由で15項目程度に絞る
-
第2層 レビュー観点化: 設計判断が絡む規約は
/laravel-reviewでセルフレビューさせる - 第3層 Hooks: 機械判定できる規約はPint/PHPStan/grepでブロックし、自動修正させる
結局のところ、今回いちばん効いたのは高度なプロンプトではなく、「人間の新人教育と同じことを仕組みでやる」という発想の転換でした。規約を読ませ、レビューで指摘し、CIで弾く——人間のチームで当たり前にやってきたことを、AI相手には設定ファイル3つで再現できます。私のように「AIの書くLaravelコードのレビューで消耗している」人こそ、まず第3層のHooksひとつから入れてみてください。
ここまで読んでいただきありがとうございます。同じように「AIにコードを書かせつつ品質線を守りたい」と格闘している方の参考になれば嬉しいです。自分のプロジェクトのCLAUDE.mdに規約を移すときの雛形として、ストックしておくと便利です。