Когда вообще нужно отдавать файлы через PHP? Например, когда нужно собрать какую-то статистику скачиваний файла или осуществить запрет на скачивание файлов, позволив скачивать по ссылке только один раз и ограниченной группе пользователей.

Допустим, чтобы скачать какой-то файл, мы отправляем пользователя не на прямую ссылку с расположением файла:

/uploads/files/program.exe

а на адрес скрипта, который отдает пользователю файл через PHP:

/files/download.php?file=$file_id

Где, в параметр file, для скрипта можно передавать идентификатор файла, который требуется скачать, после чего можно выстраивать различные проверки и выдавать пользователю файл для скачивания:

Header('location: /uploads/files/' . $file);

Сделать это можно несколькими способами, о которых речь пойдет ниже.

Функция readfile()

Этот метод будем применять в специально созданной функции. Такая функция поможет отправлять даже большие файлы, PHP будет отдавать файл пользователю по частям. Функция ждет, когда файл будет прочтен и отдан.

 /**
 * Отдача файла
 * Функция для отдачи файла через PHP
 * @param string $file путь к файлу на сервере
 * @return mixed
 */
function file_download($file) {
  if (file_exists($file)) {
    if (ob_get_level()) {
      ob_end_clean();
    }

    header('Content-Description: File Transfer');
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename=' . basename($file));
    header('Content-Transfer-Encoding: binary');
    header('Expires: 0');
    header('Cache-Control: must-revalidate');
    header('Pragma: public');
    header('Content-Length: ' . filesize($file));
    readfile($file);
    return [
		'status' => 'success',
		'message' => 'Файл успешно отдан'
	];
  }else {
	return [
		'status' => 'error',
		'message' => 'Файл не найден'
	];
  }
}

Чтение и отправка файла вручную

Эта функция аналогична той, которая описана выше, но для чтения и отдали файла используются: fopen, feof, fread, fclose. Функция ждет когда файл будет прочитан и отдан, также позволяет экономить память. 
 

 /**
 * Отдача файла
 * Функция для отдачи файла через PHP
 * @param string $file путь к файлу на сервере
 * @return mixed
 */
function file_download($file) {
  if (file_exists($file)) {
    if (ob_get_level()) {
      ob_end_clean();
    }

    header('Content-Description: File Transfer');
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename=' . basename($file));
    header('Content-Transfer-Encoding: binary');
    header('Expires: 0');
    header('Cache-Control: must-revalidate');
    header('Pragma: public');
    header('Content-Length: ' . filesize($file));
    
    if ($fd = fopen($file, 'rb')) {
      while (!feof($fd)) {
        print fread($fd, 1024);
      }
      fclose($fd);
    }

    return [
		'status' => 'success',
		'message' => 'Файл успешно отдан'
	];
  }else {
	return [
		'status' => 'error',
		'message' => 'Файл не найден'
	];
  }
}

Отдаем файл через сервер

Отдать файл можем не скриптом, через PHP, а с помощью Apache или Nginx. Отдача файла средствами сервера дает максимальное быстродействие, минимум потребляет памяти и ресурсов сервера.

Для Apache есть модуль XSendFile, который поможет с помощью специального заголовка сделать отправку файла Apache. В настройках хоста включитте директиву перехвата заголовка:

XSendFile On

Функция для отправки файла будет следующей:

 /**
 * Отдача файла
 * Функция для отдачи файла через PHP
 * @param string $file путь к файлу на сервере
 * @return mixed
 */
function file_download($file) {
  if (file_exists($file)) {
    header('X-SendFile: ' . realpath($file));
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename=' . basename($file));

    return [
		'status' => 'success',
		'message' => 'Файл успешно отдан'
	];
  }else {
	return [
		'status' => 'error',
		'message' => 'Файл не найден'
	];
  }
}

Nginx умеет отправку файла из коробки, все что нужно, настроить конфиг, указав запрет на доступ к каталогу (my/path/protected/):

location /protected/ {
  internal;
  root   /my/path;
}

Функция отправки файла выглядит так:

 /**
 * Отдача файла
 * Функция для отдачи файла через PHP
 * @param string $file путь к файлу на сервере
 * @return mixed
 */
function file_download($file) {
  if (file_exists($file)) {
    header('X-Accel-Redirect: ' . $file);
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename=' . basename($file));
    
    return [
		'status' => 'success',
		'message' => 'Файл успешно отдан'
	];
  }else {
	return [
		'status' => 'error',
		'message' => 'Файл не найден'
	];
  }
}