# 機能設計書 43-Webhook

## 概要

本ドキュメントは、Symfony Webhookコンポーネントの機能設計を記述する。WebhookはWebhookの送受信を容易にする機能を提供し、サードパーティサービスとの双方向イベント連携を実現するコンポーネントである。

### 本機能の処理概要

Webhookコンポーネントは、Webhook送信（Server）とWebhook受信（Client/Controller）の両方向の機能を提供する。送信側はRemoteEventをHTTPリクエストとしてサブスクライバーに配信し、受信側はHTTPリクエストをパースしてRemoteEventに変換しMessengerバスに配信する。

**業務上の目的・背景**：Webhookはサービス間のリアルタイムイベント連携に広く使用されている。決済完了通知、GitHubプルリクエストイベント、メール配信ステータス更新など、外部サービスからのイベント通知を受信・処理するための標準的な仕組みが必要である。また、自身のシステムから外部サービスへイベントを通知する場合にも使用される。

**機能の利用シーン**：外部決済サービスからの決済完了Webhook受信、CI/CDシステムからのビルド完了通知、メール配信サービスからのバウンス/開封通知、自社サービスから外部サービスへのイベント通知送信等。

**主要な処理内容**：
1. Webhook受信: WebhookControllerによるHTTPリクエスト受信とRequestParser経由のペイロードパース
2. 署名検証: HMAC-SHA256等のアルゴリズムによるWebhook署名の検証
3. RemoteEvent変換: パース結果をRemoteEventオブジェクトに変換
4. Messenger連携: ConsumeRemoteEventMessageとしてMessengerバスにディスパッチ
5. Webhook送信: Transport経由でSubscriberのURLにHTTP POSTリクエストを送信
6. リクエスト構成: ヘッダー設定、ボディシリアライズ、署名付与

**関連システム・外部連携**：RemoteEventコンポーネント（イベントオブジェクト）、Messengerコンポーネント（非同期処理）、HttpClientコンポーネント（HTTP送信）、HttpFoundationコンポーネント（リクエスト/レスポンス）と連携する。

**権限による制御**：Webhook受信時の署名検証（secret）によるリクエスト認証が組み込まれている。不正な署名のWebhookはRejectWebhookExceptionで拒否される。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | - | - | Webhookコンポーネントには関連画面はない |

## 機能種別

データ連携 / イベント処理

## 入力仕様

### 入力パラメータ（Webhook受信側）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| type | string | Yes | Webhookの種類（URLパスで指定） | parsers配列のキーと一致 |
| request | Request | Yes | HTTPリクエストオブジェクト | POSTメソッド、JSON形式 |
| secret | string | Yes | Webhook署名検証用シークレット | 空文字列不可 |
| Webhook-Signature | string (header) | Yes | HMAC署名ヘッダー | algo=hash形式 |
| Webhook-Event | string (header) | Yes | イベント名ヘッダー | - |
| Webhook-Id | string (header) | Yes | イベントIDヘッダー | - |

### 入力パラメータ（Webhook送信側）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| subscriber | Subscriber | Yes | Webhook送信先情報（URL + Secret） | secretは空文字列不可 |
| event | RemoteEvent | Yes | 送信するリモートイベント | - |

### 入力データソース

- HTTPリクエスト（外部サービスからのWebhook）
- DIコンテナ経由のparsers設定（type -> RequestParser + secretのマッピング）
- アプリケーションコードからのRemoteEvent生成（送信時）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| Response (受信成功) | Response | RequestParser::createSuccessfulResponse()の結果 |
| Response (受信失敗) | Response | RequestParser::createRejectedResponse()またはHTTP 404 |
| ConsumeRemoteEventMessage | ConsumeRemoteEventMessage | Messengerバスにディスパッチされるメッセージ |
| HTTP POST (送信) | - | SubscriberのURLへのHTTPリクエスト |

### 出力先

- Messengerバス（受信したWebhookの非同期処理）
- 外部サービスのURL（Webhook送信時）

## 処理フロー

### 処理シーケンス

```
【Webhook受信】
1. WebhookController::handle(type, request) 呼び出し
   └─ URLパスからtypeを抽出
2. type に対応するparserの取得
   └─ 未登録の場合はHTTP 404応答
3. RequestParser::parse(request, secret)
   └─ リクエスト形式の検証（POST、JSON）
   └─ 署名ヘッダーの検証（Webhook-Signature、Webhook-Event、Webhook-Id）
   └─ HMAC署名の検証
   └─ RemoteEvent オブジェクトの生成
4. ConsumeRemoteEventMessage の生成とMessengerバスへのディスパッチ
5. 成功レスポンスの返却

【Webhook送信】
1. Transport::send(subscriber, event) 呼び出し
2. HttpOptions の構成
   └─ HeadersConfigurator でヘッダー設定
   └─ JsonBodyConfigurator でボディシリアライズ
   └─ HeaderSignatureConfigurator で署名付与
3. HttpClient::request('POST', url, options) で送信
```

### フローチャート

```mermaid
flowchart TD
    subgraph Webhook受信
    A[HTTP POST受信] --> B[WebhookController::handle]
    B --> C{type登録あり?}
    C -->|No| D[404 Response]
    C -->|Yes| E[RequestParser::parse]
    E --> F{パース成功?}
    F -->|No| G[Rejected Response]
    F -->|Yes| H[RemoteEvent生成]
    H --> I[MessageBus::dispatch]
    I --> J[Success Response]
    end

    subgraph Webhook送信
    K[RemoteEvent] --> L[Transport::send]
    L --> M[Headers設定]
    M --> N[Body設定]
    N --> O[署名付与]
    O --> P[HTTP POST送信]
    end
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-43-01 | シークレット必須 | Subscriberのsecretは空文字列不可 | Subscriber生成時 |
| BR-43-02 | 署名検証 | Webhook-Signatureヘッダーの値がHMAC署名と一致すること | RequestParser::doParse()時 |
| BR-43-03 | POSTメソッド限定 | Webhook受信はPOSTメソッドのみ受け付ける | RequestParser::getRequestMatcher()で定義 |
| BR-43-04 | JSON形式限定 | Webhook受信のボディはJSON形式であること | IsJsonRequestMatcherで検証 |
| BR-43-05 | 必須ヘッダー | Webhook-Signature、Webhook-Event、Webhook-Idヘッダーが必須 | RequestParser::doParse()時 |

### 計算ロジック

署名計算: `hash_hmac(algo, event + id + body, secret)`（**RequestParser.php 74行目**）

## データベース操作仕様

Webhookコンポーネント自体はデータベース操作を行わない。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 404 | Not Found | 指定typeに対応するparserが未登録 | parsers設定にtypeを追加する |
| 406 | RejectWebhookException | 必須ヘッダーが不足 | 送信側で必須ヘッダーを設定する |
| 406 | RejectWebhookException | 署名検証失敗 | secretの設定を確認する |
| - | InvalidArgumentException | secretが空文字列 | 非空のsecretを設定する |

### リトライ仕様

Webhook送信時のリトライはアプリケーション層またはMessengerのリトライ機構で実装する。

## トランザクション仕様

該当なし。Messenger経由の処理はMessengerのトランザクション管理に従う。

## パフォーマンス要件

- Webhook受信は同期的にパース・検証を行い、実際のイベント処理はMessenger経由で非同期実行される
- Webhook送信はHttpClient経由でHTTP POSTを実行する

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

- HMAC署名検証により不正なWebhookリクエストを拒否する
- Subscriber.secretは`#[\SensitiveParameter]`アトリビュートで保護されている
- `hash_equals()`によるタイミング攻撃耐性のある署名比較を実装

## 備考

- RequestParserは拡張可能で、サードパーティサービス固有のWebhookフォーマットに対応するカスタムパーサーを実装できる
- RemoteEventコンポーネントと密接に連携する

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | Subscriber.php | `src/Symfony/Component/Webhook/Subscriber.php` | Webhook送信先のデータ構造（URL + Secret） |
| 1-2 | RemoteEvent.php | `src/Symfony/Component/RemoteEvent/RemoteEvent.php` | リモートイベントのデータ構造（name、id、payload） |

**読解のコツ**: Subscriber.phpの`#[\SensitiveParameter]`（**21行目**）はセキュリティ保護のためのアトリビュート。secretが空文字列の場合は即座にInvalidArgumentExceptionが発生する（**22-24行目**）。

#### Step 2: Webhook受信のエントリーポイント

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | WebhookController.php | `src/Symfony/Component/Webhook/Controller/WebhookController.php` | Webhook受信のHTTPエントリーポイント |

**主要処理フロー**:
1. **36-39行目**: type未登録時の404レスポンス返却
2. **42-43行目**: parserの取得とparse実行
3. **45-47行目**: パース失敗時のrejectedレスポンス
4. **49-53行目**: イベント配列化とMessengerバスへのディスパッチ
5. **55行目**: 成功レスポンス返却

#### Step 3: リクエストパーサーを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | RequestParser.php | `src/Symfony/Component/Webhook/Client/RequestParser.php` | デフォルトのリクエストパーサー実装 |

**主要処理フロー**:
- **37-43行目**: POST + JSONのリクエストマッチャー定義
- **45-66行目**: ペイロードパースと署名検証
- **68-77行目**: HMAC署名の計算と検証（hash_equals使用）

#### Step 4: Webhook送信を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | Transport.php (Server) | `src/Symfony/Component/Webhook/Server/Transport.php` | Webhook送信のトランスポート実装 |

**主要処理フロー**:
- **32-41行目**: ヘッダー設定、ボディ設定、署名付与の3段階でリクエスト構成後HTTP POST送信

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

```
【受信】
WebhookController::handle(type, Request)
    |
    ├─ parsers[type] から RequestParser + secret 取得
    |
    ├─ RequestParser::parse(Request, secret)
    |      ├─ getRequestMatcher() → POST + JSON検証
    |      └─ doParse(Request, secret)
    |             ├─ ヘッダー存在確認
    |             ├─ validateSignature() → HMAC検証
    |             └─ new RemoteEvent(name, id, payload)
    |
    └─ MessageBus::dispatch(ConsumeRemoteEventMessage)

【送信】
Transport::send(Subscriber, RemoteEvent)
    |
    ├─ HeadersConfigurator::configure()
    ├─ JsonBodyConfigurator::configure()
    ├─ HeaderSignatureConfigurator::configure()
    |
    └─ HttpClient::request('POST', url, options)
```

### データフロー図

```
[受信]
外部サービス ──HTTP POST──▶ WebhookController ──parse──▶ RemoteEvent ──dispatch──▶ MessageBus ──consume──▶ ConsumerInterface

[送信]
RemoteEvent ──configure──▶ Transport ──HTTP POST──▶ Subscriber URL
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| WebhookController.php | `src/Symfony/Component/Webhook/Controller/WebhookController.php` | ソース | Webhook受信コントローラー |
| Subscriber.php | `src/Symfony/Component/Webhook/Subscriber.php` | ソース | Webhook送信先データ |
| RequestParser.php | `src/Symfony/Component/Webhook/Client/RequestParser.php` | ソース | デフォルトリクエストパーサー |
| AbstractRequestParser.php | `src/Symfony/Component/Webhook/Client/AbstractRequestParser.php` | ソース | パーサー基底クラス |
| Transport.php | `src/Symfony/Component/Webhook/Server/Transport.php` | ソース | Webhook送信トランスポート |
| HeaderSignatureConfigurator.php | `src/Symfony/Component/Webhook/Server/HeaderSignatureConfigurator.php` | ソース | 署名設定 |
| HeadersConfigurator.php | `src/Symfony/Component/Webhook/Server/HeadersConfigurator.php` | ソース | ヘッダー設定 |
| JsonBodyConfigurator.php | `src/Symfony/Component/Webhook/Server/JsonBodyConfigurator.php` | ソース | JSONボディ設定 |
