Laravelのジョブ・キューで詰まった話。

2023年も12月になります！1年間は本当にあっという間ですね。
今年初頭ですが、業務でメール送信対応を担当した際にLaravelで詰まった箇所がありまして
かなり個人的な話になりますが今回まとめさせていただきます。:bowling:

## 実装したかった内容
とあるwebアプリ（ビジネス上のマッチング系）を開発していました。

通知をユーザーのアドレスへメール送信する機能があり、アドレスが間違っていたり変わっていたりなどで送信エラーが発生した場合に、エラーメッセージや送信先情報などの情報をユーザーの権限ごとに作成したエラー専用テーブルに格納する実装をしたかったです。

仮にユーザーの権限を`company`・`user`とし、格納するエラーテーブル名を`company_mail_error_infos`・`user_mail_error_infos`テーブルとします。
```
- company_mail_error_infos
- user_mail_error_infos
- ...
- ...
```
今回の実装にはLaravelのジョブ・キュー機能を使うのが良いと考えました。

## ジョブ・キューとは?
>Webアプリケーションの構築中に、アップロードされたCSVファイルの解析や保存など、通常のWebリクエスト中に実行するのでは時間がかかりすぎるタスクが発生する場合があります。幸運なことに、Laravelを使用すると、バックグラウンドで処理したい仕事をキューへ投入するジョブクラスが簡単に作成できます。時間のかかるタスクをキューに移動することで、アプリケーションはWebリクエストに驚異的な速度でレスポンスし、顧客により良いユーザーエクスペリエンスを提供できます。

引用：[Laravel 9.x キュー
](https://readouble.com/laravel/9.x/ja/queues.html)

Laravelドキュメントからの引用でしたが、Laravelでジョブクラスを作成し、キューを使ってクラスの内容をバックグラウンドで実行する機能のようですね。非同期であれば1万通送信しないといけない場合でも対応できそうです。

## 実装例
まずはジョブクラスの作成です。以下のコマンド（CompanyMailJobはクラス名）で作成できます。
`app`配下に`Jobs`というディレクトリ、その配下に`CompanyMailJob.php`ファイルが作成されます。
```
php artisan make:job CompanyMailJob
```
作成後ファイルの中身は以下になります。
```php
<?php

namespace App\Jobs;

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;

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

    public function __construct()
    {
        //
    }

    public function handle()
    {
        //
    }
}
```
handleメソッド内にキューしたいジョブの処理を記載しますが、コントローラと同じでサービスクラスを作成し、そちらに処理を書くことが多いです。


では、処理を走らせたいコントローラ（サービス）に以下のようなコードを記述します。

```php
CompanyMailJob::dispatch($request->all(), $user->id);
```
dispatch関数が実行されることによってジョブクラスが走り、`queue_job`テーブルというキューの処理を貯めておくテーブルにジョブが格納されます。

ジョブクラスで使う変数を渡すことができ、ここでは`CompanyMailJob`というジョブクラスに`$request->all(), $user->id`を渡しています。

準備ができたので、残すはキューコマンドを実行するだけです。
```
php artisan queue:work
```
こちらでキュー実行は完了です。成功すればDONE、失敗すればFAILと表示されます。

## 詰まった箇所
メール送信でエラーが起きた場合の実装をどう書こうかなと調べたところ、ジョブクラスにfailedメソッドを実装することができるということで、処理が失敗した時の処理を書きました。
```php
public function failed(Throwable $exception)
{
    $companyMailErrorInfo = CompanyMailErrorInfo::create([
        'address' => $this->email,
        'user_info' => $this->userInfo,
        'error_message' => $this->errorInfo,
    ]);
}
```
キューが失敗した時に指定したテーブルに情報を入れるという処理になります。

これでいけるかなということで異常系テストしたところ、想定通り`company_mail_error_infos`テーブルに情報が入りました。・・と思ったら別の`failed_job`テーブルにも情報が格納されていました。キュー実行結果がFAILになっているのに・・。

DONEになると`company_mail_infos`（成功時に入れるテーブル）に入り、FAILだと`failed_job`と`company_mail_error_infos`に入る。しかしデバッグをしたところfailedメソッドは動いている。`company_mail_error_infos`だけに入れたい。。
ここで調査に時間を使ってしまいました。

## 原因・解決策
調べたところ、エラーが起きた・キューが失敗した時点で自動的にfailed_jobテーブルに情報が格納されてしまい、failedメソッドが走るのはその後のようです。

色々試してみましたが、指定したテーブルに失敗情報を正攻法では入れられないのか、、と思いつつどうしても今回の要件を試したかったので、handleメソッド内でtry catchし、failed_jobに入る前にエラーテーブルに入れるという形で実装しました。ファイル内でエラーハンドリングをしているので、ジョブ実行してエラーが発生しても全て成功（DONE）と表示されます。（正攻法ではないはず）

よってfailed_jobテーブルは基本的に使いません。（これも正攻法ではないはず）

## 失敗テーブルの設定方法
キューが失敗した際に情報がfailed_jobテーブルに格納されることをお伝えしましたが、config配下のqueue.phpファイルのfailed→tableを書き変えれば別テーブルに変更できます。

こちらは1つのテーブルのみ可能で、複数テーブルの設定はできないようです。
```php
'failed' => [
    'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
    'database' => env('DB_CONNECTION', 'mysql'),
    'table' => 'failed_jobs', // ←
],
```
## まとめ
- **ジョブ**：非同期で実行させたい処理
- **キュー**：そのジョブを非同期で実行するための機能
- ジョブの処理は`handle`メソッド内に記述し、キューが失敗した場合にその情報は基本的に`failed_job`テーブルに入る。その後に存在する場合は`failed`メソッドの処理が走る

<br>
以上になります。

最後までお付き合いありがとうございました！
皆様良いお年を！！:christmas_tree: :shinto_shrine: :hand_splayed: