🔍

PHPStan(Laratan) + GitHub Actions + ReviewDog を導入して品質向上してみた

2023/12/22に公開

概要

先日入社した会社が運用するLaravelプロジェクトのCIを一通り改善するチャレンジをやりました。
その時に導入したツールや設定等を共有していきます。

今回は「PHPStan(Laratan)」の導入編です。

背景

入社してコードを読んでみると、以下のようなコードが有りました。
(※丸コピではなく、良い感じに改変しています)

AdminRepository.php
public function findById(int $id): ?Model
{
    return Admin::find($id);
}
GetUseCase.php
$admin = $this->admin_repository->findById($id);

$other = $this->other_repository->findByAdminId($admin->id);

このコードは基本的には正常に動きますが、問題点が2点あります。

  1. findById で受け取った値が、正しく取得できているか分からない。
  2. findById で受け取った値の型が Model 型なので、id というフィールドを持っているか分からない。

「1.」の問題に関しては、もしも検索に失敗してnullが返ってきてしまった場合は、後続の処理でエラーが起きます。
「2.」の問題に関しては、特別動作に支障はありませんが、型が厳密になっていないため「本当にAdminなのか?」という保証ができていない状況です。

こういった「値が本当に存在するか?」「型は期待した形になっているのか?」という部分が不透明なコードになっていたため、潜在的にバグを生んでしまうことを懸念しました。

そこで今回PHPStan(Laratan)を導入して、品質向上を試みることを決めました。

Laratanとは?

まずLarastanを紹介する前に、PHPStanと静的解析ツールのことを簡単にご説明します。

静的解析ツールとは、ソースコードを解析して「実行時にエラーは起きないか?」「安全に実行できるコードなのか?」ということを検査/報告してくれるツールです。

そしてPHPStanは、PHP専門の静的解析ツールです。
そして更にフレームワークのLaravelに特化させたものが、Larastan ということになります。

これを導入する事によって、潜在的なバグをPHP実行せずに検出できるため、「モンキーテストを実施してとにかくバグを見つけてみる!」みたいなことをせずとも、危なそうな部分を前もって潰しておくことができます。
それが自然とコードの品質担保に繋がるということになります。

別の側面として、事前に気付いて修正できるためコードレビューの負担も減らすことができるというメリットもあります。

今回のゴール

  • PHPStan(Laratan)を導入すること
  • PHPStan(Laratan)をGitHub Actions Workflowsに追加すること
  • 静的解析結果を、PRコメントで追加できるようにすること

本編

開発環境

  • PHP 8.1
  • Laravel 9

PHPStan(Laratan)のインストール

以下のコマンドを実行します。

composer require --dev nunomaduro/larastan

※LarastanとPHPStanは依存関係にあるので、Larastanのインストール時にPHPStanも一緒にインストールされます。

設定ファイルの作成

Laravelプロジェクトのルート直下に、phpstan.neon を作成します。

phpstan.neon
# 他のneonファイルの設定を現在の設定を読み込む
# 今回だと、Larastanが用意しているLaravelに特化した設定ファイルを指定している
includes:
  - ./vendor/nunomaduro/larastan/extension.neon

parameters:
    # どのディレクトリを解析させるか?を指定する
    paths:
        - app
        - bootstrap
        - config
        - database
        - resources/views
        - routes
        - tests
    # 解析レベルを指定する
    level: 5

PHPStan解析レベルについて

以下のサイトに解析レベルについての説明が記載してあります。

https://phpstan.org/user-guide/rule-levels

僕自身全く英語がわからないため、DeepLで翻訳をかけた結果を添付します。

レベル 説明
0 基本的なチェック、未知のクラス、未知の関数、$this で呼び出される未知のメソッド、これらのメソッドや関数に渡される引数の数が間違っている、常に未定義の変数。
1 未定義の可能性のある変数、__call__get を持つクラスの未知のマジックメソッドとプロパティ。
2 ($this だけでなく) すべての式で未知のメソッドをチェックし、PHPDocs を検証する。
3 戻り値の型、プロパティに代入される型
4 基本的なデッドコードチェック - instanceof やその他の型チェックが常に偽であること。
5 メソッドや関数に渡される引数の型のチェック。
6 型ヒントの欠落を報告する。
7 部分的に間違ったユニオン型を報告する - ユニオン型内のいくつかの型にのみ存在するメソッドを呼び出すと、レベル 7 はそのことを報告し始めます。
8 NULL可能な型のメソッド呼び出しやプロパティへのアクセスを報告する。
9 mixed 型を厳格に扱います - この型でできる唯一の操作は、別の mixed 型に渡すことです。

可能な限り上げたほうが、より厳格に型を扱うことができるため安心安全なシステムを作ることができると思うのですが、途中から導入するケースの場合は結構エラーが出てしまうと思います。
なので、レベルを変えながら行けそうなラインを見つけて、そこから始めてみることをおすすめします。
僕が所属する会社では一旦レベル5を指定してみましたが、結構良い感じに運用が回っています。

実行

問題なく機能するか、実行してみましょう。
laravelのプロジェクトルートに移動し、以下のコマンドを実行します。

./vendor/bin/phpstan analyse

もしもコード内のエラーや問題が検知された場合は、以下のような出力をします。

この出力に、具体的にどの行で?どういう問題が起きているか?ということが記載されているので、対応します。

対応終了したにもう一度実行してみて、以下のような出力になっていれば無事エラー解消です。

【応用編】より簡単に実行できるようにカスタマイズ

毎回 ./vendor/bin/phpstan analyse とコマンドを書くのは面倒なので、 composer.json に larastan を実行する scriptを追加します。

composer.json
{
    // ...その他の設定,
    "scripts": {
        // ...その他のscripts,
        "larastan": [
            "@php ./vendor/bin/phpstan analyze -c phpstan.neon --memory-limit=1G"
        ],
        // ...その他のscripts,
    },
    // ...その他の設定,
}

オプションも追加しています。

-c は、どの設定ファイルを指定するか?というものです。
特に付けずとも、プロジェクトルートに phpstan.neon を配置していれば自動的に読み込まれますが、念のため明示的に指定しています。
大事なのは --memory-limit=1G です。
メモリ上限のデフォルトは 128MB なのですが、このメモリ容量だと足らずに途中で処理が停止してしまう事が多いです。
そのため、オプションを使ってメモリ上限を上げることでそのようなエラーが出ないようにしています。
とりあえず1GBに設定すれば落ちることはないと思って、1GBに設定していますが、良い感じに調整することをおすすめします。

ここまで終わりましたら、以下のコマンドで実行できるようになります。

composer larastan

先程と同じ結果が出れば完了です。

GitHub Actions の設定

CIである、GitHub Actions の設定です。
PR作成時、PR作成後の追加プッシュの際に、差分箇所のエラーや問題点を検知させるようにします。
そして、その問題点がどこで起きているかを具体的に指し示したいため、今回はReviewDogを使ってそのエラーや問題箇所についてPRコメントをしてもらうように設定します。

ReviewDogとは、PR内にレビューコメントを投稿してくれるツールで、各種静的解析ツールと連携しています。
もちろん、PHPStanにも対応しています。

GitHub Actionsを動かすためには、プロジェクトルート配下の .github/workflows/ の中に設定ファイルを作成する必要があります。

今回はこの中に、 larastan.yml を追加します。

ディレクトリ構成として、laravel/ というプロジェクト配下に apptests といったLaravelプロジェクトの中身が入っている構成です。
LaravelをAPIサーバ、他にNext.jsやNuxt.jsといったフロントエンドフレームワークを使用する構成ではよく見られるかも?という前提です。

適宜、ご自身のプロジェクト構成に応じて微調整をかけてください。

larastan.yml
name: PHPStan

on:
  pull_request:
    paths:
      - '**.php'

jobs:
  phpstan:
    runs-on: ubuntu-latest

    defaults:
      run:
        working-directory: laravel

    steps:
      # リポジトリをチェックアウト
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          # GitHub上でPATを生成して環境変数を設定してください
          token: ${{ secrets.GITHUB_TOKEN }}

      # Reviewdog のセットアップ
      - name: Setup Reviewdog
        uses: reviewdog/action-setup@v1
        with:
          reviewdog_version: latest

      # PHPのインストール
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.1'
          extensions: intl, zip, pcntl, bcmath, gd, pdo_mysql, exif, redis, imagick, soap, xsl, gmp, opcache, sockets, memcached, apcu, mongodb, swoole, xdebug
          coverage: pcov

      # Composer インストール
      - name: Install dependencies
        run: composer install --prefer-dist --no-progress --no-suggest --ansi

      # PRの差分ファイル名のみを抽出
      - name: Get changed files
        id: changed-files
        uses: tj-actions/changed-files@v40
        with:
          files: |
            **/*.php
          separator: " "
          base_sha: 'develop # 差分の比較先(マージ先のブランチを指定してください)

      # 差分ファイルのパスが、 working-directoryを無視して laravel/* と始まってしまうため、 laravel/ を削除
      - name: Remove 'laravel/' from paths
        id: remove-laravel-path
        run: |
          changed_files="${{ steps.changed-files.outputs.all_changed_files }}"
          modified_files=$(echo "$changed_files" | sed 's|laravel/||g')
          echo "Modified PHP files: $modified_files"
          echo "::set-output name=modified_files::$modified_files"

      # PHPファイルが変更されていない場合は、PHPStanをスキップさせる
      - name: Check if PHP files have changed
        id: check-changed
        run: |
          if [ -z "${{ steps.remove-laravel-path.outputs.modified_files }}" ]; then
            echo "No PHP files have changed"
            echo "::set-output name=skip::true"
          else
            echo "Changed PHP files: ${{ steps.remove-laravel-path.outputs.modified_files }}"
            echo "::set-output name=skip::false"
          fi

      # PHPStan実行
      - name: Run PHPStan for Changed Files
        env:
          REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        if: ${{ steps.check-changed.outputs.skip == 'false' }}
        run: ./vendor/bin/phpstan analyze -c phpstan.neon --memory-limit=1G --error-format=raw --no-progress ${{ steps.remove-laravel-path.outputs.modified_files }} | reviewdog -reporter=github-pr-review -f=phpstan

こちらを設定した状態でPRを作成すると、CIが起動して検査してくれます。
また、一度作ったPRに対して追加プッシュした時も同様に、検査が走るようになります。

エラーを検知したときは、以下のようにコメントしてくれます。

終わりに

Larastanを導入し、最初の頃はメンバーもエラーの対応に戸惑いを見せていたり苦労をしている様子だったのですが、最近はだいぶその対応にも慣れてきたようにも思えます。

また、「エラーが発生したら極力直そう!」ぐらいの温度感にする事によって、Stanのエラー解消で疲弊させるということも少し減らしています。
本来であれば完全に直したほうが理想的なのですが、始めからそこまでハードルを上げるとしんどくなるかもしれないと思って、努力目標としています。
ゆくゆくは完璧に解消させる方向にしたいですね。

その程度の温度感でも、十分コードに気を遣う姿を見るようになったので効果は確実にあったと思います。
まだ未導入のプロジェクトがあれば、是非導入してみてください。
大変おすすめです。

参考文献

https://qiita.com/aosan/items/420e339d4b5941aac048

https://zenn.dev/bs_kansai/articles/4a476c4b28f1d6

(他にも参考にしたものがあったのですが、作成したのが少し前なので記憶が...🙇‍♂)

GitHubで編集を提案

Discussion