Laravelでメール送信を非同期化する方法:Mailable、ジョブキュー、Horizonの運用

php elephant sticker
Photo by RealToughCandy.com on Pexels.com

Laravelでメール送信を非同期化するには、メール本文をMailableにまとめ、送信処理をキューへ登録し、別プロセスのワーカーに実行させます。

HTTPリクエストの中で送信完了を待たなくてよくなるため、登録画面や通知操作の応答を返しやすくなります。

ただし、キューへの登録成功はメールの送信完了を意味しません。

実装と同時に、ワーカーの常時起動、失敗時の再試行、監視まで設計する必要があります。

Laravelの機能選定を運用まで含めて考える方法は、Laravelのキューと運用監視を含む開発設計でも整理しています。

この記事で扱う内容

  • Mailableとジョブキューの役割
  • メール接続とキュー接続の設定
  • メールテンプレートとジョブの実装
  • SupervisorとHorizonを使ったワーカー運用
  • 読みやすく操作しやすいメールの確認項目

メール送信をキューへ移す仕組み

Mailableは、件名、本文テンプレート、本文へ渡すデータなど、送信するメールの構成を表すクラスです。

ジョブは、後で実行する処理を一つの単位にしたクラスです。

キューは待機中のジョブを保存する場所であり、キューワーカーは保存されたジョブを取り出して実行するプロセスです。

  1. コントローラがメール送信ジョブをキューへ登録します。
  2. HTTPリクエストはメール送信を待たずに応答します。
  3. キューワーカーがジョブを取得します。
  4. ジョブがMailableを使ってメールを送信します。
  5. 失敗した場合は、設定した回数の範囲で再試行されます。

この分離により、画面の処理とメールサーバーとの通信を切り離せます。

一方で、ワーカーが止まるとジョブはキューに残るため、非同期化はワーカー運用とセットで考えます。

メール接続とキュー接続を設定する

メール接続はconfig/mail.php、キュー接続はconfig/queue.phpに定義され、通常は.envから環境ごとの値を読み込みます。

接続情報は利用するメールサービスや実行環境に合わせ、認証情報をリポジトリへ含めないようにします。

MAIL_MAILER=smtp
MAIL_HOST=your_mail_host
MAIL_PORT=your_mail_port
MAIL_USERNAME=your_username
MAIL_PASSWORD=your_password
MAIL_ENCRYPTION=your_encryption
MAIL_FROM_ADDRESS=no-reply@example.com
MAIL_FROM_NAME="YourApp"

QUEUE_CONNECTION=database

database接続では、ジョブを保存するテーブルが必要です。

対象のマイグレーションがプロジェクトにない場合は、次のコマンドで作成して反映します。

php artisan make:queue-table
php artisan migrate

すでにジョブ用マイグレーションがある場合は重複して作らず、既存ファイルを確認してからmigrateを実行します。

構成 向いている状況 運用上の確認
database 既存のデータベースでキューを始めたい場合 ジョブテーブルとワーカーの状態を確認する
redis Redisをキューの保存先として運用できる場合 Redisとワーカーの両方を監視する
RedisとHorizon キューの状態や処理量を画面で確認したい場合 Horizonプロセスも常時起動する

Mailableとメールテンプレートを作る

まず、Markdownテンプレートを使うMailableを生成します。

php artisan make:mail WelcomeMail --markdown=emails.welcome

生成されたクラスでは、件名をenvelope()、本文をcontent()で定義します。

利用中のLaravelで生成される雛形が異なる場合は、その雛形と同じバージョンの公式ドキュメントを基準にしてください。

<?php

namespace App\Mail;

use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class WelcomeMail extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(public User $user)
    {
    }

    public function envelope(): Envelope
    {
        return new Envelope(
            subject: 'ご登録ありがとうございます',
        );
    }

    public function content(): Content
    {
        return new Content(
            markdown: 'emails.welcome',
        );
    }
}

Markdownメールは、本文の構造を簡潔に保ちながらLaravelのメール用コンポーネントを使える方法です。

ボタンの表示文言には「こちら」ではなく、リンク先で行う操作を書きます。

<x-mail::message>
# {{ $user->name }} 様

ご登録ありがとうございます。
以下のリンクからプロフィールを設定できます。

<x-mail::button :url="url('/profile')">
プロフィールを設定する
</x-mail::button>

{{ config('app.name') }}
</x-mail::message>

ジョブからMailableを送信する

メール送信以外の処理も同じ単位で管理したい場合は、専用のジョブクラスに送信処理を置きます。

php artisan make:job SendWelcomeEmail
<?php

namespace App\Jobs;

use App\Mail\WelcomeMail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;

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

    public function __construct(public User $user)
    {
    }

    public function handle(): void
    {
        Mail::to($this->user->email)
            ->send(new WelcomeMail($this->user));
    }
}

登録処理がデータベーストランザクション内で行われる場合は、コミット後にジョブをキューへ渡すと、ワーカーが未確定のユーザー情報を参照する事態を避けられます。

SendWelcomeEmail::dispatch($user)->afterCommit();

業務上の理由で送信を遅らせる場合は、delay()を追加します。

SendWelcomeEmail::dispatch($user)
    ->delay(now()->addSeconds(10))
    ->afterCommit();

単純にMailableをバックグラウンド送信するだけなら、専用ジョブを作らずMail::queue()を使う方法もあります。

Mail::to($user->email)
    ->queue(new WelcomeMail($user));

独自ジョブは、メール送信と一緒に業務上の判定や記録をまとめたい場合に選ぶと、役割の違いが明確になります。

キューワーカーを常時動かす

ジョブを登録しただけでは処理されないため、開発環境では次のコマンドでワーカーを起動します。

php artisan queue:work --sleep=3 --tries=3

本番環境では、ワーカーが終了したときに起動し直せるよう、Supervisorなどのプロセス監視を設定します。

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/artisan queue:work --sleep=3 --tries=3
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
numprocs=2
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/laravel-worker.log
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start "laravel-worker:*"

キューワーカーは長時間動くプロセスなので、デプロイ時には新しいコードを読み込ませるために再起動します。

php artisan queue:restart

RedisキューをHorizonで監視する

Laravel HorizonはRedisキューを対象に、ジョブの状態や処理状況を確認するための画面とワーカー管理機能を提供します。

composer require laravel/horizon
php artisan horizon:install
php artisan migrate
php artisan horizon

本番環境ではphp artisan horizon自体をSupervisorなどで監視します。

同じキューに対して通常のqueue:workとHorizonを重ねて動かすのではなく、採用した構成に合わせてプロセスを管理します。

失敗を見逃さない運用チェック

非同期処理では、画面が正常に応答しても、メール送信が後から失敗する可能性があります。

次の項目を監視対象として決めておくと、原因の切り分けがしやすくなります。

  • 待機中ジョブが増え続けていないか
  • ワーカーまたはHorizonのプロセスが動いているか
  • 失敗したジョブと例外ログを確認できるか
  • メール接続の認証情報と送信元設定が正しいか
  • 再試行による重複送信を避ける方針があるか
  • デプロイ後にワーカーが新しいコードを読み込んでいるか

メールをキューへ登録できた時点と、受信側へ配送された時点は別です。

アプリ内の通知履歴も必要な場合は、Laravelで通知センターを設計する方法も参考になります。

読みやすく操作しやすいメールにする

非同期化は送信方法の改善であり、メール本文の読みやすさを保証するものではありません。

テンプレートでは、次の点を確認します。

  • 件名:メールの目的が単独で分かる文言にする
  • 見出し:本文の順序が分かる構造にし、見た目だけで階層を表さない
  • リンク:「こちら」ではなく、移動先や操作を説明する文言にする
  • 画像:意味のある画像には内容を表す代替テキストを付け、装飾画像は空の代替テキストにする
  • 色:文字と背景のコントラストを確認し、色だけで状態や重要度を伝えない
  • プレーンテキスト:HTMLを利用しにくい環境でも内容を確認できる代替版を用意する
  • 言語:独自HTMLを使う場合は文書の言語を明示する
  • 実機確認:主要なメールクライアントで、崩れ、リンク、読み上げ順を確認する

Webコンテンツ全体の基礎は、Webアクセシビリティで最初に確認する実務ポイントで解説しています。

実装から運用までの確認手順

  1. メール接続とキュー接続を設定する
  2. 必要ならジョブ用テーブルを作成する
  3. Mailableとテンプレートを作成する
  4. 送信ジョブを実装し、トランザクションとの順序を確認する
  5. 開発環境でワーカーを動かして送信を試す
  6. 本番環境でワーカーまたはHorizonを常時起動する
  7. 失敗ジョブ、ログ、再試行、重複送信を監視する
  8. メール本文の構造、リンク、画像、色、代替版を確認する

メール送信をキューへ移すだけで実装を終えず、送信結果を追える運用まで整えることで、画面の応答と通知の信頼性を両立しやすくなります。

この記事に関連する株式会社greedenの取り組み

メール通知の非同期化は、画面の応答だけでなく、失敗時の再試行や監視まで設計してこそ運用に耐えます。株式会社greedenは、Laravelを用いたWebシステム開発を要件定義から実装、テスト、保守運用まで一気通貫で支援しています。

投稿者 greeden

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)