Laravelに静的解析ツールのLarastanを導入し、厳しくチェックする

8989 ワード

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 にありますので、参照ください。

https://github.com/nunomaduro/larastan/blob/master/docs/rules.md

解析を無視するとき

どうしても解析を無視したい時があるはずです。

その時は、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 等の実行結果をプルリクエストのコメントで指摘してくれます。 詳細な説明は作者様の記事を参照するのが良いです。