大規模レガシーアプリにPHPStanを適用する時のTIPS

この記事は[ラクスAdvent Calendar 2022](https://qiita.com/advent-calendar/2022/rakus) 11日目の記事です。

最近、業務でPHPStanの導入を検討しており、まだ途中経過ですが、躓きや期待できそうな点などを書いておきます。

# PHPStanとは
PHPの静的解析ツールで近年人気のツールです。
PHPStanの詳細については世の中にたくさん情報があるのでここでは割愛しますが、かいつまんで特徴を言うと下記のような感じです。

- [解析レベル（0~9）](https://phpstan.org/user-guide/rule-levels)を設定でき、徐々に厳しく設定することができる
- [解析時に無視するエラーの定義ファイル(baseline)](https://phpstan.org/user-guide/baseline)を作成できる
- 一部動的に解析を行うため、PHPをコードとして解釈した上で問題となる箇所も指摘する
- [カスタムで解析ルール](https://phpstan.org/config-reference#custom-ruleset)を追加できる

解析対象の設定や解析レベルなど各種設定は`phpstan.neon`というファイルに定義していきます。

# 前提
- 10年以上のレガシーアプリ
- 解析対象は約4500ファイル
- 名前空間などない
- 諸事情により自作オートローダーあり

# 方針
解析をかけると既存コードのエラーがすごい数になってしまうので、直すのは今後の新規開発や修正分のみとする（まずはこれ以上傷が広がらないようにする）
→baselineを作成する

# 実行できるようになるまで
## 実行時エラーのデバッグ
まずはbaselineを作成しようと解析対象全体に対して実行してみると、そもそも失敗しますね。歴史があって大規模なアプリになってくると、どこにエラーが潜んでいるかわかりません。

```bash
$ 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`オプションをつけて実行すると、下記のようにエラーとなるソースのところでストップします。

```bash
$ php vendor/bin/phpstan --debug
/PATH_TO_APP/app/controllers/SampleController.php
...
/PATH_TO_APP/app/controllers/BadController.php # エラー原因となるソース
```

次に、`-v`オプションをつけて実行するとエラースタックを吐いてくれるので、原因が特定しやすくなります。
```bash
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
```

## 除外ファイルの設定
場合によっては都合上修正は難しい・このファイルは一旦解析対象から外しても問題ないという場合もあるかと思います。
その場合は、設定ファイルで除外設定を行うこともできます。

```phpstan.neon
parameters:
  paths:
    # 解析対象
    - ../app
    - ../config
    …
  excludePaths:
    # 設定系は除外
    - ../app/config*
    # 廃止ソースは除外
    - ../app/controllers/AbondonedController.php
    …
```

余談ですが、baselineは複数作成して読み込むこともできます。
もし与えられた環境のスペックが厳しく、baselineを一括作成するのもままならない場合はディレクトリごとにbaselineを作成していくのもありかなと思います。
```phpstan.neon
includes:
  - baseline/controllers-baseline.neon
  - baseline/models-baseline.neon
  - baseline/services-baseline.neon
　…
```

## カスタムオートローダーの設定
composerなど便利なツールのない時代から継続しているアプリでは、クラスマップを自作していたり、名前空間の設定などなかったり、あちこちで定数ファイルをrequireしていたりなどあるのではないでしょうか。

PHPStanには[bootstrap](https://phpstan.org/config-reference#bootstrap)という設定があり、PHPStanが実行される前にPHPランタイムで何かを初期化する必要がある場合 (独自のオートローダなど)、 独自のブートストラップファイルを提供することができます。

```phpstan.neon
parameters:
	bootstrapFiles:
		- custom-autoloader.php
```

当アプリでも実行に必要な定数ファイルやカスタムオートローダーがあり、これを設定しないとそもそもクラスパスを解決できませんでした。


# 実行時間やリソース消費について
解析対象数は約4500で
- 1コア・メモリ2GB環境で16分ほど
- 8コア・メモリ15GB環境で2~3分ほど
- メモリ消費はいずれも2~2.6GBほど

でした。
完全にコア数に依存していますね。

これはPHPStanが[Parallel processing](https://phpstan.org/config-reference#parallel-processing)に対応しているためです。
設定はデフォルトで有効になっています。

実際8コア環境で実行してみると、下記のようにコア数分workerが起動されています。
```bash
$ 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
```

まだ運用は確定していませんが、解析対象が多いとそれだけ要求スペックや実行時間がかかってくるため、将来的にはそこそこのコア数のマシンを実行環境にしてCIした方が良いだろうなという感じです。

# 使ってみて期待できそうなこと
日々のコードレビューについては
- 機械的にチェックされるため、人の目で見るより取りこぼしが少なく、コード品質向上が期待できる
- 上記効果によりレビュワーの負担が軽減され、より高いレイヤーでのレビューに専念できそう

また、副次的な効果として
- 大規模レガシーアプリのリファクタリングはどこから手をつけるかの判断が難しいが、PHPStanの解析結果とルールレベルを参考に、徐々に改善していくロードマップを描きやすそう

といった期待が持てそうでした。

また新たに工夫や効果が出た際には追記していきます。