サーバーサイドエンジニアの田実です!
ヤプリの一部のサービスはPHPで実装されており、それぞれのサービスは品質向上を目的として静的解析ツールである PHPStan を導入しています。 PHPStanにより多くの不具合を事前に検知することができるのですが、PHPStanに慣れていないとエラーの解消方法がわからず困惑しがちです。そこで今回はPHPStanで引っかかりがちなエラーと解消方法をまとめてみました。実際のコード例は PHPStan Playground のリンクを載せていますのでそちらも参照ください 🙏
- Level 0
- Level 1
- Level 2
- Level 3
- Level 4
- Level 5
- その他:Ignored error pattern xxxx in path /path/to/file was not matched in reported errors.
- おまけ:PHPStanの各レベルのチェック内容を確認する方法
- まとめ
Level 0
Undefined variable: ${variableName}
未定義の変数を参照しようとしたときに発生するエラーです。typoや初期化せずに変数参照しているパターンがほとんどなので、適切な値を代入するかnullに置き換えるなどして解消していくことになります。
Access to an undefined property {ClassName}::${propertyName}.
未定義のプロパティを参照しようとしたときに発生するエラーです。typoを確認するか、PHPの場合は未定義でもプロパティの代入でプロパティを自動定義することができてしまうので、明示的にプロパティを定義するなどして対応します。
Instantiated class {ClassName} not found.
存在しないクラスをインスタンス化しようとしたときに発生するエラーです。このエラーが発生する状態でPHPを実行すると Uncaught Error: Class 'Hoge' not found
というエラーが発生します。既存のコードでこのエラーがログに出ていない場合はそのコードが利用されていないか、ほとんど通らないロジックの可能性があります。typoやuse漏れが原因なので、これらを見直すことで解消できます。
Method {ClassName}::{methodName}() has invalid return type {Type}.
存在しない型(プリミティブやクラス)を関数の戻り値に指定した場合に発生します。typoかuse漏れの可能性が高いです。
Call to static method {methodName}() on an unknown class {ClassName}.
存在しないクラスに対してメソッド呼び出ししようとしたときに発生するエラーです。こちらもtypoかuse漏れを確認してください。
Level 1
Variable ${variableName} might not be defined.
条件によっては未定義になるような変数を参照したときに発生するエラーです。頻発するエラーの一つです。以下のコード例だと引数に 1
が渡ってきたときは $hoge
が定義済みになりますが、そうでない場合は未定義になります。このエラーを解消するには適切に変数が初期化されるように処理を見直す必要があります。未定義の場合はnull扱いになるので、適切な場所で $xxx = null;
という感じで変数をnullで初期化するコードを書けば安全に解消できると思います。
ただし、以下のように制御フロー的に確実に変数が定義されるようなケースであってもエラーが出てしまいます。この場合は、tryの範囲を狭める、明示的に $xxx = null
の初期化をtryの前に入れる、ignoreするなどの方法で回避することになると思います。
コード例
Variable ${variableName} on left side of ?? always exists and is not nullable.
??
演算子はnullの場合は右のオペランドを返す処理ですが、nullでないことが確定している場合は ??
演算子が不要になります。
Constructor of class {ClassName} has an unused parameter ${variableName}.
コンストラクタで利用していないパラメータが存在する場合のエラーです。ちなみに func_get_args()
で動的にパラメータを取得している場合はこのエラーは発生しません。
Anonymous function has an unused use ${variableName}.
Closureで利用していないuseパラメータが存在する場合に発生します。useのパラメータを削除すれば解消します。
Level 2
PHPDoc tag @param references unknown parameter: ${variableName}
PHPDocの@paramで不正な変数名を指定した場合に発生します。typoや引数の見直しをして解消します。
Binary operation "+" between {ValueA} and {ValueB} results in an error.
数値と非数値文字列を加算しようとした場合など、算術演算子のオペランドの型が不正な場合に発生します。非数値型のときの考慮などロジックを修正することで解消できます。
Call to an undefined method {ClassName}::{methodName}().
存在しないメソッドを呼び出そうとしたときに発生するエラーです。だいたいtypoです。このエラーが発生する状態でPHPを実行すると Uncaught Error: Call to undefined method ClassName::methodName()
というエラーが発生します。既存のコードでこのエラーがログに出ていない場合はそのコードが利用されていないか、ほとんど通らないロジックの可能性があります。typoやuse漏れが原因なので、これらを見直すことで解消できます。
Method {ClassName}::{methodName}() invoked with {N} parameters, {M} required.
メソッド呼び出しの引数の数が間違っている場合に発生するエラーです。このエラーが発生する状態でPHPを実行すると ArgumentCountError: Too few arguments to function hoge()
というエラーが発生します。既存のコードでこのエラーがログに出ていない場合はそのコードが利用されていないか、ほとんど通らないロジックの可能性があります。
Level 3
Method {ClassName}::{methodName}() should return {TypeA} but returns {TypeB}.
メソッドの定義上の戻り値の型と実際に返している型が違っている場合に発生するエラーです。戻り値の型定義か、returnの値を見直して解消します。
Offset 'xxx' does not exist on array{...}.
配列のアクセスで存在しないオフセットを指定した場合に発生するエラーです。PHPDocのarray shapeの定義やoffsetのtypoを見直して解消します。
Level 4
If condition is always [true|false].
if文の条件が常にtrueになる場合に発生します。 ==
や ===
と書こうとして =
の代入演算になっていたり、条件分岐がそもそも不要なロジックになっていると発生しがちです。
同様のエラーで [Left|Right] side of && is always [true|false].
や Call to function <functionName>() with <Type> will always evaluate to [true|false].
などのエラーもあります。
Property {ClassName}::${propertyName} is never read, only written.
privateなプロパティの書き込みを行っていて読み込みが無い場合に発生します。おそらく不要なプロパティなので削除しましょう。
Method {ClassName}::{methodName}() is unused.
利用していないprivateメソッドが存在する場合に発生するエラーです。メソッドごと削除すれば解消できます。
Dead catch - {ExceptionName} is already caught above.
Exceptionのcatchで到達しないcatch句がある場合に発生します。以下の例のように同じExceptionクラスをキャッチしたり継承関係で親クラスを先にcatchすると後ろで子クラスのcatchを書いていてもそこには到達しないので無効なcatch句となってしまいます。
Unreachable statement - code above always terminates.
到達しないステートメントがある場合に発生するエラーです。return, exit, throwなどで処理が中断されることで、後続のコードが必ず処理されないケースで発生します。
Level 5
Parameter #1 ${variableName} of [function|method] {Name} expects {TypeA}, {TypeB} given.
メソッドや関数の呼び出し側の引数の型間違いのエラーです。呼び出し側で適切にキャストするか、定義側の型を見直すことで解消できます。
その他:Ignored error pattern xxxx in path /path/to/file was not matched in reported errors.
ignoreErrors に設定したエラーパターンがマッチするファイルがない場合に発生します。 すでにエラーが解消されているので、該当のignoreErrorsの設定を削除すればOKです。
ちなみに reportUnmatchedIgnoredErrors: false
を設定すればこのエラー設定を無効化できます。ただし、無効なignoreErrorsの設定が増える可能性があるので注意が必要です。詳細は以下のリファレンスを参照ください。
おまけ:PHPStanの各レベルのチェック内容を確認する方法
PHPStanのレベルごとのルールセットは conf のディレクトリ内の config.levelX.neon
ファイルで確認できます。
各ルールに該当するrulesと、そのRuleクラスが参照するサービスクラスの設定に該当するservices、サービスクラスの振る舞いを制御するparametersを見ていくと、各レベルのチェック内容を確認できます。
例えば、Level 5だと関数・メソッドの引数の型をチェックするようになりますが、これは checkFunctionArgumentTypesのパラメータをtrueにすることで実現しています。checkFunctionArgumentTypesは FunctionCallParametersCheck クラスのコンストラクタ引数になっていて、このクラスがメソッドや関数をチェックする各ルールで利用されることで、全体的に引数の型チェックが効くようになっています。
まとめ
PHPStanで引っかかりがちなエラーについて紹介しました。
私はこれまでPHPStanの導入を7サービスほど行ったのですが、どのサービスもLevel 0〜2でもtypoや未使用コード系の不具合を多く検知できました。 Level maxまで上げるのは難しい印象なのですが、Level 5まで上げれるとメソッド・関数の引数の型チェックによってコスパ良く品質が安定するように思います。