この記事はラクス 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 it for you? [yes]:
> yes
[OK] The config is added now. Re-run command to make Rector do the work!
無事にrector.phpが作られました。
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
return RectorConfig::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を実行してみます。
型定義がされてなかったり、記法が古かったり、デッドコードが残っていたりしています。
<?php
declare(strict_types=1);
namespace App;
class Example
{
private $name;
private $items;
public function __construct($name)
{
$this->name = $name;
$this->items = array();
}
public function getName()
{
return $this->name;
}
public function addItem($item)
{
array_push($this->items, $item);
}
public function getItems()
{
return $this->items;
}
public function hasItems()
{
if (count($this->items) > 0) {
return true;
} else {
return false;
}
}
public function findItem($needle)
{
foreach ($this->items as $key => $value) {
if ($value == $needle) {
return $key;
}
}
return null;
}
private function deadCode(): 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;
- public function __construct($name)
+ public function __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>>
*/
public const RULES = [
// php 7.1, start with closure first, as safest
AddClosureVoidReturnTypeWhereNoReturnRector::class,
AddFunctionVoidReturnTypeWhereNoReturnRector::class,
AddTestsVoidReturnTypeWhereNoReturnRector::class,
ReturnIteratorInDataProviderRector::class,
ReturnTypeFromMockObjectRector::class,
TypedPropertyFromCreateMockAssignRector::class,
AddArrowFunctionReturnTypeRector::class,
BoolReturnTypeFromBooleanConstReturnsRector::class,
ReturnTypeFromStrictNewArrayRector::class,
// scalar and array from constant
ReturnTypeFromStrictConstantReturnRector::class,
StringReturnTypeFromStrictScalarReturnsRector::class,
.
.
.
AddClosureParamTypeFromIterableMethodCallRector::class,
TypedStaticPropertyInBehatContextRector::class,
];
つまり、->withTypeCoverageLevel(n)のような書き方をする場合はこの配列の順番を確認して、どこまで適用したいかを引数で指定してあげる必要があるそうです。
上記コードのPHPDocでも明記されていました。
The rule order matters, as its used in withTypeCoverageLevel() method
rector.phpを以下のように書き換えます。引数を適当に100に増やしてみました。
return RectorConfig::configure()
->withPaths([
__DIR__ . '/src',
])
->withPhpSets()
->withTypeCoverageLevel(100)
->withDeadCodeLevel(100)
->withCodeQualityLevel(100);
ルールの数をオーバーするとwarningが出るようです。最大レベルまで厳しく検証したい場合は->withPreparedSetsの使用を推奨されました。実際のプロジェクトでRectorを採用する場合はレベルの上限値をどうするかは慎重に検討する必要がありそうです。
今回はただの検証用途なので、推奨の通り->withPreparedSetsに書き換えます。
[WARNING] The "->withTypeCoverageLevel()" level contains only 63 rules, but you set level to 100.
You are using the full set now! Time to switch to more efficient "->withPreparedSets(typeDeclarations:
true)".
[WARNING] The "->withDeadCodeLevel()" level contains only 55 rules, but you set level to 100.
You are using the full set now! Time to switch to more efficient "->withPreparedSets(deadCode: true)".
[WARNING] The "->withCodeQualityLevel()" level contains only 77 rules, but you set level to 100.
You are using the full set now! Time to switch to more efficient "->withPreparedSets(codeQuality: true)".
->withPreparedSetsを使うようにrector.phpを修正して再実行してみます。
return RectorConfig::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 = [];
public function __construct(private $name)
{
- $this->items = [];
}
public function getName()
@@ @@
return $this->name;
}
- public function addItem($item)
+ public function addItem($item): void
{
- array_push($this->items, $item);
+ $this->items[] = $item;
}
public function getItems()
@@ @@
return $this->items;
}
- public function hasItems()
+ public function hasItems(): bool
{
if (count($this->items) > 0) {
return true;
@@ @@
}
}
return null;
- }
-
- private function deadCode(): 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;
}
- public function getItems()
+ public function getItems(): array
{
return $this->items;
}
@@ @@
}
}
- public function findItem($needle)
+ public function findItem($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()で早期リターンでシンプルに書けるところなど...
<?php
declare(strict_types=1);
namespace App;
class Example
{
// 型ヒントを@varで入れてあげれば良さそう?
private array $items = [];
// $nameの型定義はRectorでは難しそう
public function __construct(private $name)
{
}
public function getName()
{
return $this->name;
}
public function addItem($item): void
{
$this->items[] = $item;
}
public function getItems(): array
{
return $this->items;
}
// 早期リターンできる
public function hasItems(): bool
{
if (count($this->items) > 0) {
return true;
} else {
return false;
}
}
public function findItem($needle): int|string|null
{
foreach ($this->items as $key => $value) {
if ($value == $needle) {
return $key;
}
}
return null;
}
}
ルールを二つ追加しました。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 ----------
@@ @@
public function hasItems(): bool
{
- if (count($this->items) > 0) {
- return true;
- } else {
- return false;
- }
+ return $this->items !== [];
}
public function findItem($needle): int|string|null
@@ @@
return $key;
}
}
+
return null;
}
}
----------- end diff -----------
Applied rules:
* SimplifyIfReturnBoolRector
* CountArrayToEmptyArrayComparisonRector
* NewlineAfterStatementRector
* RemoveAlwaysElseRector
[OK] 1 file would have been changed (dry-run) by Rector
残すはname関連の型定義だけですが、想定通りRectorでの自動修正は無理そうです。
$itemsについては修正前からarray()で初期化していたのでそこから解析されていそう。
class Example
{
private $name;
private $items;
public function __construct($name)
{
$this->name = $name;
$this->items = array();
}
型定義はどこまで自動でできるのか
nameはコード上から推論できる情報がなかったのでRectorでの自動変更はできませんでした。
当然ですがコンストラクタで$nameにstringの型をつけた後にRectorを実行すると、メソッドの返り値の型を修正してくれます。
- public function getName()
+ public function getName(): 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バージョンアップを進められそうだなと思いました。
