🐡

PHPStanのデバッグを行う方法

2023/12/19に公開

この記事は カオナビ Advent Calendar 2023 19日目です。

はじめに

はいさい。@shimabox です。
気づいたら12月、12月なのに暑かったり寒かったりよくわからない感じですがみなさん元気でしょうか。

今回、何を書こうかなぁと考えて「せや、PHPStanがどうやって解析をしているのか解き明かしたろ」と思いデバッグを開始したのですけど、これはじっくりと時間をかけないと無理なやつでした。。
このままではアカンので、せめてデバッグを行えるまでの手順を記事にしたいと思います。

この記事に真のタイトルをつけるのならば、PHPStanの解析を試みたが挫けた話です。

おことわり

今回の作業はすべてVSCodeで行います。
あと、docker compose は使えるものとしています。

PHPStanとは

まずはPHPStanとはなんぞやという話ですが、PHPStanはPHPの静的解析ツールです。
Getting Started | PHPStan

コードを実行せずに

  • 関数に渡すパラメータの数が適切か?
  • 未定義のものにアクセスしようとしていないか?
  • 関数に渡す値が関数のパラメータの型宣言と一致するか?
  • PHPDocの内容と関数の戻り値は同じか?
  • etc...

などのエラーを見つけてくれます。
端的に言うと型チェックをしてくれる優れものだと認識してくれればいいのかなと思います。

PHPStanのソースはどこ?

これ、知っている人は知っているかもしれませんが、じゃあデバッグするぞとPHPStanのソースを見ようと思って phpstan/phpstan を覗いても phpstan.phar しかありません。ファーーー。
※ phpstanを実行するときの、vendor/bin/phpunit はこのpharファイルを読み込んでいるだけです

じゃあ、どこを見ればいいんや?という話ですが、phpstanのソースは phpstan/phpstan-src に大本があります。そして phpstan-src/.github/workflows/phar.yml のジョブで phpstan.phar が作成されています(たぶん)。

なので、PHPStanのデバッグはphpstan/phpstan-srcに対して行っていけばよさそうです。

拡張機能を入れておく

デバッグの前に以下2つの拡張機能をVSCodeに入れておきます。

  • Dev Containers
  • PHP Debug
    • 後述する、Dev container を使う場合は必要ないです

デバッグする

ではどのようにデバッグするかですが、今回は2つの方法を試してみたのでそれぞれ書いていきます。簡単に言うと、Dev container を使う場合と使わない場合です。

Dev container を使う

おそらく、これが一番シンプル?だと思います。
phpstan/phpstan-srcには、phpstan-src/.devcontainer があるのでこのコンテナーを利用してデバッグします。
ただ、自分が試した限り少し修正しないとうまく動かなかったのでそのあたりの対応方法も合わせて説明していきます。

1. phpstan-srcをclone

git clone https://github.com/phpstan/phpstan-src.git
cd phpstan-src

2. .devcontainer/Dockerfileを修正

.devcontainer/DockerfileENV COMPOSER_ROOT_VERSION 1.11.x-devを追記します。
こうしないと、ビルド時のcomposer installに自分は失敗しました。

.devcontainer/Dockerfile
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer

# 以下を追加
# https://github.com/phpstan/phpstan-src インストール用
# @see https://github.com/phpstan/phpstan-src/tree/1.11.x?tab=readme-ov-file#installation
ENV COMPOSER_ROOT_VERSION 1.11.x-dev

3. .vscode/launch.jsonの作成

.vscode/launch.json を作成しておきます。portは9000です。

.vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Listen for Xdebug",
            "type": "php",
            "request": "launch",
            "port": 9000
        }
    ]
}

4. VSCodeで開く

Cmd + o なり、File → Open なりでcloneしたソース(phpstan-src)を選択します。

5. Reopen in Container する

Reopen in Container と右下に表示されると思うので選択します。

6. ビルドがはじまる

ビルドがはじまるのでしばし待ちます。

7. コンテナーに接続される

ビルドが終わると、コンテナーに接続された状態になります。

8. 適当にデバッグ用ファイルを作る

適当にデバッグ用ファイルを作ります。今回は、app/User.phpを作ってみました。

app/User.php
<?php declare(strict_types=1);

class User
{
    /**
     * @param int $id
     * @param string $name
     */
    public function __construct(
        private readonly int $id,
        private readonly string $name,
    ) {}

    /**
     * @return array
     */
    public function toArray(): array
    {
        return [
            'id'   => $this->id,
            'name' => $this->name,
        ];
    }
}

9. デバッグを起動する

拡張機能のPHP Debugを選択して、Listen for Xdebug を起動しておきます。
※ PHP DebugはDevContainerの設定で勝手にインストールされているはずです

10. デバッグを実行する

src/Command/AnalyseCommand.phpexecute() に適当にブレークポイントをはって、phpstanを実行します。コマンドは以下の通りになります。bin/phpstan が実行ファイルになります。

コマンド
$ bin/phpstan analyze -l 6 app/User.php

実行すると、このようにブレイクされるはずです。

なお、ブレイクを抜けて最後まで実行した結果はこうです。

vscode ➜ /workspaces/phpstan-src (1.11.x) $ bin/phpstan analyze -l 6 app/User.php 
Note: Using configuration file /workspaces/phpstan-src/phpstan.neon.dist.
 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

 ------ ------------------------------------------------------------------------------------------- 
  Line   User.php                                                                                   
 ------ ------------------------------------------------------------------------------------------- 
  :17    Method User::toArray() return type has no value type specified in iterable type array.     
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type  
 ------ ------------------------------------------------------------------------------------------- 
                                                                                                              
 [ERROR] Found 1 error                                                                                        

こんな感じで、phpstanを実行しつつデバッグできます。
これでブレークポイントをはりまくってデバッグできますね!

なお、この環境は

$ php -v
PHP 8.1.26 (cli) (built: Nov 27 2023 23:09:30) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.26, Copyright (c) Zend Technologies
    with Xdebug v3.3.0, Copyright (c) 2002-2023, by Derick Rethans

となります。

Dev container を使わない

なんで Dev container を使わないかというと、これには特に意味はなく自分が最初に試していた方法がこれだったからです。
(phpstan-srcでDev containerを使えることに気づいていなかったし、Dev containerに詳しくなかったというのもあります)

方法としては単純にPHP Debugを使うだけです。

サンプル

というわけで、自分で試していたということもありサンプルがあります。

ではこちらのリポジトリを使ってデバッグしてみたいと思います。
ちなみに、こちらの環境は

$ php -v
PHP 8.3.0 (cli) (built: Dec 16 2023 02:34:44) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.3.0, Copyright (c) Zend Technologies
    with Xdebug v3.3.1, Copyright (c) 2002-2023, by Derick Rethans

となります。PHP8.3!!!

1. 準備する

cloneして、makeコマンド(make init)を叩けば試せる環境が整います。

git clone https://github.com/shimabox/sample-debug-phpstan.git
cd sample-debug-phpstan
make init

このコマンドを実行すると、sample-debug-phpstan/debug/ 以下に、phpstan/phpstan-srcがインストールされます。

2. コンテナーに入る

make app でコンテナーに入っておきます。

make app

3. デバッグを起動する

拡張機能のPHP Debugを選択して、Listen for Xdebug を起動しておきます。

4. デバッグを実行する

src/Command/AnalyseCommand.phpexecute() に適当にブレークポイントをはって、phpstanを実行します。コマンドは以下の通りになります。

コマンド
# debug以下に、phpstan/phpstan-srcがインストールされている
$ debug/phpstan-src/bin/phpstan analyze -l 6 src/User.php

実行すると、このようにブレイクされるはずです。

最後まで実行した結果はこうです。

root@9976e620471b:/var/www/html# debug/phpstan-src/bin/phpstan analyze -l 6 src/User.php
Note: Using configuration file /var/www/html/phpstan.neon.dist.
 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

 ------ -------------------------------------------------------------------------------------------- 
  Line   User.php                                                                                    
 ------ -------------------------------------------------------------------------------------------- 
  23     Method App\User::toArray() return type has no value type specified in iterable type array.  
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type   
 ------ -------------------------------------------------------------------------------------------- 
                                                                                                                        
 [ERROR] Found 1 error

こちらの方法でも、こんな感じでphpstanを実行しつつデバッグできます。
これでブレークポイントをはりまくって(略

自分的には、自由に試せる感じがするのでこっちのほうが好みです。はい。

おわりに

というわけで、PHPStanをデバッグする方法を書いてみました。
最初けっこうつまづいたので誰かの役に立てたら嬉しい限りですし、他にもこんなやり方あるよ!!とか教えてくれるとさらに嬉しいです(実はもっと簡単に出来るんじゃないかと内心ヒヤヒヤしています)。

本当はこれでデバッグしまくってPHPStanがどのように解析しているのかをバシッと知りたかったのですが、とりあえず環境は出来たので時間を見つけてanalyzeしていきたいと思います。来年に持ち越しだ!!!

P.S. Dev Container なにげに使ったことがなかったので最初よくわからなかったけど、使いこなせるようになったら便利なんだろうなーとちょっと思いました

Discussion