なぜ「リリースを止めない」ことが重要なのか
リリースが止まると、ただ作業が遅れるだけではありません。新規開発が完全にストップすると、事業に影響し、ひいては売上にも関わってくる可能性があります。特にWeb系企業では、突発的なリリースが大きな売上に繋がるケースも珍しくありません。
弊社の場合、ほとんどがtoC向けシステムで構成されており、リリースを止められるのはせいぜい1〜2日が限界です。
そこで今回は、「とにかくリリースを止めないこと」に注力した、Rectorを活用したPHPバージョンアップ手法について解説します。
既存のバージョンアップ手法の問題点
Rectorについて解説する前に、そもそも今までのPHPバージョンアップ手法について確認しましょう。
運用と止めない前提だと「リリースブランチ」のようなとても大きいブランチにを作成して、そこにどんどん変更内容をコミットする手法が一般的です。
ただこの方法の問題点としては以下のようなものが考えられます。
- リリースブランチが大きくなりすぎることで、影響範囲が不透明に、またレビューもコード量から事実上不可能に
- 他の人がリリースした内容をマージする際に以下のような問題が発生する
- 取り込む際にコンフリクトが発生する
- リリースした内容をPHP8.4の内容にリファクタリングしてしまい、リリースが発生するごとに開発コストが増える
これらの問題をRectorで解決します。
Rectorとは
Rectorは、PHPコードを抽象構文木(AST)レベルで自動変換できるリファクタリングツールです。バージョンアップに伴う非推奨APIの置き換えやコードスタイルの一括更新などを、手作業ではなく設定したルールに沿ってCI上で自動化できます。
これにより、手動での変更漏れやブランチの長期化によるリリース遅延を防ぎ、運用を継続しながら迅速にPHPのバージョンアップを進めることが可能になります。
- AST: ソースコードを構文的に解析した中間表現。人間が書いたコードを構造として扱いやすくするための形式
今回やったこと
- PHP7.0からPHP8.4へのバージョンアップ
- Laravel5.4からLaravel12へのバージョンアップ
具体的なバージョンアップフロー
PHP7.0環境とPHP8.4検証環境を同時並行で運用できるようにする ことが大前提です。
そのため、リリースブランチのような大きなブランチは作成しません(ブランチの寿命は短いほどよい)。
RectorとDockerを使い、CI上でPHP7.0環境からPHP8.4環境を毎回自動生成します。
イメージとしては、JavaScriptのトランスパイルのようにPHP8.4用のコードを毎回変換して動かす、という発想です。
PHP8.4環境の生成手順(ざっくり)
- Dockerfile内で、PHP7.0用の
composer.json
をPHP8.4用に書き換えた上でcomposer install
を実行 - Rectorで、PHP7.0向けコードをPHP8.4向けに変換
- 検証環境へデプロイ!
コードについては、できるだけPHP7.0とPHP8.4の両方で同じ挙動になるように事前対応をこまめにリリースします。
それでもどうしても書き分けが必要な部分や、互換が保てない箇所については、Rectorで毎回自動変換させています。
この手順を踏めば、直前までPHP7.0で開発していたとしても、比較的スムーズにPHP8.4対応が可能です。
並行稼働時のCI環境
PHP7.0とPHP8.4それぞれに対して、コミットごとにPHPStanとPHPUnitを実行しています。
どちらか一方の環境が壊れることはありませんでした。
PHP8.4に移行したタイミングでの挙動変化や破損を防ぐためにも、開発した箇所にテストコードを書くのはマストです。
今回利用したルール
Rectorでの自動変換において利用したルールは下記の通りです。
極力公式ルールやOSSのルールで対応したかったのですが、所々対応できておらずカスタムルールとして自作しています。
先行してリリース可能なルールはは、どんどんリリースしてしまっていて、最後まで残ってCIで稼働していたのが下記のルールです。
【公式ルール】RemovePhpVersionIdCheckRector
- if (PHP_VERSION_ID < 80000) {
- openssl_free_key($private_key);
- }
if (PHP_VERSION_ID < 80000) { ... }
のような条件分岐を、指定バージョンに応じて削除してくれるルールです。
このルールでメソッド単位の挙動変更は殆ど対応可能です
例えば上記のように openssl_free_key
のようにPHP8.4で非推奨になった関数を使っている場合、
PHP_VERSION_ID
の分岐を入れておけば、PHP7.0では実行され、PHP8.4では該当コードが削除されます。
【公式ルール】CoversAnnotationWithValueToAttributeRector / DataProviderAnnotationToAttributeRector
use PHPUnit\Framework\TestCase;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\CoversFunction;
-/**
- * @covers SomeClass
- */
+#[CoversClass(SomeClass::class)]
+#[CoversFunction('someFunction')]
final class SomeTest extends TestCase
{
- /**
- * @covers ::someFunction()
- */
public function test()
{
}
}
PHPUnitでPHPDocをAttributeに変換するルールです。
PHPUnit11以降ではPHPDocでのメタデータ記述が非推奨になりました。
ただPHP7.0ではAttribute構文が使えないので、上記ルールを使って一括変換しています。
【カスタムルール】PHPUnitのsetUp()
/ tearDown()
に戻り値として void
をつける
-
対象: PHPUnitの
setUp()
/tearDown()
メソッド -
背景: PHPUnitのバージョンによっては親クラスに
: void
が付与されているため、PHP8.4では同様の戻り値が必要です。 PHP7.0では: void
が書けないため、RectorでPHP8.4時のみ自動で付与します
【カスタムルール】Exception → Throwableへの置換
- public function report(Exception $exception)
+ public function report(\Throwable $e)
{
- parent::report($exception);
+ parent::report($e);
}
-
対象:
App\Exceptions\Handler
クラスのメソッド引数 -
背景: Laravel 7以降、例外ハンドラの引数が
Exception
からThrowable
に変更されました。 Laravelの公式アップグレードガイドに従い、引数を一括で変換します
【カスタムルール】デフォルトnull引数の明示的nullable化
- function __construct(int $hoge_id = null, string $fuga)
+ function __construct(?int $hoge_id, string $fuga)
-
対象: デフォルト値が
null
のメソッド引数 -
背景: PHP8.4では、デフォルトで
null
が設定されている引数は明示的にnullable型として定義する必要があります。
既存の ExplicitNullableParamTypeRector では一部のケースで変換が不十分だったため(後続の引数にデフォルト引数が存在しない場合はデフォルト引数nullを消すロジックがない)、引数の順序も考慮した独自ロジックを追加しています
カスタムルールを書くためのコツ
正直なところ、情報はそこまで多くないです。
なので実際にルールを書く際は、デバッグ中心+他人のルールを読むのが基本スタイルになります。
-
他の人が同じルールを作っていないか確認する
大体ほしいなと思ったルールは既に作られているものです。
車輪の再開発をしても仕方がないので、既存のルールが存在しないか確認しましょう。
→ find-rule
公式になくても、rector-laravelのように有志の方が作っているルールも存在します -
ASTの構造を理解する
Rector公式サイトでは、PHPコードをASTに変換して可視化できるツールが提供されています。
自分のコードがどんなノード構成になっているか、まずはこれで確認してみましょう。
→ ASTに変換して可視化できる公式ツール -
Node検索や置き換えの仕組みを知る
Rectorは、親ノードの検索を直接はサポートしていません。
その代わりにScoped Traverseという機構で、ノードの探索を行います。
NodeFinder
API やNodeVisitor
でスコープを意識した処理を行いましょう。
→ スコープ付きトラバースについての公式記事 -
公式ルールの構造に従ってカスタムルールを作成する
既存ルールのコードを参考にして、自分のプロジェクト要件に合わせたクラスを作成します。
詳しくは公式ドキュメントを参考にしてください。
→ カスタムルールの作成についての公式記事
おまけ
実は、コロプラさんでも同じような記事があって、内容としては二番煎じだったりするので、こちらの記事をよければどうぞ。(この記事がきっかけでRectorを知りました)
Rectorではじめる "運用を止めない" PHPアップグレード
コロプラさんの記事では、PHP8.0からの曖昧比較(==
)の挙動変更に対応するためPHPの独自拡張を作っていますが、小規模プロダクトなら最初から厳密比較(===
)に置き換えておく方が早く、曖昧比較の挙動変更に関してはこちらで対応しました。
この辺の「どこまで自動化するか」「どこまで書き換えるか」はプロダクトの規模や実装次第で、この柔軟に手段を入れ替えられるかどうかが、エンジニアの腕の見せどころかなと思いました。