PHPStan の使い方
PHP のコードを実行する前に、バグがあるかどうか調べられると便利だとは思いませんか? PHP はスクリプト言語ですので、いくら文法的に正しいコードであっても、実際に実行させるまでバグか発生するかどうかわからないという、スクリプト言語であるが故の本質的な問題を抱えています。 C や Java など他のコンパイル言語ではコンパイル時にエラーになるようなコードであっても、スクリプト言語である PHP では、実行させるまでエラーになるかどうかわからないのです。
PHPStan とは?
PHP のコードをチェックする似たようなツールとして PHP_CodeSniffer や、PHP の lint コマンド php -l
が思いつきますが、それらはあくまで文法レベルでのチェックにすぎません。
いくら文法が正しくともエラーは発生します。
では、PHPUnit などの単体テストを実行して見つければ良いじゃないかと思うかもしれません。 しかし、テストコードを書くのは、かなり大変なことです。 コードカバレッジ 100% のテストコードを書くなどというのは、目標ではあるのかもしれませんが、非効率的であり、非現実的な行為となります。
文法が正しくとも、テストを実行しなくとも、実際に動作させなくても、明らかにバグだとわかるコードというものは存在します。
PHPStan は、PHP コードを実行する前に(つまり静的に)、PHP コードを解析し、明らかにバグだとわかるコードを探して指摘してくれる、PHP の静的解析ツールです。
Stan という名前は、static analysis (つまり静的解析)の略のようです。
PHPStan のインストール
さっそくですがインストールしていきましょう。
Composer でインストールする
PHPStan は Composer を使ってインストールします。下記のコマンドを実行するだけです。
基本的には開発環境で使うツールだと思うので、--dev
オプションを付けておきましょう。
Composer の使い方がわからないという方は、まずは「Composer の使い方(その1:概要)」を参考にしてください。
php composer.phar require --dev "phpstan/phpstan"
インストールできたか確認する
PHPStan をインストールすると、phpstan
コマンドが使えるようになっていますので、使用できるか確認してみましょう。
実行ファイルは ".\vendor\bin\" ディレクトリにあります。
.\vendor\bin\phpstan -h
基本的な使い方
インストールできたので、さっそく使ってみます。
ファイルを解析するには phpstan analyse
コマンドを使います。
単一のファイルを解析する
1つのファイルを解析する場合は、解析したいファイル名を指定します。 解析したいファイル名はスペース区切りで複数指定できます。
.\vendor\bin\phpstan analyse .\src\hoge.php
.\vendor\bin\phpstan analyse .\src\hoge.php .\src\fuga.php
複数のファイルを解析する
複数のファイルを解析する場合は、解析したいディレクトリ名を指定します。 解析したいディレクトリ名はスペース区切りで複数指定できます。
.\vendor\bin\phpstan analyse .\src
.\vendor\bin\phpstan analyse .\src .\tests
ルールレベル
PHPStan では、どこまで厳密に解析を行うかのレベルを設定することができます。 レベルは 0 ~ 8 の 9 段階あり、レベル 0(デフォルト)が最も緩く、レベル 8 が最も厳密になります。 解析する際、設定されたレベル以下の全てのチェックが実行されます。 例えば、レベル 5 に設定すると、レベル 0 ~ 5 の全てのチェックが実行されます。
レベルを上げると、より厳密な解析になってはいきますが、その分だけ解析にかかる時間も長くなり、検出個所も多くなります。 検出個所が多くなるということは、その検出個所に対応する時間も比例して多くなってしまうということです。 修正する必要のない個所もたくさん検出されることになるでしょう。 些細な問題に対して時間をかけてしまい、実際のバグを見逃してしまっていては、本末転倒にもなりかねません。
最初はレベル 0(デフォルト)から始めて、検出個所が無くなったら、1つずつ徐々にレベルを上げていきましょう。 一気にレベルを上げてしまうと、どれから手を付けて良いかわからなくなってしまいます。
どのレベルで、どんな内容の解析が行われるかは、ここでは記載しません。 今後 PHPStan がバージョンアップして、レベルが新しく増える可能性もあるようなので、 詳しく知りたい方は、英語ですが公式のドキュメントを見てもらった方が良いと思います。
レベルを変更する
レベルを変更するには --level
オプションを使用します。
--level
オプション には、エイリアス(別名) -l
オプションがあります。
.\vendor\bin\phpstan analyse -l 6 .\src
最大のレベルに設定する
最大のレベルを設定する際には、エイリアス(別名)max
が使用できます。
現状ではレベル 8 が最大ですが、今後 PHPStan がバージョンアップして、9 以上のレベルが新しく増えた場合でも、常に最大のレベルで動作させることができます。
.\vendor\bin\phpstan analyse -l max .\src
構成ファイル
PHPStan では、特定のエラーを無視したり細かな設定ができるのですが、コマンドのパラメーターだけでは、さすがに全てを指定しきれません。 そのため、PHPStan には構成ファイルを読み込む機能があります。
構成ファイルのフォーマット
構成ファイルのフォーマットは、NEON です。 NEON は、YAMLと非常によく似ているため、YAML に慣れている方は簡単に NEON を作成することができると思います。
ここでは簡単な例だけ記載しておきます。
parameters: level: 6 paths: - src - tests
どんな設定項目があるかは、ここでは記載しません。 あまりにも大量にありますので、英語ですが公式のドキュメントを見てもらった方が良いと思います。
構成ファイルを使用する
構成ファイルを使用する方法は2つあります。
--configuration
オプションで指定する
1つ目の方法は、--configuration
オプションで使用したい構成ファイルを指定することです。
複数の構成ファイルを使用して、設定を切り替えたい場合などには便利かもしれません。
--configuration
オプション には、エイリアス(別名) -c
オプションがあります。
.\vendor\bin\phpstan analyse -c .\custom_phpstan.neon
デフォルトの構成ファイルを使用する
2つ目の方法は --configuration
オプションは指定せず、デフォルトの構成ファイルを使用する方法です。
--configuration
オプションを指定しない場合(つまりデフォルト)、
PHPStan は現在のディレクトリから構成ファイルを探します。
この時に探されるファイルの名前は、「phpstan.neon」「phpstan.neon.dist」の2つです。
この2つのファイル名のどちらかと一致するファイルが見つかった場合に、そのファイルが自動的に読み込まれます。
もし複数のファイルが見つかった場合は、前述の順番で優先的に読み込まれます。
.\vendor\bin\phpstan analyse
ルールレベルは必須項目
構成ファイルを使用する場合は、ルールレベルの指定が必須になります。
デフォルト値の 0 も、構成ファイルを使用している場合は反映されません。
コマンドの --level
オプションで指定するか、構成ファイル内に level
項目で指定するか、
どちらかは必須となるので注意してください。
複数の構成ファイル
PHPStan で自動的に読み込まれる構成ファイルは、phpstan.neon と phpstan.neon.dist の2つです。 通常は、phpstan.neon.dist の方を Git などのバージョン管理下に置き、phpstan.neon の方を .gitignore に追加します。 各ユーザーが phpstan.neon.dist を元にして、phpstan.neon を作成することで、 各環境内(各ユーザーのPCや、CIサーバーなど)で特定の設定を上書きできるようにします。 phpstan.neon と phpstan.neon.dist の両方のファイルがある場合に、phpstan.neon の方が優先して読み込まれるのはそのためです。
PHPStan の構成ファイルでは、複数のファイルから構成するための includes
の項目があります。
そのため、phpstan.neon ファイルから、phpstan.neon.dist ファイルを includes
の項目で読み込んで使用できます。
parameters: level: 6 paths: - src - tests
includes: - phpstan.neon.dist parameters: level: 8 ignoreErrors: - '#Function pcntl_open not found\.#'
エラーを無視する
PHPStan で検出されたエラーの中には、あきらかに対応できないものや、対応する必要のないものなどがあったりします。 そういったものが常に検出されていると、作業の邪魔になってしまいます。 そこで、特定のエラーを検出の対象外として、無視させることができます。
方法としては主に2つあります。
PHPDocs でエラーを無視させる
1つ目の方法は、PHP のコード上に PHPDocs として記述する方法です。 この方法は、ピンポイントで指定できるというメリットはありますが、大量に使用した場合は PHP のコードがコメントで汚れてしまうというデメリットがあります。 できるなら、後述の構成ファイルに記載する方法の方が良いでしょう。
使用するには、PHP のコメント(//
、/* */
、/** */
のいずれでも可)の中で、下記のタグを使用します。
@phpstan-ignore-line
は、それが書かれたその行を無視します。@phpstan-ignore-next-line
は、それが書かれたその次の行を無視します。
<?php echo $foo; // @phpstan-ignore-line /* @phpstan-ignore-next-line */ echo $foo;
構成ファイルに記載してエラーを無視させる
2つ目の方法は、無視したいエラーを構成ファイルに記載することです。
構成ファイルの ignoreErrors
項目の下に、無視したいエラーを正規表現で追加することで、そのエラーを無視できます。
正規表現ですので、「|」「$」「.」「(」「)」などの特殊文字はエスケープする必要があるので注意です。
プロジェクト全体でエラーを無視する
プロジェクト全体でエラーを無視するには、ignoreErrors
の項目に、正規表現によるエラーメッセージを指定します。
parameters: ignoreErrors: - '#Call to an undefined method [a-zA-Z0-9\\_]+::doFoo\(\)#' - '#Call to an undefined method [a-zA-Z0-9\\_]+::doBar\(\)#'
特定のファイル/ディレクトリでエラーを無視する
特定のファイル/ディレクトリでエラーを無視するには、message
と、path
もしくは paths
の項目を含むエントリーを追加します。
-
message
の項目には、正規表現によるエラーメッセージを指定します。 -
path
もしくはpaths
の項目には、特定のファイル/ディレクトリのパスを指定します。 PHP の fnmatch() 関数と互換性のある形式のワイルドカードが使用できます。 -
また、必須ではありませんが、
path
を使用した場合のみ、count
の項目で、エラーが予想される回数を指定することもできます。
parameters: ignoreErrors: - message: '#Access to an undefined property [a-zA-Z0-9\\_]+::\$foo#' path: some/dir/SomeFile.php - message: '#Call to an undefined method [a-zA-Z0-9\\_]+::doFoo\(\)#' path: other/dir/DifferentFile.php count: 2 # optional - message: '#Call to an undefined method [a-zA-Z0-9\\_]+::doBar\(\)#' paths: - some/dir/* - other/dir/* - '#Other error to ignore everywhere#'
ベースライン機能
PHPStan をプロジェクトの開始の時点で導入できていれば良いですが、現実ではプロジェクトの途中で導入することもあると思います。 大量のコードに対して PHPStan を実行すると、大量の検出個所が報告されることになります。
もちろん、その全ての検出個所の修正を先に終わらせるのが理想ではあります。 しかし、修正箇所があまりに多く、対処する十分な時間やエネルギーがない状況では、 現時点で見つかった検出個所の修正を一旦後回しにし、新しく発生した個所に注力していく戦略をとるというのも、決して間違いとは言えません。
ベースライン機能を使用すると、現時点を作業を開始する基準とし、現在発生している全てのエラーを無視させることができるようになります。 これにより、新しく変更されたコードのみに注力することができるようになります。
ベースライン機能を使用するには、まず現在発生している全てのエラーの一覧をベースラインファイルとして保存します。 次に、その保存したベースラインファイルを、構成ファイルから読み込みます。 すると、次回実行した際に、そのベースラインファイルに記載されているエラーは無視されるようになります。
上述の構成ファイルに記載してエラーを無視する機能の応用といった形でしょうか。
ベースラインファイルを保存する
現在発生している全てのエラーの一覧をベースラインファイルに保存するには、--generate-baseline
オプションを使用します。
ファイル名は --generate-baseline
オプションのパラメーターとして指定することができます。
ファイル名が未指定の場合、ファイル名は phpstan-baseline.neon になります。
このベースラインファイルは、構成ファイルと同じ形式の NEON ファイルですので、手動で編集することも、再度生成しなおすこともできます。
.\vendor\bin\phpstan analyse --generate-baseline
.\vendor\bin\phpstan analyse --generate-baseline=basline.neon
構成ファイルからベースラインファイルを読み込む
ベースラインファイルを読み込むには、構成ファイルの includes
の項目で、読み込むファイルを指定します。
includes: - phpstan-baseline.neon parameters: # your usual configuration options
コードとベースラインファイルの食い違い
ベースラインファイルに記載されているエラーの個所のコードが修正されエラーが発生しなくなった場合、 ベースラインファイルにはエラーを無視する記載が残ったままになってしまい、コードと食い違ってしまうという状況が発生します。 ベースラインファイルから、修正された箇所のエラーを無視する記載を削除する必要があります。
この状況が発生した場合、PHPStan が通知してくれます。
もしこの通知が不要な場合は、構成ファイルで reportUnmatchedIgnoredErrors
を false
に設定することで、オフにすることができます。
includes: - phpstan-baseline.neon parameters: reportUnmatchedIgnoredErrors: false
エラーがあってもより高いルールレベルを実行する
ルールレベルを上げるには、そのレベルでのエラーが 0 件になっているのが理想です。 しかしながら、大量に修正箇所がある場合、レベルが上がるまでかなりの時間を費やすことになります。 それはつまり、より高いレベルで見つかるようなエラーはずっと見逃している状態になることを意味します。
ベースラインを使用すると、その時点のエラーを全て無視することができますから、 低いレベルでの修正が全て終わるのを待たずに、一気にレベルを上げることが可能になります。 これにより、新しく変更されたコードには、より高いレベルでの厳密なチェックを適用することが可能です。
Composer スクリプトとして登録する
構成ファイルがあるとはいえ、コマンドを毎回入力するのは面倒に感じてくると思います。 特に、ここまでの説明で散々繰り返し記載してきた ".\vendor\bin" を記述するのが面倒に感じてくると思います。 そこで、コマンドを Composer スクリプトとして登録しておくと、入力の手間が少なくなって便利です。 Composer スクリプトでは、 ".\vendor\bin" に PATH が通っているので、".\vendor\bin" を入力する必要はありません。 Composer スクリプトの使い方がわからないという方は、「Composer の使い方(その2:スクリプト)」を参考にしてください。
{ "name": "hoge/hoge", "require-dev": { "phpstan/phpstan": "^0.12.98" }, "scripts": { "stan": "phpstan analyse" } }
php composer.phar stan
色がちゃんとつかない場合
Composer スクリプトから実行した場合に、出力内容に色がちゃんとつかない場合があります。
その場合は、--ansi
オプションを付けてみましょう。
{ "name": "hoge/hoge", "require-dev": { "phpstan/phpstan": "^0.12.98" }, "scripts": { "stan": "phpstan analyse --ansi" } }