Laravelに静的解析ツールのLarastanを導入し、厳しくチェックする
php で有名な PHPStan という静的解析ツールがあるのですが、その Laravel 版として Larastan というものがあります。
こちらを利用することで、Laravel のソースコードの静的解析が可能となります。
今回はその Larastan の導入とどう言った解析が可能になるのかをご紹介します。
インストール
Larastan は composer を使ってインストールします。php だと PHPStan ですが、この Larastan をインストールすることで、一緒に PHPStan もインストールしてくれます。
composer --dev require nunomaduro/larastan
vender ディレクトリの中にインストールされましたので、インストールされたかを確認します。
./vendor/bin/phpstan analyse -V
PHPStan - PHP Static Analysis Tool 1.5.6
上記の通り、ベースは PHPStan であることがわかります。
設定ファイルの作成
Larastan を実行する際の設定ファイルを作成します。
ファイル名は、「phpstan.neon」または「phpstan.neon.dist」で作成します。
includes:
- ./vendor/nunomaduro/larastan/extension.neon
parameters:
paths:
- app
# The level 9 is the highest level
level: 5
ignoreErrors:
- '#Unsafe usage of new static#'
excludePaths:
- ./*/*/FileToBeExcluded.php
checkMissingIterableValueType: false
解析実行
実際に解析します。
./vendor/bin/phpstan analyse
実行後、下記のようにメモリ不足のエラー起きる可能性があります。
Child process error (exit code 255): [2022-05-02 00:50:29] local.ERROR: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes) {"exception":"[object] (Symfony\\Component\\ErrorHandler\\Error\\FatalError(code: 0): Allowed memory size of 134217728
bytes exhausted (tried to allocate 20480 bytes) at phar:///Users/naokim/Documents/matsuzaki_app/nextporn/vendor/phpstan/phpstan/phpstan.phar/vendor/phpstan/phpdoc-parser/src/Lexer/Lexer.php:69)
その場合、メモリ上限を上げてコマンドを打つと良いです。
./vendor/bin/phpstan analyse --memory-limit=2G
実行後、レベルに応じたエラーが出力されるはずです。
適用ルールの解説
NoModelMake
NoUnnecessaryCollectionCall
------ ---------------------------------------------------------------------------------
Line app/Consts/SitemapUrl.php
------ ---------------------------------------------------------------------------------
51 Called 'count' on Laravel collection, but could have been retrieved as a query.
------ ---------------------------------------------------------------------------------
上記のようなエラーは、無駄なクエリの検知をしてくれます。
例えば、下記のようなコードがあります。
Category::all()->count()
カテゴリーの件数を取得するため、count()を利用していますが、collection として取得した後に件数を取得しなくても良いので、下記のように修正できます。
Category::count()
このように無駄なクエリの箇所も指摘されます。
CheckDispatchArgumentTypesCompatibleWithClassConstructorRule
------ --------------------------------------------------------------
Line app/Mail/ContactPage.php
------ --------------------------------------------------------------
22 Access to an undefined property App\Mail\ContactPage::$message.
------ --------------------------------------------------------------
ジョブのディスパッチ引数の型がジョブクラスのコンストラクターと互換性があるかをチェックします。
ジョブに限らず、コンストラクタ関数を呼んでいるファイルがあった場合、$message の定義をしていないと undefined のエラーを起こします。
/**
* Create a new message instance.
*
* @return void
*/
public function __construct($message)
{
$this->message = $message;
}
下記の通り、$message 定義をします。
/**
* お問い合わせ内容
*
* @var string
*/
protected $message;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct($message)
{
$this->message = $message;
}
RelationExistenceRule
Eloquent 経由で DB アクセスする際、存在しないカラムに対してデータ取得しようとしている場合に検知できます。
\App\User::query()->has('foo');
\App\Post::query()->has('users.transactions.foo');
下記エラーを出力します。
Relation 'foo' is not found in App\User model.
Relation 'foo' is not found in App\Transaction model.
コメントのチェック
- 返り値が実際の処理と合っているか
- 引数の型と命名は一致しているか
など、コメントを厳密にチェックします。
修正例1。
/**
* ユーザーが作成した商品を取得する。
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function products()
{
return $this->hasMany(Product::class);
}
修正例2。
/**
* 指定列を特定日で絞り込む。
*
* @param Illuminate\Database\Eloquent\Builder $builder クエリビルダ
* @param date $date 特定日
* @param date $column 絞り込みカラム名
* @return \Illuminate\Database\Eloquent\Builder
*/
private static function filterColumnsBySpecificDate($builder, $date, $column)
{
return $builder->whereDate($column, $date);
}
適用ルールは Github にありますので、参照ください。
解析を無視するとき
どうしても解析を無視したい時があるはずです。
その時は、baseline ファイルを作成すると良いです。
./vendor/bin/phpstan analyse --generate-baseline
実行後、phpstan-baseline.neon
ファイルが作成され、実際に無視されるエラー内容が記載されます。
parameters:
ignoreErrors:
-
message: "#^Call to an undefined static method Foo\\:\\:sub\\(\\)\\.$#"
count: 1
path: index.php
解析の自動化
実際に Larastan を導入しても各自がチェックしないと意味がないため、強制的に Github へ commit したらチェックするように自動化します。
まずは、.github/workflows/phpstan.yaml として下記ファイルを作成します。
name: larastan
on:
pull_request:
paths:
- "**.php"
jobs:
laravel:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.0'
tools: composer:v2
- name: Resolve dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Run larastan
run: ./vendor/bin/phpstan analyse --memory-limit=2G --configuration=phpstan.neon
Actions では、shivammathur/setup-php@v2
を利用して PHP のバージョンを指定します。
その後は、composer install を実行し、Larastan 実行するという単純な CI です。
GitHubActrions のトリガーは pull request に php ファイルへの変更が含まれている場合に実行させます。
実際にエラーが起きた箇所も指摘してくれます。
更なる自動化の検討
上記の通り、自動化ができたのですが、Github の PR にコメントを入れたい。
と思い、 reviewdog
の導入を検討しました。
reviewdog とは
reviewdog とは、各種 linter 等の実行結果をプルリクエストのコメントで指摘してくれます。 詳細な説明は作者様の記事を参照するのが良いです。
reviewdog を導入した場合の yaml ファイルです。
name: larastan
on:
pull_request:
paths:
- "**.php"
jobs:
analyse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: reviewdog/action-setup@v1
with:
reviewdog_version: latest
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.0'
tools: composer:v2
- name: Resolve dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Run PHPstan
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./vendor/bin/phpstan analyse --error-format=raw --no-progress | reviewdog -reporter=github-pr-review -f=phpstan
この時に注意しないといけないことがあります。
REVIEWDOG_GITHUB_API_TOKEN
に指定している GITHUB_TOKEN は Personal Access Tokens を利用する必要があります。
と言いますのも、reviewdog では、コメント書き込みを実行するため、workflow に書き込み権限がないとできないからです。
Personal Access Tokens を作成し、プロジェクトリポジトリに環境変数として指定しましょう。
まとめ
いかがでしたでしょうか。
最低限のことは Larastan にレビューを任せ、レビュアは主要な実装に集中してレビューすることが可能となります。
皆さんのプロジェクトにも是非導入されてみてください。
Discussion