Способы вывода файлов в PHP скриптах
Основные методы вывода файлов
Самый эффективный подход к показу содержимого файла (текст, изображение, PDF) - использование функции readfile() в сочетании с правильными HTTP заголовками. Этот метод не загружает файл в память PHP (если только не настроено иначе), а считывает его и отправляет напрямую клиенту потоком, что оптимально по скорости и потреблению ресурсов, особенно для больших файлов.
<?php
$file = 'path/to/example.pdf';
if (file_exists($file)) {
header('Content-Description: File Transfer');
header('Content-Type: application/pdf');
header('Content-Disposition: inline; filename="document.pdf"');
header('Content-Length: ' . filesize($file));
header('Pragma: public');
readfile($file);
exit;
} else {
http_response_code(404);
echo 'Файл не найден.';
}
?>
Пояснение: Заголовок Content-Type сообщает браузеру тип файла. Content-Disposition: inline предписывает отобразить файл внутри окна браузера (а не скачать). Для принудительного скачивания используется attachment. Content-Length позволяет отображать прогресс загрузки. После вызова readfile обязательно добавляется exit, чтобы не выводить случайный мусор.
Типичные ошибки и их решения:
- Ошибка "Headers already sent": возникает, если перед вызовом
header()уже был вывод (echo, пробелы, BOM). Следует убедиться, что в скрипте нет лишних пробелов до <?php. - Файл не читается, хотя существует: возможна проблема с правами доступа. Рекомендуется использовать
is_readable()для проверки. - Неверный MIME тип: браузер может попытаться показать файл как текст. Для определения реального типа применяются функции
mime_content_type()илиfinfo. - Слишком большой файл: может превысить время выполнения скрипта. Устанавливается
set_time_limit(0)или применяетсяreadfileс chunked transfer.
Как вывести содержимое текстового файла с сохранением переносов строк?
Если нужно показать обычный текст без обработки PHP, удобно применение file_get_contents() с последующим экранированием вывода для предотвращения XSS:
<?php
$content = file_get_contents('data.txt');
$escaped = htmlspecialchars($content, ENT_QUOTES, 'UTF-8');
echo '<pre>' . $escaped . '</pre>';
?>
Важно: Если файл слишком велик, file_get_contents() загрузит его в память целиком, что может привести к исчерпанию памяти. В таком случае лучше использовать построчное чтение с fgets().
Ошибки: Пропуск htmlspecialchars приводит к XSS-уязвимости при выводе пользовательского контента. Неправильная кодировка (например, UTF-8 vs Windows-1251) отобразит кракозябры. Правильный заголовок Content-Type: text/html; charset=UTF-8 устанавливается заранее.
Как отдать файл на скачивание, не отображая его?
Для принудительного скачивания используется заголовок Content-Disposition: attachment. Пример с readfile:
<?php
$file = 'confidential.docx';
if (file_exists($file)) {
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
header('Content-Length: ' . filesize($file));
readfile($file);
exit;
}
?>
Примечание: application/octet-stream заставляет браузер скачивать независимо от типа. Для лучшего пользовательского опыта можно указать настоящий MIME тип.
Проблемы: Если файл очень большой (более 2 ГБ на 32-битных системах), filesize может вернуть неверное значение. Используется fread с циклом и отправка частями (chunks). Также возможны тайм-ауты - может применяться ignore_user_abort(true).
Как безопасно отобразить изображение через PHP, блокируя горячие ссылки?
PHP может выступать шлюзом для изображений, проверяя сессию или referrer. Пример с защитой по сессии:
<?php
session_start();
if (!isset($_SESSION['user'])) {
http_response_code(403);
die('Доступ запрещён.');
}
$file = 'photos/' . basename($_GET['img']);
$allowed = ['jpg', 'png', 'gif'];
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed)) {
http_response_code(400);
die('Недопустимый формат.');
}
header('Content-Type: image/' . $ext);
readfile($file);
?>
Внимание: basename используется для предотвращения path traversal, но не защищает от подбора имен. Храните файлы вне document root, если важна безопасность.
Типичные ошибки: Неправильно определён MIME тип для PNG (image/png) или GIF (image/gif). Для точного определения используется exif_imagetype. Если изображение не отображается в браузере, следует проверить наличие ошибок в ответе (например, лишний вывод после readfile).
Как получить список файлов в папке с сортировкой и фильтрацией?
Для работы с файловой системой удобно использовать итераторы SPL. Пример с DirectoryIterator:
<?php
$dir = './documents';
$files = [];
$iterator = new DirectoryIterator($dir);
foreach ($iterator as $fileinfo) {
if (!$fileinfo->isDot() && $fileinfo->isFile()) {
$files[] = $fileinfo->getFilename();
}
}
sort($files);
echo '<ul>';
foreach ($files as $filename) {
echo '<li>' . htmlspecialchars($filename) . '</li>';
}
echo '</ul>';
?>
Альтернативы: scandir() проще, но не даёт дополнительной информации; glob() позволяет использовать шаблоны (например, *.txt).
Проблемы: При большом количестве файлов (тысячи) сканирование может быть медленным. Для надёжности перед доступом следует проверить is_readable(). Символические ссылки могут привести к зацикливанию при рекурсивном обходе - рекомендуется RecursiveDirectoryIterator с осторожностью.
Продвинутые сценарии вывода файлов
Пример 1: Динамическое определение MIME типа с помощью finfo
Вместо жёстко заданного Content-Type тип файла определяется автоматически с помощью расширения Fileinfo.
<?php
$file = 'upload/sample.png';
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file);
finfo_close($finfo);
header('Content-Type: ' . $mimeType);
header('Content-Length: ' . filesize($file));
readfile($file);
?>
Результат: браузер получает заголовок Content-Type: image/png и корректно отображает PNG изображение. Если файл был JPEG, будет соответственно image/jpeg. Такой подход универсален для любого типа файла.
Пример 2: Защищённая передача файла с проверкой прав через хеш и лимит времени
Файл доступен только определённое время или для определённого пользователя с помощью временного токена.
<?php
$secret = 'мой_секрет';
$file = '/protected/report.pdf';
$expires = time() + 3600; // ссылка живёт 1 час
$token = md5($secret . $file . $expires);
$link = "download.php?file=" . urlencode($file) . "&expires=$expires&token=$token";
echo "Временная ссылка: <a href='$link'>Скачать</a>";
?>
На странице download.php производится проверка:
<?php
$secret = 'мой_секрет';
$file = $_GET['file'] ?? '';
$expires = (int)($_GET['expires'] ?? 0);
$token = $_GET['token'] ?? '';
if (time() > $expires) {
die('Срок ссылки истёк.');
}
if (md5($secret . $file . $expires) !== $token) {
die('Неверный токен.');
}
if (!file_exists($file)) {
http_response_code(404);
die('Файл не найден.');
}
header('Content-Type: ' . mime_content_type($file));
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
header('Content-Length: ' . filesize($file));
readfile($file);
exit;
?>
Результат: пользователь, имеющий валидную ссылку, может скачать файл в течение часа. После истечения или при подделке токена доступ блокируется.
Пример 3: Чтение и вывод большого файла частями (chunked output) без использования readfile
Для очень больших файлов (несколько гигабайт) применяется собственный механизм чтения с контролем памяти.
<?php
$file = '/backup/big.iso';
$chunkSize = 1024 * 1024; // 1 MB
$handle = fopen($file, 'rb');
if (!$handle) {
die('Не удалось открыть файл.');
}
header('Content-Type: application/octet-stream');
header('Content-Length: ' . filesize($file));
header('Content-Disposition: attachment; filename="big.iso"');
while (!feof($handle)) {
$chunk = fread($handle, $chunkSize);
if ($chunk === false) break;
echo $chunk;
ob_flush();
flush();
}
fclose($handle);
exit;
?>
Результат: файл передаётся порциями по 1 МБ, что позволяет отдавать файлы любого размера без превышения лимита памяти PHP. Следует отметить, что readfile внутренне делает то же самое, но в некоторых случаях требуется тонкая настройка.
Пример 4: Вывод изображения с наложенным водяным знаком (GD)
PHP динамически генерирует изображение, накладывая водяной знак на фотографию.
<?php
$source = 'photo.jpg';
$stamp = 'watermark.png';
$im = imagecreatefromjpeg($source);
$stampIm = imagecreatefrompng($stamp);
if (!$im || !$stampIm) {
die('Ошибка загрузки изображений.');
}
$sx = imagesx($stampIm);
$sy = imagesy($stampIm);
imagecopy($im, $stampIm, imagesx($im) - $sx - 10, imagesy($im) - $sy - 10, 0, 0, $sx, $sy);
header('Content-Type: image/jpeg');
imagejpeg($im, null, 90);
imagedestroy($im);
imagedestroy($stampIm);
?>
Результат: браузер получает JPEG изображение с водяным знаком в правом нижнем углу. Качество установлено 90. Для PNG используется imagepng().
Пример 5: Рекурсивный список файлов с вложенными папками и размерами
Использование RecursiveDirectoryIterator для обхода дерева каталогов.
<?php
$dir = new RecursiveDirectoryIterator('./projects');
$iterator = new RecursiveIteratorIterator($dir, RecursiveIteratorIterator::SELF_FIRST);
echo '<table border="1">';
foreach ($iterator as $fileinfo) {
if ($fileinfo->isFile()) {
echo '<tr><td>' . htmlspecialchars($fileinfo->getPathname()) . '</td><td>' . $fileinfo->getSize() . ' байт</td></tr>';
}
}
echo '</table>';
?>
Результат: HTML-таблица со всеми файлами (рекурсивно) из папки projects и их размерами. Для папки с сотнями файлов следует добавлять кэширование или лимит.
Пример 6: Отдача файла с поддержкой докачки (Range запросы)
Поддержка HTTP Range заголовков позволяет возобновлять прерванную загрузку. Это сложный, но полезный сценарий.
<?php
$file = '/video/tutorial.mp4';
$handle = fopen($file, 'rb');
if (!$handle) {
header('HTTP/1.1 500 Internal Server Error');
exit;
}
$fileSize = filesize($file);
$range = $_SERVER['HTTP_RANGE'] ?? null;
if ($range) {
preg_match('/bytes=(\d+)-(\d*)/', $range, $matches);
$start = (int)$matches[1];
$end = $matches[2] === '' ? $fileSize - 1 : (int)$matches[2];
header('HTTP/1.1 206 Partial Content');
header("Content-Range: bytes $start-$end/$fileSize");
header('Content-Length: ' . ($end - $start + 1));
fseek($handle, $start);
while ($start <= $end) {
$chunk = fread($handle, 8192);
echo $chunk;
$start += strlen($chunk);
flush();
}
} else {
header('HTTP/1.1 200 OK');
header('Content-Length: ' . $fileSize);
header('Content-Type: video/mp4');
readfile($file);
}
fclose($handle);
exit;
?>
Результат: при обычном запросе видео передаётся целиком; при запросе с заголовком Range (например, после паузы) сервер отдаёт только запрошенный диапазон, что позволяет возобновить скачивание.
Пример 7: Отображение содержимого файла в защищённой папке с помощью файлового сервера
Все файлы хранятся вне document root, доступ только через PHP.
<?php
$safeBase = dirname(__DIR__) . '/private_files/';
$filename = basename($_GET['file']); // очищаем от путей
$fullPath = $safeBase . $filename;
if (!file_exists($fullPath)) {
http_response_code(404);
exit;
}
// Дополнительно можно проверить права пользователя
header('Content-Type: ' . mime_content_type($fullPath));
header('Content-Length: ' . filesize($fullPath));
readfile($fullPath);
?>
Результат: пользователь может скачать файл, только если знает точное имя и имеет доступ. Директория private_files недоступна через веб-сервер напрямую.