Claude CodeでレガシーPHPを型付けする — PHPStanのlevelを段階的に上げる手順
PHP 5時代の関数と配列だらけのコードを、PHPStanのlevelを段階的に上げながらClaude Codeで型付けする順序を、PHPDocのarray shapeから示します。
クラスも名前空間も無く、連想配列を関数から関数へ渡し続けるPHPのコードは、型付けの入口が見つけにくいものです。入口は、ネイティブの型宣言ではなく、PHPDocの array{...} です。配列の中身を言葉にしてからPHPStanのlevelを1段ずつ上げ、Claude Codeには「1段・1ファイル・1種類の型」で作業を渡します。
この記事は、Laravel導入前の素の関数コードが対象です。Laravelでbaselineを運用する話はClaude CodeでPHPStan(Larastan)の型エラーを減らすにあります。大規模な移行全体の進め方はClaude Codeでコードモダナイゼーションが扱います。
まず環境を決める — 解析する側のPHPと解析されるコードは別物
PHPStanを動かすには、PHP 7.4以上が必要です。ただし、解析されるコードまでPHP 7の機能を使っている必要はありません。公式のGetting Startedには、PHP 5.6以前向けに書かれたコードは7.xでもほぼそのまま動く、との注記があります。
つまり、本番がPHP 5.xのままでも、解析だけは新しいPHPのCIやコンテナで回せます。ここで効くのが phpVersion です。
# phpstan.neon.dist
parameters:
phpVersion: 70400 # 解析するコードの想定PHPバージョン(7.4)
paths:
- src
- libphpVersion は、実行中のPHPと異なるバージョン向けのコードとして解析したいときに指定します。値の書式は 70400 のような数値です。本番の最低バージョンに合わせておくと、新しい構文の提案を避けやすくなります。
未定義の関数とクラスは「型」より先に潰す
PHP 5時代のコードは、require と include で関数を読み込む作りが多く、オートローダーに載っていないことがあります。PHPStanはlevel 0で、未知のクラス、未知の関数、未知の $this のメソッド、引数の個数、常に未定義の変数を報告します。
最初の実行で出やすいのは、公式が挙げる次の4種類です。
- 関数に渡す引数が多すぎる
sprintf系のプレースホルダー数と引数の数が合わない- 到達しないコード内の明らかな誤り
- 「class not found」のような未知のシンボル
未知のシンボルは、型の問題ではなく読み込みの問題です。設定ファイルの scanFiles、scanDirectories、bootstrapFiles で、PHPStanに関数の定義場所を教えます。自前のオートローダーが必要なら bootstrapFiles に初期化スクリプトを指定します。
parameters:
scanDirectories:
- includes
bootstrapFiles:
- phpstan-bootstrap.phpこの段階でClaude Codeに頼むのは、「level 0のエラーを、読み込み設定の不足とコードの誤りに分けて一覧にする」ことだけです。コードの修正は、分類を人が確認してから渡します。
型付けの順序は「level」より「型の置き場所」で考える
PHPStanのlevelは0から10まで11段階で、累積式です。5で動かせば0〜4のチェックも含まれます。公式の概要を、レガシーコードの作業に置き換えると次のようになります。
| level | PHPStanが見るもの | 素の関数コードでの作業 |
|---|---|---|
| 0 | PHPStanが見るもの未知の関数・クラス、引数の個数 | 素の関数コードでの作業読み込み設定を整える |
| 1〜2 | PHPStanが見るもの未定義の可能性がある変数、全式でのメソッド確認、PHPDocの検証 | 素の関数コードでの作業条件分岐で初期化されない変数を直す |
| 3〜4 | PHPStanが見るもの戻り値の型、プロパティへの代入型、デッドコード | 素の関数コードでの作業戻り値のPHPDocを書き始める |
| 5 | PHPStanが見るもの引数の型 | 素の関数コードでの作業関数ごとに引数の型を揃える |
| 6 | PHPStanが見るもの型宣言(typehint)の欠落 | 素の関数コードでの作業配列の中身をarray shapeで表す |
| 7〜8 | PHPStanが見るものunion型の部分的な誤り、nullable型へのアクセス | 素の関数コードでの作業false や null を返す関数の呼び出し側を直す |
| 9〜10 | PHPStanが見るものmixed の扱い。10では暗黙の mixed も報告 | 素の関数コードでの作業型のない箇所の最終掃除 |
引数の型エラーが初めて出るのはlevel 5です。levelが2のままでは、配列を渡すべき所に文字列を渡すバグも報告されません。
PHPDocのarray shapeから入れる理由
PHP 5時代の構文には、スカラー型の宣言がありません。関数が受け取る配列も、array としか書けません。型の情報を置ける場所は、PHPDocのコメントだけです。
PHPStanのPHPDoc型には、配列の中身を表すための記法があります。ドキュメントは、さまざまな形の配列が関数やメソッドを渡り歩くコードで、強い型を使えるようにする機能だと説明しています。
<?php
/**
* @param array{id: int, name: string, email?: string|null} $user
* @return list<array{order_id: int, total: int}>
*/
function find_orders_by_user($user)
{
// ...
}使える書式は次のとおりです。
| 書き方 | 意味 |
|---|---|
array{id: int, name: string} | 意味キーごとに型が違う連想配列 |
array{id: int, email?: string} | 意味email キーは省略されうる |
array{int, int} | 意味キーが0と1のタプル |
list<Type> | 意味0から連番のキーを持つ配列 |
non-empty-array<int, Type> | 意味空でない配列 |
array shapeは既定で閉じています。書いていないキーを持つ配列は許されません。余分なキーを許したいときは、末尾に ... を付けます。
先にarray shapeを入れると何が楽になるか
レガシーコードの型エラーの多くは、「この配列にどのキーが入っているのか」が分からないことから来ます。連想配列の形を1か所で決めると、そこから先の関数すべてが同じ型を参照できます。
順序の目安は次のとおりです。
- データを組み立てる関数(DB取得、フォーム入力のパース)の戻り値に、array shapeを書く
- その配列を受け取る関数の
@paramに、同じ形を写す - 同じ形が3回以上出てきたら、クラスへの置き換えを検討する
3は、型付けの範囲を超えて構造を変える話です。まずは1と2で件数を減らし、構造変更は別の作業として切り分けます。
Claude Codeに渡す作業単位
型付けを一括で頼むと、PHPDocの内容を推測で埋められがちです。キーの存在や値の型は、コードを読んで確かめた事実だけにします。
作業単位は「1ファイル・1段のlevel」にします。依頼の例を示します。
src/order/legacy_orders.php だけを対象にします。
1. `vendor/bin/phpstan analyse src/order/legacy_orders.php --level 5`
を実行し、エラーを一覧にしてください。
2. 配列の戻り値には、呼び出し元と実際の代入を読んで確かめた
キーだけでarray shapeを書いてください。
3. 確認できないキーは `mixed` にし、行末に TODO を書いてください。
4. 実行して件数を報告します。ignoreとbaselineは触りません。3が大切です。推測で書いた型は、PHPStanに通っても誤りを抱えたままになります。mixed と明記して残せば、level 9以降で次の掃除対象として見えるようになります。
型を書き終えたら、挙動が変わっていないかを測る
PHPDocだけの変更は実行時の挙動を変えませんが、型を合わせるために (int) のキャストや isset を足すと変わりえます。PHPDocを書く作業と、コードを書き換える作業は、コミットを分けます。コードに触れたコミットだけ、既存のテストか手動確認を回します。テストが無いコードを安全に直す方法は、Claude Codeリファクタリングを安全に進める手順に詳しく書かれています。
levelを1段ずつ上げる流れ
上げるときの手順は固定にします。baselineは、今は直せないエラーを退避する手段として使います。
- 現在のlevelでエラー0件を確認する
- 設定ファイルを変えずに、1つ上のlevelで件数を測る
vendor/bin/phpstan analyse -l 4 src lib --no-progress- 数十件なら直し、多ければbaselineに退避する
baselineの生成は --generate-baseline です。
vendor/bin/phpstan analyse -l 5 src lib --generate-baseline公式は、baselineが向くのは数十から数百件のエラーを今は直せない場合だとしています。1万5千件のような規模では、設定の見直しに時間をかけるよう勧めています。生成されるのは phpstan-baseline.neon で、設定の includes に追加して読み込みます。
baselineには、新しいコードを高いlevelで検査するという使い道もあります。たとえばlevel 5で既存のエラーを全部退避すれば、これから書くコードには5までの検査がかかります。
CLAUDE.mdに書く取り決め
作業単位と禁止事項は、CLAUDE.mdに検証できる形で書きます。
## 静的解析(PHPStan・レガシーPHP)
- 実行: `vendor/bin/phpstan analyse --no-progress`
- levelは `phpstan.neon.dist` が正。勝手に上げない
- 1回の依頼で触るのは1ファイル。他のファイルは読むだけ
- 配列の型は、呼び出し元と代入を読んで確かめたキーだけで書く
- 確認できないキーは `mixed` にして、行末に TODO を残す
- `--generate-baseline` は実行しない
- PHPDocの追加と、コードの書き換えは別コミットにする指示は短いほど守られやすくなります。細部が増えたら、パスを限定したルールに切り出します。baselineの再生成のように「絶対に走らせたくない」コマンドは、文章に頼らず権限設定で止める手もあります。
編集のたびにPHPStanを回すhook
.php を編集した直後に解析を回すと、エラーが溜まる前に気づけます。公式のhooksガイドには、PostToolUse イベントを Edit|Write のマッチャーで使い、編集されたファイルのパスを jq で取り出して外部コマンドへ渡す例があります。同じ形で、PHPStanに渡します。
#!/bin/bash
# .claude/hooks/phpstan.sh
file=$(jq -r '.tool_input.file_path // empty')
case "$file" in *.php) ;; *) exit 0 ;; esac
out=$(vendor/bin/phpstan analyse "$file" --no-progress 2>&1) || {
echo "$out" >&2
exit 2
}登録は .claude/settings.json です。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": ".claude/hooks/phpstan.sh" }
]
}
]
}
}exit 2では、stderrの内容が理由として返されます。イベントによって、Claudeへのフィードバックになるか、ユーザーにだけ表示されるかが変わります。PostToolUse はツールがすでに実行された後に発火するため、編集そのものは元に戻せません。そのため、このhookの役割は「失敗内容をClaudeに伝える」ことです。
hookの組み方はPostToolUse hookのLint自動修正、テストを回す形はClaude Code hooksでテストを自動実行するにも書かれています。
1ファイルだけを解析する形は、他ファイルの関数定義を探索設定に頼ります。scanDirectories を整えてから、自分のコードで結果を確かめて常用してください。
よくあるつまずき
- level 0でもエラーが山のように出る。未知の関数の大半は読み込み設定の不足です。
scanDirectoriesとbootstrapFilesを先に整えます。 - 設定ファイルを置いたのに、levelが効かない。設定ファイルを指定したときは、levelも必ず指定します。指定がなければ既定は0です。
- 件数が多すぎて進まない。範囲を狭めます。
pathsを1ディレクトリに絞り、そこだけを上のlevelまで進めてから次へ移ります。 - 直したのにbaselineの警告が出る。直したエントリが「該当なし」と報告されます。ファイルから該当パターンを消せば止まります。
reportUnmatchedIgnoredErrors: falseで止められますが、直し漏れの目印も消えます。 - level 6で型エラーが急に増える。型宣言の欠落をすべて報告する段階です。PHP 5時代のコードにはネイティブの型がほとんど無いので、増えて当然です。array shapeを先に入れておくと、この段階で報告される内容を絞りやすくなります。
まとめ
素の関数コードの型付けは、PHPDocのarray shapeで配列の形を言葉にするところから始まります。PHPStanのlevelは0から1段ずつ上げ、今は直せないエラーだけをbaselineに退避します。Claude Codeには1ファイル・1段のlevelで依頼し、確かめられていないキーは mixed と書かせます。この取り決めをCLAUDE.mdとhookで機械的に支えれば、型の精度が作業の速さに食われにくくなります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。