前回紹介した楽しい開発ツールphanを、今回はLaravelのプロジェクトに適用してみます。さて、どれだけ楽しくなるか!

まず、Laravel 5.4の私の日本語開発用のレポジトリをインストールします。

$ git clone git@github.com:lotsofbytes/larajapan.git

次に、phanのパッケージをインストールします(注1)

$ cd larajapan
$ composer require --dev phan/phan

インストールできたところで、phanの設定をします。

.phanのディレクトリを作成して、config.phpのファイルを作成します。


return [
    // 解析のためにクラスやメソッドの情報を取得するディレクトリ。
    // これらと、以下の exclude_analysis_directory_list で指定した
    // ディレクトリとの差分が解析チェックの対象となる
    'directory_list' => [
        'app',
        'routes',
        'vendor'
    ],

    // 解析チェックの対象から除外するディレクトリ。
    'exclude_analysis_directory_list' => [
       'vendor'
    ],
];

設定ファイルが用意できたところで、最初のphanの実行をしてみましょう。

$ vendor/bin/phan -p 
app/Auth/Notifications/ResetPassword.php:51 PhanTypeMismatchArgument Argument 2 (parameters) is string but \route() takes array defined at vendor/laravel/framework/src/Illuminate/Foundation/helpers.php:707
app/Auth/Passwords/CanResetPassword.php:16 PhanUndeclaredProperty Reference to undeclared property \App\Auth\Passwords\CanResetPassword->email
app/Auth/Passwords/CanResetPassword.php:27 PhanUndeclaredMethod Call to undeclared method \App\Auth\Passwords\CanResetPassword::notify
app/Providers/RouteServiceProvider.php:54 PhanUndeclaredMethod Call to undeclared method \Illuminate\Routing\Route::namespace
app/Providers/RouteServiceProvider.php:68 PhanUndeclaredMethod Call to undeclared method \Illuminate\Routing\Route::namespace
routes/api.php:16 PhanUndeclaredClassMethod Call to method middleware from undeclared class \Route
routes/channels.php:14 PhanUndeclaredClassMethod Call to method channel from undeclared class \Broadcast
routes/console.php:16 PhanUndeclaredClassMethod Call to method command from undeclared class \Artisan
routes/console.php:17 PhanUndeclaredVariable Variable $this is undeclared
routes/web.php:14 PhanUndeclaredClassMethod Call to method get from undeclared class \Route
routes/web.php:22 PhanUndeclaredClassMethod Call to method get from undeclared class \Route
routes/web.php:23 PhanUndeclaredClassMethod Call to method post from undeclared class \Route
routes/web.php:24 PhanUndeclaredClassMethod Call to method post from undeclared class \Route
routes/web.php:27 PhanUndeclaredClassMethod Call to method get from undeclared class \Route
routes/web.php:28 PhanUndeclaredClassMethod Call to method post from undeclared class \Route
routes/web.php:31 PhanUndeclaredClassMethod Call to method get from undeclared class \Route
routes/web.php:32 PhanUndeclaredClassMethod Call to method post from undeclared class \Route
routes/web.php:33 PhanUndeclaredClassMethod Call to method get from undeclared class \Route
routes/web.php:34 PhanUndeclaredClassMethod Call to method post from undeclared class \Route
routes/web.php:36 PhanUndeclaredClassMethod Call to method get from undeclared class \Route

たくさんエラー出てきました。

その中でもPhanUndeclaredClassMethodが多い。これは宣言されていないクラスのメソッドをコールしているエラーです。

例えば、一番多いエラーとなっているRouteのクラスは、Laravel特有のFacade(ファサード)です。ネームスペースnamespaceがあるクラスと違い、phanにとっては解析が難しいようです。

調査したところ、IDEヘルパーの開発ツールを使用して、stabのファイルを作成することで回避できそうです。Laravelのstabのファイルの作成には、まず以下を実行してide-helperのパッケージをインストールします。

$ composer require --dev barryvdh/laravel-ide-helper

次に、config/app.phpを編集して以下を追加、

Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class

このパッケージは、debugbarの開発者が作成したものです。本来は、phpStormなどのIDEのエディターでのヘルパーが目的(それゆえにlaravel-ide-helper)ですが、ファサードのクラスのメソッドを宣言してくれるために、phanでも役に立つようです。

..
    'providers' => [  
..
        /*                                                                                                                                                                                                         
        * Package Service Providers...                                                                                                                                                                            
        */                                                                                                                                                                                                        
        Laravel\Tinker\TinkerServiceProvider::class,                                                                                                                                                               
        Barryvdh\Debugbar\ServiceProvider::class,
        Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,

..

そして、以下を実行。

$ php artisan ide-helper:generate

_ide_helper.phpのファイルが実行したディレクトリに作成されます。これを、.phan/stabsのディレクトリを作成して移します。

$ mkdir .phan/stabs
$ mv _ide_helper.php .phan/stabs

さらに、このファイルを利用するために、.phan/config.phpも編集が必要です。


return [
    // 解析のためにクラスやメソッドの情報を取得するディレクトリ。
    // これらと、以下の exclude_analysis_directory_list で指定した
    // ディレクトリとの差分が解析チェックの対象となる
    'directory_list' => [
     '.phan/stabs',
        'app',
        'routes',
        'vendor'
    ],

    // 解析チェックの対象から除外するディレクトリ。
    'exclude_analysis_directory_list' => [
       '.phan',
       'vendor'
    ],
];

これで実行準備完了です。再度実行してみましょう。

$ vendor/bin/phan -p
app/Auth/Notifications/ResetPassword.php:51 PhanTypeMismatchArgument Argument 2 (parameters) is string but \route() takes array defined at vendor/laravel/framework/src/Illuminate/Foundation/helpers.php:707
app/Auth/Passwords/CanResetPassword.php:16 PhanUndeclaredProperty Reference to undeclared property \App\Auth\Passwords\CanResetPassword->email
app/Auth/Passwords/CanResetPassword.php:27 PhanUndeclaredMethod Call to undeclared method \App\Auth\Passwords\CanResetPassword::notify
app/Providers/RouteServiceProvider.php:54 PhanUndeclaredStaticMethod Static call to undeclared method \Illuminate\Support\Facades\Route::middleware
app/Providers/RouteServiceProvider.php:68 PhanUndeclaredStaticMethod Static call to undeclared method \Illuminate\Support\Facades\Route::prefix
routes/api.php:16 PhanUndeclaredStaticMethod Static call to undeclared method \Route::middleware
routes/channels.php:14 PhanUndeclaredStaticMethod Static call to undeclared method \Broadcast::channel
routes/console.php:17 PhanUndeclaredVariable Variable $this is undeclared

PhanUndeclaredClassMethodのエラーが少なくなりました。しかし、残り未宣言のエラーの解決は現段階では無理そうです。実際宣言されていにも関わらず、その情報をphanに伝えることが自動のツールでは不可能だからです。将来に期待しましょう。

とりあえず、ここでは-iオプションで、この未宣言のエラーを無視することにします。

$ vendor/bin/phan -p -i
app/Auth/Notifications/ResetPassword.php:51 PhanTypeMismatchArgument Argument 2 (parameters) is string but \route() takes array defined at vendor/laravel

-iで、エラーは1つだけとなりました。

このエラーは、以下のroute()の2番目の引数が配列である必要があるということで、

..
   public function toMail($notifiable)                                                                                                                                                                            
    {                                                                                                                                                                                                              
        return (new MailMessage)                                                                                                                                                                                   
            ->subject('パスワードリセット')                                                                                                                                                                        
            ->greeting('パスワードリセット')                                                                                                                                                                       
            ->line('パスワードリセットリンクの送信のリクエストがありました。')                                                                                                                                     
            ->action('リセットパスワード', url(config('app.url').route('password.reset', [$this->token], false)))                                                                                                    
            ->line('リクエストされていなかったら、無視してください。');                                                                                                                                            
    }     
..

と編集して解決できました。

ということで、インストールは”そう楽しくなさそうな”ツールですが、もし大きなプログラムがあるなら一度でも試してください。私のプロジェクトではいくつか重要なバグをみつけてくれてそれだけでも十分な貢献です。これからも、検証のツールとしてunit testとともに重要なものになりそうです。

ひとつ注意することは、この実行には空きメモリが2GB以上必要です。残念ながら私のテスト環境のawsのsmall(メモリ2GB)のインスタンスでは実行不可能でした。仮想の開発環境(メモリ4GB)で実行可能でした。

注1

composerで、phpdocumentor/reflection-docblockのバージョンのコンフリクトが出ました。すでにインストールされているそのパッケージのバージョンは4.1.1ですが、phan/phanが必要とするものはそれよりも昔のバージョン。ということで、

..
   "require": {
        "php": ">=5.6.4",
        "barryvdh/laravel-debugbar": "~2.4",
        "laravel/framework": "5.4.*",
        "laravel/tinker": "~1.0",
        "phpdocumentor/reflection-docblock": "4.1.1 as 3.3.0"
    },
..

4.1.1 as 3.3.0として4.1.1のバージョンを3.3.0のバージョンとして扱ってもらうことで解決できました。

By khino