[PHP]stream_copy_to_streamの完全解説|ストリーム間コピーの基本から大容量ファイル転送・パイプ処理まで実践サンプルで理解する

PHP

stream_copy_to_streamとは?

stream_copy_to_stream() は、あるストリームのデータを別のストリームへコピーする関数です。

ファイル・標準出力・ネットワーク接続・メモリなど、PHPが扱うあらゆるストリームリソース間でデータを転送できます。ファイルの内容を丸ごとコピーするだけでなく、コピーする長さやコピー開始位置(オフセット)を指定できるため、大容量ファイルの部分転送や、複数ストリームをつなぐパイプ処理にも活用できます。

メモリに全データを展開しないストリーム間直接転送のため、大きなファイルを扱う際のメモリ効率が非常に優れています


基本構文

stream_copy_to_stream(
    resource $from,
    resource $to,
    ?int     $length = null,
    int      $offset = 0
): int|false
引数説明
$fromresourceコピー元のストリームリソース
$toresourceコピー先のストリームリソース
$length?intコピーするバイト数(null で残り全部)
$offsetintコピー元の読み取り開始バイト位置(デフォルト: 0
戻り値int|falseコピーしたバイト数、失敗時は false

基本的な使い方

<?php
// ファイルをストリームで開いてコピー
$src = fopen('input.txt',  'r');
$dst = fopen('output.txt', 'w');

$bytes = stream_copy_to_stream($src, $dst);

fclose($src);
fclose($dst);

echo "{$bytes} バイトコピーしました\n";
<?php
// 先頭100バイトをスキップして500バイトだけコピー
$src = fopen('largefile.bin', 'r');
$dst = fopen('chunk.bin',     'w');

$bytes = stream_copy_to_stream($src, $dst, length: 500, offset: 100);

fclose($src);
fclose($dst);

echo "{$bytes} バイトコピーしました\n";

実践クラスサンプル

サンプル1:ファイルコピーユーティリティクラス

メモリ効率よくファイルをコピー・移動します。

<?php

class StreamFileCopier
{
    private int $copiedBytes = 0;

    /**
     * ファイルをコピーする
     */
    public function copy(string $src, string $dst, ?int $length = null, int $offset = 0): int
    {
        $srcStream = fopen($src, 'rb');
        if ($srcStream === false) {
            throw new RuntimeException("コピー元を開けません: {$src}");
        }

        // コピー先ディレクトリを自動作成
        $dir = dirname($dst);
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }

        $dstStream = fopen($dst, 'wb');
        if ($dstStream === false) {
            fclose($srcStream);
            throw new RuntimeException("コピー先を開けません: {$dst}");
        }

        $bytes = stream_copy_to_stream($srcStream, $dstStream, $length, $offset);

        fclose($srcStream);
        fclose($dstStream);

        if ($bytes === false) {
            throw new RuntimeException("コピーに失敗しました: {$src} → {$dst}");
        }

        $this->copiedBytes += $bytes;
        echo "コピー完了: {$src} → {$dst} ({$bytes} バイト)\n";

        return $bytes;
    }

    /**
     * ディレクトリ内のファイルを再帰的にコピーする
     */
    public function copyDirectory(string $srcDir, string $dstDir): int
    {
        $total   = 0;
        $files   = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($srcDir, FilesystemIterator::SKIP_DOTS)
        );

        foreach ($files as $file) {
            $relative = substr($file->getPathname(), strlen($srcDir) + 1);
            $total   += $this->copy($file->getPathname(), $dstDir . '/' . $relative);
        }

        return $total;
    }

    public function getTotalCopiedBytes(): int
    {
        return $this->copiedBytes;
    }
}

// 使用例
file_put_contents('/tmp/sample_src.txt', str_repeat("PHPストリームサンプルデータ\n", 100));

$copier = new StreamFileCopier();
$bytes  = $copier->copy('/tmp/sample_src.txt', '/tmp/sample_dst.txt');

echo "合計コピーバイト数: " . $copier->getTotalCopiedBytes() . "\n";
echo "コピー先サイズ確認: " . filesize('/tmp/sample_dst.txt') . " バイト\n";

サンプル2:大容量ファイルをチャンク分割して転送するクラス

巨大なファイルを一定サイズのチャンクに分けて別々のファイルに書き出します。

<?php

class StreamChunkSplitter
{
    private int $chunkSize;

    public function __construct(int $chunkSize = 1024 * 1024) // デフォルト1MB
    {
        $this->chunkSize = $chunkSize;
    }

    /**
     * ファイルを chunkSize ごとに分割して複数ファイルに書き出す
     * @return string[] 生成したチャンクファイルのパス一覧
     */
    public function split(string $srcPath, string $outputDir): array
    {
        if (!is_dir($outputDir)) {
            mkdir($outputDir, 0755, true);
        }

        $src      = fopen($srcPath, 'rb');
        $fileSize = filesize($srcPath);
        $chunks   = [];
        $part     = 0;
        $offset   = 0;

        while ($offset < $fileSize) {
            $chunkPath = $outputDir . '/part_' . str_pad($part, 4, '0', STR_PAD_LEFT) . '.bin';
            $dst       = fopen($chunkPath, 'wb');

            // 元ストリームの現在位置を offset に合わせる
            fseek($src, $offset);

            $copied   = stream_copy_to_stream($src, $dst, $this->chunkSize);
            fclose($dst);

            if ($copied === false || $copied === 0) {
                break;
            }

            $chunks[] = $chunkPath;
            echo "チャンク {$part}: {$chunkPath} ({$copied} バイト)\n";

            $offset += $copied;
            $part++;
        }

        fclose($src);
        return $chunks;
    }

    /**
     * 分割されたチャンクを結合して元のファイルに戻す
     */
    public function merge(array $chunkPaths, string $dstPath): int
    {
        $dst   = fopen($dstPath, 'wb');
        $total = 0;

        foreach ($chunkPaths as $chunkPath) {
            $src    = fopen($chunkPath, 'rb');
            $copied = stream_copy_to_stream($src, $dst);
            fclose($src);

            if ($copied !== false) {
                $total += $copied;
            }
        }

        fclose($dst);
        echo "結合完了: {$dstPath} ({$total} バイト)\n";

        return $total;
    }
}

// 使用例
$testData = str_repeat("0123456789ABCDEF", 1024 * 20); // 約320KB
file_put_contents('/tmp/large_file.bin', $testData);

$splitter = new StreamChunkSplitter(chunkSize: 1024 * 100); // 100KBずつ分割
$chunks   = $splitter->split('/tmp/large_file.bin', '/tmp/chunks');

echo "\n--- 結合 ---\n";
$splitter->merge($chunks, '/tmp/merged_file.bin');

echo "元サイズ:    " . filesize('/tmp/large_file.bin')  . " バイト\n";
echo "結合サイズ:  " . filesize('/tmp/merged_file.bin') . " バイト\n";
echo "一致: "        . (file_get_contents('/tmp/large_file.bin') === file_get_contents('/tmp/merged_file.bin') ? 'YES' : 'NO') . "\n";

サンプル3:HTTPレスポンスをストリームで受け取りファイルに保存するクラス

ダウンロードしながらファイルに書き込むストリーミングダウンローダーです。

<?php

class StreamingDownloader
{
    private string $userAgent;
    private int    $timeout;

    public function __construct(string $userAgent = 'PHP-Downloader/1.0', int $timeout = 60)
    {
        $this->userAgent = $userAgent;
        $this->timeout   = $timeout;
    }

    public function download(string $url, string $savePath): int
    {
        $context = stream_context_create([
            'http' => [
                'method'     => 'GET',
                'timeout'    => $this->timeout,
                'user_agent' => $this->userAgent,
            ],
            'ssl' => [
                'verify_peer'      => true,
                'verify_peer_name' => true,
            ],
        ]);

        $src = fopen($url, 'rb', false, $context);
        if ($src === false) {
            throw new RuntimeException("URLを開けません: {$url}");
        }

        $dir = dirname($savePath);
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }

        $dst = fopen($savePath, 'wb');
        if ($dst === false) {
            fclose($src);
            throw new RuntimeException("保存先を開けません: {$savePath}");
        }

        $bytes = stream_copy_to_stream($src, $dst);

        fclose($src);
        fclose($dst);

        if ($bytes === false) {
            throw new RuntimeException("ダウンロードに失敗しました");
        }

        echo "ダウンロード完了: {$savePath} ({$bytes} バイト)\n";
        return $bytes;
    }

    /**
     * 部分ダウンロード(Range指定)
     */
    public function downloadRange(string $url, string $savePath, int $start, int $end): int
    {
        $context = stream_context_create([
            'http' => [
                'method'     => 'GET',
                'header'     => "Range: bytes={$start}-{$end}\r\n",
                'timeout'    => $this->timeout,
                'user_agent' => $this->userAgent,
            ],
        ]);

        $src   = fopen($url, 'rb', false, $context);
        $dst   = fopen($savePath, 'wb');
        $bytes = stream_copy_to_stream($src, $dst);

        fclose($src);
        fclose($dst);

        return $bytes !== false ? $bytes : 0;
    }
}

// 使用例
$downloader = new StreamingDownloader();
$bytes = $downloader->download(
    'https://jsonplaceholder.typicode.com/posts',
    '/tmp/posts.json'
);

$data = json_decode(file_get_contents('/tmp/posts.json'), true);
echo "取得件数: " . count($data) . " 件\n";

サンプル4:メモリストリームを使ったデータ変換パイプクラス

php://memoryphp://temp を中間バッファとして使い、データを変換しながら転送します。

<?php

class StreamTransformPipe
{
    /**
     * ストリームの内容を読み取り、変換してメモリストリームに書き込む
     */
    public static function transform(resource $src, callable $transformer): resource
    {
        // 元データをメモリに読み込む
        $buffer = fopen('php://memory', 'r+b');
        stream_copy_to_stream($src, $buffer);
        rewind($buffer);

        $content     = stream_get_contents($buffer);
        $transformed = $transformer($content);

        // 変換結果を新しいメモリストリームに書き込む
        $output = fopen('php://memory', 'r+b');
        fwrite($output, $transformed);
        rewind($output);

        fclose($buffer);
        return $output;
    }

    /**
     * 文字列をストリームに変換する
     */
    public static function fromString(string $data): resource
    {
        $stream = fopen('php://memory', 'r+b');
        fwrite($stream, $data);
        rewind($stream);
        return $stream;
    }

    /**
     * ストリームを文字列に変換する
     */
    public static function toString(resource $stream): string
    {
        rewind($stream);
        $out = fopen('php://memory', 'r+b');
        stream_copy_to_stream($stream, $out);
        rewind($out);
        return stream_get_contents($out);
    }
}

// 使用例:CSV → TSV 変換パイプ
$csvData = "名前,年齢,都市\n田中太郎,30,東京\n鈴木花子,25,大阪\n";

$src       = StreamTransformPipe::fromString($csvData);
$tsvStream = StreamTransformPipe::transform(
    $src,
    fn(string $data) => str_replace(',', "\t", $data)
);

$tsv = StreamTransformPipe::toString($tsvStream);
echo "=== CSV → TSV 変換結果 ===\n";
echo $tsv;

// 使用例:大文字変換パイプ
rewind($src);
$upperStream = StreamTransformPipe::transform(
    StreamTransformPipe::fromString("hello stream world\n"),
    fn(string $data) => strtoupper($data)
);
echo "\n=== 大文字変換 ===\n";
echo StreamTransformPipe::toString($upperStream);

サンプル5:標準出力へストリーミング出力するクラス

ファイルの内容をメモリに溜めず直接 php://output に流します。

<?php

class StreamOutputEmitter
{
    /**
     * ファイルをそのままレスポンスとして出力する
     */
    public static function emitFile(string $filePath, string $mimeType = 'application/octet-stream'): void
    {
        if (!file_exists($filePath)) {
            throw new RuntimeException("ファイルが見つかりません: {$filePath}");
        }

        $size = filesize($filePath);

        // HTTPヘッダー出力(CLI実行時はスキップ)
        if (PHP_SAPI !== 'cli') {
            header("Content-Type: {$mimeType}");
            header("Content-Length: {$size}");
            header('Content-Disposition: attachment; filename="' . basename($filePath) . '"');
            header('Cache-Control: no-cache');
        }

        $src = fopen($filePath, 'rb');
        $dst = fopen('php://output', 'wb');

        $bytes = stream_copy_to_stream($src, $dst);

        fclose($src);
        fclose($dst);

        echo "\n({$bytes} バイト出力)\n";
    }

    /**
     * 文字列データをストリームで出力する
     */
    public static function emitString(string $data, string $mimeType = 'text/plain'): void
    {
        if (PHP_SAPI !== 'cli') {
            header("Content-Type: {$mimeType}; charset=UTF-8");
            header('Content-Length: ' . strlen($data));
        }

        $src = fopen('php://memory', 'r+b');
        fwrite($src, $data);
        rewind($src);

        $dst   = fopen('php://output', 'wb');
        stream_copy_to_stream($src, $dst);

        fclose($src);
        fclose($dst);
    }
}

// 使用例(CLI)
file_put_contents('/tmp/emit_test.txt', "ストリーム出力テスト\nline2\nline3\n");

echo "=== ファイル出力 ===\n";
StreamOutputEmitter::emitFile('/tmp/emit_test.txt', 'text/plain');

echo "\n=== 文字列出力 ===\n";
StreamOutputEmitter::emitString("Hello, PHP Stream!\n");

サンプル6:ログファイルの末尾N行をストリームで取得するクラス

大容量ログファイルから末尾だけを効率的に読み出します。

<?php

class StreamLogTailer
{
    private string $filePath;

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

    /**
     * ファイルの末尾 $lines 行を返す
     */
    public function tail(int $lines = 10): string
    {
        $src      = fopen($this->filePath, 'rb');
        $fileSize = filesize($this->filePath);

        if ($fileSize === 0) {
            fclose($src);
            return '';
        }

        // 末尾から少しずつ読んで目的の行数を探す
        $chunkSize = min(4096, $fileSize);
        $offset    = max(0, $fileSize - $chunkSize);
        $found     = 0;
        $buffer    = '';

        while ($found < $lines && $offset >= 0) {
            fseek($src, $offset);
            $chunk  = fread($src, $chunkSize);
            $buffer = $chunk . $buffer;
            $found  = substr_count($buffer, "\n");

            if ($offset === 0) break;
            $offset    = max(0, $offset - $chunkSize);
            $chunkSize = min(4096, $offset + $chunkSize);
        }

        fclose($src);

        // 末尾 $lines 行だけ切り出す
        $allLines = explode("\n", rtrim($buffer, "\n"));
        $tail     = array_slice($allLines, -$lines);

        return implode("\n", $tail) . "\n";
    }

    /**
     * ログの末尾をメモリストリームに書き出して別のストリームにコピーする
     */
    public function copyTailTo(resource $dst, int $lines = 10): int
    {
        $tailContent = $this->tail($lines);

        $src = fopen('php://memory', 'r+b');
        fwrite($src, $tailContent);
        rewind($src);

        $bytes = stream_copy_to_stream($src, $dst);
        fclose($src);

        return $bytes !== false ? $bytes : 0;
    }
}

// 使用例
$logContent = implode("\n", array_map(
    fn($i) => date('Y-m-d H:i:s', time() - (100 - $i)) . " [INFO] ログ行 {$i}",
    range(1, 100)
)) . "\n";
file_put_contents('/tmp/app.log', $logContent);

$tailer = new StreamLogTailer('/tmp/app.log');
echo "=== 末尾5行 ===\n";
echo $tailer->tail(5);

// 末尾をファイルにコピー
$dst   = fopen('/tmp/tail_output.log', 'wb');
$bytes = $tailer->copyTailTo($dst, lines: 3);
fclose($dst);
echo "\n末尾3行を /tmp/tail_output.log にコピー ({$bytes} バイト)\n";

サンプル7:ストリームを複数の宛先に同時コピー(ティー分岐)するクラス

1つの入力ストリームを複数のストリームに同時出力します。

<?php

class StreamTee
{
    /** @var resource[] */
    private array $destinations;

    public function __construct(resource ...$destinations)
    {
        $this->destinations = $destinations;
    }

    /**
     * $src を全宛先に順番にコピーする
     * @return int[] 各宛先へのコピーバイト数
     */
    public function copy(resource $src): array
    {
        $results = [];

        foreach ($this->destinations as $index => $dst) {
            rewind($src); // 宛先ごとに先頭から読み直す
            $bytes       = stream_copy_to_stream($src, $dst);
            $results[$index] = $bytes !== false ? $bytes : 0;
        }

        return $results;
    }

    public function addDestination(resource $dst): void
    {
        $this->destinations[] = $dst;
    }
}

// 使用例:同じデータをファイルとメモリとSTDOUTに同時コピー
$content = "Teeストリームのテストデータです。\n複数の宛先に同時コピーします。\n";

$src = fopen('php://memory', 'r+b');
fwrite($src, $content);

$fileDst   = fopen('/tmp/tee_file.txt', 'wb');
$memDst    = fopen('php://memory',     'r+b');
$outputDst = fopen('php://output',     'wb');

$tee     = new StreamTee($fileDst, $memDst, $outputDst);
$results = $tee->copy($src);

fclose($src);
fclose($fileDst);

// メモリストリームから内容を確認
rewind($memDst);
$memContent = stream_get_contents($memDst);
fclose($memDst);
fclose($outputDst);

echo "\n--- コピー結果 ---\n";
foreach ($results as $i => $bytes) {
    echo "宛先{$i}: {$bytes} バイト\n";
}
echo "ファイルサイズ: " . filesize('/tmp/tee_file.txt') . " バイト\n";
echo "メモリ内容一致: " . ($memContent === $content ? 'YES' : 'NO') . "\n";

関連関数との比較

関数用途
stream_copy_to_stream()ストリーム間でデータを直接コピー
stream_get_contents()ストリームの残りを文字列として取得
fread() / fwrite()バッファサイズを自分で制御しながら読み書き
file_get_contents()ファイル全体を文字列として取得(メモリに展開)
file_put_contents()文字列をファイルに書き込み
copy()ファイルパス指定でファイルをコピー

まとめ

項目内容
関数名stream_copy_to_stream()
分類ストリーム関数
PHP バージョンPHP 5.0.0以上
戻り値コピーしたバイト数(int)、失敗時は false
主な用途ストリーム間の効率的なデータ転送・パイプ処理

stream_copy_to_stream() は、ファイルのコピー・ダウンロード保存・大容量データの分割転送・メモリストリームを使ったデータ変換など、さまざまなストリーム処理の中核を担う関数です。lengthoffset を組み合わせた部分転送、php://memoryphp://output との組み合わせ、複数の宛先へのティー分岐など、応用の幅が広く、メモリ効率にも優れています。file_get_contents() では対応しにくい大容量・高速なデータ転送が必要な場面でぜひ活用してください。

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