Про загрузку файлов на сервер я писал здесь. Сегодня я расскажу про асинхронную загрузку файлов на сервер.

В настоящее время довольно популярным решением для веб-сайтов является работа пользователя со страницей без ее перезагрузки. В большинстве своём это делается с помощью Ajax – технологии асинхронного взаимодействия с сервером, основанной на объекте XMLHttpRequest.

В этой статье рассмотрим простое решение одной из самых распространенных задач – асинхронная загрузка файла на сервер при помощи PHP. Задача будет следующая: На странице (предположим, что это страница в личном кабинете пользователя) есть форма с input типа file и кнопка Отправить. Ниже находится контейнер с картинкой (аватар пользователя по задумке) в который асинхронно подгружается картинка после выполнения AJAX запроса. И по умолчанию она просто выводится из базы данных. То есть, при загрузке картинки новое сгенерированное название будет записываться в БД, сама картинка под этим названием загружаться на сервер и выводится в контейнере взамен предыдущей. И всё это асинхронно.

Сразу скажу, что я по бОльшей степени преследовал цель описать сам процесс загрузки, особо не сосредотачиваясь на "элегантности" кода ). Кто как захочет использовать этот код. Кто-то кусочек нужный возьмёт, кто-то целый класс напишет на его основе, кто-то переделает для себя, кто-то вовсе не будет его использовать никак. Тем более, там действительно, нужна доработка под боевые задачи. Например, валидацию на MIME-типы (и другие проверки) нужно делать ещё в JS до попадания данных скрипту PHP. В скрипт данные должны приходить отвалидированные. Или при загрузке картинки удалять на сервере старую картинку. Поэтому я решил, что полезнее будет подробно описать процесс, нежели допиливать до идеала то, что каждый для себя доработает как нужно. Поехали.

Безусловно, подключаем jQuery (если ещё в проекте нигде данная библиотека не подключена):

<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>

Простая разметка HTML формы и картинка из базы. Предположим, что у вас уже есть база данных и в базе уже есть таблица (например, user). В блоке с id="photo-content" выводится картинка из базы. Я весь этот код помещу в файле index.php.

<?
$db = new PDO('mysql:host=localhost;dbname=test', 'root', '');
$query = "SELECT `avatar` FROM `user` WHERE `id` = ?";
$id = 1;
$stmt = $db->prepare($query);
$stmt->execute([$id]);
$image = $stmt->fetchColumn();
?>

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Ajax Upload</title>
  <link rel="stylesheet" href="main.css">
</head>
<body>
<div id="wrapper">
  <h1>Image upload</h1>
  <form method="post" enctype="multipart/form-data" id="form-file-ajax" action="upload.php">
    <input type="file" id="file" name="file" required>
    <br/>
    <button type="submit" id="btn-file-upload">Загрузить</button>
    <!-- preloader.gif - картинка имитирующая процесс загрузки -->
    <div id="process"><img src="preloader.gif" alt="Loading"></div>
    <div id="photo-content">
      <!-- Эта картинка выводится из базы по умолчанию -->
      <img src="http://site.com/upload/<?= $image ?>" alt="Image" width="400">
    </div>
  </form>
  <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
  <!-- Этот файл будет содержать код отправки данных PHP скрипту -->
  <script src="ajax.js"></script>
</div>
</body>
</html>

Немного CSS стилей:

#wrapper{
  width: 60%;
  margin: 20px auto;
}
form button{
  margin-bottom: 50px;
}
input[type=text],
input[type=file]{
  margin-bottom: 20px;
}

#process {
  display: none;
}

В файле ajax.js:

$(document).ready(function(){
  $("#form-file-ajax").on('submit', function(e){
    e.preventDefault();
    var formData = new FormData();
    var form = $(this);
    formData.append('file', $('#file').prop("files")[0]);
    $.ajax({
      url: form.attr('action'),
      type: form.attr('method'),
      processData: false,
      contentType: false,
      cache:false,
      dataType : 'text',
      data: formData,
      // Будет вызвана перед осуществлением AJAX запроса
      beforeSend: function(){
        $('#process').fadeIn();
      },
      // будет вызвана после завершения ajax-запроса
      // (вызывается позднее функций-обработчиков успешного (success) или аварийного (error)
      complete: function () {
        $('#process').fadeOut();
      },
      success: function(data){
        //form[0].reset();
        data = JSON.parse(data);
        var image = '<div class="img-item"><img src="http://site.com/upload/'+data.file+'" width="400"></div>';
        var photoContent = $("#photo-content");
        photoContent.html('');
        photoContent.append(image);
      },
      error: function(data){
        console.log(data);
      }
    });
  });
});

В файле upload.php:

<?php
if(isset($_FILES['file'])) {

  if ($_FILES['file']['name'] !== '' && $_FILES['file']['error'] == 0) {
    try {
      // MIME-типы нужно проверять ещё в JS коде и выводить ошибки пользователю
      // Сейчас они вываливаются во вкладке "Network" браузера
      $fileTmpName = $_FILES['file']['tmp_name'];
      $fi = finfo_open(FILEINFO_MIME_TYPE);
      $mime = (string) finfo_file($fi, $fileTmpName);
      if (strpos($mime, 'image') === false) die('Можно загружать только изображения с расширениями  .jpg, .jpeg, .png!');
      $image = getimagesize($fileTmpName);
      $extension = image_type_to_extension($image[2]);
      $name = randomFileName($extension);      
      $file = $name.str_replace('jpeg', 'jpg', $extension);
      if (!move_uploaded_file($fileTmpName, __DIR__ . '/upload/'.$file)) {
          throw new Exception('При загрузке изображения произошла ошибка на сервере!');
      }
      // Записать имя файла в БД
      $db = new PDO('mysql:host=localhost;dbname=ajax', 'root', 'password');
      $user_id = 1;
      $query = "UPDATE `user` SET `avatar` = :avatar WHERE `id` = :user_id";
      $params = [':avatar' => $file, ':user_id' => $user_id];
      $stmt = $db->prepare($query);
      if (!$stmt->execute($params)) {
          throw new Exception('Произошла ошибка при записи в БД!');
      }
      // Записать в $data имя файла
      $data = ['file' => $file];
      echo json_encode($data);
    } catch (Exception $e) {
      die($e->getMessage());
    }
  }
}


// Генерируем уникальное имя для файла
function randomFileName($extension = false)
{
  $extension = $extension ? '.' . $extension : '';
  do {
    $name = md5(microtime() . rand(0, 9999));
    $file = $name . $extension;
  } while (file_exists($file));

  return $file;
}

Вот и все. Ясное дело, что это не идеальное решение и как использовать его (если использовать) решать каждому самостоятельно. Может быть в дальнейшем я напишу специальный класс со всеми недочётами. А пока, как-то так.

Для работы данного примера без ошибок не забудьте заменить все адреса своими и указать корректные доступы к базе данных!