🐈

自社プロダクトのPHPのバージョンを7.3から8.4まで上げた

に公開

最近、弊社が提供しているプロダクト (lancers.jp) のPHPバージョンを7.3から8.4に上げました。

前提

PHP7.3のまま 3~4 年停滞しており、とっくにセキュリティサポートが切れている状況はさすがにマズイでしょうと1年ほど時間をとって対応することになりました。
しかしながら、現実の業務には机上とは異なり様々な障壁がつきものです。

  • 人員に余裕がない

少人数、極端にいえば1人でも無理なく修正対応ができる状態を作ることが望ましいと考えました。

  • テストが少ない

残念ながらほとんどテストコードがありません。テストが書きやすい構造になっておらず、追加するにもかなり大変な状態です。どのように変更の前後で動作を担保するかが課題です。

  • 新規開発の手もサービスの運用も止める選択肢は当然ない

他の開発者の進捗を極力阻害しないように進めることも重要でした。

方針

テストがないため、静的解析を極力活用することにしました。

PHPのバージョンアップをする上で困難なことは、下位互換性のない変更によってどこが壊れてしまうのかを「見つける」ことでしょう。
すべてのコードを目視で確認するのは現実的ではありませんし、確実でもありません。ある標準関数が廃止される、ぐらいなら大抵は grep でも済みますが、文字列結合で識別子を組み立ててコールできてしまうPHPでは完全に安心できる対応とは言えませんし、単純な検索では対応が難しい項目もあります。

幸い、baseline で大量にエラーを抑えている状態とはいえPHPStanは導入されていたので、静的解析は使える状態にありました。そこで、コードだけでなく baseline 内のエラー情報を参照しながら、下位互換性のない変更項目を一つずつ確認し、修正対応とリリースを小さく繰り返し、地道に潰していくことにしました。

また、一度対応が完了した変更項目も、並行で開発している誰かが気づかずにまたプロダクトコードに含めてしまう可能性があります。
そこで、PHPStan の設定値 phpVersion を先んじて目標のバージョン番号にしておき、下位互換性のない変更が入った場合に CI で落ちるようにしました。ただ、PHPStanも全ての下位互換性のない変更を検知できるわけではないため、他の開発者のコミットは随時目視でも確認していました。

parameters:
    phpVersion: 80410

他には、自分の作業量を減らすために、Rector も可能な範囲で活用しました。

https://getrector.com/

Rector は PHPStan に依存して作られたライブラリであり、PHPStan の型システム機能と (PHPStanが依存している) PHP-Parser によるAST操作を駆使して、変更点を「見つける」だけでなく「直す」こともやってくれるライブラリです。
既に用意されているルールを使用すれば、わずか数分で対応が完了してしまう項目もありました。特に、波括弧を使用した配列アクセスの廃止などは grep 検索では対応が難しく、AST操作の恩恵を実感しやすいケースかもしれません。
Rector でスムーズに対応できる項目はさして多くはなかったですし、足りない部分は自分で独自のルールを書いたりもしながらではありましたが、作業時間の削減に役立ったことは確かです。

ちなみに実際にPHPバージョンアップの対応に着手したのは、基本的に私(筆者)を含め2人のみでした(バージョンアップ直前の動作確認段階では、各機能ごとに詳しい方に動作テストに参加いただいたり、インフラ面での対応はSREの方に手伝っていただくことはありました)。
私が修正項目の確認と修正対応を全て行い、もう一人が Playwright を使用してE2Eテストによる動作担保を行うという役割分担です。とはいえ、全ての機能を異常系やコーナーケースも全て含めてE2Eテストでカバーするというのは現実的ではないため、サービス上重要な機能に限定する形をとりました。

問題

この方法の最大の問題は、mixed が混在したコードベースでは PHPStan が完璧には働かない点でしょう。mixed としか判定されない場合に PHPStan にできることは多くありません。型がわかるからこそ、それで何ができるのかがわかるのです。
PHPStanのLevelが10(max)であり、baselineもない状態であれば mixed を完全に排したと言えるでしょうが、それができれば苦労はしねえというものです。

また、PHPStanではそもそもカバーが難しい項目もあります。曖昧比較を使用した際の文字列と数値の比較結果の変更 はその最たるものかもしれません。
外部からの入力値が絡むケースはテストがないと難しいし、厳密比較 === に変えようにも数が多すぎて動作の確認も含めると短期間での対応は現実的ではありません。

これに対して短期間でできる効果的な対策は…残念ながら思いつきませんでした。
PHPStan を信頼しすぎず対応漏れがないか grep 検索などで地道にコードと睨めっこしたり、リリース直前は人手も借りて手動で動作テストを行ったりと、泥臭い対応と祈りで残りの部分を補いました。

リリース

上記の対応を行いながら、PHP7.4, 8.0, 8.1, …と順次リリースしていきました。
山場はやはりメジャーアップデートである8.0だったわけですが、リリース後コーナーケースを突くようなエラーが数件発生はしたものの、影響はかなり軽微なレベルに抑えられました。

残った課題

PHP8はなんとか乗り越えたわけですが、非推奨になった機能の対応には着手できていません。
そう遠くない未来に来ることが予想されるPHP9では、動的プロパティが廃止される予定だったりと、かなり対応が大変になりそうです。

所感

100%完璧に対応できたとは全く言い難いですが、期間が限られている中での対応だったため最後の数%を詰めるために多大なコストをかけるわけにもいかず、このような形になりました。おそらくミッションクリティカルなシステムでは許されないでしょう🥹
しかし、対応にかけた期間は1年強程度ですが、純粋にPHPのバージョンアップのための修正対応をしていた期間は多く見積もっても4ヶ月ぐらいです(EC2サーバーの移行対応など障壁となっていた負債の解消が半分以上の期間を占めています)。
詰めの部分は甘かったですが、その分バージョンの移行を短期間で終えられたのは良かったと言えるかもしれません。

全体通して、静的解析の威力を改めて実感する出来事でした。
テストのない環境でも静的解析は導入のコストが比較的低いので、導入していないなら絶対に入れましょう🐱

ランサーズ株式会社

Discussion