Загрузка изображений с превью 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


<form method="post" action="/save_reviews.php">
<h3>Отправить отзыв:</h3>
<div class="form-row">
<label>Ваше имя:</label>
<input type="text" name="name" required>
</div>
<div class="form-row">
<label>Комментарий:</label>
<input type="text" name="text" required>
</div>
<div class="form-row">
<label>Изображения:</label>
<div class="img-list" id="js-file-list"></div>
<input id="js-file" type="file" name="file[]" multiple accept=".jpg,.jpeg,.png,.gif">
</div>
<div class="form-submit">
<input type="submit" name="send" value="Отправить">
</div>
</form>

<script src="/jquery.min.js"></script>
<script>
$("#js-file").change(function(){
if (window.FormData === undefined) {
alert('В вашем браузере загрузка файлов не поддерживается');
} else {
var formData = new FormData();
$.each($("#js-file")[0].files, function(key, input){
formData.append('file[]', input);
});

$.ajax({
type: 'POST',
url: '/upload_image.php',
cache: false,
contentType: false,
processData: false,
data: formData,
dataType : 'json',
success: function(msg){
msg.forEach(function(row) {
if (row.error == '') {
$('#js-file-list').append(row.data);
} else {
alert(row.error);
}
});
$("#js-file").val('');
}
});
}
});

/* Удаление загруженной картинки */
function remove_img(target){
$(target).parent().remove();
}
</script>

CSS-стили для формы и вывода загруженных файлов:

.form-row {
margin-bottom: 15px;
}
.form-row label {
display: block;
color: #777;
margin-bottom: 5px;
}
.form-row input[type="text"] {
width: 100%;
padding: 5px;
box-sizing: border-box;
}

/* Стили для вывода превью */
.img-item {
display: inline-block;
margin: 0 20px 20px 0;
position: relative;
user-select: none;
}
.img-item img {
border: 1px solid #767676;
}
.img-item a {
display: inline-block;
background: url(/remove.png) 0 0 no-repeat;
position: absolute;
top: -5px;
right: -9px;
width: 20px;
height: 20px;
cursor: pointer;
}


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

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

Скрипт upload_image.php

<?php
// Локаль.
setlocale(LC_ALL, 'ru_RU.utf8');
date_default_timezone_set('Europe/Moscow');
mb_internal_encoding('UTF-8');
mb_regex_encoding('UTF-8');
mb_http_output('UTF-8');
mb_language('uni');

//ini_set('display_errors', 1);

// Название <input type="file">
$input_name = 'file';

if (!isset($_FILES[$input_name])) {
exit;
}

// Разрешенные расширения файлов.
$allow = array('jpg', 'jpeg', 'png', 'gif');

// URL до временной директории.
$url_path = '/uploads/tmp/';

// Полный путь до временной директории.
$tmp_path = $_SERVER['DOCUMENT_ROOT'] . $url_path;

if (!is_dir($tmp_path)) {
mkdir($tmp_path, 0777, true);
}

// Преобразуем массив $_FILES в удобный вид для перебора в foreach.
$files = array();
$diff = count($_FILES[$input_name]) - count($_FILES[$input_name], COUNT_RECURSIVE);
if ($diff == 0) {
$files = array($_FILES[$input_name]);
} else {
foreach($_FILES[$input_name] as $k => $l) {
foreach($l as $i => $v) {
$files[$i][$k] = $v;
}
}
}

$response = array();
foreach ($files as $file) {
$error = $data = '';

// Проверим на ошибки загрузки.
$ext = mb_strtolower(mb_substr(mb_strrchr(@$file['name'], '.'), 1));
if (!empty($file['error']) || empty($file['tmp_name']) || $file['tmp_name'] == 'none') {
$error = 'Не удалось загрузить файл.';
} elseif (empty($file['name']) || !is_uploaded_file($file['tmp_name'])) {
$error = 'Не удалось загрузить файл.';
} elseif (empty($ext) || !in_array($ext, $allow)) {
$error = 'Недопустимый тип файла';
} else {
$info = @getimagesize($file['tmp_name']);
if (empty($info[0]) || empty($info[1]) || !in_array($info[2], array(1, 2, 3))) {
$error = 'Недопустимый тип файла';
} else {
// Перемещаем файл в директорию с новым именем.
$name = time() . '-' . mt_rand(1, 9999999999);
$src = $tmp_path . $name . '.' . $ext;
$thumb = $tmp_path . $name . '-thumb.' . $ext;

if (move_uploaded_file($file['tmp_name'], $src)) {
// Создание миниатюры.
switch ($info[2]) {
case 1:
$im = imageCreateFromGif($src);
imageSaveAlpha($im, true);
break;
case 2:
$im = imageCreateFromJpeg($src);
break;
case 3:
$im = imageCreateFromPng($src);
imageSaveAlpha($im, true);
break;
}

$width = $info[0];
$height = $info[1];

// Высота превью 100px, ширина рассчитывается автоматически.
$h = 100;
$w = ($h > $height) ? $width : ceil($h / ($height / $width));
$tw = ceil($h / ($height / $width));
$th = ceil($w / ($width / $height));

$new_im = imageCreateTrueColor($w, $h);
if ($info[2] == 1 || $info[2] == 3) {
imagealphablending($new_im, true);
imageSaveAlpha($new_im, true);
$transparent = imagecolorallocatealpha($new_im, 0, 0, 0, 127);
imagefill($new_im, 0, 0, $transparent);
imagecolortransparent($new_im, $transparent);
}

if ($w >= $width && $h >= $height) {
$xy = array(ceil(($w - $width) / 2), ceil(($h - $height) / 2), $width, $height);
} elseif ($w >= $width) {
$xy = array(ceil(($w - $tw) / 2), 0, ceil($h / ($height / $width)), $h);
} elseif ($h >= $height) {
$xy = array(0, ceil(($h - $th) / 2), $w, ceil($w / ($width / $height)));
} elseif ($tw < $w) {
$xy = array(ceil(($w - $tw) / 2), ceil(($h - $h) / 2), $tw, $h);
} else {
$xy = array(0, ceil(($h - $th) / 2), $w, $th);
}

imageCopyResampled($new_im, $im, $xy[0], $xy[1], 0, 0, $xy[2], $xy[3], $width, $height);

// Сохранение.
switch ($info[2]) {
case 1: imageGif($new_im, $thumb); break;
case 2: imageJpeg($new_im, $thumb, 100); break;
case 3: imagePng($new_im, $thumb); break;
}

imagedestroy($im);
imagedestroy($new_im);

// Вывод в форму: превью, кнопка для удаления и скрытое поле.
$data = '
<div class="img-item">
<img src="' . $url_path . $name . '-thumb.' . $ext . '">
<a herf="#" onclick="remove_img(this); return false;"></a>
<input type="hidden" name="images[]" value="' . $name . '.' . $ext . '">
</div>';
} else {
$error = 'Не удалось загрузить файл.';
}
}
}

$response[] = array('error' => $error, 'data' => $data);
}

// Ответ в JSON.
header('Content-Type: application/json');
echo json_encode($response, JSON_UNESCAPED_UNICODE);
exit();

Пример ответа AJAX запроса в случаи успешной загрузки файла:

[{
"error": "",
"data": "
<div class="img-item">
<img src="/uploads/tmp/1610809179-108359805-thumb.jpg">
<a herf="#" onclick="remove_img(this); return false;"></a>
<input type="hidden" name="images[]" value="1610809179-108359805.jpg">
</div>"
}]

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

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

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

Понадобятся две таблицы, `reviews`:

CREATE TABLE `reviews` (
`id` int(11) UNSIGNED NOT NULL,
`name` varchar(255) NOT NULL,
`text` text NOT NULL,
`date_add` int(11) UNSIGNED NOT NULL DEFAULT '0'
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

ALTER TABLE `reviews` ADD PRIMARY KEY (`id`);
ALTER TABLE `reviews` MODIFY `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT;

И таблица `reviews_images` для хранения названий файлов:

CREATE TABLE `reviews_images` (
`id` int(11) UNSIGNED NOT NULL,
`reviews_id` int(11) NOT NULL DEFAULT '0',
`filename` varchar(255) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

ALTER TABLE `reviews_images` ADD PRIMARY KEY (`id`);
ALTER TABLE `reviews_images` MODIFY `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT;

Скрипт save_reviews.php

<?php
//ini_set('display_errors', 1);

// Временная директория.
$tmp_path = $_SERVER['DOCUMENT_ROOT'] . '/uploads/tmp/';

// Постоянная директория.
$path = $_SERVER['DOCUMENT_ROOT'] . '/uploads/';

// Подключение к БД.
$dbh = new PDO('mysql:dbname=db_name;host=localhost', 'логин', 'пароль');

if (isset($_POST['send'])) {
$name = htmlspecialchars($_POST['name'], ENT_QUOTES);
$text = htmlspecialchars($_POST['text'], ENT_QUOTES);

$sth = $dbh->prepare("INSERT INTO `reviews` SET `name` = ?, `text` = ?, `date_add` = UNIX_TIMESTAMP()");
$sth->execute(array($name, $text));

// Получаем id вставленной записи.
$insert_id = $dbh->lastInsertId();

// Сохранение изображений в БД и перенос в постоянную папку.
if (!empty($_POST['images'])) {
foreach ($_POST['images'] as $row) {
$filename = preg_replace("/[^a-z0-9\.-]/i", '', $row);
if (!empty($filename) && is_file($tmp_path . $filename)) {
$sth = $dbh->prepare("INSERT INTO `reviews_images` SET `reviews_id` = ?, `filename` = ?");
$sth->execute(array($insert_id, $filename));

// Перенос оригинального файла
rename($tmp_path . $filename, $path . $filename);

// Перенос превью
$file_name = pathinfo($filename, PATHINFO_FILENAME);
$file_ext = pathinfo($filename, PATHINFO_EXTENSION);
$thumb = $file_name . '-thumb.' . $file_ext;
rename($tmp_path . $thumb, $path . $thumb);
}
}
}
}

// Редирект, чтобы предотвратить повторную отправку по F5.
header('Location: /reviews.php', true, 301);
exit();

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

Скрипт reviews.php

<?php
// Подключение к БД.
$dbh = new PDO('mysql:dbname=db_name;host=localhost', 'логин', 'пароль');

$sth = $dbh->prepare("SELECT * FROM `reviews` ORDER BY `date_add` DESC");
$sth->execute();
$items = $sth->fetchAll(PDO::FETCH_ASSOC);

if (!empty($items)) {
?>
<h2>Отзывы</h2>
<div class="reviews">
<?php
foreach ($items as $row) {
$sth = $dbh->prepare("SELECT * FROM `reviews_images` WHERE `reviews_id` = ?");
$sth->execute(array($row['id']));
$images = $sth->fetchAll(PDO::FETCH_ASSOC);
?>
<div class="reviews_item">
<div class="reviews_item-name"><?php echo $row['name']; ?></div>
<div class="reviews_item-text"><?php echo $row['text']; ?></div>
<?php if (!empty($images)): ?>
<div class="reviews_item-images">
<?php foreach($images as $img): ?>
<div class="reviews_item-img">
<?php
$name = pathinfo($img['filename'], PATHINFO_FILENAME);
$ext = pathinfo($img['filename'], PATHINFO_EXTENSION);
?>
<a href="/uploads/<?php echo $img['filename']; ?>" target="_blank">
<img src="/uploads/<?php echo $name . '-thumb.' . $ext; ?>">
</a>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php
}
?>
</div>
<?php
}
?>

CSS-стили списка:

.reviews_item {
background: #efefef;
padding: 15px 30px 0px 30px;
margin-bottom: 20px;
}
.reviews_item-name {
font-weight: 900;
font-size: 18px;
margin-bottom: 5px;
}
.reviews_item-text {
margin-bottom: 15px;
font-size: 15px;
line-height: 1.5;
}
.reviews_item-img {
display: inline-block;
margin: 0 15px 15px 0;
}


  17.01.24 / 18:57 | AJAX |   191 | 1   0