# 通知設計書 7-ExceptionNotification

## 概要

本ドキュメントは、Symfony Notifierコンポーネントにおける例外通知（ExceptionNotification）の設計仕様を記述する。Notification::fromThrowable()メソッドによりThrowableオブジェクトから自動的に通知を生成する仕組みについて詳細に定義する。

### 本通知の処理概要

ExceptionNotificationは、Notification::fromThrowable()静的メソッドを使用して、PHPの例外（Throwable）から自動的に通知オブジェクトを生成する機能である。例外のクラス名、メッセージ、スタックトレースを含む通知が生成され、任意のチャネル（Email、Chat、SMS等）を通じて送信される。

**業務上の目的・背景**：アプリケーションで予期しないエラーや例外が発生した際に、開発者や運用担当者に迅速に通知を送る必要がある。手動で通知オブジェクトを構築する手間を省き、例外オブジェクトから自動的に通知を生成することで、エラー監視の実装コストを大幅に削減する。例外の種別や深刻度に応じた重要度（importance）の設定も自動化される。

**通知の送信タイミング**：アプリケーションコードのcatchブロック等でNotification::fromThrowable()が呼び出され、生成されたNotificationがNotifier::send()に渡されたときに送信される。SendFailedMessageToNotifierListenerでは、Messengerのメッセージ処理失敗時に自動的に呼び出される。

**通知の受信者**：通常の通知と同様、Notifier::send()に渡されるRecipientオブジェクトが受信者となる。多くの場合、Notifier::getAdminRecipients()で登録された管理者宛てに送信される。

**通知内容の概要**：件名は「{例外クラス名}: {例外メッセージ}」の形式で自動生成される。本文にはスタックトレースが含まれる。FlattenExceptionクラスが利用可能な場合は、より詳細な例外情報が保持される。重要度に応じたデフォルト絵文字も設定される。

**期待されるアクション**：受信者（開発者・運用担当者）は通知を確認し、例外の原因を調査・修正する。スタックトレースを参照して問題箇所を特定し、適切なバグ修正を行う。

## 通知種別

全チャネル対応（Email / SMS / Chat / Push / Desktop / Browser）
- fromThrowable()で生成されたNotificationは通常のNotificationクラスのインスタンスであり、任意のチャネルで送信可能

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 使用するチャネルに依存 |
| 優先度 | デフォルトはIMPORTANCE_HIGH（Notification.php 行45）。importanceFromLogLevelName()で例外の深刻度に応じた変更も可能 |
| リトライ | 使用するチャネルに依存 |

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

1. fromThrowable()で生成されたNotificationは通常のNotificationクラスインスタンス
2. channelsパラメータで送信先チャネルを指定可能（fromThrowable()の第2引数）
3. channels未指定の場合はChannelPolicyに従う
4. 送信先の決定はNotifier::getChannels()の通常のロジックに従う

## 通知テンプレート

### 本文テンプレート

```
件名: {例外クラスの短縮名}: {例外メッセージ}

本文（exceptionAsString）:
{例外クラス名}: {例外メッセージ}
 in {ファイルパス}:{行番号}
Stack trace:
{スタックトレース}
```

### 添付ファイル

| ファイル名 | 形式 | 条件 | 説明 |
|----------|------|------|------|
| 例外情報 | テキスト | FlattenExceptionクラスが利用可能かつEmailチャネル使用時 | NotificationEmailのexception()メソッドで添付 |

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| subject | 件名（{クラス名}: {メッセージ}） | Notification::getSubject()（exception()で自動設定） | Yes（自動生成） |
| exception | FlattenExceptionオブジェクト | Notification::getException() | No（FlattenExceptionクラスが利用可能な場合のみ） |
| exceptionAsString | 例外のスタックトレース文字列 | Notification::getExceptionAsString() | Yes（自動生成） |
| importance | 重要度 | Notification::getImportance() | Yes（デフォルト: high） |
| emoji | デフォルト絵文字（例外あり時） | Notification::getDefaultEmoji() | No（自動設定） |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| API呼び出し | Notification::fromThrowable()の実行 + Notifier::send() | Throwableオブジェクトが存在すること | アプリケーションのcatchブロック等から呼び出し |
| イベント | Messenger WorkerMessageFailedEvent | リトライ対象でないメッセージ処理失敗（SendFailedMessageToNotifierListener経由） | Messengerコンポーネントとの連携 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| なし | fromThrowable()自体には送信抑止条件なし。送信はNotifier::send()のロジックに依存 |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[Throwable発生] --> B[Notification::fromThrowable実行]
    B --> C[exception メソッド呼び出し]
    C --> D[件名自動生成: クラス名 + メッセージ]
    D --> E{FlattenExceptionクラス存在?}
    E -->|Yes| F[FlattenException生成・保持]
    E -->|No| G[スキップ]
    F --> H[exceptionAsString計算]
    G --> H
    H --> I[Notifier::send実行]
    I --> J[各チャネルで送信]
    J --> K[終了]
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| 該当なし | - | 例外通知生成はメモリ上で完結する |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| 該当なし | - | 例外通知生成自体はデータベースを更新しない |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 該当なし | fromThrowable()自体はエラーをスローしない | - |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | 使用するチャネルに依存 |
| リトライ間隔 | 使用するチャネルに依存 |
| リトライ対象エラー | 使用するチャネルに依存 |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | Notifierコンポーネント自体にはレート制限なし |
| 1日あたり上限 | 同上 |

### 配信時間帯

制限なし。

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

- スタックトレースにはファイルパス、クラス名、メソッド引数等の内部情報が含まれるため、外部ユーザーへの通知には使用しないこと
- 例外メッセージにSQLクエリやパスワード等の機密情報が含まれる場合があるため、通知内容のサニタイズを検討すること
- FlattenExceptionは例外の全情報を保持するため、メール添付時にはアクセス制限のあるチャネルで送信すること

## 備考

- fromThrowable()は静的ファクトリメソッドで、内部的にはnew self()でNotificationを生成し、exception()メソッドを呼び出す（Notification.php 行59-62）
- exception()メソッドは$thisを返すfluent interfaceパターンを使用している
- 件名の生成ではFQCNからクラス名のみを抽出するためにexplode('\\')とarray_pop()を使用（Notification.php 行142-144）
- getDefaultEmoji()は例外情報がある場合にのみ絵文字を返す。重要度に応じて異なる絵文字を使用（urgent: 雷雲、high: 雨雲、medium: 曇り時々晴れ、low: 晴れ時々曇り）（Notification.php 行183-195）
- importanceFromLogLevelName()でPSRログレベルから重要度を自動設定可能（500以上: urgent、400以上: high、それ以外: low）（Notification.php 行114-120）

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | Notification.php | `src/Symfony/Component/Notifier/Notification/Notification.php` | fromThrowable()（行59-62）、exception()（行140-151）、computeExceptionAsString()（行197-214）を重点的に理解 |

**読解のコツ**: Notification.phpは例外通知の核となるファイル。以下の順序で読むのが効果的:
1. fromThrowable()静的メソッド（行59-62）: new self()でインスタンスを生成し、exception()を呼び出す
2. exception()メソッド（行140-151）: 件名の自動生成（行142-144）、FlattenException生成（行145-147）、exceptionAsString計算（行148）
3. computeExceptionAsString()プライベートメソッド（行197-214）: FlattenException有無で分岐。getAsString()を使用するかスタックトレースを手動構築するか
4. getDefaultEmoji()（行183-195）: exceptionAsStringが設定されている場合のみ絵文字を返す

#### Step 2: FlattenExceptionの役割を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | FlattenException | `src/Symfony/Component/ErrorHandler/Exception/FlattenException.php` | createFromThrowable()でThrowableからシリアライズ可能な例外オブジェクトを生成。getAsString()で文字列表現を取得 |

#### Step 3: 重要度とデフォルト絵文字の関係を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | Notification.php | `src/Symfony/Component/Notifier/Notification/Notification.php` | IMPORTANCE定数（行34-37）、importanceFromLogLevelName()（行114-120）、getDefaultEmoji()（行183-195）の関係 |

**主要処理フロー**:
- **行34-37**: IMPORTANCE_URGENT/HIGH/MEDIUM/LOW定数定義
- **行114-120**: PSRログレベルから重要度への変換ロジック
- **行183-195**: 例外情報がある場合のみ重要度に応じた絵文字を返す

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

```
Notification::fromThrowable(throwable, channels)
    |
    +-- new self('', channels)  [空の件名で初期化]
    |
    +-- exception(throwable)
            |
            +-- explode('\\', exception::class) + array_pop()  [クラス短縮名取得]
            +-- subject = "{短縮名}: {メッセージ}"
            |
            +-- [FlattenExceptionクラス存在時]
            |       +-- FlattenException::createFromThrowable()
            |       +-- this->exception = FlattenException
            |
            +-- computeExceptionAsString(throwable)
                    |
                    +-- [FlattenException利用可能]
                    |       +-- FlattenException::getAsString()
                    |
                    +-- [FlattenException利用不可]
                            +-- 手動でクラス名+メッセージ+ファイル+行+トレース構築
```

### データフロー図

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

Throwable                       Notification::fromThrowable()
  - class             -------->   |
  - message                       v
  - file                        exception()
  - line                          |
  - trace                        v
                               件名自動生成                -------->  Notification
                               FlattenException生成                   - subject
                               exceptionAsString計算                  - exception
                                                                      - exceptionAsString
                                                                      - importance (HIGH)
                                                                      - emoji (自動)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| Notification.php | `src/Symfony/Component/Notifier/Notification/Notification.php` | ソース | 通知基本クラス。fromThrowable(), exception(), computeExceptionAsString()を含む |
| FlattenException.php | `src/Symfony/Component/ErrorHandler/Exception/FlattenException.php` | ソース | 例外のシリアライズ可能な表現。ErrorHandlerコンポーネント |
| Notifier.php | `src/Symfony/Component/Notifier/Notifier.php` | ソース | 通知送信エントリーポイント |
| SendFailedMessageToNotifierListener.php | `src/Symfony/Component/Notifier/EventListener/SendFailedMessageToNotifierListener.php` | ソース | Messenger失敗時にfromThrowable()を使用する例 |
