PHPの静的解析ツールとして、PHPStan なるものがあることを最近知りまして。
こちらの導入により、
「PHPを実行しなくても、コード内のエラーを検知してくれる」とのことで、
試してみようかと思いましたが、どうやらLaraStanなるものもあるらしく。
PHPStanの公式を軽く覗いて見たところ、
If you use a popular framework like Symfony, Doctrine or Laravel etc., make sure you install a corresponding extension. It will improve understanding of your code, and also comes with extra rules for correct usage.
参照:https://phpstan.org/user-guide/rule-levels#want-to-go-further%3F
とあり、さらにcorresponding extensionには、
参照:https://phpstan.org/user-guide/extension-library
つまり、
「Laravel を使う場合には、LaraStan(Bladestan)の拡張を入れてね(非公式だけど)」
ということみたいです。
非公式、という点が若干気になりますが、ともかく。
両者を簡単に比較検証して見たいと思います。
PHPStan 公式:URL
LaraStan 公式:URL
検証_前提
動作検証のために、Laravel で簡易的なブログアプリケーションを用意しました。
ブログ記事をCRUDできるだけの最低限なMVCのアプリケーションです。
こちらのRepositoryにPHPStan/LaraStanをそれぞれ導入&比較して見ます。
まずはPHPStanから。
About PHPStan
Install
$ composer require phpstan/phpstan
$ composer install
Usage
詳細な使い方に関しては、以下に記載あり。
基本的な使い方は以下の通り。
$ vendor/bin/phpstan analyse [options] [<paths>...]
↑ これらは、専用の設定ファイルでも設定できるようですが、
今回は用意せず、コマンドだけで実行。
level
実行時に「どの程度をエラーとして検知するか」を示すレベルを「0~9」で指定できるようです。
各レベルの概要を公式から参照すると、以下の通り。(DeepLで翻訳)
0:基本的なチェック、未知のクラス、未知の関数、$this上で呼び出される未知のメソッド、メソッドや関数に渡される引数の数の間違い、常に未定義の変数
1:未定義の可能性のある変数、__call と __get を持つクラスの未知のマジック・メソッドとプロパティ
2:未知のメソッドが ($this だけでなく) すべての式でチェックされる、PHPDocs の検証
3:戻り値の型、プロパティに代入される型
4:基本的なデッドコードのチェック - instanceof やその他の型チェックが常に偽であること。
5:メソッドや関数に渡される引数の型のチェック
6:タイプヒントの欠落を報告
7:部分的に間違ったユニオン型のレポート - ユニオン型の一部の型にのみ存在するメソッドを呼び出した場合、レベル7はそのことをレポートし始めます。
8:NULL 可能な型でのメソッドの呼び出しやプロパティへのアクセスが報告されます。
9:混合型に厳密であること - この型でできる唯一の操作は、この型を別の混合型に渡すことです。
参照: https://phpstan.org/user-guide/rule-levels
level未指定の場合は「0」が設定されるようです。
実際にいくつかレベルを指定して実行した結果を比較してみます。
level:0(最低レベル)
root@8e2c80c182a6:/workspace# vendor/bin/phpstan analyse app
21/21 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
[OK] No errors
エラーは検出されず。
level:5
root@8e2c80c182a6:/workspace# vendor/bin/phpstan analyse --level 5 app
21/21 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
------ --------------------------------------------------------------
Line Http/Controllers/Api/PostsController.php
------ --------------------------------------------------------------
21 Call to an undefined static method App\\Models\\Posts::find().
30 Call to an undefined static method App\\Models\\Posts::find().
41 Call to an undefined static method App\\Models\\Posts::find().
------ --------------------------------------------------------------
[ERROR] Found 3 errors
いくつかエラーが検出されました。
levelで言うと、以下に引っかかったものと推測されます。
1:未定義の可能性のある変数、__call と __get を持つクラスの未知のマジック・メソッドとプロパティ
ですが、これらはLaravelの場合、マジックメソッドを利用した正常なアクセスで、アプリケーションも問題なく動作します。
(おそらく、この辺りがLaraStanの場合には正常な処理として認識されると推測)
なので、
この辺りがエラーとして検出されてしまうと、煩わしいです。
level:9(最高レベル)
root@8e2c80c182a6:/workspace# vendor/bin/phpstan analyse --level 9 app
21/21 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
------ -----------------------------------------------------------------------------------------
Line Http/Controllers/Api/PostsController.php
------ -----------------------------------------------------------------------------------------
12 Method App\\Http\\Controllers\\Api\\PostsController::getList() has no return type
specified.
16 Call to an undefined method
Illuminate\\Contracts\\Routing\\ResponseFactory|Illuminate\\Http\\Response::json().
19 Method App\\Http\\Controllers\\Api\\PostsController::getBy() has no return type specified.
21 Call to an undefined static method App\\Models\\Posts::find().
23 Call to an undefined method
Illuminate\\Contracts\\Routing\\ResponseFactory|Illuminate\\Http\\Response::json().
26 Method App\\Http\\Controllers\\Api\\PostsController::save() has no return type specified.
30 Call to an undefined static method App\\Models\\Posts::find().
36 Call to an undefined method
Illuminate\\Contracts\\Routing\\ResponseFactory|Illuminate\\Http\\Response::json().
39 Method App\\Http\\Controllers\\Api\\PostsController::delete() has no return type specified.
41 Call to an undefined static method App\\Models\\Posts::find().
47 Call to an undefined method
Illuminate\\Contracts\\Routing\\ResponseFactory|Illuminate\\Http\\Response::json().
------ -----------------------------------------------------------------------------------------
------ ----------------------------------------------------------------------------
Line Http/Middleware/RedirectIfAuthenticated.php
------ ----------------------------------------------------------------------------
24 Method App\\Http\\Middleware\\RedirectIfAuthenticated::handle() should return
Symfony\\Component\\HttpFoundation\\Response but returns
Illuminate\\Http\\RedirectResponse|Illuminate\\Routing\\Redirector.
------ ----------------------------------------------------------------------------
------ --------------------------------------
Line Providers/RouteServiceProvider.php
------ --------------------------------------
28 Cannot access property $id on mixed.
------ --------------------------------------
[ERROR] Found 13 errors
最高レベルでの解析だと、
型情報の未定義エラーやLaravelが自動生成するクラスに関してもエラーを検出しています。(このエラーも要らんな…)
とりあえずは、ざっとPHPStanの基本的な使い方は抑えられたと思います。
続いて、LaraStanで同様に動作検証してみたいと思います。
About LaraStan
UnInstall
まずは、PHPStanをuninstall
$ composer remove phpstan
Install
LaraStanをInstall
$ composer require larastan/larastan:^2.0 --dev
$ composer install
Setting
LaraStanの場合は設定ファイル(phpstan.neon or phpstan.neon.dist)の使用が推奨されている模様。
公式を元に構成してみます。
- phpstan.neon
includes:
- vendor/larastan/larastan/extension.neon
parameters:
// 検証対象のディレクトリ名を指定
paths:
- app/
// 検証レベルを指定
level: 5
上記設定ファイルを作成した上で、実際に動作を検証してみます。
Usage
実行コマンドがPHPStanと同じ。
先ほどと同様にいくつかレベルを指定してみて、動作検証。
level:0(最低レベル)
// 検証レベルは設定ファイルで指定しているので、optionsは未指定です
root@8e2c80c182a6:/workspace# ./vendor/bin/phpstan analyse
Note: Using configuration file /workspace/phpstan.neon.
21/21 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
[OK] No errors
PHPStanと同様にエラーは出ませんでした。
level:5
root@8e2c80c182a6:/workspace# ./vendor/bin/phpstan analyse
Note: Using configuration file /workspace/phpstan.neon.
21/21 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
------ --------------------------------------------------------------------------
Line Http/Controllers/Api/PostsController.php
------ --------------------------------------------------------------------------
32 Access to an undefined property
App\\Models\\Posts|Illuminate\\Database\\Eloquent\\Collection<int,
App\\Models\\Posts>::$title.
💡 Learn more:
<https://phpstan.org/blog/solving-phpstan-access-to-undefined-property>
33 Access to an undefined property
App\\Models\\Posts|Illuminate\\Database\\Eloquent\\Collection<int,
App\\Models\\Posts>::$content.
💡 Learn more:
<https://phpstan.org/blog/solving-phpstan-access-to-undefined-property>
------ --------------------------------------------------------------------------
[ERROR] Found 2 errors
PHPStanの場合に出力されていた、以下エラーが無くなりました。
Line Http/Controllers/Api/PostsController.php
21 Call to an undefined static method App\Models\Posts::find().
(マジックメソッドによる正常なアクセスであることを検知してくれている模様)
ですが、新しく以下エラーが検知されました。
Line Http/Controllers/Api/PostsController.php
32 Access to an undefined property
App\Models\Posts|Illuminate\Database\Eloquent\Collection<int,
App\Models\Posts>::$title.
エラーの内容的には、
「PostsModelに定義されていない”title”プロパティにアクセスされました」というものですが、
Eloquent/Postsクラスにphpdocによる@propety title
の記載が無いことが原因で発生したものですね。
(これは何でPHPStanでは検知されないのだろうか…?)
愚直に@propetyを書くか、laravel-ide-helper を導入すれば解決しそうです。
level:9(最高レベル)
root@8e2c80c182a6:/workspace# ./vendor/bin/phpstan analyse
Note: Using configuration file /workspace/phpstan.neon.
21/21 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
------ -----------------------------------------------------------------------------------------
Line Http/Controllers/Api/PostsController.php
------ -----------------------------------------------------------------------------------------
12 Method App\\Http\\Controllers\\Api\\PostsController::getList() has no return type
specified.
19 Method App\\Http\\Controllers\\Api\\PostsController::getBy() has no return type specified.
23 Cannot call method toArray() on
App\\Models\\Posts|Illuminate\\Database\\Eloquent\\Collection<int, App\\Models\\Posts>|null.
26 Method App\\Http\\Controllers\\Api\\PostsController::save() has no return type specified.
32 Cannot access property $title on
App\\Models\\Posts|Illuminate\\Database\\Eloquent\\Collection<int, App\\Models\\Posts>|null.
33 Cannot access property $content on
App\\Models\\Posts|Illuminate\\Database\\Eloquent\\Collection<int, App\\Models\\Posts>|null.
34 Cannot call method save() on
App\\Models\\Posts|Illuminate\\Database\\Eloquent\\Collection<int, App\\Models\\Posts>|null.
39 Method App\\Http\\Controllers\\Api\\PostsController::delete() has no return type specified.
46 Call to an undefined method
App\\Models\\Posts|Illuminate\\Database\\Eloquent\\Collection<int,
App\\Models\\Posts>::delete().
------ -----------------------------------------------------------------------------------------
[ERROR] Found 9 errors
PHPStanでは出力されていた、以下のようなLaracelがデフォルトで用意するクラスで検知しているエラーが消えて、自作クラスのエラーだけ検知している!!
Line Http/Middleware/RedirectIfAuthenticated.php
24 Method App\Http\Middleware\RedirectIfAuthenticated::handle() should return
Symfony\Component\HttpFoundation\Response but returns
Illuminate\Http\RedirectResponse|Illuminate\Routing\Redirector.
で、新規でいくつかエラーが検知。
23 Cannot call method toArray() on
App\Models\Posts|Illuminate\Database\Eloquent\Collection<int, App\Models\Posts>|null.
これは、以下のエラーに引っかかっている模様。
「7:部分的に間違ったユニオン型のレポート – ユニオン型の一部の型にのみ存在するメソッドを呼び出した場合、レベル7はそのことをレポートし始めます。」
なるほどー!!!
確かに。これは直した方が良いかー。
まとめ
Laravel のApplicationなら、LaraStanを使う方が良さそう(当たり前か)
以下、理由。
- PHPStanでは、Laravel Applicationなら本来想定されている使い方であるにも関わらず、エラーとして検知するケースがあるので、煩わしい。
- 例)
- Eloquentのマジックメソッド
- Laravelがデフォルトで用意するクラス内
- 例)
- 一方で、LaraStanでだけ検知されるエラーがあったが、内容的に検知されるべきエラーではあった。
- PHPStanでも検知されるべきだと思うのだけど、何で検知されないのだろう、と内容のエラーだった(理解が浅いだけだが)
→ LaraStanの方が生産的!!!と感じました。
Reference
level=0 から始める PHPStan(Larastan) 導入ガイド:https://blog.shin1x1.com/entry/getting-stated-with-phpstan
コメント