[PHP]ob_end_flush関数完全ガイド – 出力バッファを正しく出力して終了する方法

PHP

はじめに

PHPの出力バッファリングにおいて、ob_end_flushは最も基本的で重要な関数の一つです。バッファに蓄積された内容を出力しつつバッファを終了する機能を持ち、Webアプリケーション開発の様々な場面で活用されます。

この記事では、ob_end_flushの基本的な使い方から高度な応用例まで、実践的なコード例とともに詳しく解説していきます。

ob_end_flush関数とは?

ob_end_flush出力バッファの内容を出力し、バッファを終了する関数です。バッファに蓄積された全ての内容をブラウザに送信し、同時にバッファを終了させます。

基本的な構文

bool ob_end_flush()

戻り値:

  • 成功時:true
  • 失敗時:false(アクティブなバッファが存在しない場合など)

動作:

  1. 現在のバッファ内容を出力
  2. バッファを終了
  3. バッファレベルを1つ下げる

基本的な使い方

シンプルな例

<?php
// 出力バッファリング開始
ob_start();

echo "バッファに蓄積される内容1<br>";
echo "バッファに蓄積される内容2<br>";
echo "バッファに蓄積される内容3<br>";

// この時点ではまだ何も出力されていない
echo "現在のバッファ長: " . ob_get_length() . "<br>";

// バッファ内容を出力してバッファを終了
ob_end_flush();

// この後の出力は直接表示される
echo "バッファ終了後の直接出力<br>";
?>

複数レベルのバッファ処理

<?php
echo "レベル0(初期状態)<br>";

// 第1レベルバッファ開始
ob_start();
echo "レベル1: 外側のバッファ<br>";

// 第2レベルバッファ開始  
ob_start();
echo "レベル2: 内側のバッファ<br>";
echo "現在のバッファレベル: " . ob_get_level() . "<br>";

// 内側のバッファを出力して終了
ob_end_flush(); // レベル2→1

echo "レベル1に戻りました<br>";
echo "現在のバッファレベル: " . ob_get_level() . "<br>";

// 外側のバッファを出力して終了
ob_end_flush(); // レベル1→0

echo "すべてのバッファが終了しました<br>";
?>

関連関数との比較

ob_end_flush vs ob_end_clean vs ob_flush

<?php
// 1. ob_end_flush() - 出力してバッファ終了
ob_start();
echo "出力される内容";
ob_end_flush(); // "出力される内容" が表示され、バッファ終了

// 2. ob_end_clean() - 破棄してバッファ終了
ob_start();
echo "破棄される内容";
ob_end_clean(); // 何も出力されず、バッファ終了

// 3. ob_flush() - 出力するがバッファは継続
ob_start();
echo "出力される内容";
ob_flush(); // "出力される内容" が表示されるが、バッファは継続
echo "追加の内容";
ob_end_flush(); // "追加の内容" が表示され、バッファ終了
?>

実用的な応用例

1. プログレスバー付きの長時間処理

<?php
class ProgressProcessor {
    
    public function processLargeDataset($data, $callback = null) {
        $total = count($data);
        
        // HTMLヘッダー出力
        echo "<!DOCTYPE html><html><head>";
        echo "<title>データ処理中</title>";
        echo "<style>";
        echo ".progress { width: 100%; background-color: #f0f0f0; }";
        echo ".progress-bar { height: 30px; background-color: #4CAF50; text-align: center; line-height: 30px; color: white; }";
        echo "</style>";
        echo "</head><body>";
        echo "<h1>データ処理中...</h1>";
        
        // 初期プログレスバー
        echo "<div class='progress'>";
        echo "<div id='progress-bar' class='progress-bar' style='width: 0%'>0%</div>";
        echo "</div>";
        echo "<div id='status'>準備中...</div>";
        
        // バッファを即座に出力
        ob_end_flush();
        
        foreach ($data as $index => $item) {
            // 各アイテムの処理
            if ($callback) {
                call_user_func($callback, $item, $index);
            } else {
                // デフォルト処理(重い処理をシミュレート)
                usleep(500000); // 0.5秒待機
            }
            
            // プログレス更新
            $progress = (($index + 1) / $total) * 100;
            
            echo "<script>";
            echo "document.getElementById('progress-bar').style.width = '{$progress}%';";
            echo "document.getElementById('progress-bar').textContent = '" . round($progress) . "%';";
            echo "document.getElementById('status').textContent = '処理中: " . ($index + 1) . " / {$total}';";
            echo "</script>";
            
            // バッファを強制出力
            flush();
        }
        
        echo "<script>";
        echo "document.getElementById('status').textContent = '完了!';";
        echo "</script>";
        echo "</body></html>";
    }
}

// 使用例
$processor = new ProgressProcessor();
$largeDataset = range(1, 20); // 20件のデータ

$processor->processLargeDataset($largeDataset, function($item, $index) {
    // 各アイテムに対する処理
    file_put_contents("processed/item_{$index}.txt", "Processed: {$item}");
    usleep(800000); // 0.8秒の処理時間をシミュレート
});
?>

2. ストリーミング形式のデータ出力

<?php
class StreamingDataExporter {
    
    public function exportToCsv($query, $filename = 'export.csv') {
        // HTTPヘッダー設定
        header('Content-Type: text/csv; charset=UTF-8');
        header('Content-Disposition: attachment; filename="' . $filename . '"');
        header('Cache-Control: no-cache');
        
        // BOM付きUTF-8
        echo "\xEF\xBB\xBF";
        
        // CSVヘッダー出力
        echo "ID,名前,メール,作成日\n";
        
        // バッファを即座に出力
        ob_end_flush();
        
        // データを少しずつ取得・出力
        $offset = 0;
        $limit = 100; // 100件ずつ処理
        
        while (true) {
            $data = $this->fetchData($query, $offset, $limit);
            
            if (empty($data)) {
                break;
            }
            
            foreach ($data as $row) {
                echo $this->formatCsvRow($row) . "\n";
            }
            
            // メモリ節約のため、定期的に出力
            flush();
            
            $offset += $limit;
            
            // メモリ使用量チェック
            if (memory_get_usage() > 50 * 1024 * 1024) { // 50MB制限
                error_log("メモリ使用量が上限に達しました");
                break;
            }
        }
    }
    
    private function fetchData($query, $offset, $limit) {
        // データベースから少しずつデータを取得
        // 実際の実装ではPDOなどを使用
        return [
            ['id' => 1 + $offset, 'name' => '田中太郎', 'email' => 'tanaka@example.com', 'created' => '2024-01-01'],
            ['id' => 2 + $offset, 'name' => '佐藤花子', 'email' => 'sato@example.com', 'created' => '2024-01-02'],
            // ... より多くのデータ
        ];
    }
    
    private function formatCsvRow($row) {
        return implode(',', array_map(function($field) {
            return '"' . str_replace('"', '""', $field) . '"';
        }, $row));
    }
}

// 使用例
$exporter = new StreamingDataExporter();
$exporter->exportToCsv("SELECT * FROM users WHERE active = 1", "active_users.csv");
?>

3. リアルタイムログ表示

<?php
class RealTimeLogViewer {
    
    public function streamLog($logFile, $maxLines = 1000) {
        if (!file_exists($logFile)) {
            echo "ログファイルが見つかりません: {$logFile}";
            return;
        }
        
        // HTMLストリーミング開始
        echo "<!DOCTYPE html><html><head>";
        echo "<title>リアルタイムログ</title>";
        echo "<style>";
        echo "body { font-family: monospace; background: #000; color: #0f0; }";
        echo ".log-line { margin: 2px 0; }";
        echo ".error { color: #f00; }";
        echo ".warning { color: #fa0; }";
        echo "</style>";
        echo "</head><body>";
        echo "<h1>リアルタイムログ - " . basename($logFile) . "</h1>";
        echo "<div id='log-container'>";
        
        // 初期バッファをフラッシュ
        ob_end_flush();
        
        $lastPosition = 0;
        $lineCount = 0;
        
        while (true) {
            clearstatcache();
            $currentSize = filesize($logFile);
            
            if ($currentSize > $lastPosition) {
                $file = fopen($logFile, 'r');
                fseek($file, $lastPosition);
                
                while (($line = fgets($file)) !== false && $lineCount < $maxLines) {
                    $formattedLine = $this->formatLogLine(trim($line));
                    echo "<div class='log-line'>{$formattedLine}</div>";
                    $lineCount++;
                }
                
                $lastPosition = ftell($file);
                fclose($file);
                
                // 出力を強制送信
                flush();
                
                // 最大行数に達した場合は停止
                if ($lineCount >= $maxLines) {
                    echo "<div class='log-line' style='color: yellow;'>最大表示行数に達しました。</div>";
                    break;
                }
            }
            
            // 1秒待機
            sleep(1);
            
            // スクリプトの実行時間制限チェック
            if (connection_aborted()) {
                break;
            }
        }
        
        echo "</div></body></html>";
    }
    
    private function formatLogLine($line) {
        $line = htmlspecialchars($line);
        
        // ログレベルに応じてスタイリング
        if (stripos($line, 'ERROR') !== false) {
            return "<span class='error'>{$line}</span>";
        } elseif (stripos($line, 'WARNING') !== false) {
            return "<span class='warning'>{$line}</span>";
        }
        
        return $line;
    }
}

// 使用例
$logViewer = new RealTimeLogViewer();
$logViewer->streamLog('/var/log/application.log', 500);
?>

4. チャンク形式のファイルダウンロード

<?php
class ChunkedFileDownloader {
    
    public function download($filePath, $downloadName = null, $chunkSize = 8192) {
        if (!file_exists($filePath)) {
            http_response_code(404);
            echo "ファイルが見つかりません";
            return false;
        }
        
        $fileSize = filesize($filePath);
        $downloadName = $downloadName ?: basename($filePath);
        
        // HTTPヘッダー設定
        header('Content-Type: application/octet-stream');
        header('Content-Disposition: attachment; filename="' . $downloadName . '"');
        header('Content-Length: ' . $fileSize);
        header('Cache-Control: no-cache');
        
        // バッファリングを無効にして即座に出力開始
        if (ob_get_level()) {
            ob_end_flush();
        }
        
        $file = fopen($filePath, 'rb');
        if (!$file) {
            http_response_code(500);
            echo "ファイルの読み込みに失敗しました";
            return false;
        }
        
        $totalSent = 0;
        
        while (!feof($file) && $totalSent < $fileSize) {
            $chunk = fread($file, $chunkSize);
            echo $chunk;
            
            $totalSent += strlen($chunk);
            
            // 出力を強制送信
            flush();
            
            // 接続が切断されていないかチェック
            if (connection_aborted()) {
                break;
            }
            
            // メモリ使用量を抑制
            if (memory_get_usage() > 10 * 1024 * 1024) {
                // 10MB以上使用している場合は少し休止
                usleep(10000); // 0.01秒
            }
        }
        
        fclose($file);
        return true;
    }
    
    public function downloadWithProgress($filePath, $downloadName = null) {
        if (!file_exists($filePath)) {
            echo "ファイルが見つかりません";
            return false;
        }
        
        $fileSize = filesize($filePath);
        $downloadName = $downloadName ?: basename($filePath);
        
        // プログレス表示用のHTML
        echo "<!DOCTYPE html><html><head>";
        echo "<title>ダウンロード準備中</title>";
        echo "<style>";
        echo ".progress { width: 50%; margin: 50px auto; }";
        echo ".progress-bar { height: 20px; background: #4CAF50; }";
        echo "</style>";
        echo "</head><body>";
        echo "<div class='progress'>";
        echo "<h2>ダウンロード準備中...</h2>";
        echo "<div id='progress' class='progress-bar' style='width: 0%'></div>";
        echo "<p id='status'>0%</p>";
        echo "</div>";
        
        // 準備完了までのプログレス表示
        for ($i = 0; $i <= 100; $i += 10) {
            echo "<script>";
            echo "document.getElementById('progress').style.width = '{$i}%';";
            echo "document.getElementById('status').textContent = '{$i}%';";
            echo "</script>";
            flush();
            usleep(100000); // 0.1秒
        }
        
        echo "<script>";
        echo "document.getElementById('status').textContent = 'ダウンロード開始...';";
        echo "setTimeout(function() { window.location.href = 'download.php?file=" . urlencode($filePath) . "'; }, 1000);";
        echo "</script>";
        echo "</body></html>";
    }
}

// 使用例
$downloader = new ChunkedFileDownloader();

if (isset($_GET['file'])) {
    // 実際のダウンロード
    $downloader->download($_GET['file']);
} else {
    // プログレス付きダウンロード準備
    $downloader->downloadWithProgress('./large_file.zip', 'my_download.zip');
}
?>

5. Server-Sent Events (SSE) の実装

<?php
class ServerSentEvents {
    
    public function startEventStream() {
        // SSE用のヘッダー設定
        header('Content-Type: text/event-stream');
        header('Cache-Control: no-cache');
        header('Access-Control-Allow-Origin: *');
        
        // 即座に出力開始
        ob_end_flush();
        
        $this->sendEvent('connected', ['message' => 'イベントストリーム開始']);
        
        $counter = 0;
        while (true) {
            // システム状態の取得
            $data = [
                'timestamp' => date('Y-m-d H:i:s'),
                'counter' => $counter,
                'memory_usage' => memory_get_usage(true),
                'cpu_load' => sys_getloadavg()[0] ?? 0
            ];
            
            $this->sendEvent('system_status', $data);
            
            $counter++;
            
            // 接続が切断されていないかチェック
            if (connection_aborted()) {
                break;
            }
            
            // 3秒間隔
            sleep(3);
            
            // 最大100回まで
            if ($counter >= 100) {
                $this->sendEvent('end', ['message' => 'ストリーム終了']);
                break;
            }
        }
    }
    
    private function sendEvent($eventType, $data) {
        echo "event: {$eventType}\n";
        echo "data: " . json_encode($data, JSON_UNESCAPED_UNICODE) . "\n\n";
        flush();
    }
    
    public function chatStream($roomId) {
        header('Content-Type: text/event-stream');
        header('Cache-Control: no-cache');
        
        ob_end_flush();
        
        $lastMessageId = $_GET['lastId'] ?? 0;
        
        while (true) {
            $newMessages = $this->getNewMessages($roomId, $lastMessageId);
            
            foreach ($newMessages as $message) {
                $this->sendEvent('message', [
                    'id' => $message['id'],
                    'user' => $message['user'],
                    'content' => $message['content'],
                    'timestamp' => $message['timestamp']
                ]);
                
                $lastMessageId = $message['id'];
            }
            
            if (connection_aborted()) {
                break;
            }
            
            sleep(1);
        }
    }
    
    private function getNewMessages($roomId, $lastId) {
        // 実際の実装ではデータベースから新しいメッセージを取得
        return [
            ['id' => $lastId + 1, 'user' => '田中', 'content' => 'こんにちは', 'timestamp' => date('H:i:s')],
            ['id' => $lastId + 2, 'user' => '佐藤', 'content' => 'お疲れ様です', 'timestamp' => date('H:i:s')]
        ];
    }
}

// 使用例
$sse = new ServerSentEvents();

if (isset($_GET['action'])) {
    switch ($_GET['action']) {
        case 'system':
            $sse->startEventStream();
            break;
        case 'chat':
            $sse->chatStream($_GET['room'] ?? 1);
            break;
    }
}
?>

パフォーマンスとベストプラクティス

1. メモリ効率的な出力制御

<?php
class MemoryEfficientRenderer {
    private $maxBufferSize;
    
    public function __construct($maxBufferSize = 1024 * 1024) { // 1MB
        $this->maxBufferSize = $maxBufferSize;
    }
    
    public function renderLargeContent($dataGenerator) {
        ob_start();
        
        foreach ($dataGenerator() as $chunk) {
            echo $chunk;
            
            // バッファサイズが上限に達したら出力
            if (ob_get_length() >= $this->maxBufferSize) {
                ob_end_flush();
                ob_start(); // 新しいバッファを開始
            }
        }
        
        // 残りの内容を出力
        if (ob_get_length() > 0) {
            ob_end_flush();
        }
    }
    
    public function renderWithMemoryMonitoring($callback) {
        $startMemory = memory_get_usage();
        
        ob_start();
        
        try {
            call_user_func($callback);
            
            $currentMemory = memory_get_usage();
            $memoryUsed = $currentMemory - $startMemory;
            
            // メモリ使用量情報を追加
            echo "\n<!-- メモリ使用量: " . number_format($memoryUsed) . " bytes -->";
            
            ob_end_flush();
            
        } catch (Exception $e) {
            ob_end_clean();
            echo "エラー: " . $e->getMessage();
        }
    }
}

// 使用例
$renderer = new MemoryEfficientRenderer();

$renderer->renderLargeContent(function() {
    // 大量のデータを生成
    for ($i = 0; $i < 10000; $i++) {
        yield "<div>アイテム {$i}: " . str_repeat("データ", 100) . "</div>\n";
    }
});
?>

2. エラーハンドリングの強化

<?php
class RobustOutputHandler {
    
    public function safeRender($callback, $fallback = null) {
        $originalLevel = ob_get_level();
        
        try {
            ob_start();
            
            // メイン処理実行
            call_user_func($callback);
            
            // 正常終了時は出力
            ob_end_flush();
            
        } catch (Exception $e) {
            // エラー時はバッファを破棄
            while (ob_get_level() > $originalLevel) {
                ob_end_clean();
            }
            
            // フォールバック処理
            if ($fallback) {
                call_user_func($fallback, $e);
            } else {
                $this->renderErrorPage($e);
            }
            
        } catch (Error $e) {
            // Fatal Error も同様に処理
            while (ob_get_level() > $originalLevel) {
                ob_end_clean();
            }
            
            $this->renderErrorPage($e);
        }
    }
    
    private function renderErrorPage($error) {
        echo "<div style='padding: 20px; background: #fee; border: 1px solid #fcc;'>";
        echo "<h2>エラーが発生しました</h2>";
        echo "<p><strong>メッセージ:</strong> " . htmlspecialchars($error->getMessage()) . "</p>";
        echo "<p><strong>ファイル:</strong> " . htmlspecialchars($error->getFile()) . "</p>";
        echo "<p><strong>行番号:</strong> " . $error->getLine() . "</p>";
        echo "<p><strong>時刻:</strong> " . date('Y-m-d H:i:s') . "</p>";
        echo "</div>";
    }
    
    public function renderWithTimeout($callback, $timeout = 30) {
        set_time_limit($timeout);
        
        ob_start();
        
        $startTime = microtime(true);
        
        try {
            call_user_func($callback);
            
            $endTime = microtime(true);
            $executionTime = $endTime - $startTime;
            
            echo "\n<!-- 実行時間: " . number_format($executionTime, 3) . " 秒 -->";
            
            ob_end_flush();
            
        } catch (Exception $e) {
            ob_end_clean();
            echo "処理がタイムアウトまたはエラーが発生しました: " . $e->getMessage();
        }
    }
}

// 使用例
$handler = new RobustOutputHandler();

$handler->safeRender(function() {
    echo "<h1>安全な処理</h1>";
    
    // 何らかのリスクのある処理
    if (rand(1, 10) > 7) {
        throw new Exception("ランダムエラーが発生しました");
    }
    
    echo "<p>正常に処理されました</p>";
}, function($error) {
    // カスタムエラーハンドラ
    echo "<div class='custom-error'>カスタムエラー: " . $error->getMessage() . "</div>";
});
?>

デバッグとモニタリング

出力バッファの状態監視

<?php
class BufferMonitor {
    private static $operations = [];
    private static $enabled = false;
    
    public static function enable($debug = true) {
        self::$enabled = $debug;
    }
    
    public static function monitoredEndFlush($label = 'default') {
        if (!self::$enabled) {
            return ob_end_flush();
        }
        
        $beforeLevel = ob_get_level();
        $beforeLength = ob_get_length();
        $beforeMemory = memory_get_usage();
        
        $result = ob_end_flush();
        
        $afterLevel = ob_get_level();
        $afterMemory = memory_get_usage();
        
        self::$operations[] = [
            'operation' => 'ob_end_flush',
            'label' => $label,
            'before_level' => $beforeLevel,
            'after_level' => $afterLevel,
            'buffer_length' => $beforeLength,
            'memory_before' => $beforeMemory,
            'memory_after' => $afterMemory,
            'result' => $result,
            'timestamp' => microtime(true)
        ];
        
        return $result;
    }
    
    public static function getReport() {
        if (!self::$enabled || empty(self::$operations)) {
            return "モニタリングが無効か、操作履歴がありません。";
        }
        
        $report = "<div class='buffer-monitor-report'>";
        $report .= "<h3>出力バッファ操作履歴</h3>";
        $report .= "<table border='1' style='border-collapse: collapse;'>";
        $report .= "<tr><th>ラベル</th><th>操作前レベル</th><th>操作後レベル</th><th>バッファ長</th><th>メモリ変化</th><th>結果</th><th>時刻</th></tr>";
        
        foreach (self::$operations as $op) {
            $memoryChange = $op['memory_after'] - $op['memory_before'];
            $report .= "<tr>";
            $report .= "<td>{$op['label']}</td>";
            $report .= "<td>{$op['before_level']}</td>";
            $report .= "<td>{$op['after_level']}</td>";
            $report .= "<td>" . number_format($op['buffer_length']) . "</td>";
            $report .= "<td>" . ($memoryChange >= 0 ? '+' : '') . number_format($memoryChange) . "</td>";
            $report .= "<td>" . ($op['result'] ? '成功' : '失敗') . "</td>";
            $report .= "<td>" . date('H:i:s.v', $op['timestamp']) . "</td>";
            $report .= "</tr>";
        }
        
        $report .= "</table>";
        $report .= "</div>";
        
        return $report;
    }
}

// 使用例(開発環境)
BufferMonitor::enable(true);

ob_start();
echo "テスト出力1";
BufferMonitor::monitoredEndFlush('test1');

ob_start();
echo "テスト出力2";
echo str_repeat("長いコンテンツ", 1000);
BufferMonitor::monitoredEndFlush('test2');

// レポート表示
echo BufferMonitor::getReport();
?>

まとめ

ob_end_flush関数は、PHPの出力バッファリングにおいてバッファ内容の出力とバッファの終了を同時に行う基本的かつ重要な関数です。

主な活用場面:

  • プログレス表示 – 長時間処理の進捗をリアルタイム表示
  • ストリーミング出力 – 大容量データの効率的な送信
  • リアルタイム通信 – SSEやチャット機能の実装
  • ファイルダウンロード – チャンク形式での安全な配信
  • パフォーマンス最適化 – メモリ効率的な出力制御

重要なポイント:

  • バッファレベルを適切に管理する
  • メモリ使用量を監視する
  • エラー時の適切な処理を実装する
  • 接続状態を定期的にチェックする
  • デバッグ時は操作履歴を記録する

適切なob_end_flushの活用により、ユーザーエクスペリエンスの向上と効率的なリソース管理が実現できます。特にリアルタイム処理やストリーミング配信において、この関数の正しい理解と実装は必須です。

よくある問題と解決策

1. “Cannot use output buffering” エラー

<?php
// 問題のあるコード
function problematicFunction() {
    ob_end_flush(); // バッファが存在しない場合エラー
}

// 解決策
function safeFunction() {
    if (ob_get_level() > 0) {
        return ob_end_flush();
    }
    return false;
}

// より堅牢な解決策
function robustFunction() {
    try {
        if (ob_get_level() > 0) {
            return ob_end_flush();
        }
        return false;
    } catch (Exception $e) {
        error_log("Buffer flush error: " . $e->getMessage());
        return false;
    }
}
?>

2. ヘッダー送信後の問題

<?php
class HeaderSafeOutput {
    
    public function safeOutput($content, $headers = []) {
        // ヘッダーがまだ送信されていない場合のみ設定
        if (!headers_sent()) {
            foreach ($headers as $header) {
                header($header);
            }
        }
        
        // バッファが存在する場合は内容をクリア
        if (ob_get_level() > 0) {
            ob_clean();
        }
        
        echo $content;
        
        if (ob_get_level() > 0) {
            ob_end_flush();
        }
    }
    
    public function checkHeaderStatus() {
        $file = '';
        $line = 0;
        $sent = headers_sent($file, $line);
        
        if ($sent) {
            echo "ヘッダーは既に送信されています。";
            echo "ファイル: {$file}, 行: {$line}";
        } else {
            echo "ヘッダーはまだ送信されていません。";
        }
    }
}
?>

3. 大容量データでのメモリ不足

<?php
class MemoryOptimizedStreamer {
    
    public function streamLargeData($dataSource, $chunkSize = 8192) {
        // メモリ制限を一時的に増加(必要に応じて)
        $originalLimit = ini_get('memory_limit');
        ini_set('memory_limit', '256M');
        
        try {
            while ($chunk = $this->getNextChunk($dataSource, $chunkSize)) {
                // 小さなバッファで処理
                ob_start();
                echo $chunk;
                ob_end_flush();
                
                // メモリ使用量チェック
                $memoryUsage = memory_get_usage(true);
                if ($memoryUsage > 200 * 1024 * 1024) { // 200MB制限
                    // ガベージコレクション強制実行
                    gc_collect_cycles();
                    
                    // それでも多い場合は休止
                    if (memory_get_usage(true) > 200 * 1024 * 1024) {
                        usleep(100000); // 0.1秒休止
                    }
                }
            }
        } finally {
            // メモリ制限を元に戻す
            ini_set('memory_limit', $originalLimit);
        }
    }
    
    private function getNextChunk($dataSource, $size) {
        // データソースから次のチャンクを取得
        // 実装は具体的なデータソースに依存
        static $position = 0;
        
        if ($position > 10000) { // 例:10000チャンクで終了
            return null;
        }
        
        $position++;
        return str_repeat("chunk{$position} ", $size / 10);
    }
}
?>

実践的なパターン集

1. レスポンシブな画像配信

<?php
class ResponsiveImageServer {
    
    public function serveImage($imagePath, $quality = 85, $maxWidth = null) {
        if (!file_exists($imagePath)) {
            http_response_code(404);
            echo "画像が見つかりません";
            return;
        }
        
        $imageInfo = getimagesize($imagePath);
        $mimeType = $imageInfo['mime'];
        
        // キャッシュヘッダー設定
        $lastModified = filemtime($imagePath);
        $etag = md5_file($imagePath);
        
        header("Content-Type: {$mimeType}");
        header("Last-Modified: " . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');
        header("ETag: {$etag}");
        header("Cache-Control: public, max-age=3600");
        
        // 条件付きリクエストの処理
        $ifModifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? '';
        $ifNoneMatch = $_SERVER['HTTP_IF_NONE_MATCH'] ?? '';
        
        if ($ifModifiedSince && $ifNoneMatch) {
            if (strtotime($ifModifiedSince) >= $lastModified || $ifNoneMatch === $etag) {
                http_response_code(304);
                return;
            }
        }
        
        // 画像のリサイズが必要な場合
        if ($maxWidth && $imageInfo[0] > $maxWidth) {
            $this->streamResizedImage($imagePath, $maxWidth, $quality);
        } else {
            $this->streamOriginalImage($imagePath);
        }
    }
    
    private function streamResizedImage($imagePath, $maxWidth, $quality) {
        $imageInfo = getimagesize($imagePath);
        $originalWidth = $imageInfo[0];
        $originalHeight = $imageInfo[1];
        
        // 新しいサイズを計算
        $newWidth = $maxWidth;
        $newHeight = intval(($originalHeight * $maxWidth) / $originalWidth);
        
        // 画像リサイズ
        $originalImage = imagecreatefromstring(file_get_contents($imagePath));
        $resizedImage = imagecreatetruecolor($newWidth, $newHeight);
        
        imagecopyresampled(
            $resizedImage, $originalImage,
            0, 0, 0, 0,
            $newWidth, $newHeight, $originalWidth, $originalHeight
        );
        
        // バッファリング開始
        ob_start();
        
        // 画像出力
        switch ($imageInfo['mime']) {
            case 'image/jpeg':
                imagejpeg($resizedImage, null, $quality);
                break;
            case 'image/png':
                imagepng($resizedImage);
                break;
            case 'image/gif':
                imagegif($resizedImage);
                break;
        }
        
        // メモリ解放
        imagedestroy($originalImage);
        imagedestroy($resizedImage);
        
        // 出力
        ob_end_flush();
    }
    
    private function streamOriginalImage($imagePath) {
        $fileSize = filesize($imagePath);
        header("Content-Length: {$fileSize}");
        
        // バッファ無効化して直接出力
        if (ob_get_level()) {
            ob_end_flush();
        }
        
        readfile($imagePath);
    }
}

// 使用例
$imageServer = new ResponsiveImageServer();
$imageServer->serveImage('./images/large_photo.jpg', 85, 800);
?>

2. WebSocketライクなロングポーリング

<?php
class LongPollingHandler {
    
    public function handleLongPoll($channel, $lastEventId = 0, $timeout = 30) {
        // SSEヘッダー設定
        header('Content-Type: text/event-stream');
        header('Cache-Control: no-cache');
        header('Connection: keep-alive');
        
        // バッファリングを無効化
        if (ob_get_level()) {
            ob_end_flush();
        }
        
        $startTime = time();
        $maxTime = $startTime + $timeout;
        
        // 初期接続確認
        $this->sendEvent('connected', ['channel' => $channel, 'time' => date('Y-m-d H:i:s')]);
        
        while (time() < $maxTime) {
            // 新しいイベントをチェック
            $events = $this->getNewEvents($channel, $lastEventId);
            
            foreach ($events as $event) {
                $this->sendEvent('data', $event);
                $lastEventId = $event['id'];
            }
            
            // 接続状態チェック
            if (connection_aborted()) {
                break;
            }
            
            // ハートビート送信
            if (time() % 10 == 0) { // 10秒ごと
                $this->sendEvent('heartbeat', ['time' => date('H:i:s')]);
            }
            
            sleep(1);
        }
        
        // タイムアウト通知
        $this->sendEvent('timeout', ['message' => 'ロングポーリング終了']);
    }
    
    private function sendEvent($type, $data) {
        echo "event: {$type}\n";
        echo "data: " . json_encode($data, JSON_UNESCAPED_UNICODE) . "\n\n";
        flush();
    }
    
    private function getNewEvents($channel, $lastId) {
        // 実際の実装ではデータベースやRedisから取得
        static $counter = 0;
        $counter++;
        
        if ($counter % 5 == 0) { // 5秒ごとに新しいイベント
            return [
                [
                    'id' => $lastId + 1,
                    'channel' => $channel,
                    'message' => 'サンプルメッセージ ' . $counter,
                    'timestamp' => time()
                ]
            ];
        }
        
        return [];
    }
}

// 使用例
$handler = new LongPollingHandler();
$handler->handleLongPoll($_GET['channel'] ?? 'general', $_GET['lastId'] ?? 0, 60);
?>

セキュリティ考慮事項

1. 出力のサニタイゼーション

<?php
class SecureOutputHandler {
    
    public function safeStreamOutput($data, $contentType = 'text/html') {
        // コンテンツタイプに応じたサニタイゼーション
        switch ($contentType) {
            case 'text/html':
                $data = $this->sanitizeHtml($data);
                break;
            case 'application/json':
                $data = $this->sanitizeJson($data);
                break;
            case 'text/plain':
                $data = $this->sanitizeText($data);
                break;
        }
        
        header("Content-Type: {$contentType}; charset=UTF-8");
        header("X-Content-Type-Options: nosniff");
        header("X-Frame-Options: DENY");
        
        ob_start();
        echo $data;
        ob_end_flush();
    }
    
    private function sanitizeHtml($html) {
        // HTMLの危険な要素を除去
        $allowedTags = '<p><br><strong><em><u><h1><h2><h3><div><span>';
        return strip_tags($html, $allowedTags);
    }
    
    private function sanitizeJson($data) {
        // JSONエスケープ
        return json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
    }
    
    private function sanitizeText($text) {
        // プレーンテキストのサニタイゼーション
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
}
?>

このように、ob_end_flushは単なる出力関数を超えて、Webアプリケーションの応答性、パフォーマンス、セキュリティに直接影響を与える重要な技術要素です。適切な実装により、より良いユーザー体験を提供できます。

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