[PHP]readfile関数の使い方完全ガイド!ファイルダウンロード実装も解説

PHP

はじめに

Webアプリケーションでファイルをブラウザに出力したり、ダウンロード機能を実装したりする場面は非常に多いですよね。PDFレポート、画像ファイル、CSVデータなど、さまざまなファイルを扱う必要があります。

そんな時に便利なのが、PHPのreadfile関数です。この記事では、readfile関数の基本的な使い方から実践的なダウンロード機能の実装方法まで、分かりやすく解説していきます。

readfile関数とは?

readfile関数は、ファイルの内容を読み込んで、そのまま出力バッファに送る関数です。ファイル全体を一度に読み込んで出力するため、非常にシンプルで使いやすいのが特徴です。

基本的な構文

<?php
readfile(string $filename, bool $use_include_path = false, ?resource $context = null): int|false
?>
  • $filename: 読み込むファイルのパス
  • $use_include_path: include_pathからファイルを検索するか(オプション)
  • $context: ストリームコンテキスト(オプション)
  • 戻り値: 読み込んだバイト数、失敗時はfalse

最もシンプルな使用例

<?php
readfile('sample.txt');
// sample.txtの内容がそのまま出力される
?>

たったこれだけ!ファイルの内容が画面に表示されます。

readfile関数の動作の仕組み

readfile関数は内部で以下のような処理を行っています:

  1. 指定されたファイルを開く
  2. ファイルの内容を読み込む
  3. 読み込んだ内容を出力バッファに送る
  4. ファイルを閉じる

これを手動で書くと以下のようになりますが、readfileなら1行で済みます:

<?php
// readfile()と同等の処理を手動で行う場合
$handle = fopen('sample.txt', 'rb');
while (!feof($handle)) {
    echo fread($handle, 8192);
}
fclose($handle);

// readfile()ならこれだけ!
readfile('sample.txt');
?>

実践的な使用例

1. テキストファイルの表示

<?php
header('Content-Type: text/plain; charset=UTF-8');
readfile('log.txt');
?>

2. 画像ファイルの表示

<?php
$imagePath = 'images/photo.jpg';

if (file_exists($imagePath)) {
    header('Content-Type: image/jpeg');
    header('Content-Length: ' . filesize($imagePath));
    readfile($imagePath);
} else {
    http_response_code(404);
    echo 'ファイルが見つかりません';
}
?>

3. PDFファイルのダウンロード

これが最も実用的な使い方です!

<?php
$file = 'documents/report.pdf';

if (file_exists($file)) {
    // ダウンロード用のヘッダーを設定
    header('Content-Type: application/pdf');
    header('Content-Disposition: attachment; filename="' . basename($file) . '"');
    header('Content-Length: ' . filesize($file));
    
    // キャッシュを無効化
    header('Cache-Control: no-cache, no-store, must-revalidate');
    header('Pragma: no-cache');
    header('Expires: 0');
    
    // ファイルを出力
    readfile($file);
    exit;
} else {
    http_response_code(404);
    echo 'ファイルが見つかりません';
}
?>

4. ブラウザでPDFを表示(ダウンロードさせない)

<?php
$file = 'documents/report.pdf';

if (file_exists($file)) {
    header('Content-Type: application/pdf');
    header('Content-Disposition: inline; filename="' . basename($file) . '"');
    header('Content-Length: ' . filesize($file));
    
    readfile($file);
    exit;
}
?>

ポイント: attachmentinlineに変更するだけで、ダウンロードではなくブラウザ内で表示されます!

5. CSVファイルのダウンロード

<?php
$file = 'exports/users.csv';
$downloadName = 'ユーザーリスト_' . date('Ymd') . '.csv';

if (file_exists($file)) {
    header('Content-Type: text/csv; charset=UTF-8');
    header('Content-Disposition: attachment; filename="' . $downloadName . '"');
    header('Content-Length: ' . filesize($file));
    
    readfile($file);
    exit;
}
?>

6. 日本語ファイル名のダウンロード

日本語ファイル名を扱う場合は、ブラウザ対応のためエンコードが必要です:

<?php
$file = 'documents/report.pdf';
$filename = 'レポート2024.pdf';

// 日本語ファイル名をブラウザに対応させる
$encodedFilename = rawurlencode($filename);

header('Content-Type: application/pdf');
header("Content-Disposition: attachment; filename*=UTF-8''{$encodedFilename}");
header('Content-Length: ' . filesize($file));

readfile($file);
exit;
?>

セキュリティ上の重要な注意点

readfile関数を使う際は、セキュリティに十分注意する必要があります!

❌ 危険な例:パストラバーサル攻撃に脆弱

<?php
// 絶対にこのようなコードを書かないでください!
$file = $_GET['file'];
readfile($file);
// 攻撃者が ?file=../../../../etc/passwd のようなリクエストを送ると
// システムファイルが読まれてしまう危険性があります
?>

✅ 安全な実装例

<?php
// ホワイトリスト方式
$allowedFiles = [
    'report1' => 'documents/report1.pdf',
    'report2' => 'documents/report2.pdf',
    'manual' => 'documents/manual.pdf'
];

$fileId = $_GET['file'] ?? '';

if (isset($allowedFiles[$fileId])) {
    $file = $allowedFiles[$fileId];
    
    // さらにファイルの存在確認
    if (file_exists($file)) {
        header('Content-Type: application/pdf');
        header('Content-Disposition: attachment; filename="' . basename($file) . '"');
        header('Content-Length: ' . filesize($file));
        readfile($file);
        exit;
    }
}

http_response_code(404);
echo 'ファイルが見つかりません';
?>

パス検証の実装

<?php
function isPathSafe($basePath, $requestedPath) {
    $realBase = realpath($basePath);
    $realPath = realpath($requestedPath);
    
    // realpathがfalseを返す場合はファイルが存在しない
    if ($realPath === false) {
        return false;
    }
    
    // ベースパス配下にあるかチェック
    return strpos($realPath, $realBase) === 0;
}

$basePath = '/var/www/downloads';
$filename = $_GET['file'] ?? '';
$fullPath = $basePath . '/' . $filename;

if (isPathSafe($basePath, $fullPath) && file_exists($fullPath)) {
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="' . basename($fullPath) . '"');
    readfile($fullPath);
    exit;
} else {
    http_response_code(403);
    echo 'アクセスが拒否されました';
}
?>

readfile関数の制限と注意点

1. メモリ使用量の問題

readfile関数はファイル全体をメモリに読み込むわけではありませんが、大きなファイルの場合は出力バッファリングに注意が必要です。

<?php
// 大きなファイルの場合は出力バッファリングを無効化
if (ob_get_level()) {
    ob_end_clean();
}

header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="largefile.zip"');
header('Content-Length: ' . filesize('largefile.zip'));

readfile('largefile.zip');
exit;
?>

2. 超大容量ファイルの場合

数GB以上の非常に大きなファイルの場合は、readfile関数よりもストリーミング方式が適しています:

<?php
function streamFile($file) {
    $handle = fopen($file, 'rb');
    while (!feof($handle)) {
        echo fread($handle, 8192); // 8KBずつ読み込む
        flush(); // バッファをフラッシュ
    }
    fclose($handle);
}

header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="huge-file.zip"');
header('Content-Length: ' . filesize('huge-file.zip'));

streamFile('huge-file.zip');
exit;
?>

3. エラーハンドリング

readfile関数が失敗した場合の処理も忘れずに:

<?php
$file = 'documents/report.pdf';

if (!file_exists($file)) {
    http_response_code(404);
    die('ファイルが存在しません');
}

if (!is_readable($file)) {
    http_response_code(403);
    die('ファイルの読み取り権限がありません');
}

header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
header('Content-Length: ' . filesize($file));

$bytes = readfile($file);

if ($bytes === false) {
    error_log("ファイル読み込みエラー: {$file}");
    die('ファイルの読み込みに失敗しました');
}

exit;
?>

他のファイル読み込み関数との比較

関数用途メモリ効率
readfile()ファイル全体を出力良い(バッファリング)
file_get_contents()ファイル全体を文字列で取得悪い(全体をメモリに)
fread()ファイルを部分的に読み込み最良(制御可能)
file()ファイルを配列として取得悪い(全体をメモリに)

使い分けの目安

<?php
// ファイルをそのまま出力する場合
readfile('file.txt');

// ファイル内容を加工してから出力する場合
$content = file_get_contents('file.txt');
$modified = str_replace('old', 'new', $content);
echo $modified;

// 大容量ファイルを扱う場合
$handle = fopen('huge-file.txt', 'rb');
while (!feof($handle)) {
    echo fread($handle, 8192);
}
fclose($handle);
?>

実用的な完全版ダウンロードスクリプト

最後に、実務で使える完全版のダウンロードスクリプトをご紹介します:

<?php
/**
 * ファイルダウンロード処理
 */

// 設定
$downloadDir = '/var/www/downloads';
$allowedExtensions = ['pdf', 'csv', 'xlsx', 'zip', 'jpg', 'png'];

// パラメータ取得
$fileId = $_GET['id'] ?? '';

// ファイルIDの検証(例:データベースから取得)
$files = [
    '1' => ['path' => 'reports/2024_report.pdf', 'name' => 'レポート2024.pdf', 'type' => 'application/pdf'],
    '2' => ['path' => 'exports/users.csv', 'name' => 'ユーザーリスト.csv', 'type' => 'text/csv'],
];

if (!isset($files[$fileId])) {
    http_response_code(404);
    die('ファイルが見つかりません');
}

$fileInfo = $files[$fileId];
$filePath = $downloadDir . '/' . $fileInfo['path'];

// セキュリティチェック
$realPath = realpath($filePath);
$realDownloadDir = realpath($downloadDir);

if ($realPath === false || strpos($realPath, $realDownloadDir) !== 0) {
    http_response_code(403);
    die('不正なアクセスです');
}

// 拡張子チェック
$extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
if (!in_array($extension, $allowedExtensions)) {
    http_response_code(403);
    die('このファイル形式はダウンロードできません');
}

// ファイル存在チェック
if (!file_exists($filePath) || !is_readable($filePath)) {
    http_response_code(404);
    die('ファイルが存在しないか、読み取りできません');
}

// 出力バッファをクリア
if (ob_get_level()) {
    ob_end_clean();
}

// ヘッダー送信
$encodedFilename = rawurlencode($fileInfo['name']);
header('Content-Type: ' . $fileInfo['type']);
header("Content-Disposition: attachment; filename*=UTF-8''{$encodedFilename}");
header('Content-Length: ' . filesize($filePath));
header('Cache-Control: no-cache, no-store, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');

// ファイル出力
$bytes = readfile($filePath);

if ($bytes === false) {
    error_log("ダウンロードエラー: {$filePath}");
}

exit;
?>

まとめ

readfile関数のポイントをおさらいしましょう:

  1. ファイルを読み込んで直接出力する便利な関数
  2. ダウンロード機能の実装に最適
  3. 適切なヘッダー設定が重要
  4. セキュリティ対策は必須(パストラバーサル攻撃に注意)
  5. 大容量ファイルは出力バッファリングを無効化
  6. 日本語ファイル名はエンコードが必要
  7. エラーハンドリングを忘れずに

ファイルダウンロード機能は、多くのWebアプリケーションで必要とされる重要な機能です。セキュリティに十分配慮しながら、readfile関数を活用して安全で使いやすいダウンロード機能を実装してください!

参考リンク


この記事が役に立ったら、ぜひシェアしてください!PHPに関する他の記事もお楽しみに。

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