# 通知設計書 9-MessageEvent（Notifier）

## 概要

本ドキュメントは、Symfony NotifierコンポーネントにおけるMessageEvent（Notifier）の設計仕様を記述する。MessageEventは、通知メッセージが送信される際に発火するイベントであり、メッセージのキューイング状態を含む情報を提供する。

### 本通知の処理概要

MessageEventは、Notifierコンポーネントのイベントシステムにおける「送信前」イベントである。AbstractTransport::send()メソッド内、またはChatter/Texterクラスでメッセージがキューイングされる際にディスパッチされる。このイベントを購読することで、メッセージ送信前のインターセプト、ログ記録、メッセージの変更などが可能になる。

**業務上の目的・背景**：通知メッセージの送信をモニタリング・制御するために、送信前のフックポイントが必要である。MessageEventにより、送信されるメッセージの内容確認、送信ログの記録、条件に応じた送信のキャンセルなどの横断的関心事を実装できる。プロファイリングツール（Symfony WebProfiler等）でのデータ収集にも使用される。

**通知の送信タイミング**：以下の2つのタイミングでディスパッチされる。
1. AbstractTransport::send()内で、doSend()による実際の送信の直前（同期送信、queued=false）
2. Chatter/Texterクラスで、MessageBus経由のキューイングの直前（非同期送信、queued=true）

**通知の受信者**：このイベントには「受信者」という概念はない。EventDispatcherに登録されたイベントリスナー/サブスクライバーがイベントを処理する。

**通知内容の概要**：MessageInterfaceオブジェクト（送信対象のメッセージ）と、キューイング状態（queued: boolean）を保持する。

**期待されるアクション**：イベントリスナーはメッセージ内容のログ記録、メッセージの変更、送信のモニタリングなどを行う。NotificationLoggerListenerはこのイベントをログとして記録する。

## 通知種別

イベント（EventDispatcher経由）
- 外部への通知ではなく、アプリケーション内部のイベント通知メカニズム

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | EventDispatcher::dispatch()による同期イベントディスパッチ |
| 優先度 | EventDispatcherのリスナー優先度に依存 |
| リトライ | なし（イベントディスパッチのため） |

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

1. EventDispatcherに登録されたMessageEvent用リスナーが自動的に呼び出される
2. リスナーの登録はサービスコンテナの設定またはEventSubscriberInterfaceの実装で行う

## 通知テンプレート

### 本文テンプレート

該当なし（イベントオブジェクトとしてディスパッチされるため、テンプレートは使用しない）

### 添付ファイル

| ファイル名 | 形式 | 条件 | 説明 |
|----------|------|------|------|
| 該当なし | - | - | イベントオブジェクトのため添付ファイルなし |

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| message | 送信対象のメッセージオブジェクト | MessageEvent::getMessage() | Yes |
| queued | メッセージがキューイングされたかどうか | MessageEvent::isQueued() | Yes（デフォルト: false） |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| Transport送信 | AbstractTransport::send() | EventDispatcherが設定されていること | doSend()の直前にqueued=falseでディスパッチ（AbstractTransport.php 行73） |
| Chatterキューイング | Chatter::send() | EventDispatcherが設定され、かつBus経由の非同期送信の場合 | Bus::dispatch()の直前にqueued=trueでディスパッチ（Chatter.php 行49） |
| Texterキューイング | Texter::send() | EventDispatcherが設定され、かつBus経由の非同期送信の場合 | Bus::dispatch()の直前にqueued=trueでディスパッチ（Texter.php 行49） |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| EventDispatcherが未設定（null） | AbstractTransport: dispatcher==nullの場合、イベントをディスパッチせずにdoSend()を直接実行（AbstractTransport.php 行69-71） |
| Transport直接送信（Chatter/Texter） | Chatter/TexterでBus==nullの場合、Transport::send()が直接呼ばれる。この場合Transport内でMessageEventがディスパッチされる可能性がある |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[メッセージ送信要求] --> B{送信経路}
    B -->|AbstractTransport| C{dispatcher設定あり?}
    C -->|Yes| D[MessageEvent dispatch queued=false]
    C -->|No| E[doSend直接実行]
    D --> F[doSend実行]
    F --> G{送信成功?}
    G -->|Yes| H[SentMessageEvent dispatch]
    G -->|No| I[FailedMessageEvent dispatch]
    B -->|Chatter/Texter Bus経由| J{dispatcher設定あり?}
    J -->|Yes| K[MessageEvent dispatch queued=true]
    J -->|No| L[Bus::dispatch直接実行]
    K --> M[Bus::dispatch実行]
    L --> M
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| 該当なし | - | イベントオブジェクトはデータベースを参照しない |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| 該当なし | - | イベントオブジェクト自体はデータベースを更新しない |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| リスナー例外 | イベントリスナー内で例外がスローされた場合 | リスナーの実装でtry/catchを行う |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | なし（イベントディスパッチのため） |
| リトライ間隔 | - |
| リトライ対象エラー | - |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | なし（イベントディスパッチのため） |
| 1日あたり上限 | なし |

### 配信時間帯

制限なし。通知メッセージ送信のタイミングに連動。

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

- MessageEventにはメッセージオブジェクトが含まれるため、イベントリスナー内でメッセージ内容をログ出力する際は個人情報のマスキングを検討すること
- イベントリスナーの登録はサービスコンテナ経由で行うため、不正なリスナーの注入リスクは低い

## 備考

- MessageEventはfinalクラスとして定義されている（MessageEvent.php 行20）
- MessageEventはSymfony Contracts EventDispatcher\Eventを継承している
- queuedパラメータのデフォルト値はfalse（MessageEvent.php 行24）
- AbstractTransport::send()では、MessageEventディスパッチ後にdoSend()が実行され、成功時はSentMessageEvent、失敗時はFailedMessageEventがディスパッチされる3段階のイベントフロー（AbstractTransport.php 行67-86）
- Chatter/TexterではBus経由の場合のみqueued=trueでMessageEventをディスパッチし、SentMessageEvent/FailedMessageEventはディスパッチされない

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | MessageEvent.php | `src/Symfony/Component/Notifier/Event/MessageEvent.php` | finalクラス。コンストラクタ（行22-25）でMessageInterfaceとqueued（bool, デフォルトfalse）を受け取る。getMessage()とisQueued()の2つのgetterを提供 |
| 1-2 | MessageInterface.php | `src/Symfony/Component/Notifier/Message/MessageInterface.php` | getRecipientId(), getSubject(), getOptions(), getTransport()を定義するインターフェース |

**読解のコツ**: MessageEventは非常にシンプルなデータオブジェクト。重要なのはこのイベントが「いつ」「どこから」ディスパッチされるかを理解すること。

#### Step 2: ディスパッチ元を理解する（同期送信パス）

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | AbstractTransport.php | `src/Symfony/Component/Notifier/Transport/AbstractTransport.php` | send()メソッド（行67-86）が3段階のイベントフロー: MessageEvent -> doSend() -> SentMessageEvent or FailedMessageEvent |

**主要処理フロー**:
- **行69-71**: dispatcher==nullの場合、イベントなしでdoSend()直接実行
- **行73**: MessageEvent(message)をディスパッチ（queued=false）
- **行75-81**: try/catchでdoSend()を実行。失敗時はFailedMessageEventディスパッチ
- **行83**: 成功時はSentMessageEvent(sentMessage)をディスパッチ

#### Step 3: ディスパッチ元を理解する（非同期送信パス）

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | Chatter.php | `src/Symfony/Component/Notifier/Chatter.php` | send()メソッド（行43-54）でBus経由の場合にMessageEvent(message, true)をディスパッチ |
| 3-2 | Texter.php | `src/Symfony/Component/Notifier/Texter.php` | Chatterと同じ構造。send()メソッド（行43-54） |

**主要処理フロー**:
- **行45-47**: Bus==nullの場合、Transport::send()を直接実行（Transportレベルでイベント可能）
- **行49**: dispatcher?->dispatch(new MessageEvent(message, true))でqueued=trueでディスパッチ
- **行51**: Bus::dispatch(message)でメッセージをキューに投入

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

```
[同期送信パス]
AbstractTransport::send(message)
    |
    +-- [dispatcher != null] dispatch(new MessageEvent(message, false))
    |
    +-- doSend(message)
    |       |
    |       +-- [成功] dispatch(new SentMessageEvent(sentMessage))
    |       +-- [失敗] dispatch(new FailedMessageEvent(message, error))

[非同期送信パス]
Chatter::send(message) / Texter::send(message)
    |
    +-- [bus != null]
            |
            +-- [dispatcher != null] dispatch(new MessageEvent(message, true))
            |
            +-- bus->dispatch(message)
```

### データフロー図

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

MessageInterface                AbstractTransport::send()
  - subject          -------->    |
  - transport                     v
  - options                    MessageEvent(message, false)  ------>  EventDispatcher
  - recipientId                   |                                   -> リスナー群
                                  v
                               doSend()
                                  |
                               +--+--+
                               |     |
                               v     v
                     SentMessageEvent  FailedMessageEvent  ------->  EventDispatcher
                                                                     -> リスナー群

MessageInterface                Chatter/Texter::send()
  - subject          -------->    |
  - transport                     v
                               MessageEvent(message, true)  ------->  EventDispatcher
                                  |                                    -> リスナー群
                                  v
                               Bus::dispatch(message)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| MessageEvent.php | `src/Symfony/Component/Notifier/Event/MessageEvent.php` | ソース | メッセージ送信前イベント（本通知のメインファイル） |
| SentMessageEvent.php | `src/Symfony/Component/Notifier/Event/SentMessageEvent.php` | ソース | メッセージ送信成功イベント |
| FailedMessageEvent.php | `src/Symfony/Component/Notifier/Event/FailedMessageEvent.php` | ソース | メッセージ送信失敗イベント |
| AbstractTransport.php | `src/Symfony/Component/Notifier/Transport/AbstractTransport.php` | ソース | 同期送信時のイベントディスパッチ元 |
| Chatter.php | `src/Symfony/Component/Notifier/Chatter.php` | ソース | チャットメッセージの非同期送信時のイベントディスパッチ元 |
| Texter.php | `src/Symfony/Component/Notifier/Texter.php` | ソース | テキストメッセージの非同期送信時のイベントディスパッチ元 |
| MessageInterface.php | `src/Symfony/Component/Notifier/Message/MessageInterface.php` | ソース | メッセージの共通インターフェース |
| NotificationLoggerListener.php | `src/Symfony/Component/Notifier/EventListener/NotificationLoggerListener.php` | ソース | MessageEventをログとして記録するリスナー |
