Laravel Pint を用いた環境において make:●● の後の pint の実行し忘れを防ぐ方法を考える

表題の通りですが、Laravel 向けの PHP-CS-Fixer ラッパーこと [Laravel Pint](https://laravel.com/docs/11.x/pint) を使用したプログラムのフォーマットについて Artisan CLI からの新規のクラス作成後に実行し忘れがちょくちょく起きるのでこれを改善する方法を考えてみました。

## 前置き
:::note warn
Laravel Pint についてプロジェクト固有の設定（`pint.json`）を作ってない状態では `laravel` というプリセットが適用されます。
プリセットなどデフォルトのまま使っている場合は `make:●●` のコマンド実行によって生成されるファイルについて Pint の修正が発生するという問題自体が起きないかもしれません？
:::

今回は `laravel` ではなく `psr12` というプリセットを使いつつ、3つほど適当なルールを追加した設定ファイルを作成しました

```json:pint.json
{
    "preset": "psr12",
    "rules": {
        "declare_strict_types": true, // php ファイルの先頭に declare(strict_types=1); を付ける
        "trailing_comma_in_multiline": true, // ← 複数行の配列の末尾の要素の後ろにカンマを付ける
        "single_line_empty_body": true // ← 処理の存在しない関数の中括弧を1行で書く
    }
}
```

---
マイグレーションファイルを作成するコマンドを一例として考えてみます
```bash
$ php artisan make:migration create_examples_table
```

↑のコマンドによって作成されるマイグレーションファイルは以下の通り
```php:database/migrations/2025_02_22_123409_create_examples_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('examples', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('examples');
    }
};
```

Pint を手動実行
```bash
$ vendor/bin/pint database/migrations/2025_02_22_123409_create_examples_table.php 
```

以下のようにフォーマットできました
```diff_php:database/migrations/2025_02_22_123409_create_examples_table.php (修正前後の差分)
<?php

+declare(strict_types=1);
+
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

-return new class extends Migration
-{
+return new class () extends Migration {
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('examples', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('examples');
    }
};
```
- `declare_strict_types` のルールによって php ファイルの先頭に `declare(strict_types=1);` が付くようになりました
- PSR12 のプリセットに含まれる `new_with_parentheses` というルールよって無名クラスの宣言に修正が入りました

## `make:●●` コマンド実行の後処理として Pint を自動実行する方法について
本題

まず始めに Artisan CLI から実行できるコンソールコマンドはいずれも実行終了時に `Illuminate\Console\Events\CommandFinished` というイベントを発信します（詳しくは以下）

https://readouble.com/laravel/11.x/ja/artisan.html

このイベントをリッスンしたリスナーの中で Pint を実行することで実行し忘れを防ぐことができました。
今回はサンプルということでアプリケーション内にサービスプロバイダを1つ作成し、その中に丸ごとコードを書いてしまいます。

```php:app/Providers/ArtisanAutoFormatServiceProvider.php
<?php

declare(strict_types=1);

namespace App\Providers;

use Illuminate\Console\Events\CommandFinished;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
use Symfony\Component\Process\Process;

class ArtisanAutoFormatServiceProvider extends ServiceProvider
{
    /**
     * @var array<string, string>
     */
    protected $supportedMakeCommands = [
        'make:migration' => 'database/migrations',
        'make:model' => 'app/Models',
        'make:controller' => 'app/Http/Controllers',
        'make:request' => 'app/Http/Requests',
    ];

    public function boot(): void
    {
        Event::listen(CommandFinished::class, function (CommandFinished $event) {
            if ($event->exitCode === 0 && array_key_exists($event->command, $this->supportedMakeCommands)) {
                $targetDir = $this->supportedMakeCommands[$event->command];
                $output = $event->output;
                $output?->writeln('');
                $output?->writeln("<info>Running pint on {$targetDir}</info>");
                $process = new Process(['./vendor/bin/pint', $targetDir]);

                $process->run(function ($type, $buffer) use ($output) {
                    if (Process::ERR === $type) {
                        $output?->writeln("<error>{$buffer}</error>");
                    } else {
                        $output?->write($buffer);
                    }
                });
            }
        });
    }
}
```
- コマンドごとに生成されるファイルのパスはおおよそ決まっているので、コマンドごとのフォーマット対象とするディレクトリのマッピング配列（`$supportedMakeCommands`）を用意
  - ↑ の例では `make:migration`、`make:model`、`make:controller`、`make:request` の4つをサポートしています
  - 最初は `$event` からコマンドによって生成されたファイルのパス情報を取り出して生成したファイルだけに Pint を実行することも考えたのですが断念しました...
- CommandFinished イベントの終了コードが 0 かつ、上記マッピング配列のキーに合致するコマンドが実行されていたときに Pint のフォーマットを実行します

---
作成したサービスプロバイダについて AppServiceProvider.php の中で `APP_ENV` 環境変数が local かつコンソールコマンド実行中のときだけ読み込まれるような登録を実施
```diff_php:app/Providers/AppServiceProvider.php
<?php

declare(strict_types=1);

namespace App\Providers;

use Illuminate\Support\Facades\App;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
+        if (App::runningInConsole() && App::environment('local')) {
+            App::register(\App\Providers\ArtisanAutoFormatServiceProvider::class);
+        }
    }
}
```

---
動作確認として `make:migration` コマンドから新規のマイグレーションファイルを作成してみます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/2d79c55e-606c-4a14-b73e-ab2e55bdf7cb.png)

マイグレーションファイル作成の後処理としてリスナー内のコードが実行できていそうな出力が得られました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/e370e311-0cd6-4ca4-b40a-87c94fb7735c.png)

生成されたマイグレーションファイルについて `declare(strict_types=1);` の宣言や、無名クラスの宣言などが適切に修正されていました:relaxed:

## (備考) GitHub Actions を用いた Pint 実行について

CommandFinished イベントのリスナー追加によって `make:●●` を実行した後の Pint の実行し忘れを防ぐことはできましたが、手作業によるファイル変更後の Pint の実行し忘れを防ぐといったことは達成できません...:sob:

この件について調べたところ GitHub Actions のワークフローでの Pint 実行を行うアクションが提供されていたのでこちらを活用するのも良さげでした。

https://github.com/aglipanci/laravel-pint-action

以下で紹介するワークフローは Laravel 用のパッケージ開発を行いやすくする [spatie/package-skeleton-laravel](https://github.com/spatie/package-skeleton-laravel) というスケルトンリポジトリ内に含まれていたものになります。

https://github.com/spatie/package-skeleton-laravel/blob/ce650ff98e146cd4fd21fa8d3fa342a57b4d0fdc/.github/workflows/fix-php-code-style-issues.yml

```yaml:.github/workflows/fix-php-code-style-issues.yaml
name: Fix PHP code style issues

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

permissions:
  contents: write

jobs:
  php-code-styling:
    runs-on: ubuntu-latest
    timeout-minutes: 5

    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          ref: ${{ github.head_ref }}

      - name: Fix PHP code style issues
        uses: aglipanci/laravel-pint-action@2.5

      - name: Commit changes
        uses: stefanzweifel/git-auto-commit-action@v5
        with:
          commit_message: Fix styling
```

php ファイルのコミットをトリガーとして稼働し、修正が発生したときに `Fix styling` というコミットメッセージと共に修正をプッシュしてくれました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/4d377bfe-30ba-4449-ab72-27430fa303c2.png)

## (備考) Laravel パッケージを自作した件について
この記事で紹介した Pint を後処理として実行する方法について PHP8.3 以上かつ Laravel11.x 以上のアプリケーションに composer でインストール可能なパッケージとして作成してみました。

https://github.com/imo-tikuwa/laravel-make-with-format

https://packagist.org/packages/imo-tikuwa/laravel-make-with-format

- ↑ の方に書いた [spatie/package-skeleton-laravel](https://github.com/spatie/package-skeleton-laravel) をベースとして開発しました
- [パッケージディスカバリ](https://laravel.com/docs/11.x/packages#package-discovery) の機能によって composer でインストールするだけで機能が有効になります
- こちらは Pint の他、[IDE Helper Generator for Laravel](https://github.com/barryvdh/laravel-ide-helper) の自動実行も行えるように開発しています
  - `Larvel Pint` がインストールされている場合、 `make:●●` コマンド実行後に Pint のフォーマット処理が走ります
  - `IDE Helper Generator for Laravel` がインストールされている場合、`make:model` コマンド実行後に `ide-helper:models` コマンドが走ります

---
その他 spatie/package-skeleton-laravel を用いた Laravel のパッケージ開発自体が初めてのことだったので備忘録として Zenn に以下のようなスクラップも作成しました。

https://zenn.dev/imo_tikuwa/scraps/e5d92bece51e0e