こんにちは。サーバーエンジニアの平野です。
今回の記事では、私たちが運用中のプロジェクトにPHPの静的解析ツールであるPHPStanを導入した経緯とその効果についてご紹介したいと思います。
コードの品質を高めるためのツールとして、ユニットテストやフォーマッター、プロファイリングツールなどと並んで静的解析ツールが挙げられますが、運用中のプロジェクトに導入するとなるとコストや運用上の影響を考える必要があります。
これらを踏まえ、私たちがPHPStanをどのように導入しどのような効果を得られたのかについてお話しします。
1. PHPStanについて
PHPStanとは、PHPプログラムの静的解析ツールです。型の不一致や未定義の変数・関数などの構文上の間違いを見つけ出すだけでなく、PHPDocの妥当性やデッドコードの存在もチェックできます。PHPのパッケージ管理ツールであるComposerを利用することで簡単に導入することができ、さらにCIにも組み込むことが可能なため、継続的なコード品質のチェックが行えます。
2. プロジェクトに導入した背景
近年のPHPのバージョンでは型に対するサポートが充実してきていますが、今回導入対象となったプロジェクトはPHP5系の時代から運用されてきたレガシーなコードベースで実装されてきました。
プロジェクトでは型宣言やPHPDocの整備が十分に進んでおらず、その結果、アクセスパターンによってはレビューやテストケースでカバーできずに本番環境でランタイムエラーが発生するなど、コードの見通しが立てづらく、機能改修やリファクタリングも難しい状況でした。
これまでコード品質の担保はユニットテストや実機の動作確認が中心でしたが、コードの規模が大きくなるにつれ全体を検証することが難しくなってきました。そのため、プログラム上の問題を自動で検知できるようにすることが求められていました。
社内でフレームワークにLaravelを利用している新規タイトルではPHPStanのラッパーであるLarastanを既に導入していたことから、運用タイトルでも同様に取り込めないかと考え導入に踏み込むことにしました。
3. プロジェクトへの導入
3-1. 解析対象の洗い出しとルールの設定
導入の初めに、PHPStanの設定ファイルであるphpstan.neonを作成し、検査のルールを設定しました。PHPStanが解析するレベルには段階があり、マニュアルでは低い解析レベルから順次取り組んでいくことを推奨していましたが、私たちは最大の検査レベル(Level 9)で導入することにしました。
ただし、全てを一度に解決することはできないため、検査時のルールをカスタマイズしつつ取り組むことにしました。
PHPStanでは検査対象のパスが指定できるので、まずはアプリケーションの中で重要なロジックを担うファイルを中心に解析をはじめました。
一方でチェック対象から除外したいパターンも以下のようにパスや正規表現で指定できるので、静的解析の難しい可変変数のようなコードやフレームワークの組み込み関数、都度指定の難しい自動生成ファイルなどは除外対象として設定しました。
parameters: excludePaths: - vendor/* - tests/* ignoreErrors: - message: "#Call to an undefined method [a-zA-Z0-9\\_]+::methodNotDefined\\(\\)#" - message: "#Access to an undefined property [a-zA-Z0-9\\_]+::\\$undefinedProperty#"
解析結果のうち意図的にチェックをパスしたいコードはbaselineに登録できるので、その場で直ぐに修正できないものはbaselineに登録し、後々付近を改修する時に合わせて修正できるメンバーが解決していくという方法を採りました。
3-2. CIの設定
PHPStanはGitホスティングサービスが提供しているような任意のCIツールで実行することができるため、CIの定義ファイル(コロプラの場合はgitlab-ci.yml)に検査ステップを追加し、コミットがpushされたら解析が実行され、問題があれば開発者に通知するようにしました。
設定当初は静的解析に必要なCIサーバーのメモリが足らず、タイムアウトに突き当たっていたため、CIサーバーのスペックを上げた上で解析の並列化オプションであるparallelを設定して運用していました。
3-3. 導入後のサポート
導入直後はCI上で頻繁に開発者へ通知が飛ぶため、暫くは通知チャンネルを監視しつつ個別にエラー内容を解説したり修正方法をアドバイスしていました。
落ち着いたタイミングでチーム内で共有会を開き、実行時の細かい設定の共有や運用時のフィードバックを得るようにしました。
4. 導入後の効果、課題とその対応
4-1. 導入後の効果
CIに静的解析のステップを導入したことで、実装途中やtopicブランチをマージする前の時点でメンバーがプログラム上の問題に気づくことができるようになりました。
構文上の問題検知はPHPStanに任せることで、開発メンバーはレビュー時にアプリケーションロジックのチェックに集中できるようになりました。
他に開発中のメリットとして、配列の構造をPHPDocに定義できることが挙げられます。これは解析Level9から推奨される機能ですが、PHPDocで配列の添字の名前や要素の型を明記することで、配列の構造をドキュメントから予測できるようになり、実装途中にエディターで補完が効くようになります。入れ子の多重配列の中身を確認するために何度も参照先にコードジャンプする必要がなくなり、コード自体の見通しもよくなりました。
4-2. 拡張機能の導入
導入から半年が経つ頃には、初期にペンディングしていた解析上の課題もかなり解決するようになりました。
プロジェクトではフレームワークに標準実装されていないDIを実現するためにSymfonyのサービスコンテナを使用していましたが、PHPStanの標準の解析ルールからは外れる実装になっているため解析上のノイズになっていました。
これはメンバーがPHPStan側にもフレームワークの拡張機能であるphpstan-symfonyを追加することで、サービスコンテナも含めて解析を行うようになりました。
自動生成系ファイルは都度対応が難しいことから解決を見送っていましたが、大元の生成スクリプトに型情報を付与することで解消していきました。
また、開発者がCIの結果を待たずにローカルでの解析を手軽にできるよう、ワークスペース上で差分のあるファイルだけを解析できる以下のようなmakeコマンドを追加しました。
.PHONY: phpstan-g phpstan-g: @make phpstan PHP_STAN_OPTION="$(shell git status -s | fzf -m --ansi | cut -d ' ' -f 3 | tr '\n' ' ')"
解析結果次第では連鎖的に関連するコードの修正を求められるケースもあるので、開発者はより迅速にフィードバックを得られるようにしています。
4-3. カスタムルールの追加
PHPStanでは予め用意されている検査ルールの他に、カスタムルールと呼ばれる方法でユーザーが独自に検査ルールを定義することができます。
サーバーサイドの実装ではツールで提供されているルール以外にプロジェクト固有の実装ルールがあるため、それらに則った形で実装されているかを検査するためのカスタムルールを追加しています。
例えば、抽選処理等で用いられるPHPの乱数生成の実装には、グローバルステートで副作用のある関数が含まれるため、それらのコードを検知して社内用のライブラリの使用を推奨するカスタムルールを追加しています。
/** * @param Node\Expr\FuncCall $node * @return list<string> */ public function processNode(Node\Expr\FuncCall $node, Scope $scope): array { if (!($node->name instanceof Name)) { return []; } $functionName = $this->reflectionProvider->resolveFunctionName($node->name, $scope) ?? ''; return \in_array(\strtolower($functionName), ['lcg_value', 'srand', 'mt_srand', 'rand', 'mt_rand', 'shuffle', 'str_shuffle', 'array_rand'], \true) ? ["PHP の Combined LCG 及びメルセンヌ・ツイスタを利用する関数はグローバルステートに依存しています。互換ライブラリを使用してください。 "] : [] ; }
※ コードはサンプル用に加筆修正しています。
サーバーロジックの中では計算の中で浮動小数点数を扱う箇所がありますが、丸め誤差が発生するのを防ぐため、浮動小数点数を含む四則演算等を検出し、より正確な算術関数を使用するよう促すルールも追加しています。
/** * @param Node\Expr\BinaryOp $node * @return list<string> */ public function processNode(Node\Expr\BinaryOp $node, Scope $scope): array { if (( $node instanceof Node\Expr\BinaryOp\Plus || $node instanceof Node\Expr\BinaryOp\Minus || $node instanceof Node\Expr\BinaryOp\Mul || $node instanceof Node\Expr\BinaryOp\Div || $node instanceof Node\Expr\BinaryOp\Mod || $node instanceof Node\Expr\BinaryOp\Pow ) === \false) { // 対象の演算子でない return []; } $rightType = $scope->getType($node->right); $leftType = $scope->getType($node->left); $floatType = new FloatType(); if ($floatType->isSuperTypeOf($rightType)->yes() || $floatType->isSuperTypeOf($leftType)->yes() ) { return ["浮動小数点数を含む演算処理です。BCMath関数を使用して下さい。"]; } return []; }
※ コードはサンプル用に加筆修正しています。
5. まとめ
運用中のプロジェクトでもPHPStanは導入でき、レビューコストを抑え、ランタイムエラーを防ぐことに役立ちました。また、拡張機能を導入したりカスタムルールを追加することでプロジェクト固有のチェックにも対応でき、コード品質の継続的な改善を実現しました。
本記事がPHPStanの導入を検討している方々の参考になれば幸いです。
ColoplTechについて
コロプラでは、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。
connpassおよびX(Twitter)で情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。
また、コロプラではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!
興味を持っていただいた方はぜひお気軽にご連絡ください。