3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rectorで始めるPHP自動リファクタリング

Posted at

この記事はラクス Advent Calendar 2025の7日目の記事です

はじめに

RectorというツールでPHPコードの自動リファクタリングができるらしいので、手元で試してみました。

Rectorとは

対象のPHPコードに対して事前に設定したルールベースで自動的にリファクタリングを実施してくれるツールです。

以下はREADMEからの抜粋です。

  • PHP5.3~PHP8.5まで対応可能
  • CIに組み込んで自動で違反の検知もできる

スクリーンショット 2025-12-07 17.47.40.png

インストール

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が作られました。

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以前か以降かで指定の仕方が変わってくるみたいですね。

Rectorを実行

さっそく実行してみます。
今回は検証用に、アンチパターンを詰め込んだサンプルファイルに対してRectorを実行してみます。
型定義がされてなかったり、記法が古かったり、デッドコードが残っていたりしています。

Example.php
<?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番目までのルールが適用される仕組みになっています。

vendor/rector/rector/src/Config/Level/TypeDeclarationLevel.php

/**
 * 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に増やしてみました。

rector.php
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を修正して再実行してみます。

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()で早期リターンでシンプルに書けるところなど...

Example.php
<?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()で初期化していたのでそこから解析されていそう。

修正前の一番最初のExample.php
class Example
{
   private $name;
   private $items;

   public function __construct($name)
   {
       $this->name = $name;
       $this->items = array();
   }

型定義はどこまで自動でできるのか

nameはコード上から推論できる情報がなかったのでRectorでの自動変更はできませんでした。
当然ですがコンストラクタで$nameにstringの型をつけた後にRectorを実行すると、メソッドの返り値の型を修正してくれます。

Example.php
-    public function getName()
+    public function getName(): string
     {
         return $this->name;
     }

Rector offer ruleset to fill known type declarations.

公式ドキュメントには既知の型宣言を補完するための...と書かれているのでこれが基本方針ですね。RectorはPHPStanを内部で使った静的解析で変更差分を探しているので、型情報がないコードに対して推測で型を付けることはしない設計になっているようです。

修正前後の差分

こちらから確認できます。
可能な範囲でしっかりリファクタリングしてくれました。これが自動でできるなら十分だなという感想です。

気になったところ

少し長くなってしまったので、ドキュメントを読んで他に気になったことを簡単にまとめました。

最後に

最近はClaude Codeなどのコーディングエージェントが優秀なので、Rectorを使わなくても低コストでリファクタリングはできそうな気もしています。
ただ、明確なルールを宣言的に定義できるので、破壊的な変更を行なってほしくない状況ではRectorの強みを活かせそうです。
むしろ、Rectorを補助輪としてClaude Code等と併用することで、安全かつ高速にリファクタリングやPHPバージョンアップを進められそうだなと思いました。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?