PHPで理解するストリーム処理

## はじめに

先日、大量のCSVデータを扱うAPIを実装していた時のこと…

「Laravelの`response()->stream()`って何してるんだっけ？」  
「そもそもStream処理って何だっけ？」  
「よく分からないものを使うのは気持ち悪い」

という疑問を抱き、PHPのストリーム処理について調べてみました。この記事では、ストリーム処理の基本概念から実際のパフォーマンス比較、Laravelでの活用例までをまとめています。
<br>
スライド版はこちら

https://speakerdeck.com/sonor04ls/phpdeli-jie-surusutorimuchu-li

## PHPストリームとは？

PHPのストリームとは、データを連続的な流れとして扱う仕組みです。ファイル、ネットワーク、データ圧縮など様々な操作を統一されたインターフェースで扱えるようにしています。

ストリームとは「データの流れ」を抽象化したもので、大きなデータや総量が確定していないデータを効率的に処理するために使われます。例えるなら、川の流れのように、データが少しずつ流れてくるイメージです。

よく見かける説明は以下のようなものです。

> データを連続的な流れとして扱う仕組みで、ファイル、ネットワーク、データ圧縮などの操作を共通のインターフェースで扱えるようにしたものです。

しかし、これだけでは具体的に何が良いのかがわかりにくいですよね。そこで非ストリーム処理とストリーム処理を比較してみましょう。

## 非ストリーム的な処理 vs ストリーム的な処理

### 非ストリーム的な処理

非ストリーム的な処理では、データの位置を意識する必要があります（配列の何番目？キーは何？）。また、全データを一度にメモリに読み込む特徴があります。

```php
// 非ストリーム処理
$content = file_get_contents('large_file.txt');  // 全データを一度に読み込み
$lines = explode("\n", $content);                // 行ごとに分割
foreach ($lines as $line) {
    // 各行を処理
    // ここで何かの処理をする
}
```

### ストリーム的な処理

一方、ストリーム的な処理では、先頭から最後まで順番に処理します。データの位置を意識する必要がなく、処理しながら読み込むという特徴があります。

```php
// ストリーム処理
$handle = fopen('large_file.txt', 'r');       // ファイルを開くだけ
while (($line = fgets($handle)) !== false) {  // 1行ずつ読み込み
    // 各行を処理
    // ここで何かの処理をする
}
fclose($handle);                              // 後片付け
```

## ストリーム処理の特徴

ストリーム処理には主に以下の3つの特徴があります。

### 1. メモリ効率

大きなファイルでも全体をメモリに読み込まずに少しずつ処理可能で、メモリ使用量を大幅に削減できます。特に大量データを扱う場合に効果的です。

### 2. 統一されたインターフェース

ファイル、HTTP、FTP、圧縮データなど様々なソースを同じ方法で処理できます。どのようなデータソースでも同じコードパターンで扱えるため、コードの一貫性が保てます。

### 3. リアルタイム処理

データが到着次第処理を開始できるため、全データを受信する前に処理を始められます。特にネットワーク経由のデータ処理などで効果を発揮します。

## ストリーム処理と非ストリーム処理の比較テスト

実際にどれだけ効率が違うのか、テストしてみました。

### テスト方法

1. テスト用に約100MBのテキストファイルを作成
2. テキストファイルの読み込み速度、使用メモリを比較
   - 非ストリーム処理: `file_get_contents()`
   - ストリーム処理: `fopen()` + `fgets()`
3. 処理時間とメモリ使用量を計測

### 比較結果

```
非ストリーム処理（file_get_contents）
--------------------------------------------
処理時間: 0.2070 秒
メモリ使用量: 231.75 MB
処理した行数: 209,301

ストリーム処理（fgets）
--------------------------------------------
処理時間: 0.0445 秒
メモリ使用量: 32 B  // わずか32バイト！
処理した行数: 209,300

============================================

メモリ使用量の差: 231.75 MB (非ストリームは7,594,135.0倍)
処理時間の差: 0.1625 秒 (非ストリームは4.7倍遅い)
```

結果から分かるように、ストリーム処理は非ストリーム処理と比較して、メモリ使用量が劇的に少なく（約760万倍！）、処理速度も約4.7倍速いという結果になりました。

## Laravelでのストリーム処理

Laravelでは`response()->stream()`メソッドを使って簡単にストリーム処理を実装できます。例えば大量のデータをCSVとしてダウンロードさせる場合は以下のように実装できます。

```php
// CSV出力のストリームレスポンス
return response()->stream(
    function () {
        echo "ID,名前,Email\n"; // ヘッダー
        // 1000件ずつ処理
        User::chunk(1000, function ($users) {
            foreach ($users as $user) {
                echo "{$user->id},{$user->name},{$user->email}\n";
                flush();
            }
        });
    },
    200,
    ['Content-Type' => 'text/csv']
);
```

このコードでは、`User`テーブルから1000件ずつデータを取得し、CSVフォーマットで出力しています。`chunk`メソッドと組み合わせることで、大量のデータでもメモリを効率的に使用できます。

## APIデータ受信での活用例

REST APIなどでJSONデータを受け取る場合にもストリームが活用されています。

```php
// リクエストボディからJSONデータを取得
$jsonData = file_get_contents('php://input');

// JSONをデコード
$data = json_decode($jsonData, true);

// 処理
if ($data) {
    // JSONデータを使った処理
    echo json_encode(['status' => 'success', 'data' => $data]);
}
```

ここで使われている`php://input`はPHPのストリームラッパーの一つで、生のリクエストボディにアクセスするために使用されます。

## CSVファイル処理の例

大きなCSVファイルを処理する場合も、ストリーム処理は非常に有効です。

```php
$handle = fopen('data.csv', 'r');
$headers = fgetcsv($handle); // ヘッダー行を取得

while (($data = fgetcsv($handle)) !== FALSE) {
    // 各行をヘッダーと組み合わせて連想配列に
    $row = array_combine($headers, $data);
    
    // データ処理...
    processRow($row);
}

fclose($handle);
```

このコードでは、CSVファイルを1行ずつ読み込み、各行をヘッダーと組み合わせて連想配列にしています。これにより、巨大なCSVファイルでもメモリ効率よく処理できます。

## まとめ

- ストリーム処理はデータを少しずつ処理する仕組み
- メモリ効率が高く、大量データ処理に適している
  - 100MBのファイルでもわずか32バイトのメモリで処理可能！
- 処理速度も非ストリーム処理より約5倍速い
- PHPにはさまざまなストリームラッパーが用意されている
- APIやファイル処理で活用することでパフォーマンス向上が期待できる

## おまけ：主なPHPストリームラッパー

PHPには以下のような様々なストリームラッパーが用意されています。

| ラッパー | 説明 |
|--------|------|
| `file://` | ローカルファイルシステム（デフォルト） |
| `http://`, `https://` | HTTPSプロトコルリモートアクセス |
| `php://stdin` | 標準入力 |
| `php://stdout` | 標準出力 |
| `php://input` | リクエストボディのローデータ |
| `php://memory` | メモリ内一時ストリーム |
| `php://temp` | メモリ/一時ファイル自動切替 |

詳細は[PHP公式マニュアル](https://www.php.net/manual/ja/wrappers.php)をご覧ください。

## 参考資料

- [PHP公式マニュアル - ストリーム](https://www.php.net/manual/ja/intro.stream.php)
- [PHP公式マニュアル - ストリームラッパー](https://www.php.net/manual/ja/wrappers.php)
- [Laravel - ストリームダウンロードレスポンス](https://laravel.com/docs/responses#streamed-downloads)