猫でもわかるWebプログラミングと副業

本業エンジニアリングマネージャー。副業Webエンジニア。Web開発のヒントや、副業、日常生活のことを書きます。

PHPStan の「スタブファイル」について

はじめに

このページは以下ドキュメントの翻訳です

phpstan.org

スタブファイル

スタブファイルについて

PHPStan は PHPDoc を解析に利用しています。しかし、 vendor/ ディレクトリ以下の PHPDoc が間違っていると、PHPStan で解析した際に、実装は正しいのにもかかわらず、エラーになってしまいます(false positive: 正しいのに誤りとして検出されてしまう)。

この誤った PHPDoc を訂正するために、正しい PHPDoc を書いた「スタブファイル」を使う方法があります。スタブファイルに、namespace, class, function など全く同じものを定義します。function の中身は空っぽで大丈夫です。PHPDoc だけ正しく訂正してください。

スタブファイルにおいては、PHP の型宣言は無視されて(PHPStan の解析には使われず)、あくまでも PHPDoc だけが利用されます。

スタブファイルの書き方ついては、https://github.com/phpstan/phpstan-src/tree/1.11.x/stubs (PHPStan のソースコードにあるスタブファイル)や、 https://github.com/phpstan/phpstan-doctrine/tree/1.3.x/stubs (doctrine というライブラリで利用されている例)を参考にしてください。

なお、「Functionn not found」や「Class not found」エラーを潰す目的でスタブファイルを利用しないでください(スタブファイルは、ライブラリの PHPDoc が間違っていた場合の対処方法のため)。PHPStan が Class 等を解決できない場合の対応については https://phpstan.org/user-guide/discovering-symbols のドキュメントを参考にしてください。

スタブファイルも PHPStan の解析にかけたほうがいいのか?

スタブファイルは PHPStan の解析対象の場所に配置しなくてもOKです。スタブファイルに対しては、PHPStan の通常の解析とは関係なく、「スタブファイルの PHPDoc が正しいかどうか」をチェックするようになっています。

スタブファイルが誤っている場合は、 PHPStan はエラーを返します。

スタブファイルに関する設定

スタブファイルを使う場合は、PHPStan の設定ファイルにスタブファイルのパスを追加する必要があります。

parameters:
    stubFiles:
        - stubs/Foo.stub
        - stubs/Bar.stub

スタブファイルの拡張子は何でもよいですが、.php は使わず、 .stub を使うことを推奨します。完全な PHP ファイルではないので、 IDE がエラーを吐く場合があるためです。

スタブファイルを相対パスで書いた場合は、設定ファイルがあるディレクトリからのパスになります。

スタブファイルで Class not found エラーが発生した場合

スタブファイルは通常の(PHPStan 解析対象の)コードベースとは別で解析が走ります。つまり(PHP のコードの方の autoloader などは読み込まれないため)、スタブにしたいクラスの PHPDoc で別のクラスが使われている場合は、そのクラスもスタブファイルに定義する必要があります。

これは、 optional dependency に対応するための仕組みです(optional dependency とは、「依存先のクラス等が存在していなかったとしても、実際にメソッド等が呼ばれるまではエラーにしない」といったもので、例えば、あるライブラリの中に関数Aがあって、関数AがライブラリBに依存していたとしても、関数Aを使わなければライブラリBは必要ありません。こういったライブラリBのようなものを「optional dependency」と呼んででいるのだと思われます)。

optional dependency を許容したい一方で、クラス名などを typo していた場合はエラーにしたいため、結果的にこのような実装になっています。

よって、必要なクラスは必ずスタブファイル内で改めて定義する必要があります。そのため、スタブファイルにはしばしば empty shells (空っぽの貝殻のように、中身が何も無いクラスの定義のことを)が登場します。

訳者補足: 最後の「“Class not found” in a stub file?」とは?

たとえば、スタブファイルに以下を書いたとします。

<?php

namespace Google\Service\Calendar;

class Event
{
  /**
   * @param EventDateTime
   */
  public function setStart(EventDateTime $start)
  {
  }
}

EventDateTime クラス(絶対パスで書くと\Google\Service\Calendar\EventDateTime)は、スタブファイルの中に存在していないので、 class not found エラーになります。

ちゃんと依存関係含めてスタブに定義する必要があるわけです。勝手に元のファイルを読み込んでくれるわけではありません。

ただし、依存先について PHPDoc を置き換える必要がない場合は置き換えなくて大丈夫(空のクラスで問題ない)です。

<?php

namespace Google\Service\Calendar;

class Event
{
  /**
   * @param EventDateTime
   */
  public function setStart(EventDateTime $start)
  {
  }
}

class EventDateTime
{
}