Laravelで通知機能を作るとき、メールを送信できるだけでは運用に耐えません。
MailableとNotificationsの役割を分け、キュー、送信ドメイン、配信停止、Webhook、監視、アクセシビリティまで一つの流れとして設計する必要があります。
この記事では、発送完了通知を例に、実装と運用をどの順番で組み立てるかを解説します。
コード例はLaravelの現行ドキュメントにあるAPIへ合わせています。
- MailableとNotificationsをどう使い分けるか
- キュー送信を失敗や重複に強くする方法
- SPF、DKIM、DMARCと送信後のWebhookをどうつなぐか
- 読みやすいHTMLメールとプレーンテキスト版をどう作るか
- 配信停止、計測、テストをどこまで実装するか
MailableとNotificationsの使い分け
Mailableは、一通のメールについて、件名、送信者、HTML、プレーンテキスト、添付ファイルなどを定義するクラスです。
メール固有の表現を細かく制御したい場合に向いています。
Notificationsは、「発送完了」や「支払い失敗」といった通知イベントを、メール、データベース、ブロードキャスト、SMS、Slackなどのチャネルへ振り分ける仕組みです。
同じ出来事を複数の手段で知らせたい場合に向いています。
| 判断項目 | Mailable | Notifications |
|---|---|---|
| 中心となる責務 | 一通のメールを組み立てる | 通知先とチャネルを選ぶ |
| 向いている場面 | 帳票送付、複雑なレイアウト、添付ファイル | 発送、障害、コメントなどのイベント通知 |
| 複数チャネル | メールが中心 | 同じイベントから複数チャネルへ展開しやすい |
| 併用 | NotificationsからMailableを返す構成も選べる | |
判断に迷ったら、最初に「何を知らせるか」と「どのチャネルで届けるか」をNotificationsで決め、メール固有の見た目や添付処理をMailableへ任せます。
この分け方なら、メール以外のチャネルを追加しても、通知イベントの意味を保ちやすくなります。
通知基盤を七つの処理に分ける
通知基盤は、送信メソッドの呼び出しだけで完結しません。
次の七つに分けると、失敗した場所と担当範囲を追いやすくなります。
- イベント:発送完了など、通知する出来事を確定する。
- 受信設定:受信者の言語、通知種別、許可されたチャネルを確認する。
- メッセージ生成:件名、本文、操作先URLを組み立てる。
- キュー:Webリクエストから外部送信を切り離す。
- 配信事業者:メールやSMSを宛先側へ受け渡す。
- Webhook:バウンス、苦情、配信停止などの結果を受け取る。
- 抑止と監視:再送してはいけない宛先を止め、異常を検知する。
この分割では、「キュージョブが成功した」と「メールが受信箱に届いた」を別の状態として扱います。
前者はアプリケーション内部の処理結果であり、後者は配信事業者と受信側の判定を経た結果だからです。
Notificationsでチャネルとキューを定義する
外部サービスへの送信は応答に時間がかかるため、通常はキューへ載せます。
Notificationsでは、クラスにShouldQueueを実装すると、受信者とチャネルの組み合わせごとにジョブが作られます。
<?php
namespace App\Notifications;
use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class OrderShipped extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(public Order $order) {}
public function via(object $notifiable): array
{
return ['mail', 'database'];
}
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject('発送完了:注文 '.$this->order->number)
->greeting($notifiable->name.' 様')
->line('ご注文の商品を発送しました。')
->action(
'配送状況を確認する',
route('orders.track', $this->order)
);
}
public function toDatabase(object $notifiable): array
{
return [
'order_id' => $this->order->id,
'status' => 'shipped',
];
}
}
via()は固定配列に限りません。
通知種別の設定、電話番号の有無、静穏時間などを確認し、受信者ごとにチャネルを返せます。
ただし、WebプッシュはLaravelの標準チャネルとして一律に提供されるものではありません。
採用する配信事業者に対応したカスタムチャネルやコミュニティ製パッケージについて、保守状況と失敗時の扱いを確認します。
現行のMailableでHTML版とテキスト版を作る
現在のMailableは、envelope()で件名などを、content()で本文テンプレートを定義します。
HTML版とプレーンテキスト版を併記すると、画像を表示しない環境やHTMLを簡略表示する環境でも内容を伝えられます。
<?php
namespace App\Mail;
use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class OrderShippedMail extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
public function __construct(public Order $order) {}
public function envelope(): Envelope
{
return new Envelope(
subject: '発送完了:注文 '.$this->order->number,
);
}
public function content(): Content
{
return new Content(
view: 'mail.orders.shipped',
text: 'mail.orders.shipped-text',
);
}
}
テンプレートへ渡す注文番号、金額、追跡URLは、表示直前に組み立てるのではなく、どの値を公開してよいかをアプリケーション側で決めます。
メール本文へモデル全体や不要な個人情報を渡さない設計にすると、テンプレート変更時の誤表示を減らせます。
キュー送信を失敗と重複に強くする
「すべての通知は必ずキューに載せる」という規則だけでは不十分です。
外部送信をキューへ移す場合は、ワーカー停止、再試行、重複送信、データベース更新との順序まで決めます。
- 再試行:一時的な通信障害だけを対象にし、待機時間と最大試行回数を決める。
- 冪等性:同じ通知イベントを複数回処理しても、二重送信しない識別子を持つ。
- 失敗隔離:恒久的な宛先エラーを再試行し続けず、失敗キューや抑止リストへ移す。
- 監視:待機件数、最古ジョブの経過時間、失敗数、ワーカー稼働を確認する。
- 確定後の送信:注文状態などの更新が確定してから通知ジョブを処理する。
Laravelではジョブごとに再試行間隔を設定でき、段階的なバックオフも指定できます。
キューの再試行、冪等性、失敗復旧を詳しく設計する場合は、Laravelのキュー設計と非同期処理も参照してください。
SPF、DKIM、DMARCの役割
メール送信処理が成功しても、送信ドメインの認証が整っていなければ、受信側は正当な送信元か判断しにくくなります。
三つの仕組みは似ていますが、確認している対象が異なります。
| 仕組み | 平易な説明 | 運用で確認すること |
|---|---|---|
| SPF | そのドメイン名を使って送信できるホストをDNSで示す | 利用中の配信事業者を漏れなく含め、不要な送信元を残さない |
| DKIM | 送信ドメインの秘密鍵でメッセージへ署名する | 署名の有効性、鍵の管理、対象ヘッダーを確認する |
| DMARC | SPFとDKIMの評価を送信者ドメインと結び付け、受信側の扱いとレポート先を示す | レポートを確認し、正当な送信元の漏れを直してから方針を調整する |
通知用サブドメインを使うと、DNS設定や送信種別を整理しやすくなります。
ただし、サブドメインを分けるだけで受信箱への到達が保証されるわけではありません。
Fromは送信主体が分かる名称にし、返信を受け付ける場合はReply-Toを実際の窓口へ向けます。
取引通知とマーケティングメールは、送信目的、購読設定、テンプレート、計測を分けて管理します。
アクセシブルなメール本文
メールクライアントは、ブラウザと同じHTMLやCSSをすべて扱えるわけではありません。
複雑な仕掛けに依存せず、読み上げ、画像非表示、拡大表示でも意味が残る構造を優先します。
- 件名だけで通知の種類と対象を判別できるようにする。
- 本文は見出し、段落、箇条書きの順で論理構造を作る。
- 注文番号、日時、金額、期限を画像の中だけに置かない。
- 意味のある画像には内容に応じた
altを付け、装飾画像はalt=""にする。 - リンクは「こちら」ではなく、「配送状況を確認する」のように行き先を示す。
- 色だけで成功、警告、期限を区別しない。
- レイアウト用テーブルには
role="presentation"を付け、データ表とは区別する。 - HTML版と同じ必須情報、操作先URL、問い合わせ先をテキスト版にも入れる。
プレビュー文は受信一覧で内容を判断する助けになりますが、実装方法によっては本文や読み上げへ重複して現れます。
主要なメールクライアントで、表示と読み上げの両方を確認します。
Web画面を含むアクセシビリティの優先順位は、Webアクセシビリティの基本と最初に押さえる実務ポイントで整理しています。
配信停止と購読設定を分ける
受信者が止めたい通知と、サービス利用に伴って必要な通知を一つのフラグで管理すると、必要な連絡まで止める事故につながります。
通知の目的ごとに設定単位を決めます。
| 通知種別 | 例 | 設計上の扱い |
|---|---|---|
| 取引通知 | 注文確認、領収書、発送状況 | サービス提供に必要な範囲を明示し、販促情報を混ぜない |
| セキュリティ通知 | パスワード変更、異常なログイン | 緊急度と本人確認の導線を優先する |
| マーケティング通知 | キャンペーン、新機能の案内 | 分かりやすい配信停止と頻度設定を用意する |
通常の配信停止画面では、推測しにくいトークンか署名付きURLで対象を識別します。
メール検査システムがリンクを自動で開く場合に備え、通常のGETアクセスだけで状態を変更せず、確認後のPOSTで確定する設計が安全です。
一方、RFC 8058のワンクリック配信停止は、List-UnsubscribeとList-Unsubscribe-Postを使い、対応する受信側が利用者の同意後にHTTPSのPOSTを送る別の仕組みです。
LaravelのMailableではheaders()でカスタムヘッダーを追加できますが、DKIM署名の対象やPOSTの処理は配信事業者の仕様も確認します。
use Illuminate\Mail\Mailables\Headers;
public function headers(): Headers
{
return new Headers(
text: [
'List-Unsubscribe' =>
'<https://example.com/unsubscribe/'.$this->opaqueToken.'>',
'List-Unsubscribe-Post' =>
'List-Unsubscribe=One-Click',
],
);
}
法令や配信事業者の要件は、送信先、送信量、メールの目的によって変わります。
実装時点の要件を確認し、配信停止の反映時間と例外を運用手順へ残します。
Webhookでバウンスと苦情を処理する
Webhookは、配信事業者がアプリケーションへ送信結果を通知するHTTPリクエストです。
ハードバウンス、ソフトバウンス、苦情、配信停止などを受け取り、次の送信判断へ戻します。
- 配信事業者が指定する方式で署名を検証する。
- 事業者のイベントIDを一意制約で保存し、再送された同じイベントを二重処理しない。
- 外部のイベント名を、アプリケーション内部の状態へ変換する。
- 恒久エラー、苦情、配信停止を抑止リストや購読設定へ反映する。
- 重い処理はキューへ渡し、Webhookには短時間で応答する。
- 個人情報を必要以上にログへ残さず、調査に必要な識別子を記録する。
署名検証を共通のHMAC例だけで実装するのは避けます。
ヘッダー名、署名対象、公開鍵、タイムスタンプ、再送仕様は配信事業者ごとに異なるため、公式SDKか公式手順に合わせます。
ソフトバウンスを何回で抑止するかも一律には決められません。
事業者のイベント定義を確認し、恒久エラーと一時エラーを分けたうえで、再試行回数を決めます。
多言語と複数チャネルの設計
Laravelの通知はlocale()で言語を指定でき、受信者モデルにHasLocalePreferenceを実装して希望言語を返す方法もあります。
キューへ載せた通知でも、送信時にその言語が使われます。
翻訳文は、単語を連結して作らず、文全体を翻訳キーとして管理します。
氏名が未登録の場合の呼びかけ、日時のタイムゾーン、通貨表記、長い商品名なども確認します。
複数チャネルでは、文面を完全に同一にする必要はありません。
通知の目的と操作先をそろえたうえで、SMSは短く、メールは判断材料を十分にし、アプリ内通知は後から履歴を確認できる形にします。
計測は送信の段階ごとに分ける
単一の「送信成功率」では、どこで問題が起きたか分かりません。
次の段階を分けて集計します。
| 段階 | 確認できること |
|---|---|
| ジョブ投入 | 通知イベントがキューへ渡ったか |
| ジョブ処理 | ワーカーが例外なく処理したか |
| 事業者受付 | 配信事業者がメッセージを受け付けたか |
| 配信結果 | 配信、遅延、バウンス、苦情がどう返ったか |
| 利用者の選択 | 配信停止や通知設定の変更が反映されたか |
開封とクリックは、メールクライアントの画像取得やプライバシー保護機能の影響を受けます。
事業上必要な範囲だけを計測し、個人の行動を断定する指標として扱わないようにします。
キューの滞留や外部送信を含む性能監視は、Laravelのパフォーマンス最適化と可観測性でも解説しています。
テストを四つの層で組み立てる
送信要求のテスト
Mail::fake()とNotification::fake()を使い、宛先、通知クラス、キュー投入を確認します。
外部の配信事業者へ実際に送らず、アプリケーションの判断を検証する層です。
Mail::fake();
Mail::to($user)->queue(new OrderShippedMail($order));
Mail::assertQueued(
OrderShippedMail::class,
fn (OrderShippedMail $mail) => $mail->order->is($order)
);
テンプレート内容のテスト
Mailableには、HTML版とテキスト版の内容を検査するアサーションがあります。
注文番号、操作名、問い合わせ先が両方へ含まれることを確認します。
$mail = new OrderShippedMail($order);
$mail->assertHasSubject('発送完了:注文 '.$order->number);
$mail->assertSeeInHtml('配送状況を確認する');
$mail->assertSeeInText($order->number);
Webhookのテスト
正しい署名、改ざんされた署名、同一イベントの再送、未知のイベント種別、処理途中の例外を試します。
同じイベントを二回受けても、抑止や集計が二重にならないことを確認します。
実機での表示確認
自動テストだけでは、メールクライアントごとのCSS差やダークモード、画像非表示、拡大表示を確認できません。
代表的なクライアントでHTML版とテキスト版を開き、キーボード操作と読み上げを含めて確認します。
公開前チェックリスト
実装
- 通知イベントとMailableの責務を分けたか
- キューの再試行、最大試行回数、冪等性を決めたか
- 注文などのデータ更新が確定してから送信するか
到達性
- SPF、DKIM、DMARCの設定とレポートを確認したか
- 送信目的ごとにFrom、Reply-To、ドメインを整理したか
- Webhookの署名検証とイベント重複排除があるか
受信者の体験
- 件名だけで通知の種類と対象を判断できるか
- 重要情報がテキストとして読めるか
- リンク文言が操作内容を説明しているか
- HTML版とテキスト版で必須情報が一致しているか
- 通知種別ごとの購読設定と配信停止が分かるか
運用
- キュー滞留、失敗、バウンス、苦情を別々に監視するか
- 抑止リストが送信前に参照されるか
- 変更後に主要なメールクライアントで表示を確認するか
まとめ
Laravelの通知基盤では、Notificationsでイベントとチャネルを管理し、Mailableでメール固有の内容を組み立てると責務が明確になります。
そのうえで、キュー、ドメイン認証、配信停止、Webhook、抑止、監視を一つの運用としてつなぎます。
実装の完了条件は、送信APIが成功することではありません。
受信者が内容を理解でき、不要な通知を止められ、運用者が失敗の場所を追える状態まで確認して、通知基盤は初めて継続運用できます。
参考資料
- Laravel公式ドキュメント:Mail
- Laravel公式ドキュメント:Notifications
- Laravel公式ドキュメント:Queues
- RFC 7208:SPF
- RFC 6376:DKIM
- RFC 7489:DMARC
- RFC 8058:ワンクリック配信停止
- W3C:Web Content Accessibility Guidelines 2.2
- W3C WAI:画像の代替テキスト
この記事に関連する株式会社greedenの取り組み
通知基盤は、送信処理だけでなく、購読設定、失敗時の復旧、監視まで一つの運用として設計する必要があります。株式会社greedenは、Laravelを用いたWebシステム開発で、要件整理から実装、テスト、リリース後の保守運用まで支援しています。
