13
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

rectorphp/rector deep dive ~PHPStanと併用しプロダクションのPHPアプリケーションを大規模リファクタリングする方法~

Posted at

一般的なサービスでは、DI やフレームワークを使用していると思います。その場合、デフォルトの設定でRector を入れただけではコードの追跡が不完全で、期待通りにリファクタリングできません。しかし PHPStanと併用し設定をしっかり記述すれば、力を最大限引き出せます。

弁護士ドットコムでは、Yii1 のプロダクションコードに PHP 7.4 Typed Properties のルールを適用し、530 のクラス変数に型を付けました。その過程で調べたことをまとめました。

またLaravelを例にどのような設定が必要か紹介します。(Yii1は需要ないと思うので)

3 文まとめ

  • Rector を使えば、とても簡単に PHP コードを自動リファクタリングできる
  • Rector は内部で PHPStan を利用している
  • 依存関係の複雑なプロジェクトでも、PHPStan の設定を読み込ませれば Rector で綺麗にリファクタリングできる

Rectorとは?

Rector instantly upgrades and refactors the PHP code of your application. It can help you 2 major areas:

  1. Instant Upgrades
  2. Automated Refactoring

簡単に自動でPHPコードをリファクタリングしてくれるツールです。

すでに存在するたくさんのルールを柔軟に選択し、実行できます。もちろん拡張ルールも作成できます。とても簡単です。

どんなリファクタリングができるか

現在(2021/07/31)、500 弱の公式ルールが公開されています。非公式で有志が作ったルールもたくさんあります。

私が使用した PHP7.4 Typed Properties や、最近のルールだと下記のユニオンタイプを定義してくれる PHP 8.0 UnionTypesRector は需要がありそうです。

 class SomeClass
 {
-    /**
-     * @param array|int $number
-     * @return bool|float
-     */
-    public function go($number)
+    public function go(array|int $number): bool|float
     {
     }
 }

PHPStanとは?

PHPStan focuses on finding errors in your code without actually running it. It catches whole classes of bugs even before you write tests for the code.

コードを実行することなく、バグを見つける静的解析ツールです。Rector は内部の型推論に PHPStan を使っています。

検知できるエラー

型の不整合、PHPDoc の不備、存在しない関数やデッドコードなどさまざまなエラーが検知できます。

Rector のインストールと実行

ローカルの場合

// install
$ composer require rector/rector --dev
// execute
$ vendor/bin/rector process src --dry-run

公式 Docker イメージが提供されているので、Dockerでも可能です。

$ docker run --rm -v $(pwd):/project rector/rector:latest process src --dry-run

Rector のオプション

Rectorはどのルールを適用するか、実行ディレクトリなどのオプションを指定したファイルを置く必要があります。

use Rector\Php74\Rector\Property\TypedPropertyRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    $services = $containerConfigurator->services();
    $services->set(TypedPropertyRector::class);
};

Rector は宣言が見つからなくてもエラーを出さない

Rector は、宣言(クラス、関数etc)が見つからなくてもエラーはでません。debug モードでも。私も最初気付かず「意外と動かないもんだな」と思いました。

たとえば、下記のディレクトリ構成でsrc 配下に対して実行します。

$ tree

.
├── extensions
│   └── TestService.php
├── rector.php
└── src
    └── SomeClass.php
src/SomeClass.php
declare(strict_types=1);

class SomeClass
{
    /**
     * @var TestService $service
     */
    private $service;

    private function __construct(TestService $service)
    {
        $this->service = $service;
    }
}
extensions/TestService.php
declare(strict_types=1);

class TestService
{

}
rector.php
use Rector\Php74\Rector\Property\TypedPropertyRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    $services = $containerConfigurator->services();
    $services->set(TypedPropertyRector::class);
};
$ docker run --rm -v $(pwd):/project rector/rector:latest process src --dry-run

 0/1 [░░░░░░░░░░░░░░░░░░░░░░░░░░░░]   0%
 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
 [OK] Rector is done!

ルールには PHP7.4 の Typed Properties を指定しており、SomeClass の$serviceに型がつくことを期待しています。
ですが、何も変化がありません。なぜでしょうか?

解析対象がsrc下のみで、TestService クラスが見つかっていないためです。TestService クラスをsrc 下に移動させると結果が変わります。
この例は非常にわかりやすいですが、コード量が多くなってくると一部解析に失敗していても意外と気づきません。きちんと依存クラスすべてを解析できるように準備してあげましょう。

$ docker run --rm -v $(pwd):/project rector/rector:latest process src --dry-run
 0/2 [░░░░░░░░░░░░░░░░░░░░░░░░░░░░]   0%
 1/2 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░]  50%
 2/2 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
1 file with changes
===================

1) src/SomeClass.php

    ---------- begin diff ----------
@@ @@

 class SomeClass
 {
-    /**
-     * @var TestService $service
-     */
-    private $service;
+    private \TestService $service;

     private function __construct(TestService $service)
     {
    ----------- end diff -----------

Applied rules:
 * TypedPropertyRector (https://wiki.php.net/rfc/typed_properties_v2#proposal)


 [OK] 1 file would have changed (dry-run) by Rector

Rector & PHPStan のクラス解析の仕組み

0.10 以降、Rector は composer オートロードを使わず、さらに PHP コードを実行しないでプログラムを解析しています。PSR-4 のオートロード形式に従ったコードをBetterReflectionで定義から AST を抽出しクラスを見つけます。PHPStanも同じです。

結果、下記のような副作用とクラス/関数宣言が混在したコードでも問題ありません。

function doSomething(): void
{
    // ...
}

doSomething();

うちのような composer ではない Yii1 のカスタムオートロードを使っていても、設定に記述すれば読み込めます。

rector.php
use Rector\Core\Configuration\Option;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    $parameters = $containerConfigurator->parameters();

    // 事前に読み込むファイルを追加できます。
    $parameters->set(Option::AUTOLOAD_PATHS, [
        // 特定のファイルで
        __DIR__ . '/file-with-functions.php',
        // ディレクトリすべて
        __DIR__ . '/project-without-composer',
    ]);

また Rector 実行時に、起動ファイルを読み込むことも可能です。

rector.php
    $parameters->set(Option::BOOTSTRAP_FILES, [
        __DIR__ . '/constants.php',
        __DIR__ . '/project/special/autoload.php',
    ]);

Rector の PHPStan の設定

内部の型推論に PHPStan を使っているためPHPStanの設定を拡張できます。
個人的には、このオプションが一番重要だと思います。これによってrectorが全てのクラスを読み込み最適な形でリファクタリングできるからです。

Path to phpstan with extensions, that PHPSTan in Rector uses to determine types

rector.php
    $parameters->set(
        Option::PHPSTAN_FOR_RECTOR_PATH,
        getcwd() . '/phpstan-for-config.neon'
    );

Laravelを例にPHPStan * Rector

PHPStanは、ラッパーであるlarastanを使用しました。

Rectorとlarastanで、PHPStanのバージョン不整合が出るケースがあります。そのため、今回はrectorを先にインストールしました。すでにPHPStanが入ってるプロジェクトだと、多少調整が必要です。

あまりLaravelは詳しくないので、参考程度で。

リファクタリングルールは、PHP 7.4 Typed Propertiesです。

ソースコード全体

主要packageのバージョン

composer.json
        "php": "^7.3|^8.0",
        "laravel/framework": "^8.40",
        "nunomaduro/larastan": "^0.7.12",
        "rector/rector": "^0.11.40"

Rectorの設定

rector.php
use Rector\Core\Configuration\Option;
use Rector\Core\ValueObject\PhpVersion;
use Rector\Php74\Rector\Property\TypedPropertyRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    $parameters = $containerConfigurator->parameters();

    // リファクタリングディレクトリの指定
    $parameters->set(Option::PATHS, [__DIR__ . '/app', __DIR__ . '/tests']);
    // php version指定
    $parameters->set(Option::PHP_VERSION_FEATURES, PhpVersion::PHP_74);
    // phpstan設定ファイルの読み込み
    $parameters->set(Option::PHPSTAN_FOR_RECTOR_PATH, getcwd() . '/phpstan.neon');

    $parameters->set(Option::AUTOLOAD_PATHS, [
        __DIR__ . '/config',
    ]);

    $parameters->set(Option::BOOTSTRAP_FILES, [
        __DIR__ . '/bootstrap/app.php',
        __DIR__ . '/rector-bootstrap.php',
    ]);

    $services = $containerConfigurator->services();
    $services->set(TypedPropertyRector::class);
};

rector-bootstrap.php
class_alias(Illuminate\Config\Repository::class, 'config');

classが見つからないエラーが出たので、class_aliasでごまかしました。:sweat_drops:

PHP Fatal error:  Uncaught Error: Call to undefined method config::get() in /home/taki/develop/komtaki/blog/vendor/nunomaduro/larastan/src/ReturnTypes/GuardDynamicStaticMethodReturnTypeExtension.php:47
Stack trace:
#0 phar:///home/taki/develop/komtaki/blog/vendor/rector/rector/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/MutatingScope.php(3178): NunoMaduro\Larastan\ReturnTypes\GuardDynamicStaticMethodReturnTypeExtension->getTypeFromStaticMethodCall()
#1 phar:///home/taki/develop/komtaki/blog/vendor/rector/rector/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/MutatingScope.php(1442): PHPStan\Analyser\MutatingScope->methodCallReturnType()
#2 phar:///home/taki/develop/komtaki/blog/vendor/rector/rector/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/MutatingScope.php(1448): PHPStan\Analyser\MutatingScope->PHPStan\Analyser\{closure}()
#3 phar:///home/taki/develop/komtaki/blog/vendor/rector/rector/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/MutatingScope.php(440): PHPStan\Analyser\MutatingScope->resolveType()
#4 /home/ta in /home/taki/develop/komtaki/blog/vendor/nunomaduro/larastan/src/ReturnTypes/GuardDynamicStaticMethodReturnTypeExtension.php on line 47

PHPStanの設定

larastonのトップにあるデフォルトそのままです。

igonoreErrorsで指定したエラーに該当するものがないと警告が出るので、reportUnmatchedIgnoredErrorsで無視する設定を追加しました。

phpstan.neon
includes:
    - ./vendor/nunomaduro/larastan/extension.neon
parameters:
    paths:
        - app
    level: 5
    ignoreErrors:
        - '#Unsafe usage of new static#'
    excludePaths:
        - ./*/*/FileToBeExcluded.php
    checkMissingIterableValueType: false
    reportUnmatchedIgnoredErrors: false

リファクタリングしたいファイル

/app/Http/Controllers/TestController.php
namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TestController extends Controller
{
    /** @var Request  $request */
    private $request;
}

実行結果

$ ./vendor/bin/rector process app --dry-run
 19/19 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
1 file with changes
===================

1) app/Http/Controllers/TestController.php

    ---------- begin diff ----------
@@ @@

 class TestController extends Controller
 {
-    /** @var Request  $request */
-    private $request;
+    private \Illuminate\Http\Request $request;
 }
    ----------- end diff -----------

Applied rules:
 * TypedPropertyRector (https://wiki.php.net/rfc/typed_properties_v2#proposal)

 [OK] 1 file would have changed (dry-run) by Rector

まとめ

Laravelのバージョンアップがしたい等であれば、rector-laravelを使うのがよいでしょう。CakePHPやSymfony もあり、フレームワーク固有の処理は非常になります。

それ以外でPHPの新しい言語機能を取り込みたい場合は、PHPStanとつなげると上手くリファクタリングできるでしょう。

Yii1だと何もなく、偶然PHPStan の拡張を自作し後だったため簡単に rector の力を引き出せました。

ただしrectorはリファクタリングがメインなので、修正後コードスタイルが崩れる可能性があります。その場合は、PhpStormのCode Cleanupや、PHP-CS-Fixerを使って分業するのがよいでしょう。

rectorを使いたいけど、うまく導入できていない方の参考になれば幸いです。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?