# 通知設計書 8-SendFailedMessageToNotifier

## 概要

本ドキュメントは、Symfony NotifierコンポーネントにおけるMessenger失敗通知（SendFailedMessageToNotifier）の設計仕様を記述する。SendFailedMessageToNotifierListenerは、Messengerワーカーでメッセージ処理が失敗した際にNotifierを通じて管理者に通知を送信するイベントリスナーである。

### 本通知の処理概要

SendFailedMessageToNotifierListenerは、Symfony MessengerコンポーネントのWorkerMessageFailedEventをリッスンするイベントサブスクライバーである。メッセージ処理が最終的に失敗した（リトライ対象でない）場合に、例外情報をもとにNotificationを自動生成し、Notifierに登録された全管理者（AdminRecipients）に通知を送信する。

**業務上の目的・背景**：Messengerワーカーで非同期処理されるメッセージが失敗した場合、迅速に運用担当者に通知して対応を促す必要がある。本リスナーは、手動でのエラー監視の手間を省き、メッセージ処理失敗を自動的に通知することで、システムの信頼性と運用効率を向上させる。

**通知の送信タイミング**：Messengerワーカーがメッセージの処理に失敗し、WorkerMessageFailedEventが発火した時点で通知が生成・送信される。ただし、メッセージがリトライ対象の場合は通知されない（最終失敗時のみ通知）。

**通知の受信者**：Notifier::getAdminRecipients()で登録された管理者が受信者となる。管理者はNotifier::addAdminRecipient()で事前に登録する必要がある。

**通知内容の概要**：件名は「A "{メッセージクラス名}" message has just failed: {例外クラス名}: {例外メッセージ}.」の形式で生成される。重要度はIMPORTANCE_HIGH。HandlerFailedExceptionの場合はラップされた最初の例外が使用される。

**期待されるアクション**：管理者は通知を受け取り、失敗したメッセージの原因を調査し、必要に応じてメッセージの再送やバグ修正を行う。

## 通知種別

全チャネル対応（AdminRecipientsの設定に依存）
- Email / SMS / Chat / Push / Desktop / Browser 等、AdminRecipientsがサポートするすべてのチャネル

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | AdminRecipientsに対する通常のNotifier::send()による送信 |
| 優先度 | IMPORTANCE_HIGH（固定）（SendFailedMessageToNotifierListener.php 行44） |
| リトライ | 使用するチャネルに依存 |

### 送信先決定ロジック

1. Notifier::getAdminRecipients()で登録された全管理者に送信（SendFailedMessageToNotifierListener.php 行47）
2. AdminRecipientsが未登録の場合、Notifier::send()内でNoRecipientが使用される（Notifier.php 行42-44）
3. 各AdminRecipientが実装するインターフェース（EmailRecipientInterface、SmsRecipientInterface等）に応じて利用可能なチャネルが決定される

## 通知テンプレート

### 本文テンプレート

```
件名: A "{メッセージクラス名}" message has just failed: {例外クラス短縮名}: {例外メッセージ}.

本文: Notification::fromThrowable()で生成された例外情報
（スタックトレースを含む）
```

### 添付ファイル

| ファイル名 | 形式 | 条件 | 説明 |
|----------|------|------|------|
| 例外情報 | テキスト | EmailチャネルかつNotificationEmail利用時 | FlattenExceptionによる詳細な例外情報 |

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| messageClass | 失敗したメッセージのクラス名 | envelope->getMessage()::class | Yes（自動生成） |
| throwable | 例外情報 | WorkerMessageFailedEvent::getThrowable() | Yes（自動取得） |
| subject | 件名 | 自動生成 | Yes |
| importance | 重要度 | IMPORTANCE_HIGH（固定） | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| イベント | WorkerMessageFailedEvent | willRetry()がfalseであること（リトライ対象でない） | Messengerワーカーでのメッセージ処理失敗時に自動発火 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| リトライ対象の失敗 | event->willRetry()がtrueの場合、通知は送信されない（SendFailedMessageToNotifierListener.php 行34-36） |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[WorkerMessageFailedEvent発火] --> B{willRetry?}
    B -->|Yes| C[return: 通知スキップ]
    B -->|No| D[throwable取得]
    D --> E{HandlerFailedException?}
    E -->|Yes| F[getWrappedExceptions から最初の例外を取得]
    E -->|No| G[throwableをそのまま使用]
    F --> H[Notification::fromThrowable で通知生成]
    G --> H
    H --> I[importance を HIGH に設定]
    I --> J[件名を再構成: メッセージクラス名を含める]
    J --> K[Notifier::send で AdminRecipients に送信]
    K --> L[終了]
    C --> L
```

## データベース参照・更新仕様

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| 該当なし | - | リスナーはデータベースを参照しない |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| 該当なし | - | リスナーはデータベースを更新しない |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 通知送信失敗 | チャネルの送信処理でエラーが発生した場合 | Notifier::send()内の各チャネルのエラー処理に依存 |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | リスナー自体にはリトライ機構なし。通知送信のリトライは各チャネルに依存 |
| リトライ間隔 | - |
| リトライ対象エラー | - |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | なし。大量のメッセージ失敗時には通知が大量送信される可能性あり |
| 1日あたり上限 | なし |

### 配信時間帯

制限なし。Messengerワーカーの実行タイミングに依存。

## セキュリティ考慮事項

- 例外メッセージやスタックトレースに機密情報（データベース認証情報、APIキー等）が含まれる場合があるため、通知の送信先を信頼できる管理者に限定すること
- AdminRecipientsの登録は信頼できるユーザーのみに限定すること
- 通知チャネルにパブリックなチャットチャネル（公開Slackチャンネル等）を使用する場合は、例外情報の漏洩リスクを考慮すること

## 備考

- SendFailedMessageToNotifierListenerはEventSubscriberInterfaceを実装しており、WorkerMessageFailedEvent::classをリッスンする（行50-55）
- HandlerFailedExceptionの場合は、ラップされた例外のうち最初のもの（array_key_first）を使用する（行39-41）
- 件名の構成は2段階で行われる：(1) fromThrowable()で「{例外クラス名}: {メッセージ}」を自動生成、(2) subject()で「A "{メッセージクラス名}" message has just failed: {既存の件名}.」に再構成（行44-45）
- Notifier依存はコンストラクタインジェクションで注入される（行27-29）

---

## コードリーディングガイド

本通知を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | WorkerMessageFailedEvent | `src/Symfony/Component/Messenger/Event/WorkerMessageFailedEvent.php` | getThrowable(), willRetry(), getEnvelope()メソッドの理解 |
| 1-2 | HandlerFailedException | `src/Symfony/Component/Messenger/Exception/HandlerFailedException.php` | getWrappedExceptions()でラップされた例外を取得する仕組み |

**読解のコツ**: このリスナーはMessengerコンポーネントとNotifierコンポーネントの橋渡し役。Messengerのイベント構造を先に理解しておくと読みやすい。

#### Step 2: エントリーポイントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | SendFailedMessageToNotifierListener.php | `src/Symfony/Component/Notifier/EventListener/SendFailedMessageToNotifierListener.php` | onMessageFailed()メソッド（行32-47）が本処理の全体。getSubscribedEvents()（行50-55）でイベント登録 |

**主要処理フロー**:
1. **行34-36**: willRetry()チェック。リトライ対象なら即return
2. **行38-41**: throwable取得。HandlerFailedExceptionの場合はラップ例外の最初を使用
3. **行43-44**: Notification::fromThrowable()で通知生成、importanceをHIGHに設定
4. **行45**: subject()で件名を再構成（メッセージクラス名を含める）
5. **行47**: Notifier::send()でAdminRecipientsに送信

#### Step 3: 依存するNotifier処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | Notifier.php | `src/Symfony/Component/Notifier/Notifier.php` | send()メソッド（行40-51）、getAdminRecipients()（行61-64）の理解 |
| 3-2 | Notification.php | `src/Symfony/Component/Notifier/Notification/Notification.php` | fromThrowable()（行59-62）、importance()（行97-102）、subject()（行67-72）の理解 |

### プログラム呼び出し階層図

```
[Messenger Worker] メッセージ処理失敗
    |
    +-- WorkerMessageFailedEvent dispatch
            |
            +-- SendFailedMessageToNotifierListener::onMessageFailed()
                    |
                    +-- event->willRetry()  [true: return]
                    |
                    +-- event->getThrowable()
                    |       +-- [HandlerFailedException] getWrappedExceptions()[0]
                    |
                    +-- Notification::fromThrowable(throwable)
                    |       +-- importance(IMPORTANCE_HIGH)
                    |       +-- subject("A \"{class}\" message has just failed: {subject}.")
                    |
                    +-- notifier->send(notification, ...adminRecipients)
                            +-- [各チャネルでの送信処理]
```

### データフロー図

```
[入力]                              [処理]                                  [出力]

WorkerMessageFailedEvent            SendFailedMessageToNotifierListener
  - throwable            -------->    |
  - willRetry                         v
  - envelope                        例外取得・件名生成
    - message::class     -------->    |
                                      v
Notifier                            Notification生成               -------->  各チャネル送信
  - adminRecipients      -------->    |                                       (Email/SMS/Chat等)
                                      v
                                    Notifier::send()
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| SendFailedMessageToNotifierListener.php | `src/Symfony/Component/Notifier/EventListener/SendFailedMessageToNotifierListener.php` | ソース | Messenger失敗イベントリスナー（本通知のメインファイル） |
| Notification.php | `src/Symfony/Component/Notifier/Notification/Notification.php` | ソース | fromThrowable()による通知生成 |
| Notifier.php | `src/Symfony/Component/Notifier/Notifier.php` | ソース | send()とgetAdminRecipients() |
| WorkerMessageFailedEvent.php | `src/Symfony/Component/Messenger/Event/WorkerMessageFailedEvent.php` | ソース | Messengerのメッセージ失敗イベント |
| HandlerFailedException.php | `src/Symfony/Component/Messenger/Exception/HandlerFailedException.php` | ソース | ハンドラ失敗例外（ラップ例外の取得） |
