Сегодня загрузка файлов является практически неотъемлемым атрибутом современного web приложения. В данной статье речь пойдёт о том, как же загрузить файл(ы) на сервер с помощью PHP.

Настройка php.ini

[Resource Limits]
; Максимальное время выполнения скрипта в секундах
max_execution_time = 60

; Максимальное потребление памяти одним скриптом
memory_limit = 64M

[Data Handling]
; Максимально допустимый размер данных отправляемых методом POST
post_max_size = 5M

[File Uploads]
; Разрешение на загрузку файлов
file_uploads = On

; Папка для хранения файлов во время загрузки
upload_tmp_dir = home/user/temp

; Максимальный размер загружаемого файла
upload_max_filesize = 5M

; Максимально разрешённое количество одновременно загружаемых файлов
max_file_uploads = 10

Конфигурационный файл php.ini необходимо настраивать согласно бизнес-логики проекта! Например, мы планируем загружать не более десяти файлов до 2 Мбайт, а это значит нам понадобиться ~20 Мбайт памяти.

Загрузка одного файла на сервер из формы

Для начала разберём механизм загрузки одной картинки на сервер. Для загрузки картинки с компьютера пользователя необходимо с помощью HTML-формы отправить нужный (выбранный) файл PHP-скрипту upload.php методом POST и указать способ кодирования данных enctype="multipart/form-data" (в данном случае данные не кодируются и это значение применяется только для отправки бинарных файлов).

<form action="upload.php" method="post" enctype="multipart/form-data">
  <input type="file" name="image">
  <button type="submit">Загрузить</button>
</form>

После отправки файла PHP-скрипту upload.php его можно перехватить с помощью суперглобальной переменной $_FILES с таким же именем, которая в массиве содержит информацию о файле (в нашем случае image):

var_dump($_FILES);

Получим массив:

Array
(
  [name]     => image.jpg                // оригинальное имя файла
  [type]     => image/jpeg                 // MIME-тип файла
  [tmp_name] => home\user\temp\phpD07E.tmp // бинарный файл
  [error]    => 0                          // код ошибки
  [size]     => 17170                      // размер файла в байтах
)

Не всем данным из $_FILES можно доверять: MIME-тип и размер файла можно подделать, т. к. они формируются из HTTP-ответа, а расширению в имени файла не стоит доверять в силу того, что за ним может скрываться совершенно другой файл. Тем не менее, дальше нам нужно проверить корректно ли загрузился наш файл и загрузился ли он вообще. Для этого необходимо проверить ошибки в $_FILES['image']['error'] и удостовериться, что файл загружен методом POST с помощью функции is_uploaded_file(). Если что-то идёт не по плану, значит выводим ошибку на экран:

// Если в $_FILES существует "image" и она не NULL
if (isset($_FILES['image'])) {
  $image = $_FILES['image'];
  // Получаем нужные элементы массива "image"
  $fileTmpName = $_FILES['image']['tmp_name'];
  $errorCode = $_FILES['image']['error'];
  // Проверим на ошибки
  if ($errorCode !== UPLOAD_ERR_OK || !is_uploaded_file($fileTmpName)) {
    // Массив с названиями ошибок
    $errorMessages = [
      UPLOAD_ERR_INI_SIZE   => 'Размер файла превысил значение upload_max_filesize в конфигурации PHP.',
      UPLOAD_ERR_FORM_SIZE  => 'Размер загружаемого файла превысил значение MAX_FILE_SIZE в HTML-форме.',
      UPLOAD_ERR_PARTIAL    => 'Загружаемый файл был получен только частично.',
      UPLOAD_ERR_NO_FILE    => 'Файл не был загружен.',
      UPLOAD_ERR_NO_TMP_DIR => 'Отсутствует временная папка.',
      UPLOAD_ERR_CANT_WRITE => 'Не удалось записать файл на диск.',
      UPLOAD_ERR_EXTENSION  => 'PHP-расширение остановило загрузку файла.',
    ];
    // Зададим неизвестную ошибку
    $unknownMessage = 'При загрузке файла произошла неизвестная ошибка.';
    // Если в массиве нет кода ошибки, скажем, что ошибка неизвестна
    $outputMessage = isset($errorMessages[$errorCode]) ? $errorMessages[$errorCode] : $unknownMessage;
    // Выведем название ошибки
    die($outputMessage);
  } else {
    echo 'Ошибок нет.';
  }
};

Для того, чтобы "редиска" не загрузил вредоносный код, встроенный в изображение, нельзя доверять функции getimagesize(), которая также возвращает MIME-тип. Функция ожидает, что первый аргумент является ссылкой на корректный файл изображения. Определить настоящий MIME-тип картинки можно через расширение FileInfo. Код ниже проверит наличие ключевого слова image в типе нашего загружаемого файла и если его не окажется, выдаст ошибку:

// Создадим ресурс FileInfo
$fi = finfo_open(FILEINFO_MIME_TYPE);

// Получим MIME-тип
$mime = (string) finfo_file($fi, $fileTmpName);

// Проверим ключевое слово image (image/jpeg, image/png и т. д.)
if (strpos($mime, 'image') === false) die('Можно загружать только изображения.');

Таким образом, при необходимости, делаем проверку и на другие MIME-типы. Например, для zip архивов проверка будет такая:

// Проверим ключевое слово zip (application/zip)
if (strpos($mime, 'zip') === false) die('Можно загружать только архивы ZIP.');

На данном этапе мы уже можем загружать абсолютно любые картинки на наш сервер, прошедшие проверку на MIME-тип, но для загрузки изображений по определённым характеристикам нам необходимо валидировать их с помощью функции getimagesize(), которой отдадим сам бинарный файл $_FILES['image']['tmp_name']. В результате мы получим массив из элементов:

// Результат функции запишем в переменную
$image = getimagesize($fileTmpName);
var_dump($image);die;

// Результат
Array
(
  [0]        => 1280                      // ширина
  [1]        => 768                       // высота
  [2]        => 2                         // тип
  [3]        => width="1280" height="768" // атрибуты для HTML
  [bits]     => 8                         // глубина цвета
  [channels] => 3                         // цветовая модель
  [mime]     => image/jpeg                // MIME-тип
)

Для дальнейшей валидации изображения и работы над ним нам необходимо знать только 3 значения: ширину, высоту и размер файла (для вычисления размера применим функцию filesize() для бинарного файла из временной папки).

// Результат функции запишем в переменную
$image = getimagesize($fileTmpName);

// Зададим ограничения для картинок
$limitBytes  = 1024 * 1024 * 5;
$limitWidth  = 1280;
$limitHeight = 768;

// Проверим нужные параметры
if (filesize($fileTmpName) > $limitBytes) die('Размер изображения не должен превышать 5 Мбайт.');
if ($image[1] > $limitHeight)             die('Высота изображения не должна превышать 768 точек.');
if ($image[0] > $limitWidth)              die('Ширина изображения не должна превышать 1280 точек.');

После всех проверок мы можем с уверенностью переместить наш загружаемый файл в какую-нибудь директорию с картинками. Делать лучше это через функцию move_uploaded_file(), которая работает в безопасном режиме. Перед перемещением файла нельзя забыть сгенерировать случайное имя и расширение из типа изображения для нашего файла. Вот так это выглядит:

// Сгенерируем новое имя файла на основе MD5-хеша
$name = md5_file($fileTmpName);

// Сгенерируем расширение файла на основе типа картинки
$extension = image_type_to_extension($image[2]);

// Сократим .jpeg до .jpg
$format = str_replace('jpeg', 'jpg', $extension);

// Переместим картинку с новым именем и расширением в папку /upload
if (!move_uploaded_file($fileTmpName, __DIR__ . '/upload/' . $name . $format)) {
  die('При записи изображения на диск произошла ошибка.');
}

echo 'Картинка успешно загружена!';

Вместо простого способа генерации имени файла на основе MD5-хеша можно пойти более продвинутым путём, а именно написать отдельную функцию, которая будет проверять уникальность названия картинки для того, чтобы случайно не перезаписать уже загруженный файл. Если такого названия ещё нет, функция сгенерирует его. Такая проблема появляется в больших проектах и с большим количеством картинок. Но всё же)

function getRandomFileName($path)
{
  $path = $path ? $path . '/' : '';
  do {
    $name = md5(microtime() . rand(0, 9999));
    $file = $path . $name;
  } while (file_exists($file));

  return $name;
}

Генерация имени для картинки теперь будет такой:

// Сгенерируем новое имя файла через функцию getRandomFileName()
$name = getRandomFileName($fileTmpName);

Мы реализовали простой, но в тоже время практичный (с точки зрения безопасности) механизм загрузки файла на сервер. Весь код целиком лежит здесь .

Загрузка нескольких файлов на сервер из формы

Разберём механизм загрузки нескольких изображений за один раз с локальной машины пользователя. Продолжим дальше работать с $_FILES. Наша новая HTML-форма будет немного отличаться от старой.

<form action="upload.php" method="post" enctype="multipart/form-data">
  <input type="file" name="images[]" multiple>
  <button type="submit">Загрузить</button>
</form>

Как видно в конец имени поля выбора файла name="images[]" добавились фигурные скобки и атрибут multiple, который разрешает браузеру выбрать несколько файлов. Все файлы снова загрузятся во временную папку, если не будет никаких ошибок в php.ini. Перехватить их можно в $_FILES, но на этот раз суперглобальная переменная будет иметь неудобную структуру для обработки данных в массиве. Решается эта задача небольшими манипуляциями с массивом:

// Изменим структуру $_FILES
foreach($_FILES['images'] as $key => $value) {
  foreach($value as $k => $v) {
    $_FILES['images'][$k][$key] = $v;
  }
  // Удалим старые ключи
  unset($_FILES['images'][$key]);
}

// Загружаем все картинки по порядку
foreach ($_FILES['images'] as $k => $v) {
  // Загружаем по одному файлу  
  $fileTmpName = $_FILES['images'][$k]['tmp_name'];  
  $errorCode = $_FILES['images'][$k]['error'];
	
	// Проверим на ошибки
	// ...

}

Мы реализовали механизм загрузки нескольких файлов на сервер. Весь код целиком лежит здесь .