По статистике, до 30% заявок с форм обратной связи теряются из-за сбоев почты или неправильной настройки. Менеджеры тратят время на ручную проверку, а клиенты уходят к конкурентам. Правильная архитектура — сохранять данные в базу и одновременно отправлять email. Такой подход гарантирует сохранность каждой заявки и ускоренное уведомление. Наша команда реализовала эту схему для более чем 50 проектов, включая интернет-магазин с 5000 заявок в день — после внедрения потери заявок снизились до нуля.
В этой статье мы покажем, как реализовать эту схему на Laravel 11 с использованием очередей и механизмов повторной отправки. Вы узнаете, как настроить валидацию, защиту от спама и дашборд для просмотра заявок.
Почему стоит сохранять данные формы в базу и отправлять email?
Совместное использование базы и email даёт двукратное преимущество: надёжность хранения в 2 раза выше, чем при использовании только email, а скорость уведомления остаётся мгновенной. Очередь с повторными попытками в 3 раза сокращает количество потерянных писем.
| Способ хранения | Надёжность | Скорость уведомления | Риски |
|---|---|---|---|
| Только база данных | Высокая | Нет уведомлений | Заявка остаётся незамеченной |
| Только email | Низкая (зависит от почты) | Мгновенно | Потеря данных при сбое SMTP |
| База + email (наш подход) | Максимальная | Мгновенно + дублирование | Минимальные (настраивается re-отправка) |
Структура таблицы
Минимальная таблица для хранения заявок с формы:
CREATE TABLE form_submissions ( id BIGSERIAL PRIMARY KEY, form_type VARCHAR(64) NOT NULL, payload JSONB NOT NULL, ip INET, user_agent TEXT, sent_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX idx_form_submissions_form_type ON form_submissions(form_type); CREATE INDEX idx_form_submissions_created_at ON form_submissions(created_at DESC); payload в JSONB позволяет хранить произвольную структуру без миграций при каждом изменении формы. sent_at — метка успешной отправки email, NULL означает «ещё не отправлено» или «отправка упала».
Серверная обработка (PHP/Laravel)
// app/Http/Controllers/FormController.php public function submit(Request $request): JsonResponse { $validated = $request->validate([ 'name' => 'required|string|max:255', 'email' => 'required|email|max:255', 'phone' => 'nullable|string|max:32', 'message' => 'required|string|max:4000', ]); // 1. Сохраняем в базу сразу — независимо от почты $submission = FormSubmission::create([ 'form_type' => 'contact', 'payload' => $validated, 'ip' => $request->ip(), 'user_agent' => $request->userAgent(), ]); // 2. Отправляем email через очередь Mail::to(config('mail.admin_address')) ->queue(new FormSubmissionMail($submission)); return response()->json(['ok' => true]); } Ключевой момент: ->queue() вместо ->send(). Очередь означает, что сбой SMTP не вернёт 500 пользователю — заявка уже в базе, письмо уйдёт при следующей попытке.
Mailable
// app/Mail/FormSubmissionMail.php class FormSubmissionMail extends Mailable implements ShouldQueue { use Queueable, SerializesModels; public function __construct(public FormSubmission $submission) {} public function envelope(): Envelope { return new Envelope( subject: 'Новая заявка: ' . $this->submission->form_type, replyTo: [ new Address($this->submission->payload['email'], $this->submission->payload['name']), ], ); } public function content(): Content { return new Content(view: 'emails.form-submission'); } } replyTo заполняется из данных пользователя — менеджер нажимает «Ответить» и письмо уходит сразу на клиента, а не на no-reply адрес сайта.
Отметка об успешной отправке
Слушатель события MessageSent обновляет поле sent_at:
// app/Listeners/MarkSubmissionSent.php public function handle(MessageSent $event): void { $message = $event->message; // извлекаем submission_id из заголовка X-Submission-Id $id = $message->getHeaders()->get('X-Submission-Id')?->getValue(); if ($id) { FormSubmission::where('id', $id) ->whereNull('sent_at') ->update(['sent_at' => now()]); } } Заявки с sent_at = NULL можно периодически перепосылать через Artisan-команду или просматривать в административной панели.
Как настроить повторную отправку упавших писем?
// app/Console/Commands/RetryUnsentSubmissions.php // Запускается каждые 15 минут через планировщик $submissions = FormSubmission::whereNull('sent_at') ->where('created_at', '<', now()->subMinutes(5)) ->limit(50) ->get(); foreach ($submissions as $submission) { Mail::to(config('mail.admin_address')) ->queue(new FormSubmissionMail($submission)); } Эта команда перехватывает заявки, которые не отправились в течение 5 минут, и повторяет отправку. Лимит 50 за раз предотвращает перегрузку очереди.
Подробнее о настройке очереди Redis
Для работы очереди необходимо настроить соединение redis в config/queue.php. Убедитесь, что установлен predis/predis или phpredis. Supervisor должен быть настроен для процесса php artisan queue:work redis --sleep=3 --tries=3. Это обеспечит автоматический перезапуск при сбоях.
Как защитить форму от спама?
CSRF-токен обязателен по умолчанию в Laravel. Дополнительно — honeypot-поле и rate limiting (напр., Route::post('/contact', ...)->middleware(['throttle:5,1']) — 5 запросов в минуту с одного IP). Для высоконагруженных форм — reCAPTCHA v3 или Turnstile от Cloudflare (без видимой капчи).
| Метод | Уровень защиты | Влияние на UX |
|---|---|---|
| CSRF-токен | Базовый (обязателен) | Не заметен |
| Honeypot | Средний | Не заметен |
| Rate limiting (5 запр/мин) | Высокий | Может блокировать реальных пользователей |
| reCAPTCHA v3 | Высокий | Без видимой капчи |
| Turnstile (Cloudflare) | Высокий | Без видимой капчи |
Мы рекомендуем комбинацию CSRF + honeypot + rate limiting для большинства проектов. Для высоконагруженных форм — Turnstile или reCAPTCHA v3.
Пошаговая настройка
- Создайте миграцию для таблицы
form_submissions. - Реализуйте контроллер с валидацией и сохранением.
- Создайте Mailable класс и шаблон письма.
- Настройте очередь (Redis или Database) и Supervisor для обработки.
- Добавьте scheduled task для повторной отправки упавших писем.
Что входит в работу
- Подготовка схемы базы данных и индексов под вашу форму
- Разработка контроллера с валидацией и сохранением
- Настройка почтового шаблона и отправки через очередь
- Интеграция с любым SMTP-провайдером или SES/Mailgun
- Добавление дашборда для просмотра заявок (опционально)
- Тестирование и гарантия бесперебойной работы
Срок реализации базовой версии — 1 рабочий день. С дашбордом просмотра заявок и повторной отправкой — 2–3 дня. Стоимость решения рассчитывается индивидуально под ваш проект — от базовой версии до расширенной с дашбордом. Экономия на предотвращении потери заявок окупает вложения в первые же месяцы.
Свяжитесь с нами, чтобы получить консультацию по вашему проекту. Закажите реализацию надёжной системы сбора заявок — мы оценим ваш проект и предложим оптимальное решение.
Для более детального изучения рекомендуем официальную документацию: Laravel Mail Documentation.







