10月6日、PHPStanが「PHPStan 2.3: Leap in Performance, Generics Improvements, Detecting Unused Variables, and More!」と題した記事を公開した。PHPStan 2.3.0では、WordPressのソースコード全体の解析時間が約60秒から8秒に短縮されるという大幅なパフォーマンス向上が実現している。
PHPStanはPHP向けの静的解析ツールで、コードを実際に実行せずにバグや型エラーを検出できる。CIパイプラインへの組み込みが一般的で、解析対象のコードベースが大きくなるほど実行時間がボトルネックになるため、今回のパフォーマンス改善は多くのチームにとって直接的なメリットをもたらす。
WordPressの解析が60秒→8秒に
今回のリリースで最も目を引くのがパフォーマンスの大幅な改善だ。M1 Pro搭載のMacBook Pro(5年前のモデル)で計測した結果、WordPressのソースコード全体の解析時間が、2025年12月時点の60秒から8秒にまで短縮された。
以下が各バージョンでの計測値だ:
| バージョン | リリース日 | Turbo off(秒) | Turbo on(秒) | 備考 |
|---|---|---|---|---|
| 2.1.33 | 2025/12/05 | 60.51 | — | 改善作業前の最終リリース |
| 2.1.34 | 2026/01/19 | 46.51 | — | リフレクションオブジェクト等のキャッシュ |
| 2.2.6 | 2026/07/26 | 32.02 | 24.68 | Turbo初導入 |
| 2.2.13 | 2026/09/03 | 23.06 | 14.12 | OPcacheの常時有効化など |
| 2.3.0 | 2026/10/01 | 19.47 | 7.93 | 多数のクラスをTurboでシャドウ |
この改善を支えているのがPHPStan Turboだ。PHPStan 2.2.6で導入されたオプション機能で、PHPStanの一部処理をC++で再実装することで高速化を実現する。PHARファイルからの実行では通常できない子プロセスのforkや、OPcacheのオプティマイザパスを通じたランタイム型チェックの除去なども可能にする仕組みだ。
「オプション」とはいえ、phpstan/phpstan Composerパッケージに.so/.dllファイルが同梱されており、PHPStanが実行環境に合わせて自動選択・有効化する。ユーザーが手動でセットアップしなくても、すでに最高速で動いているケースが大半だ。Turbo onの状態での比較では、PHPStan 2.3.0は10ヶ月前と比較して最大7.5倍高速になった。
ウォームキャッシュ時の改善も見逃せない
上記はキャッシュなしの「コールドラン」の話だ。実際の開発現場では、前回実行からの差分ファイルだけを再解析する結果キャッシュが効く。
より大規模なDrupalプロジェクト(フルランで22秒)での計測では、変更ゼロで再実行した場合は1.6秒、10ファイル変更で2.8秒、プロジェクト全体の10%変更で6.2秒という結果だった。
今回の改善点として、これまでは「任意のComposer依存関係の更新」や「Symfony DIコンテナの変更」でキャッシュ全体が無効化されていた問題が解消された。解析対象ファイルとインストール済みパッケージ間の依存関係をトラッキングし、必要最小限のファイルだけを再解析するようになった。
ジェネリクスの根本的な改善:4年越しの解決
PHPStanはほぼ7年前からジェネリクスをサポートしているが、当初の設計判断に起因する問題が長年報告されてきた。
具体的には、以下のような関数で doFoo(1) を呼び出した際の戻り値型の推論だ:
/**
* @template T
* @param T $a
* @return T
*/
function doFoo($a)
{
// ...
}
従来のPHPStanは 1 を int に汎化(generalize)していた。これは new Collection([1, 2, 3]) の型を Collection<int> と推論するために必要な挙動だったが、精確なスカラー型を保持したいケースで多くの問題を引き起こしていた。
2022年にArnaud Le Blanc氏が「双方向型絞り込み(Bidirectional type narrowing)」という解決策を提案し、今回ようやく実装された。4年越しの解決だ。
仕組みは、new Collection(...) 周辺のコードを複数回解析し、その変数がどこに渡されるかを観察した上で @template T の最終的な型を決定するというものだ。例えば:
/** @param Collection<int> $ints */
function takeInts(Collection $ints): void {}
function example(): void
{
$ids = new Collection([1, 2, 3]);
$ids->add(4);
$ids->add(-1); // ← ここでエラーを検出できるようになった
takeInts($ids);
}
この改善により、PHPStanはジェネリクスのコンテキストでスカラー型を汎化する必要がなくなり、関連する17件のIssueと多数の重複報告が一括でクローズされた。なお、この機能を利用するにはbleeding edge(PHPStanの先行機能フラグ。phpstan.neonで bleedingEdge: true を設定することで有効になる)の有効化が必要だ。
この双方向型絞り込みの実装は、欧州のオープンソースインフラを支援する公的機関であるSovereign Tech Fundの支援によって実現した。
未使用変数の検出
PHPStanがこれまで見逃してきた「未使用変数」のエラー検出が追加された。PHPStanは以下の5つのシナリオを区別する:
- **
variable.unused**:変数が代入されたが一度も読まれない - **
assign.unused**:関数終了まで代入値が読まれない - **
assign.overwritten**:読まれる前に別の値で上書きされる - **
assign.redundant**:すでに持っている値を再代入している - **
assign.unusedFlow**:値が使われているように見えるが、最終的にどこにも流れない(Psalmのデータフローグラフにインスパイアされた検出手法)
例えば assign.unusedFlow の検出例:
function importRows(Importer $importer, array $rows): void
{
$imported = 0;
foreach ($rows as $row) {
$importer->import($row);
$imported = $imported + 1; // 加算しているが結果を使っていない
}
}
このケースでは $imported の値が最終的に何にも使われていないことを検出できる。
配列のオフセット単位でも検出が効く。$summary['total'] を定義したが読まなかった場合(array.unusedOffset)や、foreach のキー・値変数が未使用の場合(foreach.unusedValue)なども対象だ。
そのほかの変更点
これらの改善に加え、リリースノートにはモノレポ環境でのキャッシュ動作の修正、カスタム拡張向けの新しいインターフェースやAPIの追加なども含まれる。
詳細はPHPStan 2.3: Leap in Performance, Generics Improvements, Detecting Unused Variables, and More!を参照していただきたい。