# 機能設計書 62-フック機能

## 概要

本ドキュメントは、Payload CMSのフック機能（beforeChange、afterRead等のフィールド・コレクションフック）について、その設計仕様を詳細に記載したものである。

### 本機能の処理概要

フック機能は、ドキュメントのライフサイクルの様々なタイミングで、カスタムロジックを実行するための拡張ポイントを提供する機能である。

**業務上の目的・背景**：Payload CMSは多様なビジネス要件に対応する必要がある。標準的なCRUD操作だけでは不十分な場合、開発者はフック機能を使用して独自のビジネスロジックを挿入できる。例えば、ドキュメント保存前のデータ変換、読み取り時のデータ加工、関連データの自動更新などが可能になる。これにより、コアシステムを変更することなく、柔軟なカスタマイズが実現できる。

**機能の利用シーン**：
- ドキュメント作成・更新前のデータバリデーションと変換（beforeValidate、beforeChange）
- ドキュメント読み取り時のデータ加工や計算フィールドの生成（afterRead）
- ドキュメント保存後の関連データ更新や外部API通知（afterChange）
- ドキュメント複製前のデータ調整（beforeDuplicate）
- フィールドレベルでのカスタム処理実行

**主要な処理内容**：
1. beforeValidate：データ検証前の前処理、デフォルト値の設定、アクセス制御
2. beforeChange：データ変更前のフック実行、バリデーション、データ変換
3. afterChange：データ変更後のフック実行、関連処理のトリガー
4. afterRead：データ読み取り後のフック実行、レスポンス加工
5. beforeDuplicate：ドキュメント複製前のフック実行

**関連システム・外部連携**：
- コレクション・グローバルのCRUD操作
- フィールドシステム（各フィールドタイプ）
- リッチテキストエディタ（Lexical/Slate）

**権限による制御**：フック内でアクセス制御のオーバーライドが可能（`overrideAccess`パラメータ）。フック実行自体には特別な権限は不要だが、フック内で実行する操作には通常のアクセス制御が適用される。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 10 | ドキュメント編集画面 | 主画面 | beforeChange、afterReadなどのフック実行 |

## 機能種別

データ変換処理 / イベントハンドリング / 拡張ポイント

## 入力仕様

### 入力パラメータ（beforeChangeフック）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| collection | SanitizedCollectionConfig \| null | Yes | コレクション設定 | - |
| global | SanitizedGlobalConfig \| null | Yes | グローバル設定 | - |
| data | JsonObject | Yes | 入力データ | - |
| doc | JsonObject | Yes | 既存ドキュメント | - |
| docWithLocales | JsonObject | Yes | ロケール付きドキュメント | - |
| operation | 'create' \| 'update' | Yes | 操作種別 | - |
| req | PayloadRequest | Yes | リクエストオブジェクト | - |
| context | RequestContext | Yes | リクエストコンテキスト | - |
| skipValidation | boolean | No | バリデーションスキップフラグ | - |

### 入力パラメータ（afterReadフック）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| collection | SanitizedCollectionConfig \| null | Yes | コレクション設定 | - |
| global | SanitizedGlobalConfig \| null | Yes | グローバル設定 | - |
| doc | JsonObject | Yes | 読み取ったドキュメント | - |
| depth | number | Yes | 関連ドキュメントの深さ | - |
| currentDepth | number | No | 現在の深さ | - |
| locale | string | Yes | ロケール | - |
| fallbackLocale | TypedFallbackLocale | Yes | フォールバックロケール | - |
| overrideAccess | boolean | Yes | アクセス制御オーバーライド | - |
| showHiddenFields | boolean | Yes | 隠しフィールド表示フラグ | - |

### 入力データソース

- コレクション・グローバルの操作（create、update、findByID等）
- Admin UIからのAPI呼び出し
- Local APIからの直接呼び出し

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| (返り値) | JsonObject \| void | 変更されたデータ（フックからの返り値） |

### 出力先

- 次のフック処理への入力
- データベースへの保存（beforeChange経由）
- APIレスポンス（afterRead経由）

## 処理フロー

### 処理シーケンス

```
1. 操作開始（create/update/read）
   └─ 対象フィールドの特定

2. フィールドツリーのトラバース
   └─ 各フィールドに対して再帰的に処理

3. フック実行
   └─ フィールドに定義されたフックを順次実行
   └─ フック返り値があればフィールド値を更新

4. サブフィールドの処理
   └─ array、blocks、group、tabs等のネストされたフィールドを再帰処理

5. 結果の集約
   └─ すべてのフィールドのフック処理完了後、最終データを返却
```

### フローチャート

```mermaid
flowchart TD
    A[操作開始] --> B[traverseFields呼び出し]
    B --> C{フィールドタイプ}
    C -->|単純フィールド| D[フック実行]
    C -->|array/blocks| E[各行をトラバース]
    C -->|group/tabs| F[サブフィールドをトラバース]
    D --> G{フック返り値}
    G -->|undefined以外| H[フィールド値更新]
    G -->|undefined| I[次のフック]
    E --> B
    F --> B
    H --> I
    I --> J{次のフィールド}
    J -->|あり| B
    J -->|なし| K[処理完了]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-01 | フック順序保証 | 同一フィールドの複数フックは定義順に実行 | 全フック |
| BR-02 | 返り値による更新 | フックがundefined以外を返した場合のみフィールド値更新 | 全フック |
| BR-03 | 条件付きスキップ | condition関数がfalseの場合、バリデーションをスキップ | beforeChange |
| BR-04 | ロケール処理 | ローカライズフィールドは各ロケールで個別にフック実行 | afterRead |
| BR-05 | 無限ループ防止 | afterReadのPromise処理は100回の反復で中断 | afterRead |

### 計算ロジック

- フィールドパスの計算：親パス + フィールド名（ドット区切り）
- インデックスパスの計算：配列インデックス（ハイフン区切り）

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

### 操作別データベース影響一覧

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| beforeChange | - | なし（データ変換のみ） | DBへの書き込み前にデータを変換 |
| afterChange | - | なし（トリガーのみ） | DB書き込み後の処理実行 |
| afterRead | - | SELECT（populate時） | リレーションシップのポピュレート |

### テーブル別操作詳細

フック機能自体は直接データベースを操作しない。フック内で実行されるPayload操作により間接的にデータベースが操作される。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | ValidationError | beforeChangeでバリデーションエラー | フィールドエラー情報と共にスロー |
| - | MissingEditorProp | richTextフィールドにエディタ未設定 | フィールド設定を確認 |
| - | Error | afterReadで無限ループ検出 | フックの実装を確認 |

### リトライ仕様

フック内でのエラーはそのまま上位に伝播。リトライは呼び出し元の責任。

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

- beforeChange/afterChangeは呼び出し元のトランザクション内で実行
- afterReadはトランザクション外（読み取り後の後処理）

## パフォーマンス要件

- フックは同期的に順次実行されるため、各フックは高速に完了すること
- afterReadのPromise処理は並列実行により最適化

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

- フック内でのアクセス制御チェック（triggerAccessControl）
- overrideAccessフラグによるアクセス制御のバイパス
- フック内での入力値の検証

## 備考

- フックは関数型プログラミングのパターンに基づいて設計
- リッチテキストエディタは独自のフックシステムを持つ

---

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

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

### 推奨読解順序

#### Step 1: 各フックのエントリーポイントを理解する

まず、各フックタイプのメインエントリーポイントを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | beforeValidate/index.ts | `packages/payload/src/fields/hooks/beforeValidate/index.ts` | バリデーション前フックのエントリーポイント |
| 1-2 | beforeChange/index.ts | `packages/payload/src/fields/hooks/beforeChange/index.ts` | 変更前フックのエントリーポイント |
| 1-3 | afterChange/index.ts | `packages/payload/src/fields/hooks/afterChange/index.ts` | 変更後フックのエントリーポイント |
| 1-4 | afterRead/index.ts | `packages/payload/src/fields/hooks/afterRead/index.ts` | 読み取り後フックのエントリーポイント |
| 1-5 | beforeDuplicate/index.ts | `packages/payload/src/fields/hooks/beforeDuplicate/index.ts` | 複製前フックのエントリーポイント |

**読解のコツ**: 各index.tsファイルはフック処理の開始点。引数の型定義とtraverseFields呼び出しに注目。

**主要処理フロー（beforeChange/index.ts）**:
1. **34-46行目**: 関数定義と引数の分解
2. **47行目**: データのディープコピー
3. **51-74行目**: traverseFields呼び出し
4. **76-87行目**: バリデーションエラー時のValidationError投げ
5. **89-91行目**: mergeLocaleActionsの実行
6. **93行目**: 処理済みデータの返却

**主要処理フロー（afterRead/index.ts）**:
1. **44-62行目**: 関数定義と引数の分解
2. **67-73行目**: depthの計算とmaxDepthの制限
3. **77-104行目**: traverseFields呼び出し（同期）
4. **112-126行目**: fieldPromisesとpopulationPromisesの並列実行
5. **127行目**: 処理済みドキュメントの返却

#### Step 2: フック実行の詳細を理解する（promiseファイル）

各フックタイプの実際のフック実行ロジックを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | beforeChange/promise.ts | `packages/payload/src/fields/hooks/beforeChange/promise.ts` | beforeChangeの詳細ロジック |
| 2-2 | afterChange/promise.ts | `packages/payload/src/fields/hooks/afterChange/promise.ts` | afterChangeの詳細ロジック |
| 2-3 | afterRead/promise.ts | `packages/payload/src/fields/hooks/afterRead/promise.ts` | afterReadの詳細ロジック |

**主要処理フロー（beforeChange/promise.ts）**:
1. **98-104行目**: フィールドパスの計算（getFieldPaths）
2. **114-124行目**: condition関数による条件分岐
3. **135-162行目**: beforeChangeフックの実行
4. **165-266行目**: バリデーション実行
5. **269-290行目**: ロケールマージアクション追加
6. **293-688行目**: フィールドタイプ別のサブフィールド処理

**主要処理フロー（afterRead/promise.ts）**:
1. **115-121行目**: フィールドパスの計算
2. **131-140行目**: 隠しフィールドの削除
3. **155-214行目**: ローカライズフィールドの値ホイスト
4. **283-348行目**: afterReadフックの実行
5. **374-390行目**: フィールドレベルのアクセス制御
6. **429-1026行目**: フィールドタイプ別のサブフィールド処理

#### Step 3: フィールドツリーのトラバースを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | beforeChange/traverseFields.ts | `packages/payload/src/fields/hooks/beforeChange/traverseFields.ts` | トラバース処理 |

**読解のコツ**: traverseFieldsはフィールド配列を受け取り、各フィールドに対してpromise関数を呼び出す。

#### Step 4: ユーティリティ関数を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | beforeChange/getExistingRowDoc.ts | `packages/payload/src/fields/hooks/beforeChange/getExistingRowDoc.ts` | 既存行ドキュメントの取得 |
| 4-2 | beforeValidate/getFallbackValue.ts | `packages/payload/src/fields/hooks/beforeValidate/getFallbackValue.ts` | フォールバック値の取得 |

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

```
[コレクション操作 (create/update)]
    │
    ├─ beforeValidate
    │      └─ traverseFields
    │             └─ promise (各フィールド)
    │
    ├─ beforeChange
    │      └─ traverseFields
    │             └─ promise (各フィールド)
    │                    ├─ condition評価
    │                    ├─ フック実行
    │                    ├─ バリデーション
    │                    └─ ロケールマージ
    │
    └─ afterChange
           └─ traverseFields
                  └─ promise (各フィールド)

[コレクション操作 (find/findByID)]
    │
    └─ afterRead
           └─ traverseFields
                  └─ promise (各フィールド)
                         ├─ 隠しフィールド処理
                         ├─ ロケール処理
                         ├─ フック実行
                         ├─ アクセス制御
                         └─ リレーションポピュレート
```

### データフロー図

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

入力データ ───▶ beforeValidate ───▶ beforeChange ───▶ DBへ保存
   │                │                    │
   │           サニタイズ            バリデーション
   │           デフォルト値          データ変換
   │           アクセス制御          ロケール処理
   │
   └────────────────────────────────────────────────────▶ afterChange ───▶ 後続処理

DBからの読み込み ───▶ afterRead ───▶ APIレスポンス
                          │
                     隠しフィールド除去
                     ロケールフラット化
                     フック実行
                     アクセス制御
                     リレーションポピュレート
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| index.ts | `packages/payload/src/fields/hooks/beforeValidate/index.ts` | ソース | beforeValidateエントリー |
| promise.ts | `packages/payload/src/fields/hooks/beforeValidate/promise.ts` | ソース | beforeValidate処理 |
| traverseFields.ts | `packages/payload/src/fields/hooks/beforeValidate/traverseFields.ts` | ソース | beforeValidateトラバース |
| getFallbackValue.ts | `packages/payload/src/fields/hooks/beforeValidate/getFallbackValue.ts` | ソース | フォールバック値取得 |
| index.ts | `packages/payload/src/fields/hooks/beforeChange/index.ts` | ソース | beforeChangeエントリー |
| promise.ts | `packages/payload/src/fields/hooks/beforeChange/promise.ts` | ソース | beforeChange処理 |
| traverseFields.ts | `packages/payload/src/fields/hooks/beforeChange/traverseFields.ts` | ソース | beforeChangeトラバース |
| getExistingRowDoc.ts | `packages/payload/src/fields/hooks/beforeChange/getExistingRowDoc.ts` | ソース | 既存行取得 |
| cloneDataFromOriginalDoc.ts | `packages/payload/src/fields/hooks/beforeChange/cloneDataFromOriginalDoc.ts` | ソース | オリジナルデータクローン |
| index.ts | `packages/payload/src/fields/hooks/afterChange/index.ts` | ソース | afterChangeエントリー |
| promise.ts | `packages/payload/src/fields/hooks/afterChange/promise.ts` | ソース | afterChange処理 |
| traverseFields.ts | `packages/payload/src/fields/hooks/afterChange/traverseFields.ts` | ソース | afterChangeトラバース |
| index.ts | `packages/payload/src/fields/hooks/afterRead/index.ts` | ソース | afterReadエントリー |
| promise.ts | `packages/payload/src/fields/hooks/afterRead/promise.ts` | ソース | afterRead処理 |
| traverseFields.ts | `packages/payload/src/fields/hooks/afterRead/traverseFields.ts` | ソース | afterReadトラバース |
| relationshipPopulationPromise.ts | `packages/payload/src/fields/hooks/afterRead/relationshipPopulationPromise.ts` | ソース | リレーションポピュレート |
| virtualFieldPopulationPromise.ts | `packages/payload/src/fields/hooks/afterRead/virtualFieldPopulationPromise.ts` | ソース | 仮想フィールドポピュレート |
| index.ts | `packages/payload/src/fields/hooks/beforeDuplicate/index.ts` | ソース | beforeDuplicateエントリー |
| promise.ts | `packages/payload/src/fields/hooks/beforeDuplicate/promise.ts` | ソース | beforeDuplicate処理 |
| traverseFields.ts | `packages/payload/src/fields/hooks/beforeDuplicate/traverseFields.ts` | ソース | beforeDuplicateトラバース |
