👌

RectorによるPHPバージョンアップ

2023/03/10に公開

この記事は?

PHP7.4(7.3) → 8.1へのバージョンアップ対応時にRectorを使用しての自動リファクタリングを行った際の知見を保存しておくための記事になります。
このドキュメントではRectorを初めて触る方への導入部分のみを記載しています。
より詳しく知りたいと思った方は公式のドキュメントを参照してください。

Rectorとは?

Rectorとは、アプリケーションの PHP コードを即座にアップグレードおよびリファクタリングするためのツールになります。
Rectorを使用することで、次期バージョンで非推奨となる機能を排除したり、新規に追加された機能を使用したり、置き換えたりすることを自動でおこなってくれます。
また、Rectorにはリファクタリングの機能も有しており、コード品質を高く保ち続けることができます。
これにより常に最新のPHPバージョンの情報を追い続ける必要がなくなり、よりビジネスロジックの実装に時間をかけることができるようになります。
仕組みとしては、内部的にPHP-Parserによるコード抽出構文ツリー(AST)によるコード解析を行っています。

セットアップ

Rectorのインストール

composerによるパッケージをインストールします。
(開発環境でしか使用しないため、—-dev オプションをつけてください)

$composer require rector/rector --dev

設定ファイルを配置

基本はルートディレクトリにrector.phpを設置し、内容を以下のように変更します。
(別ディレクトリに別ファイル名で作成することも可能です)
※各行のn:の番号は各行の行数を表しています

rector.php
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\SetList;
use Rector\TypeDeclaration\Rector\Property\TypedPropertyFromStrictConstructorRector;

return static function (RectorConfig $rectorConfig): void {
    // register single rule
    $rectorConfig->rule(TypedPropertyFromStrictConstructorRector::class);

    // here we can define, what sets of rules will be applied
    // tip: use "SetList" class to autocomplete sets with your IDE
    $rectorConfig->sets([
        SetList::CODE_QUALITY
    ]);
};

コードの解説

  • L5: return static function (RectorConfig $rectorConfig): void {}
    {}内にRectorの設定値を記述していきます。
    RectorConfigクラスに設定に関するメソッドが実装されています。
  • L7: $rectorConfig->rule(TypedPropertyFromStrictConstructorRector::class);
    自動リファクタにTypedPropertyFromStrictConstructorRectorルールを適用する
  • L11~L13: $rectorConfig->sets([SetList::CODE_QUALITY]);
    複数のルールがまとめてセットされているCODE_QUALITYのルールを適用する

自動リファクタの実行

$vendor/bin/rector process src                            # ディレクトリ
$vendor/bin/rector process index.php                      # 個別ファイル
$vendor/bin/rector process index.php index2.php           # 個別ファイル(複数)
$vendor/bin/rector process index.php index2.php --dry-run # 実行前確認

PHP7.4 → 8.1への自動リファクタリング

設定ファイルの作成

ルートディレクトリに/rectorディレクトリを作成し、/rectorディレクトリ配下にrector.phpファイルを作成
rector.phpの中を下記の内容に置き換えます。

rector/rector.php
use Rector\CodeQuality\Rector\ClassMethod\OptionalParametersAfterRequiredRector;
use Rector\CodeQuality\Rector\Ternary\ArrayKeyExistsTernaryThenValueToCoalescingRector;
use Rector\Config\RectorConfig;
use Rector\Core\ValueObject\PhpVersion;
use Rector\Php71\Rector\FuncCall\RemoveExtraParametersRector;
use Rector\Php73\Rector\FuncCall\JsonThrowOnErrorRector;
use Rector\Php54\Rector\Array_\LongArrayToShortArrayRector;
use Rector\Php74\Rector\Ternary\ParenthesizeNestedTernaryRector;
use Rector\Set\ValueObject\LevelSetList;

return static function (RectorConfig $rectorConfig): void {
	$rectorConfig->paths([__DIR__]);
	// define sets of rules
	$rectorConfig->sets([
	    LevelSetList::UP_TO_PHP_81,
	]);
	$rectorConfig->rules([
	    ParenthesizeNestedTernaryRector::class, // 三項演算子のネスト
	    ArrayKeyExistsTernaryThenValueToCoalescingRector::class, // array_key_exists()対応
	]);
	$rectorConfig->phpVersion(PhpVersion::PHP_81);

	$rectorConfig->skip([
	    '**/tmp/*',
	    '**/css/*',
	    '**/js/*',
	    '**/data/*',
	    '**/doc/*',
	    '**/guide/*',
	    '**/images/*',
	    '**/files/*',
	    '**/sp/*',
	    '**/tpl/*',
	    '**/tests/*',
	    '**/test/*',
	    '**/smarty*',
	    '**/vendor/*',
	    '**/lp/**',
	    '**/rector/**',
	    '**/View/**',
	    'lib/classes/MPO/PDF/tcpdf/*',
	    '/lib/emojiCfgData/*',
	    '/lib/Google/*',
	    '/lib/PHPWord/*',
	    '/lib/royalcanin/*',
	    LongArrayToShortArrayRector::class, // array() -> []への変換(php-cs-fixer側でリファクタを行う)
	    RemoveExtraParametersRector::class, // function method parameter remove
	    JsonThrowOnErrorRector::class, // json_encode()またはjson_decode()にJSON_THROW_ON_ERRORオプションを渡さない
	    OptionalParametersAfterRequiredRector::class, // デフォルト引数の順番を変更するルール
	]);
};

コードの解説

  • L12: $rectorConfig->paths([__DIR__]);
    リファクタを適用するパスを設定する
  • L14~L16: $rectorConfig->sets([LevelSetList::UP_TO_PHP_81]);
    PHP8.1へのバージョンアップをルールをまとめて適用する
  • L17~L20: $rectorConfig->rules([…]);
    個別のルールを適用する
  • L21: $rectorConfig->phpVersion(PhpVersion::PHP_81);
    実行するPHPバージョンを指定する(リファクタリング後のPHP実行バージョン)
  • L23~L50: $rectorConfig->skip([...]);
    特定のファイルやディレクトリ、ルールを除去する

ルール一覧
rector/rector_rules_overview.md at main · rectorphp/rector

独自のリファクタリングルールを作成したい

Rectorには事前に用意されているルールだけでなく、独自のルールを作成し自動リファクタリングを行うことができます。
例として、setから始まるメソッド名(setPassword())をchangeから始まるメソッド名(changePassword())に変更したい場合のリファクタリングを想定します。

  1. 独自ルールの作成
    AbstractRactorを継承した独自クラスを作成します。最低限実装しなければいけないメソッドはgetNodeTypes()rector()になります。
    2つのメソッドで何を実装すべきかはコード解説の方で解説しています。(MyRuleRector.phpはプロジェクトルートのrector/Rules/配下に作成します。)
rector/Rules/MyRuleRector.php
namespace Rector\Rules;

use PhpParser\Node;
use PhpParser\Node\Identifier;
use PhpParser\Node\Expr\MethodCall;
use Rector\Core\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

final class MyRuleRector extends AbstractRector
{
    public function getRuleDefinition(): RuleDefinition
    {
	return new RuleDefinition(
	    'Change method calls from set* to change*.', [
		new CodeSample(
		    // code before
		    '$user->setPassword("123456");',
		    // code after
		    '$user->changePassword("123456");'
		),
	    ]
	);
    }

    public function getNodeTypes(): array
    {
	return [MethodCall::class];
    }

    public function refactor(Node $node): ?Node
    {
	if (! $this->isName($node->name, 'set*')) {
	    return null;
	}

	$methodCallName = $this->getName($node->name);
	$newMethodCallName = preg_replace('#^set#', 'change', $methodCallName);

	$node->name = new Identifier($newMethodCallName);

	return $node;
    }
}

コード解説

  • L12: getRuleDefinition()
    リファクタリングの大まかな概要を記述します。before→afterを書くことで、実装部分を見なくても大まかな変更ルールを理解できるようになります
  • L26: getNodeTypes()
    リファクタリングを適用したNodeタイプを返します。今回の場合は、メソッドの呼び出しの場合のみを変更したいため、MethodCall::classのみを返します。Nodeタイプに関しては、↓こちらのドキュメントに記載があります。
    https://github.com/rectorphp/php-parser-nodes-docs
  • L31: refactor()
    実際のリファクタリング実装部分になります。NULLを返した場合は、そのノードに関してリファクタリングは行わず、受け取った引数のNodeに関して様々な変更を行い、変更したNodeを返すことでリファクタリングが行われます。
  • L33: $this->isName($node->name, 'set*')
    Nodeの名前と期待する文字列と比較を行います。$node->nameで取得できるものはNodeであることに注意してください。
  • L37: $methodCallName = $this->getName($node->name);
    メソッド名をstringで取得します
  • L38: $newMethodCallName = preg_replace('#^set#', 'change', $methodCallName);
    setchangeに置き換えます
  • L40: $node->name = new Identifier($newMethodCallName);
    メソッド名を新しく置き換えたものに変更します。
    MethodCallの場合、var name argsの3つのプロパティを持ち、それぞれのプロパティは以下のような関係を表します
    $this->setPassword('P@ssW0rd');
    -----  ----------- ----------
     var       name       args
    
  1. composerのautoloadの設定を追加
    composer.jsonファイルに以下のようにautoloadの設定を追加

    composer.json
    {
    	...
    	"autoload-dev": {
    		"psr-4": {
    			"Rector\\Rules": "rector/rules",
    		}
    	}
    }
    

    上記変更を行った後は、下記コマンドを実行します。

    $composer dump-autoload
    
  2. rector.phpで独自ルールの指定

    rector.php
    use Rector/Rules/MyRuleRector;
    
    return static function (RectorConfig $rectorConfig): void {
    	...
    	$rectorConfig->rules([
    		...,
    		MyRuleRector::class
    	]);
    }
    

トラブルシューティング

  • リファクタリングが失敗してエラーになった
    → コードが複雑のため、ASTによる解析が行えなくっている状態です。該当ファイルのみ複雑になっている箇所のコードを変更することにより成功する可能性があります

  • サーバーのPHPのバージョンは古いが次回バージョンアップに向けて非推奨のものを無くしたい
    → 設定ファイルのrector.phpで実行するPHPバージョンを指定する
    下記の指定により、PHP8.1で削除される・非推奨になる機能をリファクタリングし、PHP8.1から追加される機能は使用しないようになります。

    $rectorConfig->sets([LevelSetList::UP_TO_PHP_81]);
    $rectorConfig->phpVersion(PhpVersion::PHP_74);
    

Discussion