【Laravel Sail】Larastanの導入とGitHub ActionsのCIへの組み込み

# 前提条件

[公式ページ](https://readouble.com/laravel/9.x/ja/installation.html)のインストールガイドを参考に Sail を利用した Laravel アプリケーションのインストールが完了していること。

```bash
$ sail php -v

PHP 8.1.13 (cli) (built: Nov 26 2022 14:07:55) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.13, Copyright (c) Zend Technologies
    with Zend OPcache v8.1.13, Copyright (c), by Zend Technologies
    with Xdebug v3.1.5, Copyright (c) 2002-2022, by Derick Rethans
```

```bash
$ sail artisan --version

Laravel Framework 9.42.2
```

本記事におけるプロジェクトのディレクトリ構造は以下の通りとします。

```text
git-repository/
└── my-laravel/
    ├── app/
    ├── bootstrap/
    ├── config/
    ├── database/
    ├── composer.json
    ├── composer.lock
    └── ...
```

# Larastan を導入

ソースコードの文法上の問題点を事前に検出するための方法として、ソースコードの静的解析が挙げられます。
PHP には [PHPStan](https://phpstan.org/) という静的解析ツールがあり、[Larastan](https://github.com/nunomaduro/larastan) はこれを Laravel プロジェクト用に拡張したラッパーとなっています。

Larastan を導入することで、先に挙げたソースコードの文法上の問題点を事前に検出できるだけでなく、PSR に基づいたコーディングを強制することによって、チームで開発している場合は個人の力量に依存しない一定の品質を保ったコードを書くことに一役買ってくれます。

Larastan は [Composer](https://getcomposer.org/) でインストール可能です。
今回は事前に Laravel Sail がインストールされた環境のため、　`sail` コマンドで composer を実行します。

```bash
$ sail composer require --dev nunomaduro/larastan
```

インストールが完了していれば `./vendor/bin/phpstan --version` で PHPStan のバージョン情報が確認できるはずです。

```bash
# コンテナの bash を起動する。
$ sail shell

# インストール確認のためにバージョン情報を表示する。
$ ./vendor/bin/phpstan --version
```

続いて[公式の README](https://github.com/nunomaduro/larastan) に従い `phpstan.neon` ファイルを作成していきます。
下記ファイル内容は一例のため、解析レベルや解析対象パス、各種ルールなどは PHPStan と Larastan の各種情報を確認し、要件に合う適切な設定を施してください。

```my-laravel/phpstan.neon
includes:
  - ./vendor/nunomaduro/larastan/extension.neon

parameters:
  # 解析レベル
  # https://phpstan.org/user-guide/rule-levels
  level: 0

  # 解析対象に含めるパス
  paths:
    - app
    - bootstrap
    - config
    - resources
    - routes
    - tests

  # 解析対象に含めないパス
  excludePaths:
    - routes/console.php
```

後は PHPStan のバージョンを確認した時と同じように、コンテナの bash を起動して `./vendor/bin/phpstan analyze -- app/` といった具合にコマンドを実行すれば解析できます。

```bash
$ ./vendor/bin/phpstan analyze -- app/
Note: Using configuration file /var/www/html/phpstan.neon.
 24/24 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
                                    
 [OK] No errors                                                                      
```

# 余談 : Larastan を artisan コマンド化する

Larastan の実行はできるようになりましたが、上述した方法ですと毎回コンテナの bash を起動する一手間があり面倒だったので　`sail artisan larastan` といった形で artisan コマンド化しました。

まずは `Command` クラスのスケルトンを作成します。

```bash
$ sail artisan make:command LarastanCommand
```

Larastan を実行できるよう実装していきます。
[こちら](https://github.com/nunomaduro/collision/blob/v7.x/src/Adapters/Laravel/Commands/TestCommand.php)を参考にさせていただきました。

Larastan に定義できる設定内容は `phpstan.neon` に記述することとし、 `LarastanCommand` クラスがパースできるオプションは非常に簡素なものとしています。
この辺りは使用感に合わせて追々拡充させていけば良いと考えています。

```my-laravel/app/Console/Commands/LarastanCommand.php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Isolatable;
use RuntimeException;
use Symfony\Component\Process\Exception\ProcessSignaledException;
use Symfony\Component\Process\Process;

class LarastanCommand extends Command implements Isolatable
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'larastan
        {--without-tty : Disable output to TTY}
    ';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Run the appliation static analyze by Larastan';

    /**
     * Create a new command instance.
     * 
     * @return void
     */
    public function __construct()
    {
        parent::__construct();

        $this->ignoreValidationErrors();
    }

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        $command = ['vendor/bin/phpstan', 'analyze', '--', 'app/'];

        $process = new Process($command);
        try {
            $process->setTty(!$this->option('without-tty'));
        } catch (RuntimeException $e) {
            $this->output->writeln('Warning: ' .  $e->getMessage());
        }

        $exitCode = Command::FAILURE;
        try {
            $exitCode = $process->run(fn ($type, $line) => $this->output->write($line));
        } catch (ProcessSignaledException $e) {
            if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) {
                throw $e;
            }
        }
        $this->newLine();

        return $exitCode;
    }
}
```

artisan コマンドとして実行してみて、先ほどと同様の解析結果になることを確認します。

```bash
$ sail artisan larastan
Note: Using configuration file /var/www/html/phpstan.neon.
 24/24 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
                                      
 [OK] No errors                                                                   

```

# GitHub Actions への組み込み

Larastan を実行することへの面倒臭さは解消されました。
ただ現時点ではまだ手動実行のため、そもそも実行することを忘れる可能性があります。

[Git フック](https://git-scm.com/book/ja/v2/Git-%E3%81%AE%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%9E%E3%82%A4%E3%82%BA-Git-%E3%83%95%E3%83%83%E3%82%AF)を利用して pre-commit や pre-push のタイミングで解析を行うのも一案ですが、開発体験はむしろ悪化する恐れがある上に、チームで開発している場合などは、ローカルに設定する方法では信用に足らないと思います。

上記の課題を解決すべく [GitHub Actions](https://docs.github.com/ja/actions) で CI/CD ワークフローを構築し「GitHub の master ブランチにプッシュされたら Larastan を実行する。」といった状態を目指していきます。

GitHub Actions で実行するワークフローは、リポジトリ内に配置した `.github/workflows` ディレクトリ配下の yml ファイルにて定義できます。
本記事における yml ファイル作成後のディレクトリ構成、yml ファイルの内容については以下の通りです。

```text
git-repository/
├── .github/
|   └── workflows/
|       └── my-laravel-ci.yml
└── my-laravel/
    ├── app/
    ├── bootstrap/
    ├── config/
    ├── database/
    ├── composer.json
    ├── composer.lock
    └── ...
```

```.github/workflows/my-laravel-ci.yml
# GitHub Actions 上での当該ワークフロー名です。
# 識別しやすい名称を付けてください。
name: my-laravel-ci

# 当該ワークフローの実行タイミングに関する定義です。
# ここでは main ブランチの my-laravel ディレクトリ配下にある PHP ファイルに対して push が行われたタイミングとしています。
# トリガーや対象ブランチ、パス定義などは変更可能なため、プロジェクトの性格に合わせて変更すべきポイントです。
on:
  push:
    branches: 
      - "main"
    paths:
      - "my-laravel/**.php"

jobs:
  # ubuntu-latest 仮想マシン上で以下の流れでワークフローを実行していきます。
  #   1. 対象プロジェクトをチェックアウト。
  #   2. sail コマンドでアプリケーションコンテナ（+α）を起動。
  #   3. Larastan を実行。
  #   4. PHPUnit を実行。 
  test:
    runs-on: ubuntu-latest

    defaults:
      run:
        working-directory: ./my-laravel

    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Copy .env
        run: php -r "file_exists('.env') || copy('.env.example', '.env');"
      - name: Install Dependencies
        run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-install=dist
      - name: Start application
        run: vendor/bin/sail up -d
      - name: Generate key
        run: vendor/bin/sail artisan key:generate
      # 本記事で実装した LarastanCommand クラスを `sail artisan` で呼び出すことで Larastan を実行しています。
      - name: Execute lint via Larastan
        run: vendor/bin/sail artisan larastan --without-tty
      - name: Execute test via PHPUnit
        run: vendor/bin/sail artisan test
```

yml ファイルを commit 後 my-laravel プロジェクトの PHP ファイルに変更を加えて commit & push をしてみるとワークフローが実行されます。
GitHub 上で　test ジョブの実行履歴を開くと、無事 Larastan が実行されていることがわかります。（スクショのワークフロー名が異なっているのはご容赦ください。）

![chrome_screenshot_github.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/398732/0093a9c9-5da0-c77f-e9d7-5a2462b745cb.png)
![chrome_screenshot_github.com (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/398732/c71a89c0-452b-2046-af73-e66b93f7f235.png)

一度 GitHub Actions への登録が済んでしまえば、以降は Larastan の実行を意識することなく開発に専念できるため非常に便利だと感じました。

他にも CI/CD ツールはたくさんありますが、 GitHub でコード管理をしていれば別ツールやサービスを利用せずとも CI/CD を始められるため、取っ掛かりとしての心理的ハードルも低いと思います。
GitHub Actions は他にも様々な機能が提供されているので、使いこなせるよう勉強していきたいです🖋