2025-10-21

PHPStan導入・運用ガイド

どの会社でも同じようなwikiを書いているので、ここにまとめておきます。

PHPStan とは

PHPの静的解析ツールで、コードを実行せずにコードを検査できます。 具体的には未定義変数・関数・メソッド・プロパティ・クラスへのアクセス、引数の数・型の間違い、未使用変数・引数・useパラメータ、戻り値の型の不整合など様々なエラーを検知できます。

例えば以下のようなコードを実際に動作させることなくエラーを検知できます。

class Hoge
{
  public function someMethod(int $someArg): string
  {
    echo $nodefined;        // Undefined variable: $nodefined.
    $tmp1 = new NoExist();  // Instantiated class NoExist not found.
    $tmp2 = noFunction();   // Function noFunction not found

    return 1;                  // Method Hoge::someMethod() should return string but returns int.
  }
  private function privateMethod() {}
}

$hoge = new Hoge();
echo $hoge->fuga;              // Access to an undefined property Hoge::$fuga
echo $hoge->hello();           // Call to an undefined method Hoge::hello().
echo $hoge->someMethod();      // Method Hoge::someMethod() invoked with 0 parameters, 1 required.
echo $hoge->someMethod('123'); // Parameter #1 $someArg of method Hoge::someMethod() expects int, string given.
echo $hoge->privateMethod();   // Call to private method privateMethod() of class Hoge.

エラー検知例 https://phpstan.org/r/cc1142d9-4418-4c88-8848-9aa10c1e3753

特にPHPDocや拡張機能を使った型検査が強力で、動的型付け言語であるPHPを使ってる時にやりがちなタイポや型の指定間違いをそれなりに高い精度でエラー検知してくれます。PHPを ゆるふわ動的型付け言語 から 漸進的型付け言語 に昇格してくれるツールです。

また、テストを書かずに不具合を炙り出せるという意味では、レガシーアプリケーションに対する最初のアプローチとしても有効です。PHPStanを動かすためのバージョンは7.2以上ですが、検査対象はPHP5系も対応しているのもレガシーアプリケーションに適用する際には嬉しいポイントです。ただし、静的解析よりも動的解析であるところのテストのほうが効果が高いため、補助として考えてもらえると良いです。

類似のツールとしては他に PsalmPhan があります。静的解析に関するWeb記事やカンファレンスのトークで目にする機会が多いのは圧倒的にPHPStanで、デファクトと言っても差し支えないくらいよく利用されているツールです。

導入方法

Composerでインストールして利用する方法とDockerで利用する方法があります。

$ composer require --dev phpstan/phpstan
$ docker run -v $(pwd):/app --rm ghcr.io/phpstan/phpstan

Dockerを使う場合は上記のようにワンオフで使うよりは、/tmp ディレクトリに作られるキャッシュを効かせるためにボリュームを永続化するなどして実行できると良いです。

静的解析の実行

analyseコマンドで実行します。

$ vendor/bin/phpstan analyse

詳細な設定は phpstan.neon ファイルに書いていきます。1

parameters:
    level: 5
    paths:
        - app
    ignoreErrors:
        -
            message: ...

Level はルールや設定のプリセットになってます。Levelを上げるほど検知する項目が増え、より厳密な検査ができます。

  1. basic checks, unknown classes, unknown functions, unknown methods called on $this, wrong number of arguments passed to those methods and functions, always undefined variables
  2. possibly undefined variables, unknown magic methods and properties on classes with __call and __get
  3. unknown methods checked on all expressions (not just $this), validating PHPDocs
  4. return types, types assigned to properties
  5. basic dead code checking - always false instanceof and other type checks, dead else branches, unreachable code after return; etc.
  6. checking types of arguments passed to methods and functions
  7. report missing typehints
  8. report partially wrong union types - if you call a method that only exists on some types in a union type, level 7 starts to report that; other possibly incorrect situations
  9. report calling methods and accessing properties on nullable types
  10. be strict about the mixed type - the only allowed operation you can do with it is to pass it to another mixed

個人的には関数・メソッドの引数の型チェックを行うLevel 5まで上げていくのがオススメです。

parameters.paths には検査するディレクトリ・ファイルを指定します。Laravelだと app tests あたりになると思います。

parameters.includes では他の設定ファイルを読み込むことができます。主に拡張機能を使うときに利用します。

parameters.ignoreErrors は無視するエラーを正規表現やエラー識別子を使って記述できます。エラーを無視する対象のファイルやエラーの数も指定できます。

運用

初回はたくさんエラーが出るため、phpstan.neonファイルにignoreErrorsを設定してエラーを一時的に無視し、エラーが解消したらignoreErrorsを削除していく、という感じで運用していきます。

以下のコマンドを叩くことで、今発生しているエラーを全て無視するような parameters.ignoreErrors が設定された phpstan-baseline.neon ファイルが自動生成されます。

$ vendor/bin/phpstan analyse --generate-baseline

phpstan-baseline.neon のignoreErrorsの部分をコピペして phpstan.neon に入れるか phpstan.neon のincludesに phpstan-baseline.neon を追加するなどして反映すれば一旦CIが通る状態になります。あとは、日々の開発・運用で ignoreErrors を潰していきます。一通り潰し終わったらLevelを上げて、またignoreErrorsでエラーを無視してちょっとずつエラーを潰していって…という繰り返しになります。

デフォルトの設定では、ignoreErrorsで指定したエラーが発生しない場合に Ignored error pattern xxxx in path /path/to/file was not matched in reported errors. という別のエラーが発生します。そのため、エラーを潰したら都度 phpstan.neon のファイルを更新してください。面倒な場合はreportUnmatchedIgnoredErrors パラメータをfalseにしてこのエラーを無視することもできるのですが、それはそれで誤検知の問題が発生しそうなのであまりオススメしません。

よく発生するエラー

よく発生するエラーと対応方法は以下にまとめていたりします
https://tech.yappli.io/entry/phpstan_error

ファイルを修正したのにPHPStanの実行結果が変わらない場合

PHPStanはキャッシュを作って高速化を図っており、PHPStan実行中にファイルを修正したりするとキャッシュがおかしくなることがあります。その場合は vendor/bin/phpstan clear-result-cache コマンドを叩いて /tmp ディレクトリのキャッシュを削除すると直ります。

CIの導入

CIに入れる場合は前述のPHPStanのキャッシュを効かせると高速に動作するため、成功時に /tmp/phpstan のディレクトリをCI上でキャッシュ・利用すると良いです。

また、PHPCSなどのフォーマット系のlintと異なり、PHPStanの解析対象は差分のファイルだけではなく毎回全ファイルを検査してください。というのも、例えばあるメソッドの引数の型を変更したとき、呼び出し元を変更していなくても、呼び出し元が修正されていないと検査対象に入らないのでPHPStanが正常終了してしまいます。

PHPDoc

PHPStanはPHPDocを解釈できるため強力な型検査が可能です。 例えばPHPはジェネリクスが無かったり、array型やobject型の詳細を定義できませんが、PHPDocで型の詳細を定義してPHPStanに解釈させることができます。

PHPDocによる型の詳細定義例

ジェネリクス

/**
 * @param Collection<int, string> $hoge
 */
function hello(array $hoge): void
{
    echo $hoge['hello']; // => Offset 'hello' does not exist on Collection<int, string>.
}

Array Shape

/**
 * @param array{hoge: string} $hoge
 */
function hello(array $hoge): void
{
    echo $hoge['hello']; // => Offset 'hello' does not exist on array{hoge: string}.
}

Object Shape

/**
 * @param object{hoge: string} $hoge
 */
function hello($hoge): void
{
    echo $hoge->fuga; // => Offset 'hello' does not exist on array{hoge: string}.
}

Intersection types

/**
 * @param HogeInterface&FugaInterface $hoge
 */

Callable

/**
 * @param callable(int, int): string $callable
 */
function call($callable)
{
  $callable(1, '1'); // => Parameter #2 of callable callable(int, int): string expects int, string given.
}

既存のコードの動きを変更しないのもPHPDocの強みの1つです。例えば既存のコードが引数や戻り値に型を記述していない場合、これらに型を導入するのは少し勇気がいりますが、PHPDocであれば既存の挙動を変えないことを確実に保証した上で静的解析の精度を上げることができます。PHPDocを適切に書くことでIDEによる補完も効かせることができます。
ただし、PHPDocは何でも書けてしまうため、後述の @var 問題のように不正に型情報を上書きできます。そのため、PHPDocも補助として利用していくのが良いです。

@property @method

__get __set __call などのマジックメソッドを使っている場合、PHPStanはどの名前のプロパティ・メソッドが呼ばれうるのかを判断できません。この場合、PHPDocの @property@method を使ってプロパティやメソッドを擬似的に定義できます。

/**
 * @property int|null $foo_id
 * @method hello()
 */
class Bar{}

ちなみにlaravel-ide-helperの補完ではモデルのプロパティ・メソッドを補完できるようにするために、このPHPDocを使った方法が利用されています。

ただし、マジックメソッド自体、保守性の観点からあまり多用すべきではないと思うのと、EloquentなどのORMに関してはPHPStanの拡張があったりするので、積極的に使うケースは少ないかもしれません。

@var

@var タグを使って特定の変数に型を付与することもできます。 が、以下のように型を不正に上書きしてしまうことになるので、基本的に使わない方が良いです。

/** @var int $hoge */
$hoge = 'hoge';
\PHPStan\dumpType($hoge); // => intになってしまう…!

https://phpstan.org/r/e449b31c-5f1b-491d-a293-4ad4027b9153

@throws void

try/catchを使ったコードで Variable $hoge might not be defined. が出てしまうケースがあります。

try {
    $hoge = hello();
} catch (\Exception $e) {
}
echo $hoge; // => Variable $hoge might not be defined.

function hello(): string
{
    return 'hello';
}

この場合 functionに @throws void アノテーションを使うと回避できます。
https://phpstan.org/r/97913ab9-28de-4db4-978a-6754e86ddfb9

ちなみに上記サンプルで @throws void すると「catch Exceptionしてるけど何もエラーをスローしてなくない?」という別のエラーが発生してますね。賢い!

@phpstan- プレフィクス

PHPStanの型定義が充実しているため、IDEの補完(型定義解釈)と競合する場合があります。その場合は @phpstan- のプレフィクスを付けて定義すると、PHPStanでは解釈するがIDEではそのPHPDocを無視する、といったことができます。

/**
 * @phpstan-param Foo $param
 * @phpstan-return Bar
 */

とはいえPHPDocが増えて保守性悪かったりするので実はあまり使わないかもです

Narrowing Types

if文や型をチェックする関数によって、より詳細な型に落とし込むことができます。

$r = rand(0,1);

\PHPStan\dumpType($r); // => int<0,1> : 0 or 1のint型
if ($r === 1) {
    \PHPStan\dumpType($r); // => 1 : $r === 1 なので1に
} else {
    \PHPStan\dumpType($r); // => 0 : int<0,1> から1が除外されて0に
}

https://phpstan.org/r/6ad6cde8-0ec6-44e8-bf1f-fa6c20ff75f1

オススメ拡張

PHPStanでは拡張機能を使って検査ルールを増やしたり、型をより厳密にチェックしたりできます。

PHPStan Strict Rules

in_arrayの厳密比較(第三引数をtrueにする)を強制したり、ゆるふわPHPをお固くする拡張機能です。

$key = array_rand([1,2,3,4,5]);
if (in_array($key, [1,2,3])) { // => Call to function in_array() requires parameter #3 to be set.
    echo 1;
}
if (in_array($key, [1,2,3], true)) { // => OK
    echo 2;
}

Larastan

ゆるふわLaravelに対してPHPStanを適用できるようにする拡張機能です。 FacadeやEloquentモデルのDBカラムなどの動的プロパティに対応してます。これがないとLaravelアプリケーションのPHPStanの効果が薄れてしまう…というくらいに頑張って色々やってます。

$dig = new Hoge();
\PHPStan\dumpType($hoge->string_value); // => string|null
\PHPStan\dumpType($hoge->int_value); // => int|null

ちなみに動的プロパティはマイグレーションやsquashしたSQLファイルを見てモデルを解釈しています。マイグレーションが一部欠損していたり、sqldefで運用していく際には注意が必要です(おそらくdumpしたSQLをPHPStanに食わせる必要があると思います)。Larastanを使わずともIDE Helperで生成したヘルパーファイルを読み込ませることでプロパティやFacadeを解釈できるようになりますが、PHPDocのメンテナンスコストがあったり精度も劣るので、静的解析で利用するのはあまりオススメしません。

Disallowed calls for PHPStan

var_dump()eval() phpinfo() などプロダクションで使ってほしくない関数を呼び出すのを禁止する拡張です。デプロイ時の消し忘れ防止用です。

その他

解析の挙動を確認・共有したい

PHPStan用のPlaygroundがあり、簡易的な確認や検査したコードの共有ができます。
https://phpstan.org/try

何の型として解釈されているか確認したい

\PHPStan\dumpType() 関数を呼び出すとPHPStanで解析したときに引数の型をエラーメッセージとして表示できます。PHPStanは詳細な型解釈をしてくれるので実際に色々見てみると面白いです。
https://phpstan.org/r/cb8d75da-4d13-4055-8695-af196f0a1a30

PHPStanのソースコードを読んでみたい

phpstan/phpstan-src にコードがあります。phpstan/phpstan の方にはplayground、webサイト、Dockerのコードやリリース配布用のpharファイルが置かれています。

Stub

基本的にプロダクションコードに関しては型定義やPHPDocで対応できますが、vendor/ 配下のライブラリに関しては利用者側から型定義ができません。この vendor/ 配下のライブラリに対して利用者側が型定義を上書きする仕組みがStubです。詳細は上記リンクをご確認ください。ただし、ライブラリに関しては3rd partyのPHPStan拡張が対応していることが多いので個人的には使ったことがありません。

IDE補完とPHPStan

IDEの補完とPHPStanは別プロダクトなのでPHPStanの推論がIDEの補完では効かない、ということがよくあります。特にLaravelはLarastanが優秀だったりするので、laravel-ide-helper やIDEのLaravelプラグインも併用してうまく付き合っていく感じになります。

Hoge::all()->each(function($hoge) {
    \PHPStan\dumpType($hoge->id); // => intだが、ide-helperだと補完が効かない…
});

PHPStanとautoload

PHPStanは コードを実行せずに解析する というようなことを言ってましたが実はautoloadなどのシンボル解決の部分だけはコードを実行しています(require 'vendor/autoload.php';)。これはReflectionを使ってクラス・メソッド・関数などの定義を取得・利用しており、autoloadを使わないと指定したディレクトリ内の全ファイルを読み込み・解析する必要があるためです。そのため、autoloadやファイルの読み込みに定義以外の副作用があると予期せぬ挙動になる可能性がありますが、そのようなケースは稀かなと思います(クラス定義の外側で何か処理するとかしなければ良い)。

カスタムルール・拡張を作りたい

参考URL


  1. neonファイルはまぁyamlです。正式にはタブを使うっぽい?けど半角スペースでも動きます ↩︎