Laravel & PHPStan(Larastan)で静的コード解析を行う
- 公開日
- カテゴリ:Laravel
- タグ:PHP,Laravel,PHPStan,Parsing,Larastan

PHP で WEB アプリケーション開発を行う際の静的コード解析ツールと言えば PHPStan が有名ですが、それを Laravel 用に最適化した Larastan というパッケージがあります。
今回は、Larastan を使って Laravel アプリケーションの実装コードの静的解析を行っていきます。
Contents
開発環境
今回の開発環境は以下の通りです。
- PHP 7.3
- Laravel 5.8
- Composer 1.8
Laravel のルートディレクトリを「 laravel/」としています。
静的コード解析
静的コード解析 (Static Code Analysis) は、実行ファイルを動作させる事なく解析を行い、定義に対しての振る舞いに一貫性があるかどうかを検証する事を指します。「静的解析」「静的プログラム解析」とも呼ばれています。
前回の記事(Laravel に PHP_CodeSniffer を導入しコーディング規約( PSR)に沿った記述を行う)で紹介した構文チェックも、広義にはこれに含まれます。
Larastan
Larastan は、Laravel 用の PHPStan ラッパーです。
https://github.com/nunomaduro/larastan
FW 仕様のフォローに時間をかけない事、フォローの為のパッケージや設定ファイルを増やさない事を考えると選択肢の1つとしておすすめです。
インストール
composer にて導入します。以下のコマンドを叩いてインストールします。
# プロジェクトルートへ移動
cd /path/to/laravel
# Larastan インストール
composer require --dev nunomaduro/larastan
静的コード解析実行
まだ何も実装を行ってはいませんが、動作確認を兼ねて静的コード解析を実行してみます。
app と tests ディレクトリを対象に、レベルは MAX(最高値=7=最も厳しい, 2019年8月現在)で解析を行います。
Rule Level の最新は以下で確認できます
https://phpstan.org/user-guide/rule-levels
実行には以下の artisan コマンドを叩きます。
# 静的コード解析実行
php artisan code:analyse --paths="app,tests" --level=max
# 実行結果
30/30 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
[OK] No errors
エラーなく実行できる事が確認できました。
ちなみに Larastan は、デフォルト(オプション未指定)で実行した場合は以下の動作をします。
解析対象(ファイルやディレクトリ)未指定の場合 app ディレクトリのみを見に行く レベル未指定の場合 レベル5で実行 レベルは0~7まであり、7が最大値(厳しい)となっています。
phpstan.neon
artisan コマンドにオプションである解析対象のパスとレベルを付けて実行しましたが、Larastan も PHPStan と同じように、phpstan.neon へオプションを書き出し、コマンドを簡単にすることができます。
- laravel/phpstan.neon
-
includes: - ./vendor/nunomaduro/larastan/extension.neon parameters: level: max paths: - app - tests
ポイントは larastan の extension.neon を include する点です。 phpstan.neon を作成する場合はこれを行わないと Larastan の設定が読み込まれないので恩恵に預かる事ができません。
そして1点問題があります。
Larastan の extension.neon では「 paths 」が効きません。
つまり上記で設定した「 app 」と「 tests 」は無視されます。なので結局artisan コマンドで paths オプションは付けてあげないと app ディレクトリのみを解析してしまいます。
これはバグとして認識されているのですが、2019 年 8 月現在、まだ修正されていません。
phpstan.neon paths not working · Issue #202 · nunomaduro/larastan
修正されるまでは composer.json の scripts に登録して使うのが良いかもしれません。
- laravel/composer.json
-
"scripts": { "phpstan": [ "@php artisan code:analyse --paths=\"app,tests\" --level=max" ] }
実装コードを解析する1
ここからは実際にコーディングを行い、静的コード解析を行っていきます。
- laravel/app/Http/Controllers/SampleControllers.php
-
<?php declare(strict_types=1); namespace App\Http\Controllers; use Illuminate\Http\Request; class SampleController extends Controller { public function index() { $a = 1280; $b = 2500; return $this->addition($a, $b); } /** * 加算 * @param int $a * @param int $b * @return int */ private function addition(int $a, int $b): int { return $a + $b; } }
上記は足し算を行う処理ですが、このコードは問題無いのでテストに通ります。
ここから、一方の値を文字列型にしてみます。
public function index()
{
$a = 1280;
// $b = 2500;
$b = '2500';
return $this->addition($a, $b);
}
この状態で静的解析を実行すると、エラーとなります。
------ --------------------------------------------------------------------------------------------------------
Line app/Http/Controllers/SampleController.php
------ --------------------------------------------------------------------------------------------------------
16 Parameter #2 $b of method App\Http\Controllers\SampleController::addition() expects int, string given.
------ --------------------------------------------------------------------------------------------------------
[ERROR] Found 1 error
足し算を行う addition() メソッドの引数には数値型を指定しているので、静的コード解析で文字列型が渡ってくる事が判明した為、エラーとなりました。
この例は実装もエラー箇所も単純なのでなかなか解析の有り難みを受けにくいかもしれませんが、最大の恩恵は処理を実行せずにこうした不整合を発見できた事にあります。
例えば処理がもっともっと入り組んで複雑な機能などの場合では知らず知らずのうちに意図しない形式のデータを渡してしまう事もありますし、チームで開発していて自分以外のメンバーが実装した処理の仕様に気づかない場合もあります。
これらは間々ある事ですが、それを、コードを実行せずに判定出来るという事がこの静的コード解析の最大の利点です。
実装コードを解析する2
もう一つ例を見てみます。今度は Eloquent を絡めてデータを取得します。
- laravel/app/Http/Controllers/SampleControllers.php
-
<?php declare(strict_types=1); namespace App\Http\Controllers; use Illuminate\Http\Request; use App\User; class SampleController extends Controller { public function get() { $user = User::where('id', 1)->first(); return $this->userOperation($user); } /** * ユーザーデータを処理する(つもり) * @param User $user * @return User */ private function userOperation(User $user) { /* * 色々な処理... */ return $user; } }
上記はユーザーのデータをデータソースから取得し、それを何かしらの処理を行ってから返却するまでの一連の処理になっています。
この実装の場合、静的解析を行うとエラー無く通ります。
では以下の場合ではどうでしょうか
public function get()
{
// $user = User::where('id', 1)->first();
$user = User::where('id', 1)->get();
return $this->userOperation($user);
}
この場合はエラーになります。
前者は first() で取得しているので単数での取得となり User オブジェクトが返却されるのに対して、後者は get() で取得しているので Collection オブジェクトで返却されます。
よって、次の userOperation() に渡す際の引数のタイプヒンティングで不整合が起きる為にエラーとなってしまいます。
取得出来るデータはどちらでも目的通りのものになりますが、少しの違いでこうして意図しないものを次へ渡してしまう事が結構カジュアルに起こるものです。
もちろんこれは実行すればエラーとなりスタックトレースが表示されますが、わざわざブラウザを叩かなくても静的解析を走らせれば処理を実行しなくてもミスに気づく事ができます。
実装コードを解析する3
ちなみに、データソースから取得するデータは必ずしも期待した結果(同一の型)ではない事もあります。
// レコードは id < 10 しか存在しないのに
$user = User::where('id', 100)->first();
Eloquent の first() メソッドでの取得の場合は、データが存在する場合は User オブジェクトが返りますが、データが存在しない場合は null が返ってきます。
ちなみにこのメソッドの PHPDoc には、返り値が以下の様に記述されています。
/**
* Execute the query and get the first result.
*
* @param array $columns
* @return \Illuminate\Database\Eloquent\Model|object|static|null
*/
public function first($columns = ['*'])
{
return $this->take(1)->get($columns)->first();
}
これでは次の、引数を User オブジェクトとしてタイプヒンティングしている userOperation() メソッドには値を渡す事ができません。
PHPStan では実行ファイルを動作させるわけではないのでメソッドの返り値などを PHPDoc から読み取ったりしながら解析を行います。
つまり null が返ってくる可能性があると判断するので、静的解析を走らせるとエラーとなります。
------ --------------------------------------------------------------------------------------------------------------------
Line app/Http/Controllers/SampleController.php
------ --------------------------------------------------------------------------------------------------------------------
51 Parameter #1 $user of method App\Http\Controllers\SampleController::userOperation() expects App\User, mixed given.
------ --------------------------------------------------------------------------------------------------------------------
「 User オブジェクトの型指定してるけど、これ実際には mixed な型で入ってきますよ。」
しかしながらこれは正常な判定です。
この実装の場合、必ずユーザーデータが返ってくる(し、返ってくるであろう検索しか行わない)と暗黙的に了解し実装してしまっているので、それに伴って次の userOperation() メソッドでも、User オブジェクトの型指定を行ってしまっている事に気付かされます。
でも実際は、null である場合の状況もハンドリングしてあげるべきなのです。
という事で、しっかり処理を挟みます。
public function get()
{
// レコードは id < 10 しか存在しないのに
$user = User::where('id', 100)->first();
if(!$user) {
throw new \Exception('User not found.');
}
return $this->userOperation($user);
}
「必ずユーザーデータが返ってくる」という前提なので、今回は null なら例外を投げています。
どういう処理を行えばスムーズ且つクリーンなのかは状況によりますが、こうして然るべき状態をハンドリングしてあげることで次の処理に想定外の値(型)が渡される事が無くなるので、堅牢な実装を継続する事ができます。
今回の場合、もし型指定を外しても null 値が渡ったらエラーになるでしょう。しかも、「必ずユーザーデータが返ってくる」前提で実装されているので、そうでなかった時の処理はおそらく書かずにリリースされる可能性もあります。
気づく事が出来たからこそ予めハンドリングを行えて、異常終了しない処理を実装できそうです。
そして何よりこの実装なら、静的コード解析を無事に通過します。
[OK] No errors
エラーを無視する
にっちもさっちもいかなくなったら、エラーを無視するという手段も取れます。
その場合は設定ファイルに ignoreErrors を定義します。
- laravel/phpstan.neon
-
includes: - ./vendor/nunomaduro/larastan/extension.neon parameters: level: max paths: - app - tests ignoreErrors: - '#Parameter \#1 \$user of method App\\Http\\Controllers\\SampleController::userOperation\(\) expects App\\User, mixed given#' # 正規表現も使用可能 # - '#Parameter \#1 \$[a-z]{1}[a-zA-Z0-9_]+ of method App\\Http\\Controllers\\SampleController::userOperation\(\) expects App\\User, mixed given#'
正規表現も使えます。上記のコメントアウトされている記述は、変数名を正規表現で置き換えた場合です。これでも適用されます。
ただこれは、FW の性格上どうしても回避せざるを得ない場合とかに限り使用するのが良いと思います。きちんと動作を理解した上で無視しないと、本来エラー検知しなくてはならないところを通過させてしまう可能性があるからです。
そしてなんでもかんでも除外していると、気がついたら結構な量のエラー無視になり、管理しきれなくなります。これでは、静的コード解析を行う意味がありません。
まとめ
以上で作業は終了です。静的コード解析を行う事で実装中の気づきは沢山得られると思います。
開発はトライ&エラーの繰り返しですが、大切な事は走らせたアプリケーションを予想外の振る舞いで止めない事。未知の脅威の排除はできるだけしておきたいところです。その為にこういった解析ツールも前向きに導入していきたいですね。