Laravel9.xでQueueを使用しUXを向上させる

# はじめに
Webアプリケーションが提供する機能の中で、通常のWebリクエスト中に実行するのでは時間がかかりすぎる処理が発生する場合があります。そこで対象の処理が完了してから、リクエスト元のユーザーに対してレスポンスを返していた場合、ユーザーエクスペリエンスを大きく損ねてしまいます。
しかし時間がかかりすぎる処理をキューに移動することで、ユーザーに対しより良いユーザーエクスペリエンスを提供することが可能です。

# Queue
以下のようにキューを利用する場合、時間がかかる処理を同期的に待たずにレスポンスを返すことができます。

![queue_application_response_flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240651/1d8a6a7b-b7ab-d1e8-74bd-f2a986bc4f78.png)

![application_response_flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240651/c3456151-c30d-2f6f-b611-2dbe2d44afae.png)

# Queueを使った処理の実行
今回はLaravel9でQueueを使い、上記で説明したような時間がかかる処理をバックグラウンドで実行する手順を説明しようと思います。

例としてサンプルコードでは、ユーザーが予約サイトで予約のキャンセルした際に、お店の担当者に予約がキャンセルされた旨をメールで送信する処理をバックグラウンドで実行していきます。

1. ユーザーから予約キャンセルのリクエストを受け取る
1. ビジネスロジックに基づき予約状態をキャンセルに変更する
1. 時間がかかる処理（Job）をQueueに移動する
1. メールの送信完了を待たずに予約のキャンセルを行なったユーザーに対し、レスポンスを返す

上記で`3`の処理をキューに移動することで、メールの送信完了を待たずに、ユーザーへ予約のキャンセルが完了したことを通知することができます。

ここでお気づきだと思うのですが、上記の`3`の処理を同期処理で実行しても機能自体を作成することはできます。つまり、予約のキャンセルを行なった際に、お店の担当者にキャンセルメールの送信処理が完了したら、ユーザーに対しキャンセル完了の通知を行うということです。
しかし本来ユーザーは予約自体がキャンセル済みになれば、キャンセルが完了した事を通知してほしいはずです。
なのでここではそのメール送信処理をキューに移動し、ユーザーには予約のキャンセルが完了した段階で、その旨を通知するということをやっていきます。

# 環境
- php8.1.3
- Laravel9.2.0
- mysql5.7

# やってみる
Laravelではdatabase・Redis・Amazon SQSなど様々なキュードライバを使って機能を実装することが可能ですが、今回はdatabaseをキュードライバとして使用していきます。

## キュードライバの事前準備
`database`キュードライバを使用する為に、以下のコマンドを実行します。
```bash
php artisan queue:table

php artisan migrate
```
また`.env`ファイルの`QUEUE_CONNECTION`という環境変数に`database`を指定します。
```bash
QUEUE_CONNECTION=database
```

## ジョブクラスの生成
ジョブをキューに投入するクラスであるジョブクラスを生成します。
```bash
php artisan make:job SendReserveCancelMail
```

ジョブクラスは、ジョブがキューにより処理されるときに通常呼び出す`handle`メソッドのみを持ちます。
以下の例では、予約先のお店の担当者に予約がキャンセルになった旨を通知するメールを送信する処理を実行しています。ジョブクラスのコンストラクタで実際にメールを送信する際に必要なモデルを取得し、利用しています。また、クラス内の変数を`private`で宣言すると、テスト実行時に失敗してしまうので気をつけてください。
メールクラスの生成は本記事に関連が薄いので割愛します。確認したい方は[こちら](https://readouble.com/laravel/9.x/ja/mail "Laravel 9.x メール")を参照してください
```php
<?php

namespace App\Jobs;

use App\Mail\ReserveCanceled;
use App\Models\User;
use App\Models\Operator;
use App\Models\Reserve;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;

class SendReserveCancelMail implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $user;
    public $reserve;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct(User $user, Operator $operator, Reserve $reserve)
    {
        $this->user = $user;
        $this->operator = $operator;
        $this->reserve = $reserve;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        Mail::to($this->operator->email)
            ->send(new ReserveCanceled($this->user, $this->reserve));
    }
}
```
## ジョブのディスパッチ
ジョブクラスを作成したら、Controllerなどでジョブをディスパッチします。以下の例では、ユーザーの予約をキャンセルする処理が完了したらジョブをディスパッチしています。ジョブは、ジョブ自体で`dispatch`メソッドを使用してディスパッチできます。`dispatch`メソッドに渡した引数は、ジョブのコンストラクタに渡されます。
ここではジョブクラスのコンストラクタで引数として宣言していた3つのモデルを`dispatch`メソッドに渡しています。
``` php
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Http\Requests\StoreCancelRequest;
use App\Jobs\SendReserveCancelMail;
use App\Models\User;
use App\Models\Operator;
use App\Models\Reserve;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;

final class CancelController extends Controller
{
    /**
     * cancel reserve
     * 
     * @param  \App\Models\Reserve  $reserve
     * @param  \App\Http\Requests\StoreCancelRequest  $request
     * @return \Illuminate\Http\JsonResponse
     */
    protected function store(Reserve $reserve, StoreCancelRequest $request): JsonResponse
    {
        // 予約をキャンセルする処理だったりのビジネスロジックを処理します
        $operator = Operator::where(...)->first();

        SendReserveCancelMail::dispatch(Auth::user(), $operator, $reserve);

        return response()->json(Response::HTTP_OK);
    }
}
```

## キューワーカの実行
ここまででジョブを実行するプログラムは書けましたが、ジョブを実行するにはキューワーカを実行させる必要があります。
Laravelは、キューワーカを開始し、キューに投入された新しいジョブを処理するArtisanコマンドを用意しています。`queue:work` Artisanコマンドを使用してワーカを実行します。
```bash
php artisan queue:work
```
これで、ここまでで用意した`CancelController`の`store`メソッドが実行されると、キューによってメールが送信される処理が実行されるはずです。

キューワーカに関しては本番環境などで、`queue:work`プロセスをバックグラウンドで永続的に実行し続けるには、`Supervisor`などのプロセスモニタを使用して、キューワーカの実行が停止しないようにする必要があります。
またドキュメントに以下のような記載がありますが、キューで処理されるプログラムを修正した際には、必ず再起動してください。これに気づかずに時間を無駄にすると、とても辛い気持ちになります。
> キューワーカは長期間有効なプロセスであり、起動した時点のアプリケーションの状態をメモリに保存することを忘れないでください。その結果、起動後にコードベースの変更に気付くことはありません。したがって、デプロイメントプロセス中で、必ずキューワーカを再起動してください。さらに、アプリケーションによって作成または変更された静的状態は、ジョブ間で自動的にリセットされないことに注意してください。

# 参照元
[Laravel 9.x キュー](https://readouble.com/laravel/9.x/ja/queues.html "Laravel 9.x キュー")