Способы вывода файлов в PHP скриптах

Раздел: Веб-разработка на PHP -> Работа с файлами в 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 недоступна через веб-сервер напрямую.

Показ файлов в PHP - comments

En
Files show php file (php)