🙆
[PHP]nullチェック漏れによるエラーを静的解析によって回避したい
背景
担当しているシステムで、nullチェック漏れによる障害が直近何件か発生していました。
PHPで実装しているので、他言語と比較すると型チェックがゆるいためある程度は仕方ないのか...?となっていたところ、
PHPでも静的チェックできないか?という話になりました。
PHPStan
PHPの静的解析ツールを調べていたところ、PHPStanというツールがあることを知りました。
nullチェック防止もできるみたいなので良さそう!
-
PHPStanとは
- PHPの静的解析ツール。 型の不一致や未定義の変数・関数などの構文上の間違いを見つけ出すだけでなく、PHPDocの妥当性やデッドコードの存在もチェックできる。
-
エラーレベルについて
目的に合わせてエラーレベルを設定できる。
今回はnullチェックが目的なのでレベル8以上にすれば良さそう。- 0:基本的なチェック、未知のクラス、未知の関数、$this上で呼び出された未知のメソッド、それらのメソッドや関数に渡された引数の数が間違っている、常に未定義の変数をチェック
- 1:未定義の変数、call と get を持つクラスの未知のマジックメソッドとプロパティがある可能性がある
- 2:($this だけでなく)すべての式で未知のメソッドをチェックし、PHPDocs を検証する
- 3:戻り値の型、プロパティに割り当てられた型の確認
- 4:基本的なデッドコードチェック - instanceofやその他の型チェックが常にfalse、到達しないelse文、return後の到達不能コードなど
- 5:メソッドや関数に渡される引数の型チェック
- 6:タイプヒントの欠落を報告する
- 7:部分的に間違っている論理和型の報告 - 論理和型の一部の型にしか存在しないメソッドを呼び出した場合、レベル7はそのことを報告し始めます(その他の不正確な状況も)
- 8:null可能な型に対するメソッド呼び出しとプロパティへのアクセスを報告する
- 9:混合型に厳密であること - この型で唯一許される操作は、この型を別の混合型に渡すことである
やったこと
PHPStanの導入
- PHPStanのインストール
- コマンド:
composer require --dev phpstan/phpstan
- vender配下にphpstanのディレクトリが作成されていることを確認
- コマンド:
- 設定ファイルの作成
- phpstan.neon.distを作成
- 内容
-
parameters: paths: - app # The level 9 is the highest level level: 8 ignoreErrors: - '#Unsafe usage of new static#'
-
- 解析実行!
- コマンド:
./vendor/bin/phpstan analyse
- メモリのエラーに...
- コマンドを以下に変更
- コマンド:
./vendor/bin/phpstan analyse --memory-limit=2G
- 数万件のエラーを検知😇(想定内)
- 既存のエラーすべて解消は無理だな〜毎回全部のエラーが出てくると困るな〜🤔と思い次工程を実行
- コマンド:
- ベースライン作成
- 既存のエラーリストの作成のこと。これがあると既存のエラーを毎回検知しなくなるので、新規で変更した箇所のエラーのみを見ることができる!
- コマンド:
./vendor/bin/phpstan analyse --memory-limit=2G --generate-baseline -vv
- 数万件のエラーをphpstan-baseline.neonというファイルに保存!
- 設定ファイルにベースラインの使用を追記
- 内容
-
includes: # ベースラインの読み込みを追記 - phpstan-baseline.neon parameters: paths: - app # The level 9 is the highest level level: 8 ignoreErrors: - '#Unsafe usage of new static#'
-
- 内容
LaraStanの導入
PHPStanを導入しエラーを検知できるようになったものの、想定のエラーが検知されない...🤔
その理由は、Laravelのマジックメソッドを使用していたからでした。
そこでPHPStanにはLaravel用の拡張機能があるとのことなのでこちらを使ってみることに(ただし非公式らしい)
- Larastanのインストール
- コマンド:
composer require larastan/larastan:^2.0 --dev
- vender配下にlarastanのディレクトリが作成されていることを確認。
- LarastanをインストールするとPHPStanもインストールされます。
- コマンド:
- 設定ファイルの編集
- 先ほど作成した設定ファイルをそのまま使う。
- 内容
-
includes: - phpstan-baseline.neon # Larastan用の拡張ファイルを読み込む - vendor/larastan/larastan/extension.neon parameters: paths: - app # The level 9 is the highest level level: 8 ignoreErrors: - '#Unsafe usage of new static#'
-
- 解析実行!
- コマンドはPHPStanと同じ。
- コマンド:
./vendor/bin/phpstan analyse --memory-limit=2G
- コマンド:
- しかしまたもエラーに...
- エラーメッセージ:
Error thrown in hogehoge.php on line 12 while loading bootstrap file /var/www/hoge_project/vendor/larastan/larastan/bootstrap.php: Undefined constant "CONSTANT_FUGE"
- どうやらbootstrap配下にオリジナルで作成している、定数群の設定ファイルが読み込めていないらしい...(bootstap/app.phpは読み込んでいるが)
- エラーメッセージ:
- コマンドはPHPStanと同じ。
- bootstrapファイルの設定
- 設定ファイルで独自の Bootstrapファイルを設定できるとのこと
- 設定ファイルの編集
- 内容
-
includes: - phpstan-baseline.neon - vendor/larastan/larastan/extension.neon parameters: bootstrapFiles: # 定数群ファイルの読み込み - original_autoload.php paths: - app # The level 9 is the highest level level: 8 ignoreErrors: - '#Unsafe usage of new static#'
-
- しかし同様のエラーに...
- パスは間違っていないはずだが🤔
- いろいろ試してみたが、設定ファイルにbootstrapの独自ファイル設定をしてもうまくいかなかった
- 内容
- 解析実行コマンドのオプションでbootstrapファイルを指定
- PHPStanのコマンドのヘルプを見てみた
- コマンド:
./vendor/bin/phpstan -h
- コマンド:
- 以下の記載が!
-a, --autoload-file=AUTOLOAD-FILE Project's additional autoload file path
- これで初期化ファイルを読み込めそう
- コマンド:
./vendor/bin/phpstan analyse --memory-limit=2G -a original_autoload.php
- コマンド:
- 実行エラーにならずエラー検知成功!
- PHPStanのコマンドのヘルプを見てみた
その後の課題
- タイプヒンティングでちゃんとnullableであることを書かないと、期待しているエラーにならない
- 書き忘れる可能性があるのでそこを徹底するにはどうすれば良いか🤔
-
/** * Class Order * @property User|null $user ここでnullも定義しないとnullableにならない */ class Order { public function user(): belongsTo { return $this->belongsTo(User::class); } public function fugefuge(): int { return $this->user->id // ここでCannot access propertyのエラーになる } } /** * Class User * @property int $id */ class User { public int $id; }
-
- 他にもignoreErrorsへの設定や、設定ファイルに記入できる設定を駆使する必要がありそう
- 例:
reportUnmatchedIgnoredErrors: false
と設定すると、ignoreErrorsに設定していあるエラーを検知しなかった場合のメッセージを回避できる
- 例:
- いろいろ試してみて最適な状態にしていきたい
感想
nullとの戦いは続く
Discussion