В данной статье представлена упрощенная реализация загрузки изображений с превью через 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;
}
Файлы отправляются на сервер до отправки основной формы (возможно пользователь вовсе её не отправит), поэтому PHP-скрипт сохранит полученные файлы во временную директорию /uploads/tmp/
с новым именем и создаст картинку-превью с префиксом «thumb» в конце названия файла, далее вернёт контент для вставки обратно в форму в формате JSON.
В целях безопасности, во временной директории /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;
}
И вопрос состоит в том, как определить в какой директории находится файл(временная или постоянная) для того что бы правильно удалить. Сейчас делаю так: удаляю сразу и во временной и в постоянной папке(даже если в одной из папок файлов нет), получается в любом случае выполняется лишнее действие(удаляется файл, которого не существует).
И еще, как можно реализовать индикатор во время загрузки? Хотя бы какой-нибудь крутящийся спиннер, что бы было видно что файлы загружаются. В идеале конечно с процентным индикатором.
Спасибо за скрипт. Все настроено. Но после некоторого времени перестал скрипт отрабатывать. Проявляется это в не загрузке фотографий, картинок. При этом нет ни каких ошибок. Текстовая часть при этом работает и сохраняется. Подскажите пожалуйста что я не увидел. Заранее благодарю за помощь.
Добавлю, что пути все перепроверял.
Разобрался, Все работает. ))) Спасибо.