こんにちは。楽楽販売の開発チームに所属しているyanahmです。
最近、当チームではPHPStanの導入を段階的に始めています。 この記事ではレガシーコードへの途中からの導入の際に工夫した点についてお伝えします。 したがって、スタンダードなPHPStan導入方法とは少し異なっている部分もあります。
同じような状況の方の一助になれば幸いです。 近年人気のPHPの静的解析ツールです。
PHPStanの詳細については世の中にたくさん情報があるのでここでは割愛しますが、かいつまんで言うと下記のような特徴があります。 解析対象の設定や解析レベルなど各種設定は まず、解析対象となるアプリの現状です。 歴史あるアプリのため、現在のスタンダードとは離れている部分もあります。 静的解析を途中から導入する方針については、おおまかに下記2パターンが考えられます。 本来なら1. が望ましいですが、歴史のあるアプリの場合、既存エラーが膨大な数になってしまう/修正による影響範囲が大きいといった問題が出てくると思います。
したがって当アプリでは、まずは新規機能開発の範囲だけでも品質を担保することを目標に 2. を採用しています。 逆に全く新規開発のプロダクトや日の浅いアプリなどの場合は1. の方が全体品質を担保できるので望ましいこともあるかと思います。 まだ現在進行形ですが、現在下記のような形でフローで回しています。 ゆくゆくは各担当者の手元で事前に実行できればと考えています。 現在の運用に至るまでの工程について紹介していきます。 場合によっては現状では修正が難しい/いったん解析対象から外しても問題ないファイルがあるという場合もあるかと思います。
その場合は、設定ファイルの composerなど便利なツールのない時代から継続しているアプリでは、クラスマップを自作していたり、名前空間の設定がなかったり、複数個所で定数ファイルをrequireしていることもあるのではないでしょうか。 PHPStanにはbootstrapという設定があり、PHPStanが実行される前にPHPランタイムで何かを初期化する必要がある場合 (独自のオートローダなど)、 独自のブートストラップファイルを提供できます。 当アプリでも実行に必要な定数ファイルやカスタムオートローダーがあり、これを設定しないとそもそもクラスパスを解決できませんでした。 上記の設定を基に、開発着手前にベースとなるブランチで作成します。 ※後述しますが、解析対象が多くPHPStanの実行にメモリを消費するため 前述の通り、マージリクエストをトリガーにCI上でPHPStanが実行されるようにCIに設定を行います。 弊社ではGitLabを利用しているため、GitLab CIを使用しています。 baselineを作成するために解析対象全体に対して実行してみると、当初は実行途中で失敗していました。
解析対象が多いレガシーコードのため、最初からスムーズにはいかないことが多いです。 このような場合は、 次に、 ちなみに、 実際に解析を回し始めると、baselineで既存エラーは無視したものの、現状では修正が難しいエラーだが毎回指摘が出てしまいノイズになるというケースがあると思います。
その場合は、無視したいエラーを正規表現で定義しておくことができます。 これらをbaselineとは別ファイルに定義して読み込ませることも可能です。 当チームでは仮運用開始後、そういったものがないかを継続的にマージリクエストをモニタリングし、メンバーからも記入しもらい定期的に棚卸できるようにしています。 本運用に乗せるためにはこの作業が一番重要な作業だと考えています。 前述の通り当アプリは解析対象数が多く、全ファイル解析するとそれなりのマシンパワーを消費します。
現状検証中でCI用に割り当てたマシンがそれほどスペックが高いものではないため、暫定処置として解析対象を差分ファイルのみに絞り込むようにしました。 ※注意※ 解析対象数は約4500で実行にかかる時間は でした。コア数に依存していますね。 これはPHPStanがParallel processingに対応しているためです。
設定はデフォルトで有効になっています。 実際8コア環境で実行してみると、下記のようにコア数分workerが起動されています。 現時点では差分のみ解析対象としているためそこまで問題になっていませんが、解析対象が多いとそれだけ要求スペックや実行時間がかかってくるため、状況に応じてCI実行環境のスペックは検討したいところです。 日々のコードレビューについては また、副次的な効果として といった期待が持てました。 また新たに工夫や効果が出た際にはお知らせしていきます。はじめに
PHPStanとは
phpstan.neon
というファイルに定義できます。前提
導入戦略
運用フロー
運用に至るまでのステップ
解析対象の除外設定
excludePaths:
で除外設定を行うことができます。parameters:
paths:
# 解析対象
- ../app
- ../config
…
excludePaths:
# 設定系は除外
- ../app/config*
# 廃止ソースは除外
- ../app/controllers/AbondonedController.php
…
カスタムオートローダーの設定
parameters:
bootstrapFiles:
- custom-autoloader.php
baselineの作成
当社ではGitlabを使用しているため、パイプラインの手動実行で実行できるようにしています。--memory-limit=2G
を設定していないと途中で失敗しました。php vendor/bin/phpstan --no-progress --memory-limit=2G --generate-baseline=phpstan-baseline.neon
CIでの解析実行設定
途中で直面した課題
PHPStan実行時のエラー
$ php vendor/bin/phpstan --generate-baseline
…
[ERROR] An internal error occurred. Baseline could not be generated. Re-run PHPStan without --generate-baseline to see
what's going on.
--debug
オプションをつけて実行すると、下記のようにエラーとなるソースのところでストップします。$ php vendor/bin/phpstan --debug
/PATH_TO_APP/app/controllers/SampleController.php
...
/PATH_TO_APP/app/controllers/BadController.php # エラー原因となるソース
-v
オプションをつけて実行するとエラースタックを出力してくれるので、原因が特定しやすくなります。php vendor/bin/phpstan analyse /PATH_TO_APP/app/controllers/BadController.php -v
-vvv
オプションをつけて実行すると、各ファイル解析時点で消費された合計メモリや解析にかかった秒数も表示されるようになるので、リソースの問題で問題が起きた時のデバッグに役立ちます。/PATH_TO_APP/app/controllers/Sample1Controller.php
--- consumed 36 MB, total 82 MB, took 8.35 s
...
/PATH_TO_APP/app/controllers/Sample2Controller.php
--- consumed 0 B, total 1.25 GB, took 0.74 s
/PATH_TO_APP/app/controllers/Sample3Controller.php
--- consumed 0 B, total 1.29 GB, took 0.16 s
プロジェクト固有のエラー
parameters:
ignoreErrors:
-
message: '#^Access to an undefined property App\\Foo\:\:\$bar\.$#'
paths:
- /{APP_PATH}/app/foo/*
...
includes:
- project-ignore.neon
- phpstan-baseline.neon
パフォーマンスについて
git diff
コマンドを用いて差分ファイルを抽出しています。--diff-filter
オプションでA: 追加 / C: コピー / M: 変更 / R: リネームされたファイルを対象にしています。 # 差分のあるファイルだけ抽出
- cd $CI_PROJECT_DIR
- git diff --name-only --diff-filter=ACMR origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}...${CI_COMMIT_SHA} -- 'app/*.php' | sed -e "s|^|${APP_HOME}\/|g" | sed "s|\n| |g" > target.txt
- >
if [ ! -s target.txt ]; then
echo "No target exists."
exit 0
fi
# 解析実行
- php vendor/bin/phpstan analyse --no-progress --memory-limit=2G $(cat $CI_PROJECT_DIR/target.txt) | sed -e s@$APP_HOME/@@g
実はgit diff
を使用するやり方は公式では推奨されておらず、毎回全体を対象に解析することが望ましいです。
1回解析実行されると結果はキャッシュされるため、2回目以降の解析速度は上がるのですが、ファイル数が多いと1回目にかなり時間がかかってしまうため、暫定処置として行っています。
将来的にはCIマシンスペックの調整を行う想定です。余談:リソース消費とスペックについて
$ top
PID PPID USER P S %CPU %MEM TIME SWAP DATA COMMAND
7755 7736 root 7 R 100.0 4.9 0:17 0 787604 /usr/bin/php -c /usr/lib/php.ini vendor/bin/phpstan worker --configuration /usr/…
7757 7736 root 5 R 100.0 4.9 0:17 0 791832 /usr/bin/php -c /usr/lib/php.ini vendor/bin/phpstan worker --configuration /usr/…
7760 7736 root 1 R 100.0 4.9 0:17 0 787604 /usr/bin/php -c /usr/lib/php.ini vendor/bin/phpstan worker --configuration /usr/…
7753 7736 root 4 R 99.7 5.0 0:17 0 797844 /usr//bin/php -c /usr/lib/php.ini vendor/bin/phpstan worker --configuration /usr/…
7756 7736 root 2 R 99.7 4.9 0:17 0 787604 /usr/bin/php -c /usr/lib/php.ini vendor/bin/phpstan worker --configuration /usr/…
7759 7736 root 6 R 99.7 4.9 0:17 0 781460 /usr/bin/php -c /usr/lib/php.ini vendor/bin/phpstan worker --configuration /usr/…
7758 7736 root 0 R 99.3 4.9 0:17 0 781460 /usr/bin/php -c /usr/lib/php.ini vendor/bin/phpstan worker --configuration /usr/…
7754 7736 root 3 R 99.0 4.9 0:17 0 783508 /usr/bin/php -c /usr/lib/php.ini
使ってみて期待できそうなこと