В этой статье мы разберём, как реализовать систему отзывов на сайте, построенном на MODX, с возможностью для клиентов отправлять отзывы с прикреплёнными фотографиями. Отзывы сохраняются в статусе “неопубликовано” и публикуются только после того, как администратор нажмёт на кнопку “Опубликовать” в письме-уведомлении. Этот подход позволяет модерировать контент, сохраняя удобство публикации. Мы будем использовать пакет SendIt для обработки форм и отправки писем, а я подробно объясню каждый шаг и предоставлю разъяснения к коду. Для этого нам нужно
Шаг 1: Создание формы для отправки отзывов
Первый шаг — создать удобную форму, через которую клиенты смогут отправлять свои отзывы. Форма включает поля для имени, электронной почты, текста отзыва и возможность загрузки фотографий. Мы используем пресет form_with_file из SendIt для обработки текстовых данных и файлов.
{set $fullurl = $_modx->resource.id | url : ['scheme' => 'full']}
<form
data-si-event="submit"
data-si-form="Review"
data-si-preset="form_with_file"
enctype="multipart/form-data"
id="form2">
<input type="text" name="name" placeholder="Ваше имя" required />
<input type="email" name="email" placeholder="Электронная почта" required />
<textarea class="c-form_textarea" id="input-mes" name="message" placeholder="Ваш отзыв" required></textarea>
<div class="c-form_item dragdrop" data-fu-wrap data-si-preset="upload_drop_file" data-si-nosave>
<div data-fu-progress=""></div>
<input type="hidden" name="filelist" data-fu-list>
<label data-fu-dropzone>
<input type="file" name="files" data-fu-field multiple class="v_hidden">
<span class="drag_title">Перетащите ваши фотографии</span>
</label>
<template data-fu-tpl>
<button type="button" data-fu-path="$path" class="drop-btn" style="background-image:url($path)"></button>
</template>
</div>
<button class="fasong" type="submit">Отправить</button>
<div id="sendit-spinner" style="display: none;">⏳ Отправка...</div>
</form>
<script type="text/javascript" defer>
document.addEventListener("DOMContentLoaded", function() {
const status = document.getElementById("sendit-spinner");
const button = document.querySelector("button.fasong");
document.addEventListener("si:send:before", function () {
status.style.display = "block";
button.disabled = true;
});
document.addEventListener("si:send:finish", (e) => {
const { action, target, result, headers, Sending } = e.detail;
status.style.display = "none";
button.disabled = false;
});
});
</script>
Code language: HTML, XML (xml)
Объяснение
- Структура формы: Форма использует пресет form_with_file от SendIt для обработки текстовых полей и загрузки файлов. Атрибуты data-si-* настраивают SendIt для обработки отправки формы.
- Поля формы: Включают name (имя), email (электронная почта), message (текст отзыва) и поле для загрузки файлов с поддержкой drag-and-drop через пресет upload_drop_file.
- JavaScript: Обрабатывает события SendIt (si:send:before и si:send:finish), показывая индикатор загрузки и отключая кнопку отправки во время обработки, что улучшает пользовательский опыт.
- Интеграция с MODX: Сниппет {set $fullurl …} генерирует полный URL текущей страницы, что полезно для обработки формы или редиректов.
Шаг 2: Настройка SendIt с пользовательским хуком
Для обработки данных формы и создания нового ресурса отзыва добавим пользовательский хук SaveToReviews в пресеты SendIt. Этот хук обрабатывает данные формы, сохраняет отзыв как ресурс MODX, сжимает загруженные изображения и генерирует уникальный токен для публикации через email.
Добавление в пресеты SendIt
В SendIt добавьте хук SaveToReviews в пресет form_with_file (или создайте новый пресет). Это обеспечит вызов хука при отправке формы. Про пресеты расписано подробно на официальной документации SendIt
<?php
function resizeImage($sourcePath, $destinationPath, $maxWidth = 1200, $maxHeight = 1200, $quality = 85) {
$info = getimagesize($sourcePath);
if (!$info) return false;
list($width, $height) = $info;
$mime = $info['mime'];
$scale = min($maxWidth / $width, $maxHeight / $height, 1);
$newWidth = (int)($width * $scale);
$newHeight = (int)($height * $scale);
$dst = imagecreatetruecolor($newWidth, $newHeight);
switch ($mime) {
case 'image/jpeg':
$src = imagecreatefromjpeg($sourcePath);
break;
case 'image/png':
$src = imagecreatefrompng($sourcePath);
imagealphablending($dst, false);
imagesavealpha($dst, true);
break;
case 'image/gif':
$src = imagecreatefromgif($sourcePath);
break;
default:
return false;
}
imagecopyresampled($dst, $src, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
switch ($mime) {
case 'image/jpeg':
imagejpeg($dst, $destinationPath, $quality);
break;
case 'image/png':
imagepng($dst, $destinationPath, 6);
break;
case 'image/gif':
imagegif($dst, $destinationPath);
break;
}
imagedestroy($src);
imagedestroy($dst);
return true;
}
$name = strip_tags($hook->getValue('name'));
$email = strip_tags($hook->getValue('email'));
$message = strip_tags($hook->getValue('message'));
$filelist = $hook->getValue('filelist');
$randomNumber = random_int(10000, 99999);
$alias = preg_replace('/[^a-zA-Z0-9]+/', '-', $name . $randomNumber);
$alias = strtolower(trim($alias, '-'));
if ($modx->getCount('modResource', ['parent' => 32, 'alias' => $alias]) == 0) {
$newReview = $modx->newObject('modResource');
$token = bin2hex(random_bytes(16));
$newReview->fromArray([
'pagetitle' => 'Отзыв от ' . $name,
'longtitle' => $name,
'content' => $message,
'parent' => 32,
'template' => 11,
'published' => 0,
'publishedon' => time(),
'hidemenu' => 1,
'alias' => $alias,
'show_in_tree' => 0,
]);
if ($newReview->save()) {
$publishUrl = $modx->makeUrl(59, '', [
'action' => 'publish_review',
'review_id' => $newReview->get('id'),
'token' => $token
], 'full');
$fields = $hook->getValues();
$fields['publish_url'] = $publishUrl;
$hook->setValues($fields);
$modx->setPlaceholder('publish_url', $publishUrl);
$migxData = [];
if (!empty($filelist)) {
$basePath = $modx->getOption('base_path') . 'assets/images/portfolio/';
$baseUrl = $modx->getOption('base_url') . 'assets/images/portfolio/';
if (!file_exists($basePath)) {
mkdir($basePath, 0755, true);
}
$sessionId = session_id();
$sourceDir = $modx->getOption('base_path') . 'assets/components/sendit/uploaded_files/' . $sessionId . '/';
$files = explode(',', $filelist);
foreach ($files as $fileName) {
$sourcePath = $sourceDir . $fileName;
$destinationPath = $basePath . $randomNumber . '_' . $fileName;
if (file_exists($sourcePath)) {
if (resizeImage($sourcePath, $destinationPath, 1200, 1200)) {
$migxData[] = [
'image' => 'assets/images/portfolio/' . $randomNumber . '_' . $fileName,
'MIGX_id' => count($migxData) + 1,
];
} else {
$modx->log(modX::LOG_LEVEL_ERROR, "Не удалось сжать файл: {$sourcePath}");
}
}
}
if (!empty($migxData)) {
$newReview->setTVValue('ReviewsImgMigx', json_encode($migxData));
} else {
$modx->log(modX::LOG_LEVEL_ERROR, "Нет данных для MIGX: filelist={$filelist}");
}
}
$newReview->setTVValue('ReviewToken', $token);
$modx->log(modX::LOG_LEVEL_INFO, "Создан отзыв: {$name}");
} else {
$modx->log(modX::LOG_LEVEL_ERROR, "Не удалось сохранить отзыв: {$name}");
}
unset($newReview);
} else {
$modx->log(modX::LOG_LEVEL_ERROR, "Отзыв уже существует: {$name}");
}
return true;
Code language: HTML, XML (xml)
Объяснение
- Сжатие изображений: Функция resizeImage сжимает и изменяет размер загруженных изображений до максимум 1200×1200 пикселей, поддерживая форматы JPEG, PNG и GIF.
- Данные формы: Очищает входные данные (name, email, message) с помощью strip_tags для защиты от XSS-атак.
- Уникальный alias: Генерирует уникальный alias для ресурса отзыва, используя имя клиента и случайное число.
- Создание ресурса: Создаёт новый ресурс MODX под родительским ID 32 с шаблоном ID 11, изначально в статусе “неопубликовано” (published => 0).
- Обработка файлов: Переносит загруженные файлы из временной директории SendIt в постоянное хранилище, сжимает их и сохраняет пути в поле MIGX TV (ReviewsImgMigx) для отображения.
- Генерация токена: Создаёт уникальный 32-символьный токен для безопасной публикации через email.
- URL для публикации: Формирует URL с параметрами action, review_id и token, передавая его в письмо через плейсхолдер publish_url.
Шаг 3: Создание шаблона письма
При отправке отзыва администратору отправляется письмо с кнопкой “Опубликовать”. Мы создадим чанк emailReview.tpl для форматирования этого письма.
{set $fields = $fields | replace: '"' : '"' | fromJSON}
{set $fieldsAliases = $fieldsAliases | replace: '"' : '"' | fromJSON}
<h3>Получен новый отзыв с сайта test.com</h3>
<p>Подробности:</p>
<style>
#table2024 {
width: 100%;
border-collapse: collapse;
}
#table2024 tr:nth-child(even) {
background-color: #f2f2f2;
}
#table2024 td {
padding: 12px;
border: 1px solid #ddd;
text-align: left;
box-sizing: border-box;
}
</style>
<table id="table2024">
<tbody>
{foreach $fields as $k => $v}
{if $v}
<tr>
<td>{$fieldsAliases[$k] ?: $k}:</td>
<td><strong>{$v}</strong></td>
</tr>
{/if}
{/foreach}
</tbody>
</table>
<p><strong>Требуется действие:</strong> Чтобы опубликовать этот отзыв, нажмите на кнопку ниже.</p>
<p>
<a href="{$publish_url}" target="_blank" style="color:#fff;font-weight:600;padding:10px 20px;border-radius:5px;background:#007bff;text-decoration:none">Опубликовать отзыв</a>
</p>
<p><em>Примечание:</em> Чтобы привязать этот отзыв к конкретному проекту, обновите его в админ-панели.</p>
Code language: HTML, XML (xml)
Объяснение
- Динамические поля: Цикл {foreach} отображает все поля формы в стилизованной таблице, используя $fieldsAliases для удобных меток.
- Стилизация: Чистый и отзывчивый дизайн таблицы с чередующимися цветами строк для удобства чтения.
- Кнопка публикации: Стилизованная ссылка <a> ведёт на плейсхолдер publish_url, позволяя опубликовать отзыв одним кликом.
- Примечание: Указывает администратору, что для привязки отзыва к проекту нужно обновить данные в админ-панели MODX.
Настройте SendIt, указав параметр emailTpl как emailReview.tpl в пресете.
Шаг 4: Обработка действия публикации
Для обработки клика по кнопке “Опубликовать” из письма создадим сниппет PublishReview. Его разместим на скрытом ресурсе MODX (например, ID 59) с пользовательским шаблоном.
<?php
$action = $modx->getOption('action', $_GET, '');
$reviewId = (int)$modx->getOption('review_id', $_GET, 0);
$token = $modx->getOption('token', $_GET, '');
if ($action !== 'publish_review' || $reviewId <= 0 || empty($token)) {
$modx->log(modX::LOG_LEVEL_ERROR, "Некорректные параметры для публикации: action={$action}, review_id={$reviewId}, token={$token}");
return 'Ошибка: некорректные параметры.';
}
$review = $modx->getObject('modResource', $reviewId);
if (!$review) {
$modx->log(modX::LOG_LEVEL_ERROR, "Отзыв с ID {$reviewId} не найден.");
return 'Ошибка: отзыв не найден.';
}
if ($review->get('published')) {
return 'Отзыв уже опубликован.';
}
$storedToken = $review->getTVValue('ReviewToken');
if ($storedToken !== $token) {
$modx->log(modX::LOG_LEVEL_ERROR, "Неверный токен для отзыва ID {$reviewId}. Получен: {$token}, Ожидался: {$storedToken}");
return 'Ошибка: неверный токен для отзыва ID ' . $reviewId . '. Получен: ' . $token . ', Ожидался: ' . $storedToken;
}
$review->set('published', 1);
$review->set('publishedon', time());
$review->setTVValue('ReviewToken', '');
if ($review->save()) {
$modx->log(modX::LOG_LEVEL_INFO, "Отзыв ID {$reviewId} успешно опубликован.");
return 'Отзыв успешно опубликован!';
} else {
$modx->log(modX::LOG_LEVEL_ERROR, "Не удалось опубликовать отзыв ID {$reviewId}.");
return 'Ошибка при публикации отзыва.';
}
Code language: HTML, XML (xml)
Объяснение
- Проверка параметров: Проверяет наличие корректных параметров action, review_id и token в URL.
- Поиск ресурса: Находит ресурс отзыва по ID.
- Проверка токена: Убеждается, что предоставленный токен совпадает с сохранённым значением ReviewToken в TV-поле.
- Публикация: Устанавливает published в 1, обновляет publishedon, очищает токен для безопасности и сохраняет ресурс.
- Обработка ошибок: Логирует ошибки и возвращает понятные сообщения для некорректных случаев.
Инструкции по настройке
- Создайте скрытый ресурс MODX (например, ID 59) с минимальным шаблоном.
- Разместите сниппет PublishReview на этом ресурсе с помощью {‘!PublishReview’ | snippet}.
- Убедитесь, что ресурс доступен, но не отображается в меню (hidemenu => 1).
Заключение
Эта система обеспечивает надёжный механизм для работы с отзывами на сайте MODX, сочетая удобную форму отправки, безопасную обработку файлов и модерируемую публикацию через email. Использование SendIt, пользовательских хуков и системы ресурсов MODX гарантирует удобство как для клиентов, так и для администраторов. Токены добавляют слой безопасности, предотвращая несанкционированную публикацию, а кнопка в письме упрощает процесс одобрения.