Как сделать форму обратной связи на PHP — простая и безопасная инструкция ☑️✉️
- Коротко о том, что будем делать
- Что нужно заранее
- Структура проекта (пример)
- 1) HTML-форма: простая и доступная
- 2) Клиентская валидация (не вместо, а вместе с серверной)
- 3) Сервер: базовые принципы
- 4) Полный обработчик: send.php
- 5) Объяснение ключевых частей обработчика
- 6) Класс Db: простой пример
- 7) SQL схема таблицы feedbacks
- 8) Защита от XSS при выводе
- 9) Безопасная отправка писем
- 10) Защита от спама: дополнительные меры
- 11) Логирование
- 12) Работа с вложениями: что ещё учесть
- 13) Accessibility и UX
- 14) Тестирование
- 15) Деплой и безопасность сервера
- 16) Дополнительные идеи и расширения
- 17) Частые ошибки и как их избежать
- 18) Минимальный чеклист перед запуском
- 19) Пример простого рабочего потока разработчика
- 20) Заключение — что важно запомнить
Форма обратной связи — вещь простая, но её легко сделать небезопасной или неудобной. В этой статье мы пройдём весь путь: от разметки до работы с базой, отправки письма и защиты от спама. Покажу реальные код-фрагменты, объясню, почему так, а не иначе, и дам проверенные практики. Поехали.
Коротко о том, что будем делать
-
HTML-форма с минимальным дизайном и доступностью.
-
Клиентская валидация (JS) — для удобства пользователя.
-
Серверная логика на PHP: валидация, защита, запись в БД, отправка письма.
-
Защита: CSRF, XSS, SQL-инъекции (PDO), защита от заголовков почты, ограничение скорости, honeypot, CAPTCHA.
-
Обработка вложений (безопасно).
-
Рекомендации по деплою и тестированию.
Яркой магии здесь нет. Главное — последовательность и простые практики.
Что нужно заранее
-
PHP 7.4 или новее (лучше PHP 8.x).
-
Веб-сервер (Apache, Nginx).
-
База данных MySQL / MariaDB или PostgreSQL (примеры для MySQL).
-
Composer, если планируешь использовать PHPMailer или другие пакеты.
-
SSL (HTTPS) на сайте — обязательно для форм.
Структура проекта (пример)
/project
/public
index.php <-- страница с формой
send.php <-- обработчик формы (POST)
assets.css
app.js
/src
Db.php
Mailer.php
Helpers.php
/logs
form.log
composer.json
Можно делать проще, но такая структура понятна и расширяема.
1) HTML-форма: простая и доступная
Основные правила: поля с label
, required
там, где нужно, aria
-атрибуты, минимальная семантика. Добавим и скрытое поле-honeypot.
<!-- public/index.php -->
<!doctype html>
<html lang="ru">
<head>
<meta✱ charset="utf-8">
<meta✱ name="viewport" content="width=device-width,initial-scale=1">
<title>Обратная связь</title>
<link rel="stylesheet" href="assets.css">
</head>
<body>
<main>
<h1>Связаться с нами</h1>
<form id="contactForm" action="send.php" method="post" enctype="multipart/form-data" novalidate>
<!-- CSRF token -->
<input type="hidden" name="csrf_token" value="<?php
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
echo htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES);
?>">
<div>
<label for="name">Имя *</label>
<input id="name" name="name" type="text" required maxlength="100" />
</div>
<div>
<label for="email">Email *</label>
<input id="email" name="email" type="email" required maxlength="255" />
</div>
<div>
<label for="subject">Тема</label>
<input id="subject" name="subject" type="text" maxlength="150" />
</div>
<div>
<label for="message">Сообщение *</label>
<textarea id="message" name="message" rows="6" required maxlength="5000"></textarea>
</div>
<!-- honeypot: поле для ботов, скрываемое через CSS -->
<div class="hp-field" style="position:absolute;left:-10000px;top:auto;height:1px;overflow:hidden;">
<label for="website">Website</label>
<input id="website" name="website" type="text" tabindex="-1" autocomplete="off">
</div>
<div>
<label for="file">Вложение (jpg, png, pdf) — до 2 МБ</label>
<input id="file" name="attachment" type="file" accept=".jpg,.jpeg,.png,.pdf" />
</div>
<button type="submit">Отправить</button>
<div id="formMessage" role="status" aria-live="polite"></div>
</form>
<script src="app.js"></script>
</main>
</body>
</html>
Пояснения:
-
novalidate
убирает встроенную в браузер валидацию, если ты будешь делать свою. Но можно её оставить. Я показал вариант с собственной обработкой. -
honeypot
— простая и эффективная ловушка для ботов. -
CSRF-токен хранится в сессии и проверяется на сервере.
2) Клиентская валидация (не вместо, а вместе с серверной)
Ни в коем случае не полагайся только на JS. Но удобство для пользователя важнее — показывать ошибки сразу. Простой пример fetch-запроса, который отправляет форму через AJAX и показывает ответ.
// public/app.js
document.addEventListener('DOMContentLoaded', function () {
const form = document.getElementById('contactForm');
const msg = document.getElementById('formMessage');
form.addEventListener('submit', async function (e) {
e.preventDefault();
msg.textContent = 'Отправка...';
const data = new FormData(form);
try {
const resp = await fetch(form.action, {
method: 'POST',
body: data,
credentials: 'same-origin'
});
const json = await resp.json();
if (json.success) {
msg.textContent = 'Спасибо! Сообщение отправлено. 😊';
form.reset();
} else {
msg.textContent = 'Ошибка: ' + (json.error || 'неизвестная ошибка');
}
} catch (err) {
console.error(err);
msg.textContent = 'Сеть недоступна. Попробуйте позже.';
}
});
});
Преимущество AJAX: не перегружается страница, можно быстро показать ошибки и подсвечивать поля.
3) Сервер: базовые принципы
На сервере мы должны:
-
Проверить CSRF-токен.
-
Проверить honeypot (если заполнено — это бот).
-
Валидировать и санитизировать данные.
-
Обработать файл безопасно.
-
Записать в БД подготовленным запросом (PDO).
-
Отправить письмо через безопасный SMTP (PHPMailer).
-
Логировать действия.
-
Ограничить частоту запросов (rate limit).
Дадим полный пример send.php, а затем разберём его по частям.
4) Полный обработчик: send.php
<?php
// public/send.php
declare(strict_types=1);
session_start();
header('Content-Type: application/json; charset=utf-8');
// минимальные настройки
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2 MB
const ALLOWED_MIMES = ['image/jpeg','image/png','application/pdf'];
const UPLOAD_DIR = __DIR__ . '/../uploads'; // создайте папку вне публичной директории
if (!is_dir(UPLOAD_DIR)) mkdir(UPLOAD_DIR, 0700, true);
// простая функция ответа
function jsonResponse($data) {
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
// 1) Проверка метода
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['success' => false, 'error' => 'Неверный метод запроса.']);
}
// 2) Rate limit (файл, хранит timestamps по IP)
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$limitFile = __DIR__ . '/../logs/ratelimit_' . md5($ip) . '.txt';
$maxRequests = 5; // например 5 запросов
$window = 60; // в секундах
$now = time();
if (file_exists($limitFile)) {
$data = json_decode(file_get_contents($limitFile), true);
if (!is_array($data)) $data = [];
// очистка старых
$data = array_filter($data, function($ts) use ($now, $window) {
return ($now - $ts) <= $window;
});
} else $data = [];
if (count($data) >= $maxRequests) {
jsonResponse(['success' => false, 'error' => 'Слишком много запросов. Попробуйте позже.']);
}
$data[] = $now;
file_put_contents($limitFile, json_encode($data));
// 3) CSRF
$csrf = $_POST['csrf_token'] ?? '';
if (empty($csrf) || !hash_equals($_SESSION['csrf_token'] ?? '', $csrf)) {
jsonResponse(['success' => false, 'error' => 'Ошибка CSRF.']);
}
// 4) Honeypot
if (!empty($_POST['website'] ?? '')) {
// бот, молча завершаем (можно логировать)
jsonResponse(['success' => false, 'error' => 'Неверный запрос.']);
}
// 5) Входные данные
$name = trim((string)($_POST['name'] ?? ''));
$email = trim((string)($_POST['email'] ?? ''));
$subject = trim((string)($_POST['subject'] ?? ''));
$message = trim((string)($_POST['message'] ?? ''));
// 6) Валидация
$errors = [];
if ($name === '' || mb_strlen($name) < 2) $errors[] = 'Имя слишком короткое.';
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = 'Неправильный email.';
if ($message === '' || mb_strlen($message) < 5) $errors[] = 'Сообщение слишком короткое.';
if (mb_strlen($subject) > 150) $errors[] = 'Тема слишком длинная.';
if (!empty($errors)) {
jsonResponse(['success' => false, 'error' => implode(' ', $errors)]);
}
// 7) Обработка вложения
$attachmentPath = null;
if (!empty($_FILES['attachment']) && $_FILES['attachment']['error'] !== UPLOAD_ERR_NO_FILE) {
$f = $_FILES['attachment'];
if ($f['error'] !== UPLOAD_ERR_OK) {
jsonResponse(['success' => false, 'error' => 'Ошибка при загрузке файла.']);
}
if ($f['size'] > MAX_FILE_SIZE) {
jsonResponse(['success' => false, 'error' => 'Файл слишком большой.']);
}
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $f['tmp_name']);
finfo_close($finfo);
if (!in_array($mime, ALLOWED_MIMES, true)) {
jsonResponse(['success' => false, 'error' => 'Недопустимый тип файла.']);
}
// Генерация безопасного имени
$ext = pathinfo($f['name'], PATHINFO_EXTENSION);
$safeName = bin2hex(random_bytes(16)) . '.' . ($ext ?: 'dat');
$target = UPLOAD_DIR . '/' . $safeName;
if (!move_uploaded_file($f['tmp_name'], $target)) {
jsonResponse(['success' => false, 'error' => 'Не удалось сохранить файл.']);
}
// права
chmod($target, 0600);
$attachmentPath = $target;
}
// 8) Очистка для безопасности и логики
function e($s) { return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
$cleanName = e($name);
$cleanEmail = e($email);
$cleanSubject = e($subject);
$cleanMessage = e($message);
// 9) Сохранение в БД (PDO)
require_once __DIR__ . '/../src/Db.php';
try {
$db = (new Db())->getPdo(); // класс Db возвращает PDO (ниже пример)
$stmt = $db->prepare('INSERT INTO feedbacks (name, email, subject, message, attachment, ip, created_at) VALUES (:name, :email, :subject, :message, :attachment, :ip, NOW())');
$stmt->execute([
':name' => $name,
':email' => $email,
':subject' => $subject,
':message' => $message,
':attachment' => $attachmentPath ? basename($attachmentPath) : null,
':ip' => $ip
]);
} catch (Exception $ex) {
// логируем ошибку
error_log('DB error: ' . $ex->getMessage());
jsonResponse(['success' => false, 'error' => 'Внутренняя ошибка сервера.']);
}
// 10) Отправка письма (PHPMailer)
require_once __DIR__ . '/../vendor/autoload.php';
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
try {
$mail = new PHPMailer(true);
// Настройки SMTP
$mail->isSMTP();
$mail->Host = 'smtp.example.com'; // поменять
$mail->SMTPAuth = true;
$mail->Username = 'user@example.com';
$mail->Password = 'secret';
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
$mail->setFrom('no-reply@example.com', 'Сайт');
$mail->addAddress('admin@example.com', 'Админ'); // кому приходит письмо
$mail->addReplyTo($cleanEmail, $cleanName);
$mail->Subject = 'Новая форма: ' . ($subject ?: 'Без темы');
$body = "Новое сообщение с сайта:\n\n";
$body .= "Имя: {$cleanName}\n";
$body .= "Email: {$cleanEmail}\n";
$body .= "IP: {$ip}\n\n";
$body .= "Тема: {$cleanSubject}\n\n";
$body .= "Сообщение:\n{$cleanMessage}\n";
$mail->Body = $body;
if ($attachmentPath) {
$mail->addAttachment($attachmentPath);
}
$mail->send();
} catch (Exception $e) {
// не фатально — логируем, но всё равно возвращаем успех, т.к. данные в БД есть
error_log('Mail error: ' . $e->getMessage());
}
jsonResponse(['success' => true]);
Это рабочий шаблон. Разберём важные моменты.
5) Объяснение ключевых частей обработчика
CSRF
Мы генерируем токен на странице формы и проверяем его в send.php
. Это защищает от подделки формы со сторонних сайтов.
Honeypot
Поле website
скрыто от людей. Боты часто заполняют все поля — если поле заполнено, это бот.
Rate limit (ограничение скорости)
Простой файл на сервере хранит отметки времени запросов от IP. Это не идеальное решение для большого трафика, но работает для маленького сайта. Для масштаба лучше Redis или memcached.
Работа с файлами
-
Проверяем
error
,size
, MIME черезfinfo
. -
Сохраняем файл вне публичной папки (
uploads
выше публичной), чтобы его нельзя было выполнить. -
Даём безопасное случайное имя.
-
Ставим права
0600
.
PDO и подготовленные запросы
Используем подготовленные выражения, чтобы избежать SQL-инъекций. Строки сохраняем «сырыми» в БД, а при выводе на страницу — экранируем htmlspecialchars
.
6) Класс Db: простой пример
<?php
// src/Db.php
declare(strict_types=1);
class Db {
private $pdo;
public function __construct() {
$host = '127.0.0.1';
$db = 'mydb';
$user = 'dbuser';
$pass = 'dbpass';
$charset = 'utf8mb4';
$dsn = "mysql:host={$host};dbname={$db};charset={$charset}";
$options = [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
\PDO::ATTR_EMULATE_PREPARES => false,
];
$this->pdo = new \PDO($dsn, $user, $pass, $options);
}
public function getPdo(): \PDO {
return $this->pdo;
}
}
Настрой параметры под своё окружение. Не храните пароли в репозитории — используйте переменные окружения или отдельный конфиг за пределами публичной директории.
7) SQL схема таблицы feedbacks
Простой пример для MySQL:
CREATE TABLE feedbacks (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL,
subject VARCHAR(150),
message TEXT NOT NULL,
attachment VARCHAR(255),
ip VARCHAR(45),
created_at DATETIME NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Можно добавить индекс по email
или created_at
по необходимости.
8) Защита от XSS при выводе
Если позже выводишь сообщения в админке или в письмах, используй htmlspecialchars
. Никогда не встраивай сырые данные в HTML.
echo htmlspecialchars($row['message'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
9) Безопасная отправка писем
Используй PHPMailer или похожую библиотеку. Не передавай в From
e-mail пользователя напрямую. Лучше:
-
From
— ваш домен (no-reply@yourdomain). -
Reply-To
— email пользователя. -
Так избегаем проблем с SPF/DMARC и подделкой заголовков.
Также проверяй и нормализуй значения, чтобы избежать header injection (в PHPMailer это решено).
Установка PHPMailer через Composer:
composer require phpmailer/phpmailer
10) Защита от спама: дополнительные меры
-
Google reCAPTCHA v2/v3. Работает надёжно, но добавляет зависимость от внешних сервисов.
-
Honeypot — уже есть.
-
Таймер: проверять, сколько секунд прошло с загрузки формы до отправки — если слишком мало (меньше 3−5 секунд), вероятно бот.
-
Blacklist: блокировать IP-адреса с подозрительной активностью.
-
Текстовые проверки: фильтровать частые спам-фразы.
Пример таймера:
<input type="hidden" name="ts" value="<?php echo time(); ?>">
И в send.php
:
$ts = (int)($_POST['ts'] ?? 0);
if ($ts && (time() - $ts) < 3) {
jsonResponse(['success' => false, 'error' => 'Слишком быстро.']);
}
11) Логирование
Важные события логируй: ошибки БД, ошибки почты, подозрительные попытки. Логи храни вне публичной директории и с ротацией.
Простой лог:
file_put_contents(__DIR__ . '/../logs/form.log', date('c') . " | {$ip} | {$cleanEmail} | saved\n", FILE_APPEND | LOCK_EX);
Для продакшена используй мониторы, ротацию (logrotate).
12) Работа с вложениями: что ещё учесть
-
Не позволяй пользователям загружать скрипты. Ограничивай по MIME и расширениям.
-
Храни файлы вне веб-корня или с настройкой веб-сервера, запрещающей исполнение.
-
Если даёшь ссылки на файлы — используйте скрипт доставки, который проверяет права и отдаёт контент через
readfile()
с корректными заголовками и безопасным путём.
Пример безопасной отдачи файла:
// download.php?file=abc123.pdf
$token = $_GET['file'] ?? '';
$path = __DIR__ . '/../uploads/' . basename($token);
if (!is_file($path)) { http_response_code(404); exit; }
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($path) . '"');
readfile($path);
13) Accessibility и UX
-
Пометки полей (
label
) иaria-live
для сообщений помогают людям с экранными читалками. -
Удобные подсказки при ошибках.
-
Сохранять ввод при ошибке — чтобы пользователю не приходилось вводить всё заново.
-
Мобильная адаптация: форма должна работать на небольших экранах.
14) Тестирование
Проверь всё вручную и автоматизированно:
-
Попробуй отправить форму с пустыми полями, длинными строками, инъекциями.
-
Попробуй загружать запрещённые файлы.
-
Проверь, что CSRF работает (отключи токен — запросу быть не должно).
-
Тесты на нагрузку и rate-limit.
-
Тестируй отправку и доставку писем (SMTP).
Для автоматических тестов можно написать PHPUnit тесты для классов, и интеграционные тесты с помощью HTTP клиента.
15) Деплой и безопасность сервера
-
Обязательно HTTPS.
-
Размещай конфиги и пароли вне веб-корня.
-
Обновляй PHP и зависимости.
-
Ограничь доступ к папкам
uploads
иlogs
. -
Настрой бэкапы БД и файлов.
-
Проанализируй права на файлы и папки (не давай 0777, лучше 0755/0644).
16) Дополнительные идеи и расширения
-
Панель для просмотра сообщений с аутентификацией.
-
Метки «прочитано/непрочитано».
-
Фильтрация и поиск в админке.
-
Ответ пользователю через админку с записью истории.
-
Интеграция с CRM или тикетной системой.
-
Webhooks: отправка данных в сторонний сервис.
17) Частые ошибки и как их избежать
-
Только клиентская валидация. Всегда делай валидацию на сервере.
-
Хранение файлов в публичной папке без защиты. Скрипты могут быть исполнены.
-
Передача пользовательского email в From. Используй
Reply-To
. -
Хранение паролей и секретов в коде. Используй переменные окружения.
-
Нет CSRF и rate-limit. Это быстро превращает форму в бот-мишень.
18) Минимальный чеклист перед запуском
-
HTTPS включён.
-
CSRF проверка работает.
-
Honeypot и/или CAPTCHA.
-
Rate limit настроен.
-
Файлы сохраняются вне веб-корня.
-
Логи ведутся и ротация настроена.
-
Тестовое письмо уходит и доходит.
-
Права на папки корректны.
-
БД защищена и ещё есть бэкап.
19) Пример простого рабочего потока разработчика
-
Разработал HTML и JS.
-
Написал простой
send.php
, который просто сохраняет в файл. -
Добавил PDO и таблицу в БД.
-
Подключил PHPMailer через Composer.
-
Добавил CSRF и honeypot.
-
Настроил загрузку файлов и защиту upload папки.
-
Тестировал на локали, затем на staging с HTTPS.
-
После успешных тестов задеплоил на прод.
20) Заключение — что важно запомнить
Форма обратной связи — это мост между пользователем и вами. Сделать её просто — незначительный труд. Сделать её правильно — требует внимания к безопасности и удобству. Всегда проверяй ввод с обеих сторон (клиент и сервер). Храни файлы и секреты безопасно. Логи и бэкапы спасут при проблемах. И главное: делай шаги по защите заранее, чтобы потом не исправлять последствия.
Удачи в разработке — и пусть сообщения приходят без спама и проблем 🚀
* Упомянутые организации запрещены на территории РФ