Claude Media
Claude CodeでレガシーPHPを型付けする — PHPStanのlevelを段階的に上げる手順

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
        - lib

phpVersion は、実行中の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のチェックも含まれます。公式の概要を、レガシーコードの作業に置き換えると次のようになります。

levelPHPStanが見るもの素の関数コードでの作業
0PHPStanが見るもの未知の関数・クラス、引数の個数素の関数コードでの作業読み込み設定を整える
1〜2PHPStanが見るもの未定義の可能性がある変数、全式でのメソッド確認、PHPDocの検証素の関数コードでの作業条件分岐で初期化されない変数を直す
3〜4PHPStanが見るもの戻り値の型、プロパティへの代入型、デッドコード素の関数コードでの作業戻り値のPHPDocを書き始める
5PHPStanが見るもの引数の型素の関数コードでの作業関数ごとに引数の型を揃える
6PHPStanが見るもの型宣言(typehint)の欠落素の関数コードでの作業配列の中身をarray shapeで表す
7〜8PHPStanが見るものunion型の部分的な誤り、nullable型へのアクセス素の関数コードでの作業false や null を返す関数の呼び出し側を直す
9〜10PHPStanが見るもの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か所で決めると、そこから先の関数すべてが同じ型を参照できます。

順序の目安は次のとおりです。

  1. データを組み立てる関数(DB取得、フォーム入力のパース)の戻り値に、array shapeを書く
  2. その配列を受け取る関数の @param に、同じ形を写す
  3. 同じ形が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は、今は直せないエラーを退避する手段として使います。

  1. 現在のlevelでエラー0件を確認する
  2. 設定ファイルを変えずに、1つ上のlevelで件数を測る
vendor/bin/phpstan analyse -l 4 src lib --no-progress
  1. 数十件なら直し、多ければ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で機械的に支えれば、型の精度が作業の速さに食われにくくなります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. phpstan.org/user-guide/rule-levels
  2. phpstan.org/user-guide/getting-started
  3. phpstan.org/user-guide/baseline
  4. phpstan.org/writing-php-code/phpdoc-types
  5. phpstan.org/config-reference
  6. code.claude.com/docs/en/hooks-guide
この記事を共有:XはてブLinkedIn