はじめに
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関数は内部で以下のような処理を行っています:
- 指定されたファイルを開く
- ファイルの内容を読み込む
- 読み込んだ内容を出力バッファに送る
- ファイルを閉じる
これを手動で書くと以下のようになりますが、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;
}
?>
ポイント: attachmentをinlineに変更するだけで、ダウンロードではなくブラウザ内で表示されます!
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関数のポイントをおさらいしましょう:
- ファイルを読み込んで直接出力する便利な関数
- ダウンロード機能の実装に最適
- 適切なヘッダー設定が重要
- セキュリティ対策は必須(パストラバーサル攻撃に注意)
- 大容量ファイルは出力バッファリングを無効化
- 日本語ファイル名はエンコードが必要
- エラーハンドリングを忘れずに
ファイルダウンロード機能は、多くのWebアプリケーションで必要とされる重要な機能です。セキュリティに十分配慮しながら、readfile関数を活用して安全で使いやすいダウンロード機能を実装してください!
参考リンク
この記事が役に立ったら、ぜひシェアしてください!PHPに関する他の記事もお楽しみに。
