За статистикою, до 30% заявок з форм зворотного зв'язку втрачаються через збої пошти або неправильне налаштування. Менеджери витрачають час на ручну перевірку, а клієнти йдуть до конкурентів. Правильна архітектура — зберігати дані в базу й одночасно надсилати email. Такий підхід гарантує збереження кожної заявки та прискорене повідомлення. Наша команда реалізувала цю схему для більш ніж 50 проєктів, включаючи інтернет-магазин з 5000 заявок на день — після впровадження втрати заявок знизилися до нуля.
У цій статті ми покажемо, як реалізувати цю схему на Laravel 11 з використанням черг і механізмів повторної відправки. Ви дізнаєтеся, як налаштувати валідацію, захист від спаму та дашборд для перегляду заявок.
Чому варто зберігати дані форми в базу та надсилати email?
Спільне використання бази та email дає подвійну перевагу: надійність зберігання в 2 рази вища, ніж при використанні тільки email, а швидкість повідомлення залишається миттєвою. Черга з повторними спробами в 3 рази скорочує кількість втрачених листів.
| Спосіб зберігання | Надійність | Швидкість повідомлення | Ризики |
|---|---|---|---|
| Тільки база даних | Висока | Немає повідомлень | Заявка залишається непоміченою |
| Тільки email | Низька (залежить від пошти) | Миттєво | Втрата даних при збої SMTP |
| База + email (наш підхід) | Максимальна | Миттєво + дублювання | Мінімальні (налаштовується повторна відправка) |
Структура таблиці
Мінімальна таблиця для зберігання заявок з форми:
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.







