PHP

PHPStanとは?レベル0〜10の違いと導入・ベースライン運用【2.3対応】

PHPStanとは?レベル0〜10の違いと導入・ベースライン運用【2.3対応】

PHPStanは、PHPのコードを実行せずに読み、存在しないメソッドの呼び出しや型の食い違いを見つける静的解析ツールです。厳しさを0〜10の11段階から選べるので、型宣言の少ない既存コードにも低いレベルから入れられます。この記事では2026年10月6日公開のPHPStan 2.3.0を使い、同じ検証コードを全レベルで解析して、どのエラーがどのレベルで初めて出るかを示します。導入手順、既存プロジェクト向けのベースライン、Larastan、GitHub Actionsへの組み込みもあわせて扱います。

まとめ:PHPStanの導入で決めること

  • PHPStanはMITライセンスの無料ツールで、最新版は2.3.0です。動かすにはPHP 7.4以上が要り、composer require --dev phpstan/phpstanで入ります。
  • レベルは0〜10の11段階で、上のレベルは下のレベルの検査をすべて含みます。設定ファイルを使うときはレベルの指定が必須です。
  • 新規プロジェクトは6以上から始められます。既存プロジェクトは、目標のレベルでベースラインを作り、新しく書くコードだけを厳しく検査するのが現実的です。
  • 個別のエラーを抑えるときは、エラー識別子つきの@phpstan-ignoreに理由を添えます。識別子を間違えると、その抑制コメント自体がエラーになります。
  • LaravelはPHPStanにLarastanを追加しないと、Eloquentのマジックメソッドが「未定義」と誤検出されます。
  • CIでは終了コード1で失敗します。GitHub Actions上では、既定の出力形式のままでプルリクエストに注釈が付きます。

PHPStanが実行前に見つけるバグの範囲

PHPStanは、型宣言とPHPDocを手がかりに、値がどの型を通るかを追いかけます。intを返すはずのメソッドが文字列を返している、nullかもしれない値でstrlen()を呼んでいる、といった不整合は、テストを書かなくても見つかります。

逆に、計算式の誤りや業務ルールの取り違えのように、型が正しいまま結果だけが間違うバグは検出できません。型の整合はPHPStan、振る舞いの正しさはPHPUnitやPestのテストと役割を分けます。静的解析とテストで検出できるバグの違いは静的解析と動的テストの違いで詳しく扱っています。

開発はOndřej Mirtes氏が中心で、GitHubのリポジトリphpstan/phpstanはMITライセンスで公開されています。PackagistでのPHPStanのダウンロード数は2026年10月9日時点で累計約4億1,163万回、直近1か月で約1,504万回です。有料版のPHPStan Proもありますが、無視したエラーを含めてWebの画面で閲覧する機能などが加わるもので、解析の検査内容は無料版と同じです。

レベル0〜10で検出されるエラーの違い(実測)

レベルは-l(--level)で指定します。何も指定しないと0で動き、maxを指定すると常に最上位のレベルになります。下の表は、意図的に誤りを1つずつ仕込んだクラスをPHPStan 2.3.0で解析し、各エラーが初めて出たレベルをまとめたものです。

レベル 公式の説明(要約) 検証コードで初めて出たエラー
0 未知のクラス・関数、$thisの未知メソッド $this->sendMail()が未定義
1 未定義の可能性がある変数 if内でだけ代入した$total
2 $this以外の未知メソッド、PHPDoc検証 $o->cancel()が未定義、引数の数違い
3 戻り値の型、プロパティへの代入型 intを返すメソッドが'100'を返す
4 基本的なデッドコード 常にfalseになるinstanceof
5 引数の型 int引数に'500'を渡す
6 型宣言の欠落 引数と戻り値に型が無いメソッド
7 union型の一部だけが持つメソッド Order|DateTimeImmutableでamount()
8 nullableへのメソッド呼び出し等 ?stringをstrlen()へ渡す
9 明示的なmixedの扱い mixed引数やjson_decode()の結果に['id']
10 暗黙のmixed(型なし)も対象 型宣言の無い引数をcount()へ渡す

検証環境はmacOS(x86_64)、PHP 8.5.8、PHPStan 2.3.0で、同じファイルにvendor/bin/phpstan analyse src -l 0から-l 10までを順に実行しました。表の中で公式の要約だけでは読み取りにくいのは、次の3点です。

  • 引数の数違いはレベル0とは限らない:レベル0が引数の数を検査するのは、関数の呼び出しと$this経由のメソッド呼び出しです。$o->amount(10)のように別のオブジェクト経由で余計な引数を渡すと、レベル2まで出ませんでした。
  • json_decodeの結果はレベル9で引っかかる:PHPStanはjson_decode()の戻り値をmixedとして明示的に扱います。このため、型宣言の無い変数より1段早く、レベル9で配列アクセスがエラーになりました。
  • レベル10で増えたのは1件だけ:型宣言の無い引数$itemsをcount()に渡す箇所です。レベル6で「型が無い」と警告された同じ引数を、レベル10は「mixedを渡している」と2重に指摘します。

新規と既存でのレベルの決め方

新しく書くプロジェクトなら、最初から6以上、できれば8で始めます。レベル6で引数と戻り値の型宣言の欠落を潰しておくと、それらがmixed扱いのまま残らず、7以降のunionやnullの検査が働く範囲が広がります。null参照による実行時エラーを止めたいなら、少なくとも8が必要です。

既存のプロジェクトで最初に失敗しやすいのは、いきなり高いレベルを設定して数千件のエラーを出し、導入そのものが止まる形です。この場合はレベルを下げるのではなく、次の章のベースラインで既存分を棚上げしたうえで、目標のレベルから始めます。--level maxは、PHPStanを更新するたびに新しい検査が加わり、それまで通っていたCIが急に落ちることがあるので、CIでは数字で固定しておくほうが安全です。

Composerでの導入とphpstan.neonの最小構成

PHPStanは開発依存として入れます。PHARを直接落としても使えますが、Composerで入れないとLarastanなどの拡張を使えません。

composer require --dev phpstan/phpstan
vendor/bin/phpstan analyse src tests --level 6

毎回レベルやパスを打たずに済むよう、プロジェクト直下にphpstan.neonを置きます。チームで共有する設定はphpstan.neon.distとしてGitに入れ、個人で上書きしたい場合だけphpstan.neonを使うのが公式の想定です。

parameters:
    level: 6
    paths:
        - src
        - tests
    excludePaths:
        - src/Legacy/Generated

解析するのは自分たちが書いたコードだけです。vendorは解析対象に入れません。PHPStanは依存ライブラリのクラスをオートローダー経由で読み取るため、パスに含めなくても型は解決されます。

導入直後によく出る失敗は2つです。1つはメモリ不足で、PHPStanはphp.iniのmemory_limit(既定128M)に従うため、大きなプロジェクトではAllowed memory size exhaustedで止まります。--memory-limit 1Gを付けて解決します。もう1つは解析に使うPHPのバージョンの取り違えです。既定では実行中のPHPのバージョンで解析するので、本番より新しいPHPで動かすと、本番では使えない構文を見逃します。2.3.0からはcomposer.jsonのrequire.phpも解析時のバージョン判定に使われるようになりましたが、明示したい場合はphpVersionパラメータで指定します。

既存プロジェクトへのベースライン導入と更新

ベースラインは、いま出ているエラーを一覧として保存し、次回以降の解析で報告しないようにする機能です。既存のエラーは棚上げしたまま、新しく書いたコードと変更したコードにだけ目標のレベルを適用できます。

vendor/bin/phpstan analyse --level 6 --generate-baseline

検証コードでは9件のエラーがphpstan-baseline.neonに記録されました。各項目には、メッセージの正規表現、エラー識別子、件数、ファイルパスが入ります。

parameters:
    ignoreErrors:
        -
            message: '#^Call to an undefined method App\\Order\:\:cancel\(\)\.$#'
            identifier: method.notFound
            count: 1
            path: src/Order.php

生成したファイルを設定ファイルのincludesで読み込むと、解析結果は「No errors」になりました。この状態で別のファイルに未定義メソッドの呼び出しを1行足すと、その1件だけが報告されます。

includes:
    - phpstan-baseline.neon

parameters:
    level: 6
    paths:
        - src

ベースラインに記録したエラーを1件直すと、今度は「Ignored error pattern … was not matched in reported errors.」というエラーで解析が失敗しました。直した分をベースラインから消すよう求めているので、修正済みの項目を削除するか、--generate-baselineで再生成し、新規エラーが追加されていないことを差分で確認します。この挙動はreportUnmatchedIgnoredErrors: falseで止められますが、止めるとベースラインが減らなくなるので、既定のままにします。

ベースラインには、運用で外してはいけない約束が2つあります。

  • 目標のレベルで生成する:レベル5で作ったベースラインのままレベル8へ上げると、8で新しく出る既存コードのエラーが一斉に出ます。上げるときは、上げたレベルで作り直します。
  • 新しいエラーを隠すために作り直さない:公式ドキュメントは「ベースラインの最終目標は存在しなくなること」と書いています。新規コードのエラーまで再生成で吸収すると、ファイルは減らずに増え続けます。どうしても抑える必要がある新規のエラーは、次の章の@phpstan-ignoreで行ごとに理由を残します。

公式は、ベースラインが向くのは数十〜数百件のエラーで、1万5,000件規模ならレベルを下げるか設定を見直すべきだとしています。何万件も出るなら、まず拡張の入れ忘れ(Laravelなら次の次の章のLarastan)を疑います。

エラー識別子による個別抑制と理由の記録

PHPStanのエラーにはmethod.notFoundやargument.typeのような識別子が付いています。既定のtable形式では各エラーの下に🪪の印とともに表示され、json形式ではidentifierの項目に入ります。

行単位で抑えるときは、@phpstan-ignoreに識別子を書き、括弧の中に理由を添えます。コメントだけの行に書くと次の行が、コードと同じ行に書くとその行が対象です。

// @phpstan-ignore method.notFound (税計算は次の改修でOrderに追加する)
return $o->amount() + $o->tax();

識別子を間違えて@phpstan-ignore return.typeと書いたところ、本来のmethod.notFoundはそのまま報告され、さらに「No error with identifier return.type is reported on line 17.」が加わりました。このエラーの識別子ignore.unmatchedIdentifierは抑制できない種類です。識別子なしで何でも黙らせる@phpstan-ignore-next-lineも使えますが、別のエラーまで巻き込んで隠すので、識別子つきの書き方にそろえます。

設定ファイルのignoreErrorsでも、メッセージの正規表現ではなく識別子とパスで指定できます。生成コードのディレクトリで特定のエラーだけを抑える、といった用途に使います。

parameters:
    ignoreErrors:
        -
            identifier: missingType.iterableValue
            path: src/Legacy/*

array shapeとジェネリクスによる配列の型指定

PHPの型宣言ではarrayとしか書けませんが、PHPDocを使うと配列のキーごとの型や、要素の型をPHPStanに伝えられます。下のコードでは、emailキーを省略可能(email?)とした配列を返し、それを汎用のコレクションクラスに包んでいます。

/**
 * @template T
 */
final class Collection
{
    /** @param list<T> $items */
    public function __construct(private array $items) {}

    /** @return T|null */
    public function first(): mixed
    {
        return $this->items[0] ?? null;
    }
}

final class UserRepository
{
    /** @return list<array{id: int, name: string, email?: string}> */
    public function rows(): array
    {
        return [['id' => 1, 'name' => 'sato']];
    }

    public function label(): string
    {
        $row = $this->rows()[0];
        return $row['name'] . ' <' . $row['email'] . '>';
    }

    public function idPlusOne(): int
    {
        $first = (new Collection($this->rows()))->first();
        return $first['id'] + 1;
    }
}

PHPStanはnew Collection(...)の引数からTを推論し、first()の戻り値をarray{id: int, name: string, email?: string}|nullと判定しました。そのうえで、レベル7で$row['email']に「Offset ‘email’ might not exist」を、レベル8で$first['id']に「nullかもしれない値へのキー参照」を報告します。PHPDocに書いた形と実際の使い方が食い違う箇所を、テストの前に洗い出せます。

ここで失敗しやすいのは、エラーを消すために@var arrayやmixedで型を緩めることです。@var arrayで緩めると、キーごとの型の情報が失われて省略可能キーの検査が効かなくなります。mixedにした場合は、レベル9以上で配列アクセスそのものが別のエラーになるだけです。直すべきなのは使う側で、isset()で存在を確かめるか、emailを必須キーにするかを決めます。

Laravel・Symfony向け拡張の導入と対応バージョン

Laravelのファサードやモデルのマジックメソッドは、実行時にしか呼び出し先が決まりません。PHPStan単体ではUser::where()などが「未定義のメソッド」になるため、LaravelではLarastanを入れます。LarastanはPHPStanの拡張で、アプリケーションのサービスコンテナを起動して実行時にしか分からない型を解決します。そのため、README自身が「静的解析」ではなく「コード解析」と呼んでいます。

composer require --dev "larastan/larastan:^3.0"
includes:
    - vendor/larastan/larastan/extension.neon
    - vendor/nesbot/carbon/extension.neon

parameters:
    paths:
        - app/
    level: 5

2026年10月7日公開のLarastan 3.13.0は、PHP 8.2以上、Laravelのコンポーネントilluminate/*が11.44.2以上・12.4.1以上・13のいずれか、PHPStan 2.3.0以上を要求します。READMEの対応表には「Laravel 11.16以降は3.0以降」とありますが、3.13.0を入れるには11系なら11.44.2まで上げておく必要があります。Laravel 9・10のプロジェクトはLarastan 2.x(最終の2.11.2はPHPStan 1.12.17以上の1系を要求)にとどまり、PHPStan 2系の機能は使えません。

Laravel以外のフレームワークや用途にも公式・準公式の拡張があります。

  • phpstan/phpstan-symfony(2.1.0):Symfonyのサービスコンテナやリクエストの型を解決します。
  • phpstan/phpstan-phpunit(2.1.1):PHPUnitのモックやアサーションの型を理解させます。
  • phpstan/phpstan-strict-rules(2.1.0):緩い比較や暗黙の型変換を禁止する、レベル10より厳しいルール集です。
  • phpstan/phpstan-deprecation-rules(2.0.5):@deprecatedの付いた関数やクラスの使用を報告します。

phpstan/extension-installerを一緒に入れると、各拡張のextension.neonをincludesに手で書かなくても自動で読み込まれます。拡張は対応するPHPStanのバージョンに縛られるため、PHPStanを2.3へ上げるときは拡張もまとめて更新します。

GitHub ActionsとAIエージェントでの実行

PHPStanはエラーがあると終了コード1を返すので、CIのジョブにそのまま組み込めば、エラーのあるプルリクエストでジョブが失敗します。GitHub Actionsでは、既定のtable形式が実行環境を自動で判別し、表に加えて::error file=src/New.php,line=17のような注釈用の行も出力しました。--error-format=githubを指定しなくても、差分の該当行にエラーが表示されます。

name: phpstan
on: [pull_request]
jobs:
  phpstan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
      - run: composer install --no-progress --prefer-dist
      - run: vendor/bin/phpstan analyse --no-progress --memory-limit 1G

PHPの版は本番環境と合わせます。ワークフローの基本的な書き方はGitHub Actionsでビルド・自動テストを設定する方法、PHPのコード整形を同じCIで確かめる方法はLaravel Pintの記事で扱っています。

Claude CodeなどのAIエージェントからPHPStanを呼ぶ場合にも、知っておくべき挙動があります。環境変数AI_AGENTが設定されていると、PHPStan 2.3.0は結果の前に英語の指示文を付けました。指示文は、修正前に識別子の解説ページを読むこと、@phpstan-ignoreやベースラインへの追加で黙らせないこと、型を広げてエラーを消さないことを求めています。エージェントに型エラーを直させる運用では、抑制コメントの追加や型の拡大で回避していないか、修正差分を確認します。

PHPStan 2.3の変更点:速度・Turbo・未使用変数の検出

2.3.0のリリースノートが最初に挙げているのは解析速度です。公式ブログの計測(M1 Pro搭載のMacBook ProでWordPress本体を解析)では、2025年12月の2.1.33で60.51秒だった解析時間が、2.3.0ではTurboなしで19.47秒、Turboありで7.93秒になりました。

Turboは、解析の重い処理をC++で実装したPHP拡張です。同じ計測では、Turboの有無で解析時間が約59%短くなっています。Composerで入れたPHPStanには、PHP 8.3以上向けにLinux(x86_64・arm64)、macOS(arm64)、Windows(x86_64)のビルド済みバイナリが同梱され、自動で読み込まれます。インテルCPUのMacは対象外で、検証環境(macOS・x86_64)でvendor/bin/phpstan diagnoseを実行すると「Turbo platform: unsupported」と表示されました。Turboは解析結果を変えず、速度だけを変えます。

次のメジャー版で入る予定の機能を先に試せる「bleeding edge」には、未使用変数の検出が加わりました。設定ファイルにvendor/phpstan/phpstan/conf/bleedingEdge.neonをincludesで読み込むと、代入しただけで一度も読まない変数がvariable.unusedとして報告されます。検証では、通常の設定ではレベル10でも0件だったコードで、$tax = $base * 0.1;の行に「Variable $tax is never read.」が出ました。bleeding edgeはマイナー版の更新でも結果が変わることがあるので、CIで必須にするのは避け、手元で試す程度にします。

Psalm・Magoとの使い分け

PHPの型を検査するツールには、PHPStanのほかにPsalmと、Rustで書かれたMagoがあります。Packagistの月間ダウンロード数(2026年10月9日時点)はPHPStanが約1,504万回、Psalmが約219万回で7倍ほどの差があり、LaravelとSymfonyの拡張がそろっている点で、新しく1本選ぶならPHPStanが無難です。

Psalmを選ぶ理由になるのは、外部入力がSQLやHTMLの出力に届く経路を追うtaint解析です。PHPStan本体はこの機能を持ちません。Psalmとの検出の違いはPsalmとは?PHP静的解析の使い方とPHPStanとの違い、解析速度を重視する場合の選択肢はMagoとは?PHPのRust製リンター・静的解析の使い方とPHPStanとの違いで比べています。複雑度や命名などの型以外の指標を見たいなら、PHPMDを同じCIに並べます。

静的解析が出す警告のすべてが本物のバグとは限らない点にも注意が要ります。誤検知とどう付き合うかは静的コード解析の誤検知と限界にまとめています。

よくある質問

PHPStanとは何ですか?

PHPのコードを実行せずに解析し、未定義のメソッド呼び出し、型の不一致、nullの扱い漏れなどを見つけるツールです。MITライセンスで無料公開されており、2026年10月時点の最新版は2.3.0です。

PHPStanのレベルはいくつありますか?

0から10までの11段階です。0が最も緩く、10が最も厳しい設定で、指定しないと0になります。10は2.0で追加されたレベルで、型宣言の無い値(暗黙のmixed)まで厳しく扱います。

PHPStanとLarastanの違いは何ですか?

LarastanはPHPStanに追加するLaravel向けの拡張です。解析エンジンはPHPStanで、Larastanはファサードや Eloquent のマジックメソッドの型をPHPStanに教えます。Laravelのプロジェクトでは、PHPStanとLarastanを両方入れて使います。

PHPStanのインストール方法は?

プロジェクトのルートでcomposer require --dev phpstan/phpstanを実行し、vendor/bin/phpstan analyse srcで解析します。レベルと対象パスはphpstan.neonに書いておくと、毎回の指定が要らなくなります。

レベル8とレベル9・10の違いは何ですか?

レベル8はnullかもしれない値へのメソッド呼び出しや関数への受け渡しを報告します。レベル9は明示的にmixedと宣言された値の操作を、レベル10は型宣言が無いためにmixedとみなされる値の操作も禁止します。外部のJSONをjson_decode()した戻り値に、型を絞り込まず配列アクセスした箇所は、レベル9からエラーになりました。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

ほか 9 件の記事からもリンクされています。

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.11.03 テックブログ クラブツーリズム不正アクセス:約432万件の対象確認と偽メールの見分け方・流出検知の設計
  2. 2026.11.05 テックブログ Linuxカーネルの脆弱性CVE-2026-98432:修正版の見分け方とnftablesの緩和策
  3. 2026.10.29 テックブログ ソニー生命の情報漏洩38.5万件|ログインできない理由、再開状況と窓口の電話番号
  4. 2026.11.04 テックブログ CVE-2026-119977:WordPress更新中のRCEと安全な更新手順
  5. 2026.10.31 テックブログ 松屋の不正アクセス:237万件の対象確認と窓口、漏れたパスワードの守り方

RELATED POSTS 関連記事

目次