Загрузка изображений с превью AJAX + PHP + MySQL

В данной статье представлена упрощенная реализация загрузки изображений с превью через AJAX с сохранением в базу данных MySQL, а также дальнейший их вывод на примере модуля отзывов.

В примерах используется следующая структура файлов и директорий, находящихся в корне сайта:

index.php
upload_image.php
save_reviews.php
reviews.php
jquery.min.js
uploads/
├── tmp/

Первое что понадобится: HTML форма и JS скрипт, который после выбора одного или несколькольких файлов отправит их на upload_image.php через AJAX.

index.php

  1. <form method="post" action="/save_reviews.php">
  2. <h3>Отправить отзыв:</h3>
  3. <div class="form-row">
  4. <label>Ваше имя:</label>
  5. <input type="text" name="name" required>
  6. </div>
  7. <div class="form-row">
  8. <label>Комментарий:</label>
  9. <input type="text" name="text" required>
  10. </div>
  11. <div class="form-row">
  12. <label>Изображения:</label>
  13. <div class="img-list" id="js-file-list"></div>
  14. <input id="js-file" type="file" name="file[]" multiple accept=".jpg,.jpeg,.png,.gif">
  15. </div>
  16. <div class="form-submit">
  17. <input type="submit" name="send" value="Отправить">
  18. </div>
  19. </form>
  20.  
  21. <script src="/jquery.min.js"></script>
  22. <script>
  23. $("#js-file").change(function(){
  24. if (window.FormData === undefined) {
  25. alert('В вашем браузере загрузка файлов не поддерживается');
  26. } else {
  27. var formData = new FormData();
  28. $.each($("#js-file")[0].files, function(key, input){
  29. formData.append('file[]', input);
  30. });
  31.  
  32. $.ajax({
  33. type: 'POST',
  34. url: '/upload_image.php',
  35. cache: false,
  36. contentType: false,
  37. processData: false,
  38. data: formData,
  39. dataType : 'json',
  40. success: function(msg){
  41. msg.forEach(function(row) {
  42. if (row.error == '') {
  43. $('#js-file-list').append(row.data);
  44. } else {
  45. alert(row.error);
  46. }
  47. });
  48. $("#js-file").val('');
  49. }
  50. });
  51. }
  52. });
  53.  
  54. /* Удаление загруженной картинки */
  55. function remove_img(target){
  56. $(target).parent().remove();
  57. }
  58. </script>

CSS-стили для формы и вывода загруженных файлов:
  1. .form-row {
  2. margin-bottom: 15px;
  3. }
  4. .form-row label {
  5. display: block;
  6. color: #777;
  7. margin-bottom: 5px;
  8. }
  9. .form-row input[type="text"] {
  10. width: 100%;
  11. padding: 5px;
  12. box-sizing: border-box;
  13. }
  14.  
  15. /* Стили для вывода превью */
  16. .img-item {
  17. display: inline-block;
  18. margin: 0 20px 20px 0;
  19. position: relative;
  20. user-select: none;
  21. }
  22. .img-item img {
  23. border: 1px solid #767676;
  24. }
  25. .img-item a {
  26. display: inline-block;
  27. background: url(/remove.png) 0 0 no-repeat;
  28. position: absolute;
  29. top: -5px;
  30. right: -9px;
  31. width: 20px;
  32. height: 20px;
  33. cursor: pointer;
  34. }


В целях безопасности, во временной директории /uploads/ должно быть отключено выполнение PHP-скриптов и выключен листинг каталогов.

Тек же временную директорию /uploads/tmp/ будет нужно периодически очищать от старых файлов.

Скрипт upload_image.php
  1. <?php
  2. // Локаль.
  3. setlocale(LC_ALL, 'ru_RU.utf8');
  4. date_default_timezone_set('Europe/Moscow');
  5. mb_http_output('UTF-8');
  6. mb_language('uni');
  7.  
  8. //ini_set('display_errors', 1);
  9.  
  10. // Название <input type="file">
  11. $input_name = 'file';
  12.  
  13. if (!isset($_FILES[$input_name])) {
  14. }
  15.  
  16. // Разрешенные расширения файлов.
  17. $allow = array('jpg', 'jpeg', 'png', 'gif');
  18.  
  19. // URL до временной директории.
  20. $url_path = '/uploads/tmp/';
  21.  
  22. // Полный путь до временной директории.
  23. $tmp_path = $_SERVER['DOCUMENT_ROOT'] . $url_path;
  24.  
  25. if (!is_dir($tmp_path)) {
  26. mkdir($tmp_path, 0777, true);
  27. }
  28.  
  29. // Преобразуем массив $_FILES в удобный вид для перебора в foreach.
  30. $files = array();
  31. $diff = count($_FILES[$input_name]) - count($_FILES[$input_name], COUNT_RECURSIVE);
  32. if ($diff == 0) {
  33. $files = array($_FILES[$input_name]);
  34. } else {
  35. foreach($_FILES[$input_name] as $k => $l) {
  36. foreach($l as $i => $v) {
  37. $files[$i][$k] = $v;
  38. }
  39. }
  40. }
  41.  
  42. $response = array();
  43. foreach ($files as $file) {
  44. $error = $data = '';
  45.  
  46. // Проверим на ошибки загрузки.
  47. $ext = mb_strtolower(mb_substr(mb_strrchr(@$file['name'], '.'), 1));
  48. if (!empty($file['error']) || empty($file['tmp_name']) || $file['tmp_name'] == 'none') {
  49. $error = 'Не удалось загрузить файл.';
  50. } elseif (empty($file['name']) || !is_uploaded_file($file['tmp_name'])) {
  51. $error = 'Не удалось загрузить файл.';
  52. } elseif (empty($ext) || !in_array($ext, $allow)) {
  53. $error = 'Недопустимый тип файла';
  54. } else {
  55. $info = @getimagesize($file['tmp_name']);
  56. if (empty($info[0]) || empty($info[1]) || !in_array($info[2], array(1, 2, 3))) {
  57. $error = 'Недопустимый тип файла';
  58. } else {
  59. // Перемещаем файл в директорию с новым именем.
  60. $name = time() . '-' . mt_rand(1, 9999999999);
  61. $src = $tmp_path . $name . '.' . $ext;
  62. $thumb = $tmp_path . $name . '-thumb.' . $ext;
  63.  
  64. if (move_uploaded_file($file['tmp_name'], $src)) {
  65. // Создание миниатюры.
  66. switch ($info[2]) {
  67. case 1:
  68. $im = imageCreateFromGif($src);
  69. imageSaveAlpha($im, true);
  70. break;
  71. case 2:
  72. $im = imageCreateFromJpeg($src);
  73. break;
  74. case 3:
  75. $im = imageCreateFromPng($src);
  76. imageSaveAlpha($im, true);
  77. break;
  78. }
  79.  
  80. $width = $info[0];
  81. $height = $info[1];
  82.  
  83. // Высота превью 100px, ширина рассчитывается автоматически.
  84. $h = 100;
  85. $w = ($h > $height) ? $width : ceil($h / ($height / $width));
  86. $tw = ceil($h / ($height / $width));
  87. $th = ceil($w / ($width / $height));
  88.  
  89. $new_im = imageCreateTrueColor($w, $h);
  90. if ($info[2] == 1 || $info[2] == 3) {
  91. imagealphablending($new_im, true);
  92. imageSaveAlpha($new_im, true);
  93. $transparent = imagecolorallocatealpha($new_im, 0, 0, 0, 127);
  94. imagefill($new_im, 0, 0, $transparent);
  95. imagecolortransparent($new_im, $transparent);
  96. }
  97.  
  98. if ($w >= $width && $h >= $height) {
  99. $xy = array(ceil(($w - $width) / 2), ceil(($h - $height) / 2), $width, $height);
  100. } elseif ($w >= $width) {
  101. $xy = array(ceil(($w - $tw) / 2), 0, ceil($h / ($height / $width)), $h);
  102. } elseif ($h >= $height) {
  103. $xy = array(0, ceil(($h - $th) / 2), $w, ceil($w / ($width / $height)));
  104. } elseif ($tw < $w) {
  105. $xy = array(ceil(($w - $tw) / 2), ceil(($h - $h) / 2), $tw, $h);
  106. } else {
  107. $xy = array(0, ceil(($h - $th) / 2), $w, $th);
  108. }
  109.  
  110. imageCopyResampled($new_im, $im, $xy[0], $xy[1], 0, 0, $xy[2], $xy[3], $width, $height);
  111.  
  112. // Сохранение.
  113. switch ($info[2]) {
  114. case 1: imageGif($new_im, $thumb); break;
  115. case 2: imageJpeg($new_im, $thumb, 100); break;
  116. case 3: imagePng($new_im, $thumb); break;
  117. }
  118.  
  119. imagedestroy($new_im);
  120.  
  121. // Вывод в форму: превью, кнопка для удаления и скрытое поле.
  122. $data = '
  123. <div class="img-item">
  124. <img src="' . $url_path . $name . '-thumb.' . $ext . '">
  125. <a herf="#" onclick="remove_img(this); return false;"></a>
  126. <input type="hidden" name="images[]" value="' . $name . '.' . $ext . '">
  127. </div>';
  128. } else {
  129. $error = 'Не удалось загрузить файл.';
  130. }
  131. }
  132. }
  133.  
  134. $response[] = array('error' => $error, 'data' => $data);
  135. }
  136.  
  137. // Ответ в JSON.
  138. header('Content-Type: application/json');
  139. echo json_encode($response, JSON_UNESCAPED_UNICODE);
  140. exit();

Пример ответа AJAX запроса в случаи успешной загрузки файла:
  1. [{
  2. "error": "",
  3. "data": "
  4. <div class="img-item">
  5. <img src="/uploads/tmp/1610809179-108359805-thumb.jpg">
  6. <a herf="#" onclick="remove_img(this); return false;"></a>
  7. <input type="hidden" name="images[]" value="1610809179-108359805.jpg">
  8. </div>"
  9. }]

Полученный из AJAX запроса контент вставляется в конец дива id="js-file-list" с помощью jQuery метода append().

Скрытое поле «images» передает названия загруженных файлов следующему скрипту для сохранения в базе данных.

Сохранение формы в базе данных
После нажатия на кнопку «Отправить», форма отправляется методом POST на обработчик save_reviews.php. В нём полученные данные сохраняются в базе данных, а файлы переносятся в постоянную директорию хранения.

Понадобятся две таблицы, `reviews`:
  1. CREATE TABLE `reviews` (
  2. `id` int(11) UNSIGNED NOT NULL,
  3. `name` varchar(255) NOT NULL,
  4. `text` text NOT NULL,
  5. `date_add` int(11) UNSIGNED NOT NULL DEFAULT '0'
  6. ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
  7.  
  8. ALTER TABLE `reviews` ADD PRIMARY KEY (`id`);
  9. ALTER TABLE `reviews` MODIFY `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT;

И таблица `reviews_images` для хранения названий файлов:
  1. CREATE TABLE `reviews_images` (
  2. `id` int(11) UNSIGNED NOT NULL,
  3. `reviews_id` int(11) NOT NULL DEFAULT '0',
  4. `filename` varchar(255) NOT NULL
  5. ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
  6.  
  7. ALTER TABLE `reviews_images` ADD PRIMARY KEY (`id`);
  8. ALTER TABLE `reviews_images` MODIFY `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT;

Скрипт save_reviews.php
  1. <?php
  2. //ini_set('display_errors', 1);
  3.  
  4. // Временная директория.
  5. $tmp_path = $_SERVER['DOCUMENT_ROOT'] . '/uploads/tmp/';
  6.  
  7. // Постоянная директория.
  8. $path = $_SERVER['DOCUMENT_ROOT'] . '/uploads/';
  9.  
  10. // Подключение к БД.
  11. $dbh = new PDO('mysql:dbname=db_name;host=localhost', 'логин', 'пароль');
  12.  
  13. if (isset($_POST['send'])) {
  14. $name = htmlspecialchars($_POST['name'], ENT_QUOTES);
  15. $text = htmlspecialchars($_POST['text'], ENT_QUOTES);
  16.  
  17. $sth = $dbh->prepare("INSERT INTO `reviews` SET `name` = ?, `text` = ?, `date_add` = UNIX_TIMESTAMP()");
  18. $sth->execute(array($name, $text));
  19.  
  20. // Получаем id вставленной записи.
  21. $insert_id = $dbh->lastInsertId();
  22.  
  23. // Сохранение изображений в БД и перенос в постоянную папку.
  24. if (!empty($_POST['images'])) {
  25. foreach ($_POST['images'] as $row) {
  26. $filename = preg_replace("/[^a-z0-9\.-]/i", '', $row);
  27. if (!empty($filename) && is_file($tmp_path . $filename)) {
  28. $sth = $dbh->prepare("INSERT INTO `reviews_images` SET `reviews_id` = ?, `filename` = ?");
  29. $sth->execute(array($insert_id, $filename));
  30.  
  31. // Перенос оригинального файла
  32. rename($tmp_path . $filename, $path . $filename);
  33.  
  34. // Перенос превью
  35. $file_name = pathinfo($filename, PATHINFO_FILENAME);
  36. $file_ext = pathinfo($filename, PATHINFO_EXTENSION);
  37. $thumb = $file_name . '-thumb.' . $file_ext;
  38. rename($tmp_path . $thumb, $path . $thumb);
  39. }
  40. }
  41. }
  42. }
  43.  
  44. // Редирект, чтобы предотвратить повторную отправку по F5.
  45. header('Location: /reviews.php', true, 301);
  46. exit();

И последнее, PHP-скрипт для вывода отзывов с фотографиями из базы данных.

Скрипт reviews.php
  1. <?php
  2. // Подключение к БД.
  3. $dbh = new PDO('mysql:dbname=db_name;host=localhost', 'логин', 'пароль');
  4.  
  5. $sth = $dbh->prepare("SELECT * FROM `reviews` ORDER BY `date_add` DESC");
  6. $sth->execute();
  7. $items = $sth->fetchAll(PDO::FETCH_ASSOC);
  8.  
  9. if (!empty($items)) {
  10. ?>
  11. <h2>Отзывы</h2>
  12. <div class="reviews">
  13. <?php
  14. foreach ($items as $row) {
  15. $sth = $dbh->prepare("SELECT * FROM `reviews_images` WHERE `reviews_id` = ?");
  16. $sth->execute(array($row['id']));
  17. $images = $sth->fetchAll(PDO::FETCH_ASSOC);
  18. ?>
  19. <div class="reviews_item">
  20. <div class="reviews_item-name"><?php echo $row['name']; ?></div>
  21. <div class="reviews_item-text"><?php echo $row['text']; ?></div>
  22. <?php if (!empty($images)): ?>
  23. <div class="reviews_item-images">
  24. <?php foreach($images as $img): ?>
  25. <div class="reviews_item-img">
  26. <?php
  27. $name = pathinfo($img['filename'], PATHINFO_FILENAME);
  28. $ext = pathinfo($img['filename'], PATHINFO_EXTENSION);
  29. ?>
  30. <a href="/uploads/<?php echo $img['filename']; ?>" target="_blank">
  31. <img src="/uploads/<?php echo $name . '-thumb.' . $ext; ?>">
  32. </a>
  33. </div>
  34. <?php endforeach; ?>
  35. </div>
  36. <?php endif; ?>
  37. </div>
  38. <?php
  39. }
  40. ?>
  41. </div>
  42. <?php
  43. }
  44. ?>

CSS-стили списка:
  1. .reviews_item {
  2. background: #efefef;
  3. padding: 15px 30px 0px 30px;
  4. margin-bottom: 20px;
  5. }
  6. .reviews_item-name {
  7. font-weight: 900;
  8. font-size: 18px;
  9. margin-bottom: 5px;
  10. }
  11. .reviews_item-text {
  12. margin-bottom: 15px;
  13. font-size: 15px;
  14. line-height: 1.5;
  15. }
  16. .reviews_item-img {
  17. display: inline-block;
  18. margin: 0 15px 15px 0;
  19. }


  17.01.24 / 21:57 | AJAX |   85 | 1   0