RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

レガシーコードにPHPStanを導入するためのTIPS

はじめに

こんにちは。楽楽販売の開発チームに所属しているyanahmです。 最近、当チームではPHPStanの導入を段階的に始めています。

この記事ではレガシーコードへの途中からの導入の際に工夫した点についてお伝えします。

したがって、スタンダードなPHPStan導入方法とは少し異なっている部分もあります。 同じような状況の方の一助になれば幸いです。

PHPStanとは

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

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

前提

まず、解析対象となるアプリの現状です。

  • 15年選手のレガシーアプリ
  • 解析対象は約4500ファイル
  • 名前空間なし
  • 諸事情により自作オートローダーあり

歴史あるアプリのため、現在のスタンダードとは離れている部分もあります。

導入戦略

静的解析を途中から導入する方針については、おおまかに下記2パターンが考えられます。

  1. 解析レベルを1番緩いものから始めて、エラーをなくしたら段階的に厳しいものに引き上げる
  2. 既存コードのエラーは無視して、新規開発の範囲だけは厳しいルールで品質を担保する

本来なら1. が望ましいですが、歴史のあるアプリの場合、既存エラーが膨大な数になってしまう/修正による影響範囲が大きいといった問題が出てくると思います。 したがって当アプリでは、まずは新規機能開発の範囲だけでも品質を担保することを目標に 2. を採用しています。

逆に全く新規開発のプロダクトや日の浅いアプリなどの場合は1. の方が全体品質を担保できるので望ましいこともあるかと思います。

運用フロー

まだ現在進行形ですが、現在下記のような形でフローで回しています。

  1. 新規開発着手前に、ベースとなるブランチでの既存エラーを無視するため、baselineを作成
  2. baseline作成済のベースブランチから開発用ブランチを作成
  3. コードレビューが可能な段階でマージリクエストを作成
  4. マージリクエストの作成をトリガーにCI上でPHPStanが実行され、指摘が出る
  5. 担当者が指摘箇所を修正&レビュワーがチェック
  6. 問題なければメインブランチへマージ

ゆくゆくは各担当者の手元で事前に実行できればと考えています。

運用に至るまでのステップ

現在の運用に至るまでの工程について紹介していきます。

解析対象の除外設定

場合によっては現状では修正が難しい/いったん解析対象から外しても問題ないファイルがあるという場合もあるかと思います。 その場合は、設定ファイルのexcludePaths:で除外設定を行うことができます。

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

カスタムオートローダーの設定

composerなど便利なツールのない時代から継続しているアプリでは、クラスマップを自作していたり、名前空間の設定がなかったり、複数個所で定数ファイルをrequireしていることもあるのではないでしょうか。

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

parameters:
    bootstrapFiles:
        - custom-autoloader.php

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

baselineの作成

上記の設定を基に、開発着手前にベースとなるブランチで作成します。
当社ではGitlabを使用しているため、パイプラインの手動実行で実行できるようにしています。

※後述しますが、解析対象が多くPHPStanの実行にメモリを消費するため--memory-limit=2Gを設定していないと途中で失敗しました。

php vendor/bin/phpstan --no-progress --memory-limit=2G --generate-baseline=phpstan-baseline.neon

CIでの解析実行設定

前述の通り、マージリクエストをトリガーにCI上でPHPStanが実行されるようにCIに設定を行います。

弊社ではGitLabを利用しているため、GitLab CIを使用しています。

途中で直面した課題

PHPStan実行時のエラー

baselineを作成するために解析対象全体に対して実行してみると、当初は実行途中で失敗していました。 解析対象が多いレガシーコードのため、最初からスムーズにはいかないことが多いです。

$ 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

プロジェクト固有のエラー

実際に解析を回し始めると、baselineで既存エラーは無視したものの、現状では修正が難しいエラーだが毎回指摘が出てしまいノイズになるというケースがあると思います。 その場合は、無視したいエラーを正規表現で定義しておくことができます。

parameters:
  ignoreErrors:
    -
      message: '#^Access to an undefined property App\\Foo\:\:\$bar\.$#'
      paths:
        -  /{APP_PATH}/app/foo/*
...

これらをbaselineとは別ファイルに定義して読み込ませることも可能です。

includes:
  - project-ignore.neon
  - phpstan-baseline.neon

当チームでは仮運用開始後、そういったものがないかを継続的にマージリクエストをモニタリングし、メンバーからも記入しもらい定期的に棚卸できるようにしています。

本運用に乗せるためにはこの作業が一番重要な作業だと考えています。

パフォーマンスについて

前述の通り当アプリは解析対象数が多く、全ファイル解析するとそれなりのマシンパワーを消費します。 現状検証中でCI用に割り当てたマシンがそれほどスペックが高いものではないため、暫定処置として解析対象を差分ファイルのみに絞り込むようにしました。

  • 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マシンスペックの調整を行う想定です。

余談:リソース消費とスペックについて

解析対象数は約4500で実行にかかる時間は

  • 1コア・メモリ2GB環境:16分ほど
  • 8コア・メモリ15GB環境:2~3分ほど
  • メモリ消費はいずれも2~2.6GBほど

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

これはPHPStanがParallel processingに対応しているためです。 設定はデフォルトで有効になっています。

実際8コア環境で実行してみると、下記のようにコア数分workerが起動されています。

$ 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の解析結果を参考に徐々に改善していく一定の指標になる

といった期待が持てました。

また新たに工夫や効果が出た際にはお知らせしていきます。

Copyright © RAKUS Co., Ltd. All rights reserved.