Claude Media
Claude CodeでRectorを使いPHP 7から8へ上げる手順 — dry-runだけ許可する

Claude CodeでRectorを使いPHP 7から8へ上げる手順 — dry-runだけ許可する

PHP 7から8への移行を、Rectorのdry-runだけを許可する権限設定で進めます。差分の読ませ方、適用の区切り方、Rectorの範囲外の挙動変更をテストで洗う流れまで。

PHP 7から8への移行は、Rectorに機械変換を任せ、Claude Codeには差分の読み取りと検証を担当させる分担にすると回しやすくなります。鍵になるのは権限の切り方です。vendor/bin/rector のdry-runだけを許可しておけば、変換の適用は毎回確認の画面で止まります。

Rector(PHPコードの自動アップグレード・リファクタリングツール)は、AST(抽象構文木)を使って書き換えるため、同じルールなら毎回同じ結果になります。Claude Codeに「PHP 8に書き換えて」と丸ごと頼むより、変換は決定的なツールに寄せ、Claudeは差分の分類とテストの実行に使うほうが、レビューする側の負担が読めます。

RectorとClaude Codeの役割を分ける

Rectorの公式ドキュメントは、2種類の使い方を示しています。PHP 5.3から8.5までを対象にした「インスタントアップグレード」と、コード品質を保つ「自動リファクタリング」です。この記事で使うのは前者です。

作業担当理由
match や型宣言などの機械的な書き換え担当Rector理由同じ入力から同じ差分が出る
dry-runの差分を読み、危うい箇所を分類する担当Claude Code理由人間が全行を読む前の下読みになる
テスト・静的解析の実行と失敗の切り分け担当Claude Code理由実行して出力を読む反復が得意
変換の適用可否の判断担当人間理由権限の確認画面で止める
実行時の挙動が変わる箇所の判断担当人間とテスト理由書き換えでは表れない変化がある

Rectorは、PHPとHTMLが混在するファイルでは、適用後に手作業の確認が必要になることがあると明記しています。また、ASTは空白を保持しないため、書き出されるコードの整形が崩れます。整形はECSなどのコーディング規約ツールで戻す前提です。これらはClaude Codeに丸投げせず、人間の目に残す部分です。

権限は「dry-runだけ許可」で切る

Claude CodeのBashルールは、* を含む文字列でコマンド全体にマッチさせます。末尾の * は、直前にスペースがあるとき、引数なしの素のコマンドにもマッチします。この性質を使い、dry-runの形だけを許可します。

{
  "permissions": {
    "allow": [
      "Bash(vendor/bin/rector --dry-run *)"
    ]
  }
}

これを .claude/settings.json に置きます。ルールの効果は次のとおりです。

  • vendor/bin/rector --dry-run と vendor/bin/rector --dry-run src は、確認なしで走ります
  • vendor/bin/rector src のような適用コマンドは、ルールにマッチしないため確認の画面が出ます
  • vendor/bin/rector src --dry-run は、--dry-run が先頭にないためマッチしません。確認の画面が出るだけで、実害はありません

Bashコマンドは、Manualモードでは組み込みの読み取り専用コマンド以外すべて承認が必要です。allowに書いていないコマンドは確認が出ます。つまり、許可したい形をallowに書くだけで、適用は自然に止まります。

denyやaskで適用を塞ごうとしない

「適用だけを塞ぎたい」と考えて、Bash(vendor/bin/rector *) をaskやdenyに入れる方法は逆効果です。ルールはdeny、ask、allowの順に評価され、先にマッチしたものが結果を決めます。askに vendor/bin/rector * を入れると、dry-runもaskにマッチして毎回確認が出ます。denyに入れれば、dry-runも許可できなくなります。allowのほうが具体的でも、順序は変わりません。

権限ルールでは守れない経路

Bashルールは、Claudeが書いたコマンドの文字列に対して働きます。同じプログラムを別の形で呼ぶと、ルールにはマッチしません。ルールを守る側の注意点をまとめます。

  • php vendor/bin/rector --dry-run は、vendor/bin/rector で始まらないため、allowにマッチしません。毎回確認が出るので、CLAUDE.mdに書く実行行は vendor/bin/rector に統一します
  • Dockerなどの環境ランナーは、組み込みの除去対象に入っていません。docker compose exec app vendor/bin/rector --dry-run のように実行するなら、ランナーまで含めた具体的なルール(Bash(docker compose exec app vendor/bin/rector --dry-run *))を書きます
  • && や ; で連結したコマンドは、サブコマンドごとにルールと照合されます。vendor/bin/rector --dry-run && vendor/bin/rector のうち後半が許可されることはありません

deny/askのルールで名前を打ち間違えたときの起動時警告は、Claude Codeの警告「matches no tool」の意味とdenyルールの直し方に書いています。

acceptEditsモードとrector.phpの扱い

acceptEdits モードは、作業ディレクトリ内のファイル編集を自動で受け入れます。このモードでは、Claudeが rector.php を書き換える操作も確認なしで通ります。Rectorのルールセットを広げる変更は適用範囲を左右するので、移行作業ではManualモードのままにして、rector.php の編集は都度見る運用が安全です。

rector.phpはレベル0から始める

Rectorは、vendor/bin/rector を最初に実行すると、rector.php がなければ生成するか尋ねます。移行用の最小構成は次のとおりです。

<?php
 
use Rector\Config\RectorConfig;
 
return RectorConfig::configure()
    ->withPaths([
        __DIR__ . '/src',
        __DIR__ . '/tests',
    ])
    ->withPhpLevel(0);

ここで withPhpLevel() を選ぶ理由は、Rectorのレベルの仕組みにあります。ドキュメントによると、準備済みのルールセットをそのまま有効にすると、レガシーなプロジェクトでは1回の実行でファイルの9割が変わり、レビューできなくなります。レベルは、ルールを安全なものから複雑なものへ並べた先頭N個を有効にする仕組みです。レベル0で1つ、レベル1で2つ、と増えます。

流れは「レベルを1つ上げる → dry-run → 差分をレビュー → 適用してマージ」の繰り返しです。Rectorは composer.json に書かれたPHPバージョンまでのルールしか足さないため、レベルがランタイムの対応を超えて先走ることはありません。ルールが出揃ったら withPhpSets() に切り替えます。

注意点が2つあります。

  • withPhpSets() と withPhpLevel() など、withPhp*() 系のメソッドを同時に使うと、ルールが衝突するためエラーになります。どちらか一方にします
  • withPhpSets() で版を明示するのは、PHP 8以上で動かしているときの書き方です(withPhpSets(php80: true) のように名前付き引数で渡します)。引数なしなら composer.json の版を使います

Laravelなど特定のフレームワークの規約や開発手順は、Claude CodeでLaravel開発 — CLAUDE.md・Laravel Boost・Artisan権限の設定で扱っています。ここでは、フレームワークに依存しない言語バージョンの移行に絞ります。

dry-runの差分をClaude Codeに分類させる

dry-runは、ファイルを変更せずに差分だけを表示します。この出力をClaude Codeに読ませ、変更を種類ごとに分けさせます。

vendor/bin/rector --dry-run --rules-summary

--rules-summary を付けると、どのルールが何ファイルを変えるかが出力されます。長い差分は、まずこの要約で全体の見通しを立てます。出力形式は --output-format で json や github にも切り替えられます。なお出力をファイルへリダイレクトすると、書き込み先のチェックが別に走るので、画面への出力のまま読ませます。

依頼文は、判断を求める観点を絞ると返ってくる内容が安定します。

vendor/bin/rector --dry-run --rules-summary を実行して、
ルールごとの変更ファイル数を表にしてください。
そのうち、実行時の挙動が変わりうるルールと、
見た目だけが変わるルールに分けてください。
ファイルは変更しないでください。

分類の結果は、あくまで下読みです。Claudeが「安全」と判断した変更も、適用の前にレビューを挟みます。差分の検証を別の文脈のサブエージェントに任せる設計は、敵対的レビューとはで整理しています。

変更件数を絞って適用する

適用を許可するときは、変更件数に上限を付けられます。--max-changes=N は、N件の変更で止めて、残りには手を付けません。最初の小さなプルリクエストに向いたオプションで、並列実行は使われなくなります。

vendor/bin/rector --max-changes=20

このコマンドは --dry-run で始まらないため、権限の確認画面が出ます。画面で承認する前に、同じ条件のdry-runの結果を見て、差分の規模が想定どおりかを確かめます。

Rectorの差分だけでは見えない挙動変更を洗う

PHP 8には、構文の書き換えでは表せない非互換な変更があります。PHPのマニュアルが挙げる変更のうち、移行で踏みやすいものを並べます。

変更PHP 7での挙動PHP 8での挙動
数値と非数値文字列の比較PHP 7での挙動0 == "foo" はtruePHP 8での挙動0 == "foo" はfalse
数値と空文字列の比較PHP 7での挙動0 == "" はtruePHP 8での挙動0 == "" はfalse
先頭だけ数値の文字列PHP 7での挙動42 == "42foo" はtruePHP 8での挙動42 == "42foo" はfalse
非数値文字列での算術演算PHP 7での挙動E_WARNINGPHP 8での挙動TypeError
既定のerror_reportingPHP 7での挙動E_NOTICEとE_DEPRECATEDを除くPHP 8での挙動E_ALL
クラス名と同名のメソッドPHP 7での挙動コンストラクタとして扱われるPHP 8での挙動コンストラクタではない

これらは、== の比較結果や、警告の出方が変わる性質のものです。Rectorの差分に現れるかどうかは、ルールの有無によります。差分の確認だけで済ませず、テストで洗うのが確実です。

Claude Codeに頼む作業は、次のように具体化します。

  1. == や != の比較に、文字列と数値が混ざりうる箇所を grep で洗い出させる
  2. 該当箇所に、境界値(0、空文字列、"42foo" など)を入れたテストを追加させる
  3. vendor/bin/phpunit を実行させ、失敗を種類ごとに報告させる
  4. エラーログの設定を確認し、E_ALL で出るようになった警告を一覧にさせる

テストや静的解析の実行も、頻繁に回すなら権限を許可しておきます。

{
  "permissions": {
    "allow": [
      "Bash(vendor/bin/rector --dry-run *)",
      "Bash(vendor/bin/phpunit *)",
      "Bash(vendor/bin/phpstan analyse *)"
    ]
  }
}

型エラーを段階的に減らす流れは、Claude CodeでPHPStan(Larastan)の型エラーを減らすにまとめています。Rectorの型付け系のレベルと組み合わせると、型の穴をつぶす順序を決めやすくなります。

match は、PHP 8で予約語になりました。match という名前を識別子として使っているコードがあると、移行後にそのままでは動きません。Rectorの ChangeSwitchToMatchRector は switch を match 式に書き換えるルールですが、これは構文を新しくする側の変換です。旧コードが match という名前を使っていないかは、別途 grep で調べます。

CLAUDE.mdに書く取り決め

移行作業中の取り決めは、CLAUDE.mdに書いておくと、セッションが変わっても守られます。

## PHP 8 移行
 
- Rectorの実行は `vendor/bin/rector --dry-run ...` の形で書く
  (`--dry-run` を必ず先頭の引数にする)
- 変換を適用するコマンドは、実行前にdry-runの結果を要約して見せる
- 1回の適用は、レベルを1つ上げた分だけにする
- `rector.php` を編集するときは、変更内容を先に説明する
- 適用後は `vendor/bin/phpunit` を実行し、失敗を報告する

CLAUDE.mdは、Claudeの振る舞いを方向づけるだけで、強制する仕組みではありません。強制は先に書いた権限ルールが担当し、CLAUDE.mdは「どの形で書くか」を揃える役目です。

よくあるつまずき

  • dry-runなのに毎回確認が出る。コマンドの形がルールとずれています。vendor/bin/rector src --dry-run のように --dry-run が後ろにあるか、php vendor/bin/rector と書かれていないかを見ます。CLAUDE.mdで書き方を揃えます。
  • レベルを上げても差分が出ない。composer.json のPHPバージョンまでのルールしか有効になりません。バージョンが7系のままだと、8系のルールは足されません。
  • 整形が崩れた差分が出る。RectorはASTを使うため、空白や整形を保持しません。コーディング規約ツールで整形し直す手順を、適用の後に入れます。
  • PHPとHTMLが混在するテンプレートが壊れた。混在ファイルは、適用後に手作業での確認が必要です。ビュー層は、Rectorの対象パスから外す選択もあります。
  • withPhpLevel() と withPhpSets() の併用でエラーになる。どちらか一方に統一します。

まとめ

PHP 7から8への移行は、変換をRectorに、読み取りと検証をClaude Codeに分けると進めやすくなります。権限は Bash(vendor/bin/rector --dry-run *) のallowだけに絞り、適用は確認の画面に任せます。askやdenyで適用を塞ぐ方法は、dry-runまで巻き込むので避けます。

差分は、レベルを1つずつ上げて小さく保ちます。Rectorの範囲外にある比較演算の結果や警告の出方の変化は、境界値のテストで人間が確かめる領域です。Claude Codeにはそのテストを書かせ、実行させ、失敗を報告させるところまでを任せると、移行の全体を見渡しやすくなります。

Terraformのplan差分のように「変更を適用する前に差分を読む」運用は、Claude CodeでTerraformのコードを書く手順にも通じる考え方です。

参照した一次ソース

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

  1. code.claude.com/docs/en/permissions
  2. github.com/rectorphp/rector
  3. getrector.com/documentation/levels
  4. php.net/manual/en/migration80.incompatible.php
この記事を共有:XはてブLinkedIn