# 帳票設計書 27-バイナリファイルレスポンス

## 概要

本ドキュメントは、Symfony HttpFoundationコンポーネントにおける `BinaryFileResponse` クラスの帳票設計書である。ファイルをHTTPレスポンスとして配信し、Content-Disposition（添付/インライン）ヘッダの自動設定、Range リクエスト対応、X-Sendfile対応等の機能について、出力仕様・処理フロー・データ構造を定義する。

### 本帳票の処理概要

`BinaryFileResponse` は、サーバー上のファイルをHTTPレスポンスとしてクライアントに配信するためのクラスである。`Response` クラスを継承し、ファイルダウンロード・インライン表示に必要なHTTPヘッダの自動管理、部分コンテンツ配信（Range リクエスト）、X-Sendfile/X-Accel-Redirect によるWebサーバー委譲、ETag/Last-Modified によるキャッシュ制御を提供する。

**業務上の目的・背景**：Webアプリケーションにおいて、PDF・画像・CSV等のファイルをダウンロードまたはインライン表示させる際、適切なHTTPヘッダ（Content-Type, Content-Disposition, Content-Length, Accept-Ranges等）を設定し、効率的なファイル配信を行う必要がある。BinaryFileResponseは、これらのHTTPプロトコル準拠のファイル配信機能をカプセル化し、開発者が簡単にファイルレスポンスを実装できるようにする。

**帳票の利用シーン**：ファイルダウンロード機能、PDF表示、画像配信、CSV/Excelエクスポート、静的ファイル配信等で使用される。

**主要な出力内容**：
1. ファイル本体のバイナリストリーム
2. Content-Type（MIMEタイプ自動検出）
3. Content-Disposition（attachment/inline + ファイル名）
4. Content-Length, Accept-Ranges, Content-Range ヘッダ
5. ETag, Last-Modified キャッシュ関連ヘッダ

**帳票の出力タイミング**：コントローラーからBinaryFileResponseが返された際、Symfony Kernelがレスポンスをクライアントに送信する時点で出力される。

**帳票の利用者**：Webブラウザ、ファイルダウンロードクライアント

## 帳票種別

ファイル配信レスポンス（バイナリストリーム出力）

## 利用画面

| 画面No | 画面名 | URL/ルーティング | 出力操作 |
|--------|--------|-----------------|---------|
| - | ファイルダウンロード | アプリケーション定義のルート | ダウンロードボタン/リンク |
| - | インライン表示 | アプリケーション定義のルート | ブラウザ表示 |

## 出力形式

### 基本仕様

| 項目 | 内容 |
|-----|------|
| ファイル形式 | 任意（ファイルのMIMEタイプに依存） |
| 用紙サイズ | N/A |
| 向き | N/A |
| ファイル名 | ファイルのbasename（Content-Dispositionで指定） |
| 出力方法 | HTTPレスポンスストリーム |
| 文字コード | ファイル内容に依存 |

### PDF固有設定

該当なし（任意のファイル形式に対応）

### Excel固有設定

該当なし

## 帳票レイアウト

### レイアウト概要

HTTPレスポンスとしてのファイルストリーム出力。ヘッダー部にHTTPレスポンスヘッダー、ボディ部にファイルのバイナリコンテンツが配置される。

```
┌─────────────────────────────────────┐
│  HTTP/1.1 200 OK                     │
│  Content-Type: application/pdf       │
│  Content-Disposition: attachment;    │
│    filename="report.pdf"             │
│  Content-Length: 12345               │
│  Accept-Ranges: bytes                │
│  ETag: "xxh128hash"                  │
│  Last-Modified: Mon, 01 Jan 2026 ... │
├─────────────────────────────────────┤
│  [ファイルバイナリコンテンツ]          │
└─────────────────────────────────────┘
```

### ヘッダー部

| No | 項目名 | 説明 | データ取得元 | 表示形式 |
|----|-------|------|-------------|---------|
| 1 | Content-Type | ファイルのMIMEタイプ | `File::getMimeType()` | `application/pdf` 等（フォールバック: `application/octet-stream`） |
| 2 | Content-Disposition | 添付/インライン指定 | `setContentDisposition()` | `attachment; filename="..."` または `inline; filename="..."` |
| 3 | Content-Length | ファイルサイズ（バイト） | `File::getSize()` / `fstat()['size']` | 数値 |
| 4 | Accept-Ranges | Range リクエスト対応 | リクエストのメソッド安全性 | `bytes`（安全メソッド時）/ `none` |
| 5 | ETag | ファイルハッシュ | `hash_file('xxh128', ...)` (Base64) | ダブルクォート囲み |
| 6 | Last-Modified | ファイル最終更新日時 | `File::getMTime()` | RFC 7231形式 |
| 7 | Content-Range | 部分コンテンツ範囲（206時） | Rangeリクエスト解析 | `bytes {start}-{end}/{total}` |

### 明細部

| No | 項目名 | 説明 | データ取得元 | 表示形式 | 列幅 |
|----|-------|------|-------------|---------|-----|
| 1 | ファイルコンテンツ | バイナリデータストリーム | `SplFileObject::fread()` | チャンク送信（デフォルト16KB） | 可変 |

### フッター部

該当なし

## 出力条件

### 抽出条件

| 条件名 | 説明 | 必須 |
|-------|------|-----|
| file | 配信対象ファイル（SplFileInfo/string） | Yes |
| status | HTTPステータスコード | No（デフォルト: 200） |
| public | キャッシュ制御（Public） | No（デフォルト: true） |
| contentDisposition | 配信方式（attachment/inline） | No |
| autoEtag | ETag自動設定 | No（デフォルト: false） |
| autoLastModified | Last-Modified自動設定 | No（デフォルト: true） |

### ソート順

該当なし（単一ファイル出力）

### 改ページ条件

該当なし

## データベース参照仕様

### 参照テーブル一覧

該当なし（ファイルシステムからの読み取り）

### テーブル別参照項目詳細

該当なし

## 計算仕様

### 計算項目一覧

| 項目名 | 計算式 | 端数処理 | 備考 |
|-------|-------|---------|------|
| ETag | `base64_encode(hash_file('xxh128', $path, true))` | なし | xxh128ハッシュのBase64エンコード |
| チャンク読み取りサイズ | `min($this->chunkSize, $remaining)` | なし | デフォルト16KB (16*1024) |
| Range応答Content-Length | `$end - $start + 1` | なし | 部分コンテンツのサイズ |
| ファイル名フォールバック | 非ASCII文字を`_`に置換 | なし | RFC 6266準拠 |

## 処理フロー

### 出力フロー

```mermaid
flowchart TD
    A[BinaryFileResponse生成] --> B[setFile: ファイル設定]
    B --> C{autoEtag?}
    C -->|Yes| D[setAutoEtag: xxh128ハッシュ]
    C -->|No| E[skip]
    D --> F{autoLastModified?}
    E --> F
    F -->|Yes| G[setAutoLastModified: mtime]
    F -->|No| H[skip]
    G --> I{contentDisposition?}
    H --> I
    I -->|指定あり| J[setContentDisposition]
    I -->|なし| K[skip]
    J --> L[prepare: リクエスト解析]
    K --> L
    L --> M[Content-Type設定]
    M --> N[Content-Length設定]
    N --> O{X-Sendfile対応?}
    O -->|Yes| P[X-Sendfileヘッダー設定]
    O -->|No| Q{Rangeリクエスト?}
    P --> R[maxlen=0]
    Q -->|Yes| S[Range解析 → 206/416]
    Q -->|No| T[通常応答]
    S --> U[sendContent]
    T --> U
    R --> U
    U --> V[チャンク読み取り+書き込みループ]
    V --> W{deleteFileAfterSend?}
    W -->|Yes| X[ファイル削除]
    W -->|No| Y[完了]
    X --> Y
```

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 表示メッセージ | 対処方法 |
|----------|---------|--------------|---------|
| ファイル読み取り不可 | ファイルが存在しない/読み取り権限なし | `File must be readable.` (FileException) | ファイルパスとパーミッションを確認 |
| 不正チャンクサイズ | chunkSize < 1 | `The chunk size of a BinaryFileResponse cannot be less than 1.` (InvalidArgumentException) | 1以上の値を指定 |
| コンテンツ設定エラー | setContent()にnull以外を指定 | `The content cannot be set on a BinaryFileResponse instance.` (LogicException) | BinaryFileResponseにはsetContent不可 |
| X-Accel-Mappingなし | X-Sendfile-TypeがX-Accel-RedirectだがMappingなし | `The "X-Accel-Mapping" header must be set...` (LogicException) | X-Accel-Mappingヘッダーを設定 |
| Range不正 | Rangeヘッダーの範囲指定が無効 | 416 Range Not Satisfiable | 正しいRange形式を指定 |

## パフォーマンス要件

| 項目 | 内容 |
|-----|------|
| 想定データ件数 | 1ファイル（サイズ無制限） |
| 目標出力時間 | ファイルサイズ依存（ストリーム出力） |
| 同時出力数上限 | Webサーバー設定依存 |

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

- `deleteFileAfterSend` オプションで一時ファイルの自動削除が可能
- X-Sendfileヘッダーの信頼は `trustXSendfileTypeHeader()` で明示的に有効化が必要
- ファイルパスにディレクトリトラバーサル等の脆弱性がないことを呼び出し側で確認すること
- Content-Dispositionのファイル名はRFC 6266に準拠したエスケープ処理が行われる
- 一時ファイル（SplTempFileObject）対応によりメモリ上のデータ配信も可能

## 備考

- SplTempFileObject対応により、ファイルシステムを経由しないメモリ上のファイル配信が可能
- `ignore_user_abort(true)` により、クライアント切断後もファイル送信を完了する
- connection_aborted()チェックにより、クライアント切断時に送信を中断
- If-Rangeヘッダーによる条件付きRange対応
- `getContent()` は常にfalseを返す（バイナリファイルのため）

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | Response.php | `src/Symfony/Component/HttpFoundation/Response.php` | 親クラス。HTTPレスポンスの基本構造 |
| 1-2 | File.php | `src/Symfony/Component/HttpFoundation/File/File.php` | ファイルラッパー。getMimeType(), getSize()等 |
| 1-3 | HeaderUtils.php | `src/Symfony/Component/HttpFoundation/HeaderUtils.php` | ヘッダー解析ユーティリティ |

**読解のコツ**: BinaryFileResponseはResponseを継承しつつ、ファイル固有のヘッダー管理とストリーム出力を追加する。

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | BinaryFileResponse.php | `src/Symfony/Component/HttpFoundation/BinaryFileResponse.php` | コンストラクタ（46-55行目）、setFile()（64-96行目） |

**主要処理フロー**:
1. **46行目**: コンストラクタ - 親Response生成、setFile()呼び出し、publicフラグ設定
2. **64-96行目**: `setFile()` - Fileオブジェクト生成、読み取り可能チェック、autoEtag/autoLastModified/contentDisposition設定

#### Step 3: リクエスト準備とコンテンツ送信を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | BinaryFileResponse.php | `src/Symfony/Component/HttpFoundation/BinaryFileResponse.php` | prepare()（181-289行目）、sendContent()（304-357行目） |

**主要処理フロー**:
- **181-289行目**: `prepare()` - Content-Type設定、Content-Length設定、X-Sendfile処理、Range処理
- **218-248行目**: X-Sendfile処理 - X-Accel-Redirect(nginx)とX-Sendfile(Apache)の分岐
- **249-282行目**: Range処理 - Rangeヘッダー解析、206/416レスポンス生成
- **304-357行目**: `sendContent()` - チャンクループでファイルを読み取り出力

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

```
BinaryFileResponse::__construct($file, $status, $headers, ...)
    |
    +-- Response::__construct()
    +-- setFile($file, ...)
    |       +-- new File($path)
    |       +-- setAutoEtag() --> hash_file('xxh128')
    |       +-- setAutoLastModified() --> File::getMTime()
    |       +-- setContentDisposition()
    |               +-- ResponseHeaderBag::makeDisposition()
    |
    +-- prepare(Request)
    |       +-- File::getMimeType() --> Content-Type
    |       +-- File::getSize() --> Content-Length
    |       +-- [X-Sendfile] headers->set(X-Sendfile-Type, path)
    |       +-- [Range] offset/maxlen設定 --> 206/416
    |
    +-- sendContent()
            +-- SplFileObject::fseek(offset)
            +-- loop: fread(chunkSize) --> fwrite(output)
            +-- [deleteFileAfterSend] unlink()
```

### データフロー図

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

ファイルパス -----> new File()
                      |
                      v
File -----> prepare(Request)
                |
                +-- getMimeType() -------> Content-Type
                +-- getSize() -----------> Content-Length
                +-- hash_file() ---------> ETag
                +-- getMTime() ----------> Last-Modified
                +-- Range解析 -----------> offset, maxlen
                |
                v
            sendContent()
                |
                +-- fseek(offset)
                +-- fread(chunkSize) ----> fwrite(php://output)
                |       [チャンクループ]
                +-- unlink() [条件付き]
                |
                v
            HTTPレスポンスストリーム -----> クライアント
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| BinaryFileResponse.php | `src/Symfony/Component/HttpFoundation/BinaryFileResponse.php` | ソース | メインクラス（397行） |
| Response.php | `src/Symfony/Component/HttpFoundation/Response.php` | ソース | 親クラス |
| File.php | `src/Symfony/Component/HttpFoundation/File/File.php` | ソース | ファイルラッパー |
| Request.php | `src/Symfony/Component/HttpFoundation/Request.php` | ソース | リクエスト（Range等） |
| ResponseHeaderBag.php | `src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php` | ソース | Content-Disposition生成 |
| HeaderUtils.php | `src/Symfony/Component/HttpFoundation/HeaderUtils.php` | ソース | ヘッダー解析ユーティリティ |
| FileException.php | `src/Symfony/Component/HttpFoundation/File/Exception/FileException.php` | ソース | ファイル例外 |
