はじめに
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
パラメータ
| パラメータ | 型 | デフォルト | 説明 |
|---|---|---|---|
$stream | resource | 必須 | 読み取り対象のストリームリソース |
$length | int | -1 | 読み取る最大バイト数。-1 で末尾まで全読み取り |
$offset | int | -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() では手が届かない「すでに開いているストリームからの柔軟な取得」が必要な場面で、ぜひ積極的に活用してください。
