[PHP]stream_get_contents完全ガイド|ストリームの残りデータを一括取得してファイル・ネットワーク・メモリ処理を効率化する

PHP

はじめに

PHPでストリームからデータを読み取る方法はいくつかありますが、「現在位置から末尾まで全部読みたい」「指定バイト数だけ読みたい」「特定オフセットから読み始めたい」という場面で最もシンプルに使えるのが stream_get_contents() です。

fread() のループや file_get_contents() との違いを理解しながら、ファイル・メモリストリーム・ネットワーク通信など幅広い場面での活用方法を解説します。


関数の基本情報

項目内容
関数名stream_get_contents()
対応バージョンPHP 5.0.0 以降
返り値string(成功時)/ false(失敗時)
カテゴリストリーム関数

構文

stream_get_contents(resource $stream, int $length = -1, int $offset = -1): string|false

パラメータ

パラメータデフォルト説明
$streamresource必須読み取り対象のストリームリソース
$lengthint-1読み取る最大バイト数。-1 で末尾まで全読み取り
$offsetint-1読み取り開始オフセット(バイト)。-1 で現在位置から読む

返り値

  • string:読み取ったデータ
  • false:エラー発生時(無効なストリームなど)

注意: $offset を指定すると関数内部でシーク処理が走ります。シーク不可能なストリーム(ネットワーク、php://stdin など)では $offset は機能しません。


基本的な使い方

<?php

// ファイル全体を読む
$fp = fopen('/tmp/sample.txt', 'r');
$content = stream_get_contents($fp);
fclose($fp);
echo $content;

// 先頭100バイトをスキップして読む
$fp = fopen('/tmp/sample.txt', 'r');
$content = stream_get_contents($fp, length: -1, offset: 100);
fclose($fp);
echo $content;

// 先頭から最大200バイトだけ読む
$fp = fopen('/tmp/sample.txt', 'r');
$content = stream_get_contents($fp, length: 200);
fclose($fp);
echo $content;

file_get_contents() / fread() との違い

比較項目stream_get_contents()file_get_contents()fread()
入力開いたストリームリソースファイルパス / URL 文字列開いたストリームリソース
オフセット指定◯($offset 引数)◯($offset 引数)✗(自前で fseek()
長さ指定◯($length 引数)◯($length 引数)◯($length 引数・必須)
ストリームの途中から読む✗(常に先頭から)◯(fseek() 併用)
末尾まで一括読み取り◯($length = -1✗(サイズ指定が必要)
すでに開いたストリームを使える

使い分けの指針:

  • パスから一発で読むだけなら → file_get_contents()
  • 開いているストリームの現在位置から末尾まで読む → stream_get_contents()
  • 開いているストリームをループで少しずつ読む → fread()

実践的なクラスベースの活用例


例1:メモリストリームを使ったバッファクラス(MemoryBuffer)

php://memory をバッファとして使い、書き込んだ内容をいつでも全量取得できるクラスです。テスト用出力キャプチャや中間データの蓄積に役立ちます。

<?php

class MemoryBuffer
{
    private $fp;

    public function __construct()
    {
        $this->fp = fopen('php://memory', 'w+');
    }

    public function write(string $data): void
    {
        fwrite($this->fp, $data);
    }

    /** バッファ全体を返す(ポインタ位置は変わらない) */
    public function getAll(): string
    {
        return stream_get_contents($this->fp, offset: 0);
    }

    /** 現在位置から末尾まで返す */
    public function readRest(): string
    {
        return stream_get_contents($this->fp);
    }

    /** 先頭 $n バイトだけ返す */
    public function peek(int $n): string
    {
        return stream_get_contents($this->fp, length: $n, offset: 0);
    }

    public function size(): int
    {
        return strlen(stream_get_contents($this->fp, offset: 0));
    }

    public function clear(): void
    {
        ftruncate($this->fp, 0);
        rewind($this->fp);
    }

    public function __destruct()
    {
        fclose($this->fp);
    }
}

// 使用例
$buf = new MemoryBuffer();
$buf->write("Hello, ");
$buf->write("PHP Stream!");

echo $buf->getAll() . PHP_EOL;   // → Hello, PHP Stream!
echo $buf->peek(5) . PHP_EOL;    // → Hello
echo $buf->size() . PHP_EOL;     // → 18

例2:ファイル部分読み取りクラス(FileChunkReader)

大きなログファイルやデータファイルから、任意のオフセットと長さで部分読み取りを行うクラスです。ページング処理やバイナリ解析に活用できます。

<?php

class FileChunkReader
{
    private string $filePath;
    private int    $fileSize;

    public function __construct(string $filePath)
    {
        if (!is_file($filePath)) {
            throw new \RuntimeException("ファイルが見つかりません: {$filePath}");
        }
        $this->filePath = $filePath;
        $this->fileSize = filesize($filePath);
    }

    /**
     * オフセットと長さを指定して読み取る
     */
    public function read(int $offset = 0, int $length = -1): string
    {
        $fp = fopen($this->filePath, 'rb');
        $content = stream_get_contents($fp, length: $length, offset: $offset);
        fclose($fp);
        return $content !== false ? $content : '';
    }

    /**
     * ファイルを $chunkSize バイトずつページング読み取り
     *
     * @return \Generator<int, string>
     */
    public function paginate(int $chunkSize = 4096): \Generator
    {
        $fp     = fopen($this->filePath, 'rb');
        $offset = 0;

        while ($offset < $this->fileSize) {
            $chunk = stream_get_contents($fp, length: $chunkSize, offset: $offset);
            if ($chunk === false || $chunk === '') break;
            yield $offset => $chunk;
            $offset += strlen($chunk);
        }

        fclose($fp);
    }

    /**
     * 末尾 $n バイトを取得(ログのテール読み取りなど)
     */
    public function tail(int $n): string
    {
        $offset = max(0, $this->fileSize - $n);
        return $this->read($offset);
    }

    public function size(): int
    {
        return $this->fileSize;
    }
}

// 使用例
$reader = new FileChunkReader('/var/log/syslog');

// 末尾512バイトを取得
echo $reader->tail(512) . PHP_EOL;

// 先頭1024バイトをスキップして次の512バイトを取得
echo $reader->read(offset: 1024, length: 512) . PHP_EOL;

// 4096バイトずつページング処理
foreach ($reader->paginate(4096) as $offset => $chunk) {
    echo "offset={$offset} size=" . strlen($chunk) . PHP_EOL;
}

例3:HTTPレスポンスパーサー(HttpResponseParser)

ソケットやストリームから受け取った生の HTTP レスポンスをヘッダーとボディに分割し、ステータスコードやヘッダー値を取り出すパーサーです。

<?php

class HttpResponseParser
{
    private string $raw;
    private string $headerSection;
    private string $body;
    private int    $statusCode;
    private array  $headers = [];

    public function __construct($stream)
    {
        // ストリームから全データを一括取得
        $this->raw = stream_get_contents($stream);
        $this->parse();
    }

    private function parse(): void
    {
        // ヘッダーとボディを \r\n\r\n で分割
        $separatorPos = strpos($this->raw, "\r\n\r\n");
        if ($separatorPos === false) {
            $this->headerSection = $this->raw;
            $this->body          = '';
        } else {
            $this->headerSection = substr($this->raw, 0, $separatorPos);
            $this->body          = substr($this->raw, $separatorPos + 4);
        }

        $lines = explode("\r\n", $this->headerSection);

        // ステータス行をパース
        if (preg_match('/^HTTP\/[\d.]+\s+(\d+)/', array_shift($lines), $m)) {
            $this->statusCode = (int)$m[1];
        }

        // ヘッダー行をパース
        foreach ($lines as $line) {
            if (str_contains($line, ':')) {
                [$name, $value] = explode(':', $line, 2);
                $this->headers[strtolower(trim($name))] = trim($value);
            }
        }
    }

    public function getStatusCode(): int    { return $this->statusCode; }
    public function getBody(): string       { return $this->body; }
    public function getHeader(string $name): ?string
    {
        return $this->headers[strtolower($name)] ?? null;
    }
    public function isSuccess(): bool       { return $this->statusCode >= 200 && $this->statusCode < 300; }
}

// 使用例(モックストリームで確認)
$rawResponse = "HTTP/1.1 200 OK\r\n"
             . "Content-Type: application/json\r\n"
             . "Content-Length: 27\r\n"
             . "\r\n"
             . '{"status":"ok","code":200}';

$fp = fopen('php://memory', 'r+');
fwrite($fp, $rawResponse);
rewind($fp);

$parser = new HttpResponseParser($fp);
fclose($fp);

echo "ステータス: "  . $parser->getStatusCode()           . PHP_EOL; // 200
echo "Content-Type: " . $parser->getHeader('content-type') . PHP_EOL; // application/json
echo "ボディ: "       . $parser->getBody()                 . PHP_EOL; // {"status":"ok","code":200}
echo "成功: "         . ($parser->isSuccess() ? 'YES' : 'NO') . PHP_EOL; // YES

例4:ストリーム差分チェッカー(StreamDiffChecker)

2つのストリームの内容を比較し、差分(追加・削除・変更行)を検出するクラスです。設定ファイルのバージョン比較やデータ変換前後の検証に使えます。

<?php

class StreamDiffChecker
{
    /**
     * 2つのストリームを読み取って行単位で差分を返す
     *
     * @return array{added: string[], removed: string[], unchanged: int}
     */
    public function diff($streamA, $streamB): array
    {
        $linesA = explode("\n", rtrim(stream_get_contents($streamA, offset: 0)));
        $linesB = explode("\n", rtrim(stream_get_contents($streamB, offset: 0)));

        $removed   = array_diff($linesA, $linesB);
        $added     = array_diff($linesB, $linesA);
        $unchanged = count(array_intersect($linesA, $linesB));

        return [
            'added'     => array_values($added),
            'removed'   => array_values($removed),
            'unchanged' => $unchanged,
        ];
    }

    /**
     * 差分を読みやすい形式で出力する
     */
    public function render(array $diff): string
    {
        $lines = [];
        foreach ($diff['removed'] as $line) {
            $lines[] = "- {$line}";
        }
        foreach ($diff['added'] as $line) {
            $lines[] = "+ {$line}";
        }
        $lines[] = "  ({$diff['unchanged']} lines unchanged)";
        return implode("\n", $lines);
    }
}

// 使用例
$oldConfig = "debug=false\ndb_host=localhost\ndb_port=3306\napp_env=production";
$newConfig = "debug=true\ndb_host=db.example.com\ndb_port=3306\napp_env=staging\nlog_level=verbose";

$fpOld = fopen('php://memory', 'r+');
fwrite($fpOld, $oldConfig);

$fpNew = fopen('php://memory', 'r+');
fwrite($fpNew, $newConfig);

$checker = new StreamDiffChecker();
$diff    = $checker->diff($fpOld, $fpNew);

echo $checker->render($diff) . PHP_EOL;
// 出力:
// - debug=false
// - db_host=localhost
// - app_env=production
// + debug=true
// + db_host=db.example.com
// + app_env=staging
// + log_level=verbose
//   (1 lines unchanged)

fclose($fpOld);
fclose($fpNew);

例5:ストリーム暗号化・復号クラス(StreamCipher)

ストリームの内容を stream_get_contents() で一括取得し、AES-256-CBC で暗号化・復号するクラスです。

<?php

class StreamCipher
{
    private const CIPHER    = 'AES-256-CBC';
    private const IV_LENGTH = 16;

    public function __construct(private readonly string $key)
    {
        if (strlen($key) !== 32) {
            throw new \InvalidArgumentException('鍵は32バイト(256ビット)である必要があります');
        }
    }

    /**
     * ストリームの内容を暗号化して返す
     */
    public function encrypt($stream): string
    {
        $plaintext = stream_get_contents($stream, offset: 0);
        $iv        = random_bytes(self::IV_LENGTH);
        $encrypted = openssl_encrypt($plaintext, self::CIPHER, $this->key, OPENSSL_RAW_DATA, $iv);

        // IV を先頭に付与して返す
        return base64_encode($iv . $encrypted);
    }

    /**
     * 暗号化された文字列を復号してストリームに書き込む
     */
    public function decrypt(string $ciphertext, $outputStream): void
    {
        $decoded   = base64_decode($ciphertext);
        $iv        = substr($decoded, 0, self::IV_LENGTH);
        $encrypted = substr($decoded, self::IV_LENGTH);
        $plaintext = openssl_decrypt($encrypted, self::CIPHER, $this->key, OPENSSL_RAW_DATA, $iv);

        fwrite($outputStream, $plaintext);
    }
}

// 使用例
$key    = str_repeat('k', 32); // 実際は安全な鍵生成を使うこと
$cipher = new StreamCipher($key);

// 暗号化
$fpSrc = fopen('php://memory', 'r+');
fwrite($fpSrc, '機密データ: ユーザーID=12345, 残高=¥500,000');

$encrypted = $cipher->encrypt($fpSrc);
fclose($fpSrc);

echo "暗号化: " . substr($encrypted, 0, 40) . "..." . PHP_EOL;

// 復号
$fpDst = fopen('php://memory', 'w+');
$cipher->decrypt($encrypted, $fpDst);

echo "復号: " . stream_get_contents($fpDst, offset: 0) . PHP_EOL;
// 出力: 復号: 機密データ: ユーザーID=12345, 残高=¥500,000
fclose($fpDst);

例6:マルチストリームアグリゲーター(StreamAggregator)

複数のストリームを結合して1つの文字列として返すクラスです。ログファイルのマージやテンプレートパーツの合成に活用できます。

<?php

class StreamAggregator
{
    /** @var array{stream: resource, offset: int, length: int}[] */
    private array $sources = [];

    /**
     * ストリームを追加する
     *
     * @param resource $stream
     * @param int      $offset  読み取り開始バイト(-1 = 現在位置)
     * @param int      $length  読み取りバイト数(-1 = 末尾まで)
     */
    public function add($stream, int $offset = -1, int $length = -1): static
    {
        $this->sources[] = ['stream' => $stream, 'offset' => $offset, 'length' => $length];
        return $this;
    }

    /**
     * 全ストリームの内容を $separator で結合して返す
     */
    public function merge(string $separator = ''): string
    {
        $parts = [];
        foreach ($this->sources as $src) {
            $content = stream_get_contents($src['stream'], $src['length'], $src['offset']);
            if ($content !== false) {
                $parts[] = $content;
            }
        }
        return implode($separator, $parts);
    }

    /**
     * 合計バイト数を返す
     */
    public function totalSize(): int
    {
        return strlen($this->merge());
    }
}

// 使用例:HTMLテンプレートのパーツ合成
$header = fopen('php://memory', 'r+');
fwrite($header, "<header><h1>My Site</h1></header>\n");

$body = fopen('php://memory', 'r+');
fwrite($body, "<main><p>コンテンツ本文</p></main>\n");

$footer = fopen('php://memory', 'r+');
fwrite($footer, "<footer>© 2025 My Site</footer>\n");

$aggregator = new StreamAggregator();
$html = $aggregator
    ->add($header, offset: 0)
    ->add($body,   offset: 0)
    ->add($footer, offset: 0)
    ->merge();

echo $html;
// 出力:
// <header><h1>My Site</h1></header>
// <main><p>コンテンツ本文</p></main>
// <footer>© 2025 My Site</footer>

fclose($header);
fclose($body);
fclose($footer);

例7:ストリームスナップショット管理クラス(StreamSnapshot)

ストリームの特定位置のスナップショット(内容の文字列コピー)を複数保存し、任意の時点の状態に戻したり差分を確認したりするクラスです。

<?php

class StreamSnapshot
{
    /** @var array{label: string, offset: int, content: string}[] */
    private array $snapshots = [];
    private $stream;

    public function __construct($stream)
    {
        $this->stream = $stream;
    }

    /**
     * 現在のストリーム全体をスナップショットとして保存
     */
    public function take(string $label): void
    {
        $pos     = ftell($this->stream);
        $content = stream_get_contents($this->stream, offset: 0);

        $this->snapshots[] = [
            'label'   => $label,
            'offset'  => $pos,
            'content' => $content,
        ];
    }

    /**
     * ラベルで指定したスナップショットの内容を返す
     */
    public function get(string $label): ?string
    {
        foreach (array_reverse($this->snapshots) as $snap) {
            if ($snap['label'] === $label) {
                return $snap['content'];
            }
        }
        return null;
    }

    /**
     * ラベルで指定したスナップショット時点の内容にストリームを戻す
     */
    public function restore(string $label): bool
    {
        $content = $this->get($label);
        if ($content === null) return false;

        ftruncate($this->stream, 0);
        rewind($this->stream);
        fwrite($this->stream, $content);
        rewind($this->stream);
        return true;
    }

    /**
     * 2つのスナップショットの差分バイト数を返す
     */
    public function diff(string $labelA, string $labelB): int
    {
        $a = $this->get($labelA) ?? '';
        $b = $this->get($labelB) ?? '';
        return abs(strlen($b) - strlen($a));
    }

    public function list(): array
    {
        return array_column($this->snapshots, 'label');
    }
}

// 使用例
$fp = fopen('php://memory', 'w+');
fwrite($fp, "初期データ\n");

$snap = new StreamSnapshot($fp);
$snap->take('initial');

fwrite($fp, "追記データA\n");
$snap->take('after_A');

fwrite($fp, "追記データB\n");
$snap->take('after_B');

echo "スナップショット一覧: " . implode(', ', $snap->list()) . PHP_EOL;
// → initial, after_A, after_B

echo "after_A の内容:\n" . $snap->get('after_A') . PHP_EOL;
// → 初期データ\n追記データA\n

echo "A→Bの増加バイト数: " . $snap->diff('after_A', 'after_B') . PHP_EOL;
// → 追記データB\n のバイト数

// 'initial' 時点に戻す
$snap->restore('initial');
echo "復元後: " . stream_get_contents($fp, offset: 0) . PHP_EOL;
// → 初期データ\n

fclose($fp);

関連する関数との比較

関数主な用途
stream_get_contents()開いたストリームの現在位置〜末尾(またはオフセット指定)を一括取得
file_get_contents()ファイルパス / URL から一括取得(ストリームコンテキスト利用可)
fread()開いたストリームから指定バイト数を読む(ループ処理向き)
fgets()開いたストリームから1行を読む
stream_copy_to_stream()ストリームの内容を別ストリームへコピー
ob_get_contents()出力バッファの内容を文字列として取得

注意点とベストプラクティス

1. $offset 指定時はシーク可否を確認する

ネットワークストリームや php://stdin はシーク不可のため、$offset を指定しても無視されます。シーク可能かどうかは stream_get_meta_data()seekable キーで確認できます。

$meta = stream_get_meta_data($fp);
if ($meta['seekable']) {
    $content = stream_get_contents($fp, offset: 100);
} else {
    // シーク不可:現在位置から読むしかない
    $content = stream_get_contents($fp);
}

2. 大容量ストリームは全読みに注意

stream_get_contents() はデフォルトで末尾まで全て読み込みます。数GB のファイルに対してそのまま使うとメモリを圧迫します。大容量ファイルは $length で分割読み取りするか、fread() ループを使いましょう。

3. ポインタ位置に注意する

$offset = -1(デフォルト)の場合、現在のポインタ位置から読み始めます。すでに fread()fgets() で途中まで読んでいる場合、残りしか返りません。先頭から全量を取得したいときは $offset: 0 を明示するか、事前に rewind() を呼びます。

// 方法1: offset を明示
$content = stream_get_contents($fp, offset: 0);

// 方法2: rewind してから読む
rewind($fp);
$content = stream_get_contents($fp);

4. 戻り値の false チェック

無効なストリームや読み取りエラー時に false を返します。空ファイル・空ストリームの場合は空文字列 "" を返すため、false との厳密比較が重要です。

$content = stream_get_contents($fp);
if ($content === false) {
    throw new \RuntimeException('ストリームの読み取りに失敗しました');
}

まとめ

ポイント内容
基本動作開いたストリームの現在位置〜末尾を文字列として一括取得
$length読み取る最大バイト数(-1 = 末尾まで)
$offset読み取り開始バイト位置(-1 = 現在位置、シーク可能ストリームのみ有効)
全量取得offset: 0 を指定するか rewind() してから呼ぶ
空と失敗の区別空なら ""、エラーなら false=== false で厳密判定
大容量対策$length で分割読み取りか fread() ループに切り替える
活用シーンメモリバッファの取得・ファイル部分読み取り・HTTPパース・暗号化・スナップショット管理など

stream_get_contents() はシンプルな API の裏に、オフセット指定・長さ制限・ポインタ非依存の読み取りという強力な仕組みを持っています。file_get_contents() では手が届かない「すでに開いているストリームからの柔軟な取得」が必要な場面で、ぜひ積極的に活用してください。

タイトルとURLをコピーしました