Go to list of users who liked
Share on X(Twitter)
Share on Facebook
この記事はラクス Advent Calendar 2025の7日目の記事です
はじめに
RectorというツールでPHPコードの自動リファクタリングができるらしいので、手元で試してみました。
Rectorとは
対象のPHPコードに対して事前に設定したルールベースで自動的にリファクタリングを実施してくれるツールです。
以下はREADMEからの抜粋です。
- PHP5.3~PHP8.5まで対応可能
- CIに組み込んで自動で違反の検知もできる
インストール
Composer経由でインストールします
composer require rector/rector--dev動かしてみる
インストールが完了したので、READMEに書いてある内容を一通り上から試してみます。
rector.php(設定ファイル)を作る
To use them, create a rector.php in your root directory:
rector.phpをルートディレクトリに作ります。
手動で作成してもいいそうですが、今回はコマンド経由で作ってみます。
vendor/bin/rector上記のコマンドを実行すると以下の質問が出るのでyesと回答
No"rector.php" config found. Should we generate itforyou?[yes]:>yes[OK] The config is added now. Re-runcommandto make Rectordothe work!無事にrector.phpが作られました。
<?phpdeclare(strict_types=1);useRector\Config\RectorConfig;returnRectorConfig::configure()->withPaths([__DIR__.'/src',])// uncomment to reach your current PHP version// ->withPhpSets()->withTypeCoverageLevel(0)->withDeadCodeLevel(0)->withCodeQualityLevel(0);生成された初期設定について
- withPaths
リファクタリングを実行するディレクトリやファイルパスの指定です。今回はsrcディレクトリが自動で指定されましたが、もちろんファイル単位での指定も可能です。拡張子単位での設定もできるようです。 - withPhpSets
The best practise is to use PHP version defined in composer.json. Rector will automatically pick it up with empty ->withPhpSets() method:
ベストプラクティスはcomposer.jsonに定義されているPHPバージョンを指定することのようで、引数の指定はしなくても自動でいい感じに読み取ってくれるそうです。
この辺はドキュメントを読んだ限りではPHP8以前か以降かで指定の仕方が変わってくるみたいですね。
withTypeCoverageLevelwithDeadCodeLevelwithCodeQualityLevel
事前にRector側で用意されているルールを定義しているようです。
引数のレベルの最大値の定義はルールによって異なるようで、src/Configuration/Levels/LevelRulesResolver.phpに実装の詳細がありました。レベルの引数は初期値で0になっていますが、この状態でどう動くかも確認したいので一旦そのまま進めます。
Rectorを実行
さっそく実行してみます。
今回は検証用に、アンチパターンを詰め込んだサンプルファイルに対してRectorを実行してみます。
型定義がされてなかったり、記法が古かったり、デッドコードが残っていたりしています。
<?phpdeclare(strict_types=1);namespaceApp;classExample{private$name;private$items;publicfunction__construct($name){$this->name=$name;$this->items=array();}publicfunctiongetName(){return$this->name;}publicfunctionaddItem($item){array_push($this->items,$item);}publicfunctiongetItems(){return$this->items;}publicfunctionhasItems(){if(count($this->items)>0){returntrue;}else{returnfalse;}}publicfunctionfindItem($needle){foreach($this->itemsas$key=>$value){if($value==$needle){return$key;}}returnnull;}privatefunctiondeadCode():void{}}以下コマンドを実行します。
vendor/bin/rector process--dry-run実行結果は以下のようになりました。
1/1[▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%1 file with changes===================1) src/Example.php:7---------- begin diff----------@@ @@ class Example{- private$name; private$items;- publicfunction__construct($name)+ publicfunction__construct(private$name){-$this->name=$name;-$this->items= array();+$this->items=[];}----------- end diff-----------Applied rules:* LongArrayToShortArrayRector* ClassPropertyAssignToConstructorPromotionRector[OK] 1 file would have been changed(dry-run) by Rector型宣言のあたりとかが検知されていないようですが、--dry-runオプションを外して以下のコマンドを実行し、変更を反映します。
vendor/bin/rector src変更は無事適用されましたが、--dry-run実行時に確認した通りコンストラクタプロパティプロモーションと配列の短縮記法しか修正されませんでした。
型定義やデッドコード削除のあたりは初期設定のままだと直してくれなさそうなので、設定を見直してみます。
設定変更
rector.phpで使用しているwithTypeCoverageLevel()の内部実装を確認してみました。
TypeDeclarationLevelクラスにルールの配列が定義されており、withTypeCoverageLevel(n)を呼ぶと、この配列の先頭からn番目までのルールが適用される仕組みになっています。
/** * The rule order matters, as its used in withTypeCoverageLevel() method * Place the safest rules first, follow by more complex ones * * @var array<class-string<RectorInterface>> */publicconstRULES=[// php 7.1, start with closure first, as safestAddClosureVoidReturnTypeWhereNoReturnRector::class,AddFunctionVoidReturnTypeWhereNoReturnRector::class,AddTestsVoidReturnTypeWhereNoReturnRector::class,ReturnIteratorInDataProviderRector::class,ReturnTypeFromMockObjectRector::class,TypedPropertyFromCreateMockAssignRector::class,AddArrowFunctionReturnTypeRector::class,BoolReturnTypeFromBooleanConstReturnsRector::class,ReturnTypeFromStrictNewArrayRector::class,// scalar and array from constantReturnTypeFromStrictConstantReturnRector::class,StringReturnTypeFromStrictScalarReturnsRector::class,...AddClosureParamTypeFromIterableMethodCallRector::class,TypedStaticPropertyInBehatContextRector::class,];つまり、->withTypeCoverageLevel(n)のような書き方をする場合はこの配列の順番を確認して、どこまで適用したいかを引数で指定してあげる必要があるそうです。
上記コードのPHPDocでも明記されていました。
The rule order matters, as its used in withTypeCoverageLevel() method
rector.phpを以下のように書き換えます。引数を適当に100に増やしてみました。
returnRectorConfig::configure()->withPaths([__DIR__.'/src',])->withPhpSets()->withTypeCoverageLevel(100)->withDeadCodeLevel(100)->withCodeQualityLevel(100);ルールの数をオーバーするとwarningが出るようです。最大レベルまで厳しく検証したい場合は->withPreparedSetsの使用を推奨されました。実際のプロジェクトでRectorを採用する場合はレベルの上限値をどうするかは慎重に検討する必要がありそうです。
今回はただの検証用途なので、推奨の通り->withPreparedSetsに書き換えます。
[WARNING] The"->withTypeCoverageLevel()" level contains only 63 rules, but yousetlevel to 100. You are using the fullsetnow! Time to switch to more efficient"->withPreparedSets(typeDeclarations: true)".[WARNING] The"->withDeadCodeLevel()" level contains only 55 rules, but yousetlevel to 100. You are using the fullsetnow! Time to switch to more efficient"->withPreparedSets(deadCode: true)".[WARNING] The"->withCodeQualityLevel()" level contains only 77 rules, but yousetlevel to 100. You are using the fullsetnow! Time to switch to more efficient"->withPreparedSets(codeQuality: true)".->withPreparedSetsを使うようにrector.phpを修正して再実行してみます。
returnRectorConfig::configure()->withPaths([__DIR__.'/src',])->withPhpSets()->withPreparedSets(typeDeclarations:true,deadCode:true,codeQuality:true,);再実行結果は以下です。
変更差分は明らかに増えました。
1/1[▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%1 file with changes===================1) src/Example.php:6---------- begin diff----------@@ @@ class Example{- private$items;+ private array$items=[]; publicfunction__construct(private$name){-$this->items=[];} publicfunctiongetName()@@ @@return$this->name;}- publicfunctionaddItem($item)+ publicfunctionaddItem($item): void{- array_push($this->items,$item);+$this->items[]=$item;} publicfunctiongetItems()@@ @@return$this->items;}- publicfunctionhasItems()+ publicfunctionhasItems(): bool{if(count($this->items)> 0){returntrue;@@ @@}}returnnull;-}-- privatefunctiondeadCode(): void-{}}----------- end diff-----------Applied rules:* InlineConstructorDefaultToPropertyRector* ChangeArrayPushToArrayAssignRector* RemoveUnusedPrivateMethodRector* AddVoidReturnTypeWhereNoReturnRector* BoolReturnTypeFromBooleanConstReturnsRector* TypedPropertyFromAssignsRector[OK] 1 file would have been changed(dry-run) by Rectorもう一度--dry-runしてみます。
$itemsにarrayの型定義が追加されたことでさらに変更点が見つかったようです。
Rector実行→修正→Rector実行...とこの辺りのハンドリングは人間がやる必要がありそう?詳しくは調べていないのでもっと効率の良いやり方があるかもしれません。
1/1[▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%1 file with changes===================1) src/Example.php:22---------- begin diff----------@@ @@$this->items[]=$item;}- publicfunctiongetItems()+ publicfunctiongetItems(): array{return$this->items;}@@ @@}}- publicfunctionfindItem($needle)+ publicfunctionfindItem($needle): int|string|null{ foreach($this->items as$key=>$value){if($value==$needle){----------- end diff-----------Applied rules:* ReturnTypeFromStrictTypedPropertyRector* ReturnUnionTypeRector[OK] 1 file would have been changed(dry-run) by Rectorルールを追加
まだ変更してほしいポイントが残っています。
引数に型定義がないところや、hasItems()で早期リターンでシンプルに書けるところなど...
<?phpdeclare(strict_types=1);namespaceApp;classExample{// 型ヒントを@varで入れてあげれば良さそう?privatearray$items=[];// $nameの型定義はRectorでは難しそうpublicfunction__construct(private$name){}publicfunctiongetName(){return$this->name;}publicfunctionaddItem($item):void{$this->items[]=$item;}publicfunctiongetItems():array{return$this->items;}// 早期リターンできるpublicfunctionhasItems():bool{if(count($this->items)>0){returntrue;}else{returnfalse;}}publicfunctionfindItem($needle):int|string|null{foreach($this->itemsas$key=>$value){if($value==$needle){return$key;}}returnnull;}}ルールを二つ追加しました。earlyReturnで早期リターンにしてくれそう。
->withPreparedSets(typeDeclarations:true,deadCode:true,codeQuality:true,earlyReturn:true,codingStyle:true,);早期リターンについては想定通り修正されました。
earlyReturn + codingStyleでcountの結果比較から!== []へとロジックの変更もさらっと行われています。
1/1[▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%1 file with changes===================1) src/Example.php:29---------- begin diff----------@@ @@ publicfunctionhasItems(): bool{-if(count($this->items)> 0){-returntrue;-}else{-returnfalse;-}+return$this->items!==[];} publicfunctionfindItem($needle): int|string|null@@ @@return$key;}}+returnnull;}}----------- end diff-----------Applied rules:* SimplifyIfReturnBoolRector* CountArrayToEmptyArrayComparisonRector* NewlineAfterStatementRector* RemoveAlwaysElseRector[OK] 1 file would have been changed(dry-run) by Rector残すはname関連の型定義だけですが、想定通りRectorでの自動修正は無理そうです。
$itemsについては修正前からarray()で初期化していたのでそこから解析されていそう。
classExample{private$name;private$items;publicfunction__construct($name){$this->name=$name;$this->items=array();}型定義はどこまで自動でできるのか
nameはコード上から推論できる情報がなかったのでRectorでの自動変更はできませんでした。
当然ですがコンストラクタで$nameにstringの型をつけた後にRectorを実行すると、メソッドの返り値の型を修正してくれます。
-publicfunctiongetName()+publicfunctiongetName():string{return$this->name;}Rector offer ruleset to fill known type declarations.
公式ドキュメントには既知の型宣言を補完するための...と書かれているのでこれが基本方針ですね。RectorはPHPStanを内部で使った静的解析で変更差分を探しているので、型情報がないコードに対して推測で型を付けることはしない設計になっているようです。
修正前後の差分
こちらから確認できます。
可能な範囲でしっかりリファクタリングしてくれました。これが自動でできるなら十分だなという感想です。
気になったところ
少し長くなってしまったので、ドキュメントを読んで他に気になったことを簡単にまとめました。
- ドキュメントにサンドボックス環境があります。さらっとお試しで実行するのに良さそうです。
- composerベースの設定ができるそうです。バージョンを意識しなくても自動でいい感じに設定してくれるのはうれしいです。
- ルールの検索ページがあります。現時点で787個もルールがあるそう
- カスタムルールの設定もできるそうです。カスタムルールに対するテストコードの説明もありました。
- Rectorはコードの解析にAST(抽象構文木)を使っている
- ASTへ変換してくれるサンドボックスもありました。カスタムルールを作る時とかに役立ちそうです。
- ブログでテストコードの改善例も紹介されていました。プロダクトコードももちろんですが、テストコードも古い記法が残ったままだったり適切なassertで実装されていないところもありがちなので活用できそうです。
最後に
最近はClaude Codeなどのコーディングエージェントが優秀なので、Rectorを使わなくても低コストでリファクタリングはできそうな気もしています。
ただ、明確なルールを宣言的に定義できるので、破壊的な変更を行なってほしくない状況ではRectorの強みを活かせそうです。
むしろ、Rectorを補助輪としてClaude Code等と併用することで、安全かつ高速にリファクタリングやPHPバージョンアップを進められそうだなと思いました。
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme

